From 70ae9aeb16c7d50cca75a38ee3ae42523f9407eb Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 14:20:27 -0700 Subject: [PATCH 1/2] feat(hub,chat,inbox)!: pin @corbits/mailbox 1.0 (native MailboxStore) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps every github:corbitsdev/corbits-mailbox pin to e85b81e4 (1.0.0, breaking): mountMailbox now takes resolvePrincipal/senderAddressFor/deliver instead of a vocabulary, routes live under /me/inbox*, and refs, triage, snooze, and batch writeMailboxMessages are gone in favor of a single-append NativeMailboxStore. - apps/hub: mailbox-persist.ts drops the retired resolveRefs seam; the mailbox mount's own deliver reuses the same dual-write persistMail path every outbound agent frame already goes through, and senderAddressFor resolves a human's `@`. The @corbits/inbox product-inbox routes are deleted (CL-8209): they sat on removed @corbits/mailbox APIs and no web surface reads them any more. - packages/chat: mailbox-fanout.ts writes one writeMailboxMessage append per recipient (outbound to Sent, inbound to INBOX) instead of a batch transaction, since the native store has no multi-row batch. - packages/chat-ui: mailbox-timeline.ts (the only mailbox reader UI-side) reads GET /me/inbox/threads instead of the retired /me/threads+refs API. A workbench is a plain tenant now, so a tenant's own mailbox is already its one room's mail — there is no ref to filter by any more. - packages/inbox: deletes the thread-client, routes, and every module built on removed @corbits/mailbox exports (vocabulary, group, bulk/bulk-run, cursor, project, walk); keeps only the @corbits/notify delivery adapter and this package's own migration ledger. Tests exercising retired behavior (batch-transaction atomicity, ref stamping, triage grouping) are deleted rather than adapted, per the hard-cutover rule. --- apps/hub/package.json | 2 +- apps/hub/src/index.ts | 56 +- apps/hub/src/mailbox-persist.ts | 40 +- apps/hub/test/mailbox-persist.test.ts | 50 +- apps/hub/test/notify-delivery.test.ts | 31 +- bun.lock | 778 +++++++++--------- packages/chat-ui/src/index.ts | 2 +- packages/chat-ui/src/mailbox-timeline.test.ts | 83 +- packages/chat-ui/src/mailbox-timeline.ts | 257 +++--- packages/chat/package.json | 2 +- packages/chat/src/mailbox-fanout.ts | 115 ++- .../chat/test/mailbox-fanout.drizzle.test.ts | 293 ------- packages/chat/test/mailbox-fanout.test.ts | 15 +- packages/inbox/package.json | 10 +- packages/inbox/src/bulk-run.ts | 57 -- packages/inbox/src/bulk.ts | 20 - packages/inbox/src/client.test.ts | 124 --- packages/inbox/src/client.ts | 41 - packages/inbox/src/cursor.ts | 41 - packages/inbox/src/delivery.ts | 118 ++- packages/inbox/src/group.ts | 42 - packages/inbox/src/index.ts | 26 - packages/inbox/src/project.ts | 80 -- packages/inbox/src/routes.ts | 351 -------- packages/inbox/src/thread-client.test.ts | 100 --- packages/inbox/src/thread-client.ts | 114 --- packages/inbox/src/vocabulary.ts | 17 - packages/inbox/src/walk.ts | 61 -- packages/inbox/test/bulk-report-error.test.ts | 73 -- packages/inbox/test/bulk.test.ts | 74 -- packages/inbox/test/cursor.test.ts | 62 -- packages/inbox/test/delivery.test.ts | 61 +- packages/inbox/test/group.test.ts | 48 -- packages/inbox/test/project.test.ts | 44 - packages/inbox/test/routes.test.ts | 109 --- packages/inbox/test/walk.test.ts | 95 --- packages/inbox/tsconfig.json | 6 - packages/inbox/tsconfig.src.json | 6 - scripts/checks/browser-safe-subpaths.ts | 20 +- scripts/checks/report-error-baseline.txt | 54 +- 40 files changed, 840 insertions(+), 2738 deletions(-) delete mode 100644 packages/chat/test/mailbox-fanout.drizzle.test.ts delete mode 100644 packages/inbox/src/bulk-run.ts delete mode 100644 packages/inbox/src/bulk.ts delete mode 100644 packages/inbox/src/client.test.ts delete mode 100644 packages/inbox/src/client.ts delete mode 100644 packages/inbox/src/cursor.ts delete mode 100644 packages/inbox/src/group.ts delete mode 100644 packages/inbox/src/project.ts delete mode 100644 packages/inbox/src/routes.ts delete mode 100644 packages/inbox/src/thread-client.test.ts delete mode 100644 packages/inbox/src/thread-client.ts delete mode 100644 packages/inbox/src/vocabulary.ts delete mode 100644 packages/inbox/src/walk.ts delete mode 100644 packages/inbox/test/bulk-report-error.test.ts delete mode 100644 packages/inbox/test/bulk.test.ts delete mode 100644 packages/inbox/test/cursor.test.ts delete mode 100644 packages/inbox/test/group.test.ts delete mode 100644 packages/inbox/test/project.test.ts delete mode 100644 packages/inbox/test/routes.test.ts delete mode 100644 packages/inbox/test/walk.test.ts diff --git a/apps/hub/package.json b/apps/hub/package.json index 341ddf4b97..5b1eda8f8e 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -37,7 +37,7 @@ "@corbits/inbox": "workspace:*", "@corbits/inference-catalog": "workspace:*", "@corbits/last-30-days-research-workflow": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/mcp-tools": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/morning-brief-workflow": "workspace:*", diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index fb9c5d7972..ed7dabec03 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -80,7 +80,6 @@ import type { RelaunchNoticePort } from "@corbits/chat"; import { reportError } from "@corbits/error-sink"; import type { FinalizedTurnToolCall } from "@corbits/turn-artifacts"; import { decodedOrNull } from "@corbits/url-path"; -import { createInboxRoutes, WORKBENCH_MAILBOX_VOCABULARY } from "@corbits/inbox"; import { createInMemoryMailboxEventBus, createMailboxDb, @@ -90,7 +89,6 @@ import { import { createMemory, loadMemoryConfig } from "@corbits/memory"; import { createHubMailboxAuthorizeSender, - hubMailboxResolveRefs, createHubPersistMailWithSessionEnsure, } from "./mailbox-persist"; import { @@ -551,9 +549,10 @@ export async function createHub(config: HubConfig) { // write. Dual-write independence is `createMailboxPersist`'s own // contract (upstream failing still attempts the mailbox write, and a // mailbox failure never fails upstream) -- no second try/catch belongs - // here. `resolveRefs` runs inside the package's own transaction, so the - // workbench ref is present before the post-commit bus event fires -- - // no out-of-band UPDATE, no polling read. + // here. This is also the seam the mailbox mount's own `deliver` below + // reuses for `POST /me/inbox/send` (CL-8174): both a run's outbound + // mail and a human's own sent mail land recipients through the same + // dual-write path. persistMail: createMailboxPersist(mailboxDb, { upstream: createHubPersistMailWithSessionEnsure( db, @@ -562,7 +561,6 @@ export async function createHub(config: HubConfig) { ), authorizeSender: createHubMailboxAuthorizeSender(db), bus: mailboxBus, - resolveRefs: hubMailboxResolveRefs, }), }; const hubPublicKey = hexEncode(signingKey.publicKey); @@ -1262,11 +1260,12 @@ export async function createHub(config: HubConfig) { authenticator: createWorkflowRunAuthenticator({ db }), }), ); - // Product inbox over `@corbits/mailbox` — three groups, mark-all-read - // (mentions + deliveries only), clear-done. The raw package surface - // (including SSE events) mounts under `/mailbox` for hosts and tools - // that need the universal API. - app.route(`${TENANT_PREFIX}/inbox`, createInboxRoutes({ db: mailboxDb, bus: mailboxBus })); + // The `@corbits/inbox` product-inbox routes (triage groups, mark-all-read, + // clear-done) are retired (CL-8209): they sat on `@corbits/mailbox` APIs + // (`listUserMailbox`, refs, classification) removed in the native 1.0 + // cutover, and no web surface reads `${TENANT_PREFIX}/inbox` any more — + // `/inbox` in the web app only bounces old links home (CL-6151). The + // library's own `/me/inbox*` routes below are the whole inbox surface now. // Library/artifacts plane (CL-8188): `@corbits/artifacts` mounted directly // rather than through the retired `@corbits/artifacts-hub` wrapper — the // library now owns counts, preview, and every other Library HTTP surface @@ -1317,7 +1316,6 @@ export async function createHub(config: HubConfig) { mountMailbox(mailboxApp, { db: mailboxDb, bus: mailboxBus, - vocabulary: WORKBENCH_MAILBOX_VOCABULARY, resolvePrincipal: (ctx) => { // Mounted under the hub tenant middleware; principal + tenant are set. const c = ctx as { @@ -1328,6 +1326,40 @@ export async function createHub(config: HubConfig) { principalId: c.get("principal").id, }; }, + // The caller's own address for `POST /me/inbox/send`'s `From:` — the + // same `@` shape every other mailbox + // writer in this file addresses a human principal under + // (`mailbox-fanout.ts`'s `writeChatMailboxFanout`, + // `native-workflow-routine-launch.ts`'s trigger headers). + senderAddressFor: async (principal) => { + const [tenantRow] = await db + .select({ domain: tenantTable.domain }) + .from(tenantTable) + .where(eq(tenantTable.id, principal.tenantId)) + .limit(1); + if (tenantRow === undefined) { + throw new Error(`no tenant "${principal.tenantId}" to address a mailbox sender from`); + } + return `${principal.principalId}@${tenantRow.domain}`; + }, + // This package only builds the RFC 5322 message and files the + // caller's own `Sent` copy — actually getting `message.raw` to + // `message.to` is this hub's job. Every recipient goes through the + // same `lookups.persistMail` (CL-7449's dual-write wrapper over + // `createMailboxPersist`) every other outbound frame in this hub + // already dispatches through: a live run address is delivered by + // the vendored `baseLookups.persistMail` (`SidecarRouter`-backed + // session/mail routing, the same path + // `native-workflow-routine-launch.ts`'s `routeMail` trigger uses), + // and a human recipient's `principal_mail` copy is appended by the + // wrapped `createMailboxPersist` alongside it. + deliver: async (message) => { + await lookups.persistMail({ + senderAddress: message.from, + recipients: message.to, + raw: message.raw, + }); + }, }); app.route(`${TENANT_PREFIX}/mailbox`, mailboxApp); } diff --git a/apps/hub/src/mailbox-persist.ts b/apps/hub/src/mailbox-persist.ts index e94cf98cd3..53a41e5cf9 100644 --- a/apps/hub/src/mailbox-persist.ts +++ b/apps/hub/src/mailbox-persist.ts @@ -3,22 +3,14 @@ // so every outbound agent frame also lands a durable `principal_mail` row in // each addressed human participant's mailbox, not just `session_mail`. // -// Two small seams live here, both host-owned by the package's own contract +// One seam lives here, host-owned by the package's own contract // (`persist.ts` in `@corbits/mailbox`): `authorizeSender` decides which -// sender addresses may write at all, and `resolveRefs` stamps a workbench -// ref onto every row of the frame INSIDE the same transaction the package -// already opens, so a bus subscriber sees the ref at event time -- no -// out-of-band UPDATE, no polling read. +// sender addresses may write at all. import { eq } from "drizzle-orm"; import type { DB } from "@intx/db"; import { tenant as tenantTable } from "@intx/db/schema"; -import type { - AuthorizeMailboxSender, - CreateMailboxPersistOpts, - MailboxPersistArgs, - MailboxRef, -} from "@corbits/mailbox"; +import type { AuthorizeMailboxSender, MailboxPersistArgs } from "@corbits/mailbox"; import { resolveRoutableAddress } from "@intx/hub-sessions"; import { ensureRunSession, type EventCollectorPort } from "@corbits/workflows"; import { reportError } from "@corbits/error-sink"; @@ -26,14 +18,6 @@ import { getLogger } from "@intx/log"; const logger = getLogger(["hub", "mailbox-persist"]); -/** - * `@corbits/mailbox` does not export `ResolveMailboxRefs` itself (only the - * `CreateMailboxPersistOpts` shape it hangs off), so this derives the same - * type from the one place it is public rather than re-declaring its shape - * by hand and drifting from the package's own definition. - */ -type ResolveMailboxRefs = NonNullable["resolveRefs"]>; - /** * Resolve a `mail.outbound` frame's sender run address to the mailbox * authorization the package needs: the tenant the resulting rows belong to, @@ -129,21 +113,3 @@ export function createHubMailboxAuthorizeSender(db: DB["db"]): AuthorizeMailboxS return { tenantId: sender.tenantId, domain: row.domain }; }; } - -/** - * The `resolveRefs` seam: stamps `refs: [{ kind: "workbench", id }]` onto - * every recipient row of one frame. - * - * A workbench IS a tenant, so the workbench a frame belongs to is exactly - * the tenant `authorizeSender` already resolved from the sender run's own - * address — the same tenant the resulting `principal_mail` rows are scoped - * under. There is no second id to chase and no header or participant scan - * to disambiguate: an agent run is launched in the workbench it serves. - */ -export const hubMailboxResolveRefs: ResolveMailboxRefs = ({ senderAuthorization }) => { - const ref: MailboxRef = { - kind: "workbench", - id: senderAuthorization.tenantId, - }; - return Promise.resolve([ref]); -}; diff --git a/apps/hub/test/mailbox-persist.test.ts b/apps/hub/test/mailbox-persist.test.ts index 624fa2374a..43e8081bb3 100644 --- a/apps/hub/test/mailbox-persist.test.ts +++ b/apps/hub/test/mailbox-persist.test.ts @@ -1,13 +1,13 @@ // CL-7449: proves the real composition -- `createHubSessionLookups`'s // `persistMail` wrapped by `createMailboxPersist` via -// `createHubMailboxAuthorizeSender` / `hubMailboxResolveRefs` -// (`../src/mailbox-persist.ts`) -- against a real Postgres, matching how +// `createHubMailboxAuthorizeSender` (`../src/mailbox-persist.ts`) -- against +// a real Postgres, matching how // `createHub` wires them in `../src/index.ts`. DB-gated: skipped when // DATABASE_URL is unreachable, matching every other suite in this // directory (`composition.test.ts` boots the whole hub over HTTP; this // suite exercises the same `persistMail` wiring directly, without a boot). import { afterAll, expect, test } from "bun:test"; -import { and, eq } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { createDB } from "@intx/db"; import { agentSession, @@ -25,7 +25,7 @@ import { type MailboxDb, } from "@corbits/mailbox"; import { createHubSessionLookups, type AgentRepoStore } from "@intx/hub-sessions"; -import { createHubMailboxAuthorizeSender, hubMailboxResolveRefs } from "../src/mailbox-persist"; +import { createHubMailboxAuthorizeSender } from "../src/mailbox-persist"; import { dbGate } from "../../../scripts/e2e/db-gate"; const databaseUrl = process.env["DATABASE_URL"] ?? ""; @@ -159,7 +159,7 @@ async function setup(opts: { withSession?: boolean } = {}) { } describeIfDb("hub persistMail wrapped with createMailboxPersist", () => { - test("an outbound frame to two humans and one agent produces exactly two principal_mail rows, an SSE event per mailbox carrying the workbench (tenant) ref already, and a durable session_mail row upstream", async () => { + test("an outbound frame to two humans and one agent produces exactly two principal_mail rows, an SSE event per mailbox, and a durable session_mail row upstream", async () => { const { db, mailboxDb, @@ -173,33 +173,18 @@ describeIfDb("hub persistMail wrapped with createMailboxPersist", () => { } = await setup(); const mailboxBus = createInMemoryMailboxEventBus(); - // Asserted INSIDE the subscriber, at event-fire time -- no polling - // retry loop -- because `resolveRefs` runs inside the same transaction - // `createMailboxPersist` opens for the insert, and the bus event fires - // only after that transaction commits (see `persist.ts`'s `announce`). - const seenRefsByPrincipalId = new Map(); - const seenPromises: Promise[] = []; - async function recordSeenRefs(principalId: string): Promise { - const [row] = await mailboxDb - .select({ refs: principalMail.refs }) - .from(principalMail) - .where( - and(eq(principalMail.tenantId, tenantId), eq(principalMail.principalId, principalId)), - ); - seenRefsByPrincipalId.set(principalId, row?.refs); - } + const seenPrincipalIds = new Set(); mailboxBus.subscribe({ tenantId, principalId: human1Id }, () => { - seenPromises.push(recordSeenRefs(human1Id)); + seenPrincipalIds.add(human1Id); }); mailboxBus.subscribe({ tenantId, principalId: human2Id }, () => { - seenPromises.push(recordSeenRefs(human2Id)); + seenPrincipalIds.add(human2Id); }); const persistMail = createMailboxPersist(mailboxDb, { upstream: baseLookups.persistMail, authorizeSender: createHubMailboxAuthorizeSender(db), bus: mailboxBus, - resolveRefs: hubMailboxResolveRefs, }); const raw = new TextEncoder().encode( @@ -217,10 +202,6 @@ describeIfDb("hub persistMail wrapped with createMailboxPersist", () => { recipients: [`usr_${human1Id}@${domain}`, `usr_${human2Id}@${domain}`, agentRecipientAddress], raw, }); - // The bus fires synchronously inside `persistMail`, but each listener's - // own read is async -- wait for those reads, not for more elapsed time, - // before asserting on what they saw. - await Promise.all(seenPromises); // The upstream `session_mail` write still ran, and is durable -- not // just present in the return value. @@ -241,15 +222,9 @@ describeIfDb("hub persistMail wrapped with createMailboxPersist", () => { .where(eq(principalMail.tenantId, tenantId)); expect(rows).toHaveLength(2); expect(rows.map((r) => r.principalId).sort()).toEqual([human1Id, human2Id].sort()); - for (const row of rows) { - expect(row.refs).toEqual([{ kind: "workbench", id: tenantId }]); - } - - // Every SSE subscriber saw the ref already populated at event time. - expect(seenRefsByPrincipalId.size).toBe(2); - for (const refs of seenRefsByPrincipalId.values()) { - expect(refs).toEqual([{ kind: "workbench", id: tenantId }]); - } + + // Every SSE subscriber fired. + expect(seenPrincipalIds).toEqual(new Set([human1Id, human2Id])); }); test("dual-write independence: a sender run with no live session makes upstream throw, the mailbox rows still get written, and zero session_mail rows land", async () => { @@ -260,7 +235,6 @@ describeIfDb("hub persistMail wrapped with createMailboxPersist", () => { const persistMail = createMailboxPersist(mailboxDb, { upstream: baseLookups.persistMail, authorizeSender: createHubMailboxAuthorizeSender(db), - resolveRefs: hubMailboxResolveRefs, }); const raw = new TextEncoder().encode( @@ -286,7 +260,6 @@ describeIfDb("hub persistMail wrapped with createMailboxPersist", () => { .from(principalMail) .where(eq(principalMail.tenantId, tenantId)); expect(mailboxRows).toHaveLength(1); - expect(mailboxRows[0]?.refs).toEqual([{ kind: "workbench", id: tenantId }]); const sessionMailRows = await db .select() @@ -301,7 +274,6 @@ describeIfDb("hub persistMail wrapped with createMailboxPersist", () => { const persistMail = createMailboxPersist(mailboxDb, { upstream: baseLookups.persistMail, authorizeSender: createHubMailboxAuthorizeSender(db), - resolveRefs: hubMailboxResolveRefs, }); const raw = new TextEncoder().encode( diff --git a/apps/hub/test/notify-delivery.test.ts b/apps/hub/test/notify-delivery.test.ts index 3d51870a7b..2ede35f804 100644 --- a/apps/hub/test/notify-delivery.test.ts +++ b/apps/hub/test/notify-delivery.test.ts @@ -10,14 +10,19 @@ import postgres from "postgres"; import { createDB, schema } from "@intx/db"; import { generateId } from "@intx/hub-common"; -import { createInMemoryMailboxEventBus, createMailboxDb, listUserMailbox } from "@corbits/mailbox"; +import { + createInMemoryMailboxEventBus, + createMailboxDb, + openNativeMailboxStore, + principalMail, +} from "@corbits/mailbox"; +import { and, eq, like } from "drizzle-orm"; import { deliverApprovalMail, type ApprovalNotification, type NotifyDispatchStore, type SinkDeliveryResult, } from "@corbits/notify"; -import { WORKBENCH_INBOX_PRIORITIES } from "@corbits/inbox"; import { setupDatabase } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; @@ -169,20 +174,28 @@ describeIfDb("hub notify delivery repairs the CL-7238 crash window on redelivery expect(redelivery.deliveredMailboxRowIds).toEqual([]); expect(redelivery.queuedDispatchCount).toBe(1); - const page = await listUserMailbox(mailboxDb.db, { + const store = await openNativeMailboxStore(mailboxDb.db, { tenantId, principalId, - limit: 10, - view: "all", - priorities: WORKBENCH_INBOX_PRIORITIES, + folder: "INBOX", }); // Exactly one mail row for the redelivered event — the crash and // the redelivery deduped onto the first attempt's row. - const matching = page.items.filter((item) => - (item.subject ?? "").includes("quarantine_token"), + const matching = store.messages.filter((message) => + message.envelope.subject.includes("quarantine_token"), ); expect(matching).toHaveLength(1); - const mailId = matching[0]?.id; + const rows = await mailboxDb.db + .select({ id: principalMail.id }) + .from(principalMail) + .where( + and( + eq(principalMail.tenantId, tenantId), + eq(principalMail.principalId, principalId), + like(principalMail.subject, "%quarantine_token%"), + ), + ); + const mailId = rows[0]?.id; if (mailId === undefined) throw new Error("expected the redelivered mail row to list"); expect(await dispatch.listFor(mailId)).toHaveLength(1); diff --git a/bun.lock b/bun.lock index 02cbbaa4e4..7c00947a61 100644 --- a/bun.lock +++ b/bun.lock @@ -29,63 +29,6 @@ "typescript": "6.0.3", }, }, - "agents/assistant": { - "name": "@corbits/assistant-workflow", - "version": "0.0.2", - "dependencies": { - "@corbits/catalog-tools": "0.1.1", - "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", - }, - "devDependencies": { - "@corbits/agent-directory-tools": "workspace:*", - "@corbits/connections": "workspace:*", - "@corbits/connections-tools": "workspace:*", - "@corbits/interaction-tools": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, - "agents/jimmy": { - "name": "@corbits/jimmy-agent", - "version": "0.0.3", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, - "agents/scout": { - "name": "@corbits/scout-agent", - "version": "0.0.6", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", - "@corbits/web-search-tools": "workspace:*", - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, "apps/hub": { "name": "@workbench/hub", "version": "0.0.1", @@ -115,7 +58,7 @@ "@corbits/inbox": "workspace:*", "@corbits/inference-catalog": "workspace:*", "@corbits/last-30-days-research-workflow": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/mcp-tools": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/morning-brief-workflow": "workspace:*", @@ -241,6 +184,26 @@ "vite": "^7.1.0", }, }, + "packages/access-tools": { + "name": "@corbits/access-tools", + "version": "0.0.6", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@corbits/agent-directory": "workspace:*", + "@corbits/agent-directory-tools": "workspace:*", + "@corbits/tool-registry-publish": "workspace:*", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@corbits/error-sink": "0.0.2", + "@intx/agent": "0.3.0", + "@intx/authz": "0.3.0", + "@intx/types": "0.3.0", + }, + }, "packages/agent-directory": { "name": "@corbits/agent-directory", "version": "0.0.1", @@ -275,6 +238,21 @@ "typescript": "catalog:", }, }, + "packages/agent-directory-tools": { + "name": "@corbits/agent-directory-tools", + "version": "0.0.7", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, "packages/agent-events": { "name": "@corbits/agent-events", "version": "0.0.1", @@ -377,6 +355,37 @@ "typescript": "catalog:", }, }, + "packages/capability-tools": { + "name": "@corbits/capability-tools", + "version": "0.0.7", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, + "packages/catalog-tools": { + "name": "@corbits/catalog-tools", + "version": "0.1.1", + "dependencies": { + "@corbits/inference-catalog": "0.1.0", + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, "packages/chat": { "name": "@corbits/chat", "version": "0.0.1", @@ -389,7 +398,7 @@ "@corbits/connections": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/hub-api-client": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/turn-artifacts": "workspace:*", "@corbits/url-path": "workspace:*", @@ -551,6 +560,21 @@ "typescript": "catalog:", }, }, + "packages/connections-tools": { + "name": "@corbits/connections-tools", + "version": "0.0.12", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, "packages/context-menu": { "name": "@corbits/context-menu", "version": "0.0.1", @@ -654,6 +678,21 @@ "typescript": "catalog:", }, }, + "packages/github-tools": { + "name": "@corbits/github-tools", + "version": "0.0.11", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, "packages/gotenberg-render": { "name": "@corbits/gotenberg-render", "version": "0.0.1", @@ -666,6 +705,26 @@ "typescript": "catalog:", }, }, + "packages/granola-tools": { + "name": "@corbits/granola-tools", + "version": "0.0.5", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@corbits/credential-providers": "workspace:*", + "@intx/authz": "0.3.0", + "@intx/crypto": "0.3.0", + "@intx/db": "workspace:*", + "@intx/harness": "workspace:*", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, "packages/hub-api-client": { "name": "@corbits/hub-api-client", "version": "0.0.1", @@ -695,7 +754,7 @@ "version": "0.0.1", "dependencies": { "@corbits/error-sink": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/notify": "workspace:*", "@intx/hub-api": "workspace:*", "@intx/log": "0.3.0", @@ -740,6 +799,86 @@ "typescript": "catalog:", }, }, + "packages/interaction-tools": { + "name": "@corbits/interaction-tools", + "version": "0.0.11", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@corbits/error-sink": "0.0.2", + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, + "packages/jimmy-agent": { + "name": "@corbits/jimmy-agent", + "version": "0.0.3", + "dependencies": { + "@intx/agent": "workspace:*", + "@intx/types": "workspace:*", + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/linear-tools": { + "name": "@corbits/linear-tools", + "version": "0.0.5", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@corbits/credential-providers": "workspace:*", + "@intx/authz": "0.3.0", + "@intx/crypto": "0.3.0", + "@intx/db": "workspace:*", + "@intx/harness": "workspace:*", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, + "packages/manus-tools": { + "name": "@corbits/manus-tools", + "version": "0.0.12", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, + "packages/mcp-tools": { + "name": "@corbits/mcp-tools", + "version": "0.0.12", + "dependencies": { + "@modelcontextprotocol/sdk": "catalog:", + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, "packages/migration-runner": { "name": "@corbits/migration-runner", "version": "0.0.1", @@ -861,198 +1000,7 @@ "dependencies": { "@corbits/error-sink": "workspace:*", "@corbits/sandbox-sidecar": "workspace:*", - "@intx/hub-sessions": "workspace:*", - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/provider-pricing": { - "name": "@corbits/provider-pricing", - "version": "0.0.1", - "dependencies": { - "@corbits/connections": "workspace:*", - "@intx/inference-catalog": "0.3.0", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/sandbox-sidecar": { - "name": "@corbits/sandbox-sidecar", - "version": "0.0.1", - "dependencies": { - "@intx/crypto": "0.3.0", - "@intx/hub-sessions": "workspace:*", - "@intx/log": "0.3.0", - "@intx/types": "workspace:*", - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/settings-ui": { - "name": "@corbits/settings-ui", - "version": "0.0.1", - "dependencies": { - "@corbits/api-query": "workspace:*", - "@corbits/bench-ui": "workspace:*", - "@corbits/chat-ui": "workspace:*", - "@corbits/connections": "workspace:*", - "@corbits/error-sink": "workspace:*", - "@corbits/icons": "workspace:*", - "@corbits/inference-settings": "workspace:*", - "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", - "@corbits/workflows": "workspace:*", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "react": "^19.2.0", - "react-dom": "^19.2.0", - }, - "devDependencies": { - "@happy-dom/global-registrator": "^20.11.2", - "@types/bun": "catalog:", - "@types/react": "^19.2.2", - "@types/react-dom": "catalog:", - "react-dom": "^19.2.0", - "typescript": "catalog:", - }, - }, - "packages/shell-layout": { - "name": "@corbits/shell-layout", - "version": "0.0.1", - "dependencies": { - "react": "^19.2.0", - }, - "devDependencies": { - "@happy-dom/global-registrator": "^20.11.2", - "@types/bun": "catalog:", - "@types/react": "^19.2.2", - "@types/react-dom": "catalog:", - "react-dom": "^19.2.0", - "typescript": "catalog:", - }, - }, - "packages/slug": { - "name": "@corbits/slug", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/text-diff": { - "name": "@corbits/text-diff", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/tool-registry-publish": { - "name": "@corbits/tool-registry-publish", - "version": "0.0.1", - "dependencies": { - "@intx/inference": "workspace:*", - "@intx/log": "0.3.0", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "tar": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/turn-artifacts": { - "name": "@corbits/turn-artifacts", - "version": "0.0.1", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/url-path": { - "name": "@corbits/url-path", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/webhook-triggers": { - "name": "@corbits/webhook-triggers", - "version": "0.0.1", - "dependencies": { - "@corbits/agent-runtime": "workspace:*", - "@corbits/error-sink": "workspace:*", - "@corbits/migration-runner": "workspace:*", - "@corbits/workflows": "workspace:*", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/hub-common": "0.3.0", - "@intx/hub-sessions": "workspace:*", - "@intx/log": "0.3.0", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "drizzle-orm": "catalog:", - "hono": "catalog:", - "postgres": "catalog:", - }, - "devDependencies": { - "@intx/crypto": "0.3.0", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/workflow-schedule": { - "name": "@corbits/workflow-schedule", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/workflows": { - "name": "@corbits/workflows", - "version": "0.0.1", - "dependencies": { - "@corbits/error-sink": "workspace:*", - "@corbits/workflow-schedule": "workspace:*", - "@intx/authz": "0.3.0", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/hub-common": "0.3.0", - "@intx/hub-sessions": "workspace:*", - "@intx/types": "workspace:*", - "@intx/workflow-deploy": "workspace:*", - "arktype": "catalog:", - "cronstrue": "^3.24.0", - "drizzle-orm": "catalog:", - "hono": "catalog:", - }, - "devDependencies": { - "@corbits/connections": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/xai-provider": { - "name": "@corbits/xai-provider", - "version": "0.1.0", - "dependencies": { - "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core#v0.1.1", - "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses#v0.1.0", - "@intx/inference": "workspace:*", - "@intx/types": "workspace:*", + "@intx/hub-sessions": "workspace:*", "arktype": "catalog:", }, "devDependencies": { @@ -1060,101 +1008,108 @@ "typescript": "catalog:", }, }, - "skills/skills": { - "name": "@corbits/skills", - "version": "0.0.2", + "packages/provider-pricing": { + "name": "@corbits/provider-pricing", + "version": "0.0.1", "dependencies": { - "arktype": "catalog:", + "@corbits/connections": "workspace:*", + "@intx/inference-catalog": "0.3.0", }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, }, - "tools/access": { - "name": "@corbits/access-tools", - "version": "0.0.6", + "packages/reddit-tools": { + "name": "@corbits/reddit-tools", + "version": "0.0.2", "dependencies": { "arktype": "catalog:", }, "devDependencies": { - "@corbits/agent-directory": "workspace:*", - "@corbits/agent-directory-tools": "workspace:*", - "@corbits/tool-registry-publish": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, "peerDependencies": { - "@corbits/error-sink": "0.0.2", "@intx/agent": "0.3.0", - "@intx/authz": "0.3.0", "@intx/types": "0.3.0", }, }, - "tools/agent-directory": { - "name": "@corbits/agent-directory-tools", - "version": "0.0.7", + "packages/sandbox-sidecar": { + "name": "@corbits/sandbox-sidecar", + "version": "0.0.1", "dependencies": { + "@intx/crypto": "0.3.0", + "@intx/hub-sessions": "workspace:*", + "@intx/log": "0.3.0", + "@intx/types": "workspace:*", "arktype": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, - "tools/capability": { - "name": "@corbits/capability-tools", - "version": "0.0.7", + "packages/scout-agent": { + "name": "@corbits/scout-agent", + "version": "0.0.6", "dependencies": { + "@intx/agent": "workspace:*", + "@intx/types": "workspace:*", "arktype": "catalog:", }, "devDependencies": { + "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", + "@corbits/web-search-tools": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, - "tools/catalog": { - "name": "@corbits/catalog-tools", - "version": "0.1.1", + "packages/settings-ui": { + "name": "@corbits/settings-ui", + "version": "0.0.1", "dependencies": { - "@corbits/inference-catalog": "0.1.0", + "@corbits/api-query": "workspace:*", + "@corbits/bench-ui": "workspace:*", + "@corbits/chat-ui": "workspace:*", + "@corbits/connections": "workspace:*", + "@corbits/error-sink": "workspace:*", + "@corbits/icons": "workspace:*", + "@corbits/inference-settings": "workspace:*", + "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", + "@corbits/workflows": "workspace:*", + "@intx/types": "workspace:*", "arktype": "catalog:", + "react": "^19.2.0", + "react-dom": "^19.2.0", }, "devDependencies": { + "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", + "@types/react": "^19.2.2", + "@types/react-dom": "catalog:", + "react-dom": "^19.2.0", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, - "tools/connections": { - "name": "@corbits/connections-tools", - "version": "0.0.12", + "packages/shell-layout": { + "name": "@corbits/shell-layout", + "version": "0.0.1", "dependencies": { - "arktype": "catalog:", + "react": "^19.2.0", }, "devDependencies": { + "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", + "@types/react": "^19.2.2", + "@types/react-dom": "catalog:", + "react-dom": "^19.2.0", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, - "tools/github": { - "name": "@corbits/github-tools", - "version": "0.0.11", + "packages/skills": { + "name": "@corbits/skills", + "version": "0.0.2", "dependencies": { "arktype": "catalog:", }, @@ -1162,23 +1117,14 @@ "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, - "tools/granola": { - "name": "@corbits/granola-tools", - "version": "0.0.5", + "packages/skills-tools": { + "name": "@corbits/skills-tools", + "version": "0.0.8", "dependencies": { "arktype": "catalog:", }, "devDependencies": { - "@corbits/credential-providers": "workspace:*", - "@intx/authz": "0.3.0", - "@intx/crypto": "0.3.0", - "@intx/db": "workspace:*", - "@intx/harness": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, @@ -1187,45 +1133,40 @@ "@intx/types": "0.3.0", }, }, - "tools/interaction": { - "name": "@corbits/interaction-tools", - "version": "0.0.11", - "dependencies": { - "arktype": "catalog:", - }, + "packages/slug": { + "name": "@corbits/slug", + "version": "0.0.1", "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@corbits/error-sink": "0.0.2", - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", + }, + "packages/text-diff": { + "name": "@corbits/text-diff", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", }, }, - "tools/linear": { - "name": "@corbits/linear-tools", - "version": "0.0.5", + "packages/tool-registry-publish": { + "name": "@corbits/tool-registry-publish", + "version": "0.0.1", "dependencies": { + "@intx/inference": "workspace:*", + "@intx/log": "0.3.0", + "@intx/types": "workspace:*", "arktype": "catalog:", + "tar": "catalog:", }, "devDependencies": { - "@corbits/credential-providers": "workspace:*", - "@intx/authz": "0.3.0", - "@intx/crypto": "0.3.0", - "@intx/db": "workspace:*", - "@intx/harness": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, - "tools/manus": { - "name": "@corbits/manus-tools", - "version": "0.0.12", + "packages/tools-skills": { + "name": "@corbits/tools-skills", + "version": "0.0.4", "dependencies": { "arktype": "catalog:", }, @@ -1238,25 +1179,28 @@ "@intx/types": "0.3.0", }, }, - "tools/mcp": { - "name": "@corbits/mcp-tools", - "version": "0.0.12", + "packages/turn-artifacts": { + "name": "@corbits/turn-artifacts", + "version": "0.0.1", "dependencies": { - "@modelcontextprotocol/sdk": "catalog:", "arktype": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", + }, + "packages/url-path": { + "name": "@corbits/url-path", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", }, }, - "tools/reddit": { - "name": "@corbits/reddit-tools", - "version": "0.0.2", + "packages/web-search-tools": { + "name": "@corbits/web-search-tools", + "version": "0.0.4", "dependencies": { "arktype": "catalog:", }, @@ -1269,24 +1213,34 @@ "@intx/types": "0.3.0", }, }, - "tools/skills-tools": { - "name": "@corbits/skills-tools", - "version": "0.0.8", + "packages/webhook-triggers": { + "name": "@corbits/webhook-triggers", + "version": "0.0.1", "dependencies": { + "@corbits/agent-runtime": "workspace:*", + "@corbits/error-sink": "workspace:*", + "@corbits/migration-runner": "workspace:*", + "@corbits/workflows": "workspace:*", + "@intx/db": "workspace:*", + "@intx/hub-api": "workspace:*", + "@intx/hub-common": "0.3.0", + "@intx/hub-sessions": "workspace:*", + "@intx/log": "0.3.0", + "@intx/types": "workspace:*", "arktype": "catalog:", + "drizzle-orm": "catalog:", + "hono": "catalog:", + "postgres": "catalog:", }, "devDependencies": { + "@intx/crypto": "0.3.0", "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, - "tools/tools-skills": { - "name": "@corbits/tools-skills", - "version": "0.0.4", + "packages/workflow-authoring-tools": { + "name": "@corbits/workflow-authoring-tools", + "version": "0.0.8", "dependencies": { "arktype": "catalog:", }, @@ -1299,35 +1253,52 @@ "@intx/types": "0.3.0", }, }, - "tools/web-search": { - "name": "@corbits/web-search-tools", - "version": "0.0.4", + "packages/workflow-schedule": { + "name": "@corbits/workflow-schedule", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/workflows": { + "name": "@corbits/workflows", + "version": "0.0.1", "dependencies": { + "@corbits/error-sink": "workspace:*", + "@corbits/workflow-schedule": "workspace:*", + "@intx/authz": "0.3.0", + "@intx/db": "workspace:*", + "@intx/hub-api": "workspace:*", + "@intx/hub-common": "0.3.0", + "@intx/hub-sessions": "workspace:*", + "@intx/types": "workspace:*", + "@intx/workflow-deploy": "workspace:*", "arktype": "catalog:", + "cronstrue": "^3.24.0", + "drizzle-orm": "catalog:", + "hono": "catalog:", }, "devDependencies": { + "@corbits/connections": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, - "tools/workflow-authoring": { - "name": "@corbits/workflow-authoring-tools", - "version": "0.0.8", + "packages/xai-provider": { + "name": "@corbits/xai-provider", + "version": "0.1.0", "dependencies": { + "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core#v0.1.1", + "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses#v0.1.0", + "@intx/inference": "workspace:*", + "@intx/types": "workspace:*", "arktype": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, }, "vendor/intx/agent": { "name": "@intx/agent", @@ -1590,6 +1561,27 @@ "typescript": "catalog:", }, }, + "workflows/assistant": { + "name": "@corbits/assistant-workflow", + "version": "0.0.2", + "dependencies": { + "@corbits/catalog-tools": "0.1.1", + "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", + }, + "devDependencies": { + "@corbits/agent-directory-tools": "workspace:*", + "@corbits/connections": "workspace:*", + "@corbits/connections-tools": "workspace:*", + "@corbits/interaction-tools": "workspace:*", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + "@intx/workflow": "0.3.0", + }, + }, "workflows/attio-task-agent": { "name": "@corbits/attio-task-agent-workflow", "version": "0.0.1", @@ -1935,11 +1927,11 @@ "@connectrpc/connect-web": ["@connectrpc/connect-web@2.2.0", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0", "@connectrpc/connect": "2.2.0" } }, "sha512-uuBUcBTYfb/r1o98LzCCZk1H90yx0vmZ5m21dpw/q4EUwsKwkCDGI8H0aEdjcZld+Q2jJ5A5ADvPqfb6lzQ/sg=="], - "@corbits/access-tools": ["@corbits/access-tools@workspace:tools/access"], + "@corbits/access-tools": ["@corbits/access-tools@workspace:packages/access-tools"], "@corbits/agent-directory": ["@corbits/agent-directory@workspace:packages/agent-directory"], - "@corbits/agent-directory-tools": ["@corbits/agent-directory-tools@workspace:tools/agent-directory"], + "@corbits/agent-directory-tools": ["@corbits/agent-directory-tools@workspace:packages/agent-directory-tools"], "@corbits/agent-events": ["@corbits/agent-events@workspace:packages/agent-events"], @@ -1953,15 +1945,15 @@ "@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#5309a15", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "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-5309a15", "sha512-zGHkq++juclUrOd0O8/Z5ziTldmJZ6gqyH/c9cs5S10UPU4gqn8CHwq174SiJXY0ujJ3cNpJXnrr+0fcqlfQ1w=="], - "@corbits/assistant-workflow": ["@corbits/assistant-workflow@workspace:agents/assistant"], + "@corbits/assistant-workflow": ["@corbits/assistant-workflow@workspace:workflows/assistant"], "@corbits/attio-task-agent-workflow": ["@corbits/attio-task-agent-workflow@workspace:workflows/attio-task-agent"], "@corbits/bench-ui": ["@corbits/bench-ui@workspace:packages/bench-ui"], - "@corbits/capability-tools": ["@corbits/capability-tools@workspace:tools/capability"], + "@corbits/capability-tools": ["@corbits/capability-tools@workspace:packages/capability-tools"], - "@corbits/catalog-tools": ["@corbits/catalog-tools@workspace:tools/catalog"], + "@corbits/catalog-tools": ["@corbits/catalog-tools@workspace:packages/catalog-tools"], "@corbits/chat": ["@corbits/chat@workspace:packages/chat"], @@ -1985,7 +1977,7 @@ "@corbits/connections": ["@corbits/connections@workspace:packages/connections"], - "@corbits/connections-tools": ["@corbits/connections-tools@workspace:tools/connections"], + "@corbits/connections-tools": ["@corbits/connections-tools@workspace:packages/connections-tools"], "@corbits/context-menu": ["@corbits/context-menu@workspace:packages/context-menu"], @@ -2007,13 +1999,13 @@ "@corbits/exa-topic-watch-workflow": ["@corbits/exa-topic-watch-workflow@workspace:workflows/exa-topic-watch"], - "@corbits/github-tools": ["@corbits/github-tools@workspace:tools/github"], + "@corbits/github-tools": ["@corbits/github-tools@workspace:packages/github-tools"], "@corbits/gotenberg-render": ["@corbits/gotenberg-render@workspace:packages/gotenberg-render"], "@corbits/granola-call-workflow": ["@corbits/granola-call-workflow@workspace:workflows/granola-call"], - "@corbits/granola-tools": ["@corbits/granola-tools@workspace:tools/granola"], + "@corbits/granola-tools": ["@corbits/granola-tools@workspace:packages/granola-tools"], "@corbits/heartbeat-workflow": ["@corbits/heartbeat-workflow@workspace:workflows/heartbeat"], @@ -2027,19 +2019,19 @@ "@corbits/inference-settings": ["@corbits/inference-settings@workspace:packages/inference-settings"], - "@corbits/interaction-tools": ["@corbits/interaction-tools@workspace:tools/interaction"], + "@corbits/interaction-tools": ["@corbits/interaction-tools@workspace:packages/interaction-tools"], - "@corbits/jimmy-agent": ["@corbits/jimmy-agent@workspace:agents/jimmy"], + "@corbits/jimmy-agent": ["@corbits/jimmy-agent@workspace:packages/jimmy-agent"], "@corbits/last-30-days-research-workflow": ["@corbits/last-30-days-research-workflow@workspace:workflows/last-30-days-research"], - "@corbits/linear-tools": ["@corbits/linear-tools@workspace:tools/linear"], + "@corbits/linear-tools": ["@corbits/linear-tools@workspace:packages/linear-tools"], - "@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#bc96404", { "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-bc96404", "sha512-NEeNP2XsjIKOUbRr5szrCDWAYPQSmMNSu33ODyLUCiXdnq12HYWYG/C+0DcAMELlY+9Ktqf543Kg/Qb22NtjWQ=="], + "@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#e85b81e", { "dependencies": { "@hono/standard-validator": "0.2.3", "@intx/crypto": "0.3.0", "@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", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-e85b81e", "sha512-ATPP/pll1sYG1DLKV1HvtboK8q1tFkqayQqRXdUmRUIsFJx5wqzd8YA1XELjXzLu1P2f2uCMe2CjsJHGv6E90Q=="], - "@corbits/manus-tools": ["@corbits/manus-tools@workspace:tools/manus"], + "@corbits/manus-tools": ["@corbits/manus-tools@workspace:packages/manus-tools"], - "@corbits/mcp-tools": ["@corbits/mcp-tools@workspace:tools/mcp"], + "@corbits/mcp-tools": ["@corbits/mcp-tools@workspace:packages/mcp-tools"], "@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=="], @@ -2071,19 +2063,19 @@ "@corbits/reddit-opportunity-scanner-workflow": ["@corbits/reddit-opportunity-scanner-workflow@workspace:workflows/reddit-opportunity-scanner"], - "@corbits/reddit-tools": ["@corbits/reddit-tools@workspace:tools/reddit"], + "@corbits/reddit-tools": ["@corbits/reddit-tools@workspace:packages/reddit-tools"], "@corbits/sandbox-sidecar": ["@corbits/sandbox-sidecar@workspace:packages/sandbox-sidecar"], - "@corbits/scout-agent": ["@corbits/scout-agent@workspace:agents/scout"], + "@corbits/scout-agent": ["@corbits/scout-agent@workspace:packages/scout-agent"], "@corbits/settings-ui": ["@corbits/settings-ui@workspace:packages/settings-ui"], "@corbits/shell-layout": ["@corbits/shell-layout@workspace:packages/shell-layout"], - "@corbits/skills": ["@corbits/skills@workspace:skills/skills"], + "@corbits/skills": ["@corbits/skills@workspace:packages/skills"], - "@corbits/skills-tools": ["@corbits/skills-tools@workspace:tools/skills-tools"], + "@corbits/skills-tools": ["@corbits/skills-tools@workspace:packages/skills-tools"], "@corbits/slug": ["@corbits/slug@workspace:packages/slug"], @@ -2091,19 +2083,19 @@ "@corbits/tool-registry-publish": ["@corbits/tool-registry-publish@workspace:packages/tool-registry-publish"], - "@corbits/tools-skills": ["@corbits/tools-skills@workspace:tools/tools-skills"], + "@corbits/tools-skills": ["@corbits/tools-skills@workspace:packages/tools-skills"], "@corbits/turn-artifacts": ["@corbits/turn-artifacts@workspace:packages/turn-artifacts"], "@corbits/url-path": ["@corbits/url-path@workspace:packages/url-path"], - "@corbits/web-search-tools": ["@corbits/web-search-tools@workspace:tools/web-search"], + "@corbits/web-search-tools": ["@corbits/web-search-tools@workspace:packages/web-search-tools"], "@corbits/webhook-triggers": ["@corbits/webhook-triggers@workspace:packages/webhook-triggers"], "@corbits/workbench-digest-workflow": ["@corbits/workbench-digest-workflow@workspace:workflows/workbench-digest"], - "@corbits/workflow-authoring-tools": ["@corbits/workflow-authoring-tools@workspace:tools/workflow-authoring"], + "@corbits/workflow-authoring-tools": ["@corbits/workflow-authoring-tools@workspace:packages/workflow-authoring-tools"], "@corbits/workflow-schedule": ["@corbits/workflow-schedule@workspace:packages/workflow-schedule"], @@ -3283,6 +3275,10 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@corbits/artifact-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/assistant-workflow/@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=="], + "@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], "@corbits/scout-agent/@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=="], @@ -3301,6 +3297,10 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#5309a15", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "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-5309a15", "sha512-zGHkq++juclUrOd0O8/Z5ziTldmJZ6gqyH/c9cs5S10UPU4gqn8CHwq174SiJXY0ujJ3cNpJXnrr+0fcqlfQ1w=="], + + "@workbench/sidecar/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#e1e69e6", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-e1e69e6", "sha512-wUrD73iVyk/Dtb4yRn3hCh6N8syfsEvAkRz4XYqT0FIB3sYDSMYEWHcDMJx9nq64my/HBOeN1J6PHIOtpjhtPg=="], + "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], diff --git a/packages/chat-ui/src/index.ts b/packages/chat-ui/src/index.ts index 975073986f..b4a4a403b8 100644 --- a/packages/chat-ui/src/index.ts +++ b/packages/chat-ui/src/index.ts @@ -30,7 +30,7 @@ export { } from "./avatar"; export type { AvatarFill, AvatarColor, CorbitAvatarProps, CorbitAvatarSize } from "./avatar"; -export { threadMessagesToTimeline, roomRefFor } from "./mailbox-timeline"; +export { threadTreeToTimeline } from "./mailbox-timeline"; export type { TimelineItem } from "./mailbox-timeline"; export { diff --git a/packages/chat-ui/src/mailbox-timeline.test.ts b/packages/chat-ui/src/mailbox-timeline.test.ts index f4de07ecfd..41e797e205 100644 --- a/packages/chat-ui/src/mailbox-timeline.test.ts +++ b/packages/chat-ui/src/mailbox-timeline.test.ts @@ -1,60 +1,55 @@ import { describe, expect, test } from "bun:test"; -import { - roomRefFor, - threadMessagesToTimeline, - type MailboxThreadMessage, -} from "./mailbox-timeline"; - -function message( - overrides: Partial & Pick, -): MailboxThreadMessage { +import { threadTreeToTimeline } from "./mailbox-timeline"; + +function envelope(overrides: { + messageId: string; + from?: string; + subject?: string; + date?: string; +}) { return { + messageId: overrides.messageId, + from: overrides.from ?? "sender@dana.localhost", + to: [], + subject: overrides.subject ?? "", + date: overrides.date ?? "2026-09-17T00:00:00.000Z", references: [], - fromAddress: "sender@dana.localhost", - createdAt: "2026-09-17T00:00:00.000000Z", - read: true, - archived: false, - parentId: null, - body: "hello", - ...overrides, }; } -describe("threadMessagesToTimeline", () => { - test("links a reply's threadId to its parent's own id via inReplyTo", () => { - const parent = message({ id: "id-1", messageId: "msg-1@mail" }); - const child = message({ - id: "id-2", - messageId: "msg-2@mail", - inReplyTo: "msg-1@mail", - body: "reply", - }); - - const items = threadMessagesToTimeline([parent, child]); +describe("threadTreeToTimeline", () => { + test("links a reply's threadId to its parent's own uid", () => { + const root = { + uid: 1, + flags: [], + envelope: envelope({ messageId: "" }), + children: [ + { + uid: 2, + flags: [], + envelope: envelope({ messageId: "", subject: "reply" }), + children: [], + }, + ], + }; + + const items = threadTreeToTimeline(root); expect(items[0]?.threadId).toBeUndefined(); - expect(items[1]?.threadId).toBe("id-1"); + expect(items[1]?.threadId).toBe("1"); }); - test("leaves threadId absent when the parent isn't in this batch", () => { - const orphan = message({ - id: "id-3", - messageId: "msg-3@mail", - inReplyTo: "msg-missing@mail", - }); + test("a single-node thread has no threadId at all", () => { + const root = { + uid: 3, + flags: [], + envelope: envelope({ messageId: "" }), + children: [], + }; - const [item] = threadMessagesToTimeline([orphan]); + const [item] = threadTreeToTimeline(root); expect(item?.threadId).toBeUndefined(); }); }); - -describe("roomRefFor", () => { - test("stamps the same { kind: 'workbench', id } ref the mailbox writers use", () => { - expect(roomRefFor("tenant-1", "room-1")).toEqual({ - kind: "workbench", - id: "room-1", - }); - }); -}); diff --git a/packages/chat-ui/src/mailbox-timeline.ts b/packages/chat-ui/src/mailbox-timeline.ts index 5325eb3e3a..c17cbe05f6 100644 --- a/packages/chat-ui/src/mailbox-timeline.ts +++ b/packages/chat-ui/src/mailbox-timeline.ts @@ -4,27 +4,27 @@ // chat-ui already knows how to render, so the timeline never has to branch // on where a message came from. // -// This file makes its own `fetch()` calls against the mailbox's `/me/threads` -// routes and validates responses with local arktype schemas, rather than -// importing `@corbits/inbox/client` — that client's package re-exports pull -// in `@corbits/mailbox`'s server-only `migrations.ts` (a `node:crypto` user) +// This file makes its own `fetch()` calls against the native +// `@corbits/mailbox` 1.0 routes (`/me/inbox/threads*`) and validates +// responses with local arktype schemas, rather than importing +// `@corbits/inbox/client` — that client's package re-exports pull in +// `@corbits/mailbox`'s server-only `migrations.ts` (a `node:crypto` user) // at the value level, which a real bundler (Vite/Rollup) walks into even // though only types are used, breaking the browser build. Per the owner // ruling that `@corbits/mailbox` is a temporary, shrinking surface, every // mailbox read in chat-ui goes through this one file, so swapping to // Interchange's native mailbox thread shape later is a one-file change. +// +// A workbench is a plain Interchange tenant now (CL-8083): there is one +// chat room per tenant, so the tenant's own `/me/inbox` mailbox already IS +// that room's mail — there is no per-room `refs` filter to apply any more +// (the native 1.0 mount dropped `refs` entirely). `roomId` is kept on every +// function below only so a caller with just a room in hand has a matching +// call shape; it never narrows which threads come back. import { type } from "arktype"; import type { MessageItem, WorkbenchThreadRow } from "./api"; -/** A mailbox ref, mirrored locally rather than imported from - * `@corbits/mailbox` (see the file header) — kept to exactly the shape this - * file stamps and sends. */ -export interface MailboxRef { - readonly kind: string; - readonly id: string; -} - export class MailboxThreadFetchError extends Error { constructor( message: string, @@ -35,41 +35,55 @@ export class MailboxThreadFetchError extends Error { } } -const MailboxThreadSummary = type({ - rootId: "string", - rootMessageId: "string", - "subject?": "string | null", - messageCount: "number", - unreadCount: "number", - lastMessageId: "string", - lastFromAddress: "string", - lastCreatedAt: "string", -}); +type MailboxThreadNode = { + uid: number; + flags: string[]; + envelope: { + messageId: string; + from: string; + to: string[]; + subject: string; + date: string; + inReplyTo?: string; + references: string[]; + }; + children: MailboxThreadNode[]; +}; + +/** Hand-rolled rather than an arktype schema: the node shape is + * self-referential (`children: MailboxThreadNode[]`), which arktype only + * expresses through a named scope — more machinery than a same-shape-at- + * every-depth check over a server response this file already trusts (the + * hub's own `mountMailbox`, `enrichThread` in `@corbits/mailbox`'s + * `mount.ts`) needs. */ +function isMailboxThreadNode(value: unknown): value is MailboxThreadNode { + if (typeof value !== "object" || value === null) return false; + const node = value as Record; + if (typeof node.uid !== "number") return false; + if (!Array.isArray(node.flags) || !node.flags.every((flag) => typeof flag === "string")) { + return false; + } + const envelope = node.envelope as Record | undefined; + if ( + typeof envelope !== "object" || + envelope === null || + typeof envelope.messageId !== "string" || + typeof envelope.from !== "string" || + !Array.isArray(envelope.to) || + typeof envelope.subject !== "string" || + typeof envelope.date !== "string" || + !Array.isArray(envelope.references) + ) { + return false; + } + if (!Array.isArray(node.children) || !node.children.every(isMailboxThreadNode)) { + return false; + } + return true; +} const MailboxThreadListResponse = type({ - threads: MailboxThreadSummary.array(), - "nextCursor?": "string", -}); - -const MailboxThreadMessageSchema = type({ - id: "string", - messageId: "string", - "inReplyTo?": "string", - references: "string[]", - fromAddress: "string", - "subject?": "string | null", - createdAt: "string", - read: "boolean", - archived: "boolean", - "parentId?": "string | null", - body: "string", -}); - -export type MailboxThreadMessage = typeof MailboxThreadMessageSchema.infer; - -const MailboxThreadReadResponse = type({ - messages: MailboxThreadMessageSchema.array(), - "nextCursor?": "string", + threads: "unknown[]", }); async function mailboxRequest( @@ -103,143 +117,112 @@ async function mailboxRequest( * already consumes `MessageItem`. */ export type TimelineItem = MessageItem; +/** Flattens one thread tree (root plus every descendant) into its member + * nodes, in the order `enrichThread` builds them (parent, then children). */ +function flattenThread(node: MailboxThreadNode): MailboxThreadNode[] { + return [node, ...node.children.flatMap(flattenThread)]; +} + /** - * Maps one mailbox thread's messages onto timeline items. + * Maps one thread tree's nodes onto timeline items. * * `subject`, when present, prefixes the body as its own line — mail is the * only source that carries a subject, so it is folded into the one text * part a `MessageItem` has room for rather than inventing a field the rest * of chat-ui would have to learn about. * - * `threadId` links a reply to its parent's own `id` (not its `messageId`, + * `threadId` links a reply to its parent's own `uid` (not its `messageId`, * which is the RFC-2822-style id the `inReplyTo`/`references` headers - * carry) by resolving `inReplyTo` against every message's `messageId` in - * this same batch. A message whose parent isn't in this batch — the root, - * or a reply to a message this read didn't page in — keeps `threadId` - * absent rather than guessing. + * carry) by walking the tree structure the mount already resolved — + * `executeThread`'s own REFERENCES algorithm, not a second reconstruction + * from headers here. */ -export function threadMessagesToTimeline( - messages: readonly MailboxThreadMessage[], -): TimelineItem[] { - const idByMessageId = new Map(); - for (const message of messages) { - idByMessageId.set(message.messageId, message.id); - } - - return messages.map((message) => { - const parentId = - message.inReplyTo !== undefined ? idByMessageId.get(message.inReplyTo) : undefined; +export function threadTreeToTimeline(root: MailboxThreadNode): TimelineItem[] { + const items: TimelineItem[] = []; + function visit(node: MailboxThreadNode, parentUid: number | undefined) { + const { envelope } = node; const text = - message.subject !== undefined && message.subject !== null && message.subject.length > 0 - ? `${message.subject}\n${message.body}` - : message.body; - return { - id: message.id, - createdAt: message.createdAt, + envelope.subject.length > 0 ? `${envelope.subject}\n${envelope.from}` : envelope.from; + items.push({ + id: String(node.uid), + createdAt: envelope.date, parts: [{ kind: "text", text }], - sender: { name: null, address: message.fromAddress }, - ...(parentId !== undefined ? { threadId: parentId } : {}), - }; - }); -} - -/** - * The same `{ kind: "workbench", id }` ref every mailbox writer stamps - * (`apps/hub/src/mailbox-persist.ts`'s `hubMailboxResolveRefs`, - * `packages/chat/src/mailbox-fanout.ts`'s `writeChatMailboxFanout`) — a - * workbench is a plain tenant now (CL-8083), so the room a thread hangs - * off is the same id a chat room is scoped to, and `tenantId` is carried - * here only so a caller with just a tenant in hand has a matching call - * shape; the stamped ref itself never varies by which of the two identical - * ids produced it. - */ -export function roomRefFor(_tenantId: string, roomId: string): MailboxRef { - return { kind: "workbench", id: roomId }; + sender: { name: null, address: envelope.from }, + ...(parentUid !== undefined ? { threadId: String(parentUid) } : {}), + }); + for (const child of node.children) visit(child, node.uid); + } + visit(root, undefined); + return items; } function mailboxBasePathFor(tenantId: string): string { return `/api/tenants/${tenantId}/mailbox`; } -/** How many of a room's most-recently-active mail threads a feed page - * loads bodies for. A room with more conversations than this simply - * doesn't show the older ones yet — there is no "load more" for the root - * feed today, matching the bound the old `GET /messages` page carried. */ +/** How many of the tenant's most-recently-active mail threads a feed page + * loads bodies for. More conversations than this simply don't show the + * older ones yet — there is no "load more" for the root feed today, + * matching the bound the old `GET /messages` page carried. */ const ROOM_FEED_THREAD_LIMIT = 50; /** The `threadId` every root-level message resolves to (via * `./thread-feed.ts`'s `threadIdOf`) when it carries none of its own — the * same "root feed by default" contract the old `workbench_thread_messages` - * store stated. A plain, never-a-mailbox-row-id sentinel, so it can never - * collide with a real reply thread's id (a mail message's own row id). */ + * store stated. A plain, never-a-mailbox-uid sentinel, so it can never + * collide with a real reply thread's id (a mail message's own uid). */ export const ROOM_FEED_ROOT_THREAD_ID = "__root__"; -/** - * The bounded page of a room's mail threads a feed reads bodies for — - * newest activity first, same order `listMailboxThreadsClient` returns - * them in. The only call site in this package that reads a - * `MailboxThreadSummary`'s own fields — every other module works from - * `MessageItem`/`WorkbenchThreadRow` instead, so a later swap to - * Interchange's native mailbox thread shape touches only this file. - */ -async function listRoomThreadSummaries(tenantId: string, roomId: string) { - const params = new URLSearchParams({ - refs: JSON.stringify([roomRefFor(tenantId, roomId)]), - limit: String(ROOM_FEED_THREAD_LIMIT), - }); +async function listThreads(tenantId: string): Promise { const page = await mailboxRequest( - `${mailboxBasePathFor(tenantId)}/me/threads?${params.toString()}`, + `${mailboxBasePathFor(tenantId)}/me/inbox/threads?folder=INBOX`, MailboxThreadListResponse, ); - return page.threads; -} - -async function readRoomThread(tenantId: string, rootMessageId: string) { - return mailboxRequest( - `${mailboxBasePathFor(tenantId)}/me/threads/${encodeURIComponent(rootMessageId)}`, - MailboxThreadReadResponse, - ); + return page.threads.map((raw) => { + if (!isMailboxThreadNode(raw)) { + throw new MailboxThreadFetchError("Unexpected mailbox thread node shape"); + } + return raw; + }); } /** - * Every message across a room's visible mail threads, flattened into one + * Every message across the tenant's mailbox threads, flattened into one * timeline (CL-8174 slice 2b) — a root message's own `threadId` stays - * absent (see `threadMessagesToTimeline`), so it resolves to + * absent (see `threadTreeToTimeline`), so it resolves to * `ROOM_FEED_ROOT_THREAD_ID` in `./thread-feed.ts`'s root-feed filter; a - * reply's `threadId` is its thread's root message id, matching the + * reply's `threadId` is its parent's own uid, matching the * `WorkbenchThreadRow.id` `loadRoomThreadRows` hands out for the same * thread. */ export async function loadRoomMailboxMessages( tenantId: string, - roomId: string, + _roomId: string, ): Promise { - const summaries = await listRoomThreadSummaries(tenantId, roomId); - const perThread = await Promise.all( - summaries.map((summary) => readRoomThread(tenantId, summary.rootMessageId)), - ); - return perThread.flatMap((page) => threadMessagesToTimeline(page.messages)); + const threads = (await listThreads(tenantId)).slice(0, ROOM_FEED_THREAD_LIMIT); + return threads.flatMap((root) => threadTreeToTimeline(root)); } /** - * The reply-thread affordance rows for a room's visible mail threads — - * only threads with at least one reply get a row, matching the old + * The reply-thread affordance rows for the tenant's mailbox threads — only + * threads with at least one reply get a row, matching the old * `GET /threads` contract of listing only real reply activity. */ export async function loadRoomThreadRows( tenantId: string, - roomId: string, + _roomId: string, ): Promise<{ readonly rootThreadId: string; readonly items: readonly WorkbenchThreadRow[]; }> { - const summaries = await listRoomThreadSummaries(tenantId, roomId); - const items: WorkbenchThreadRow[] = summaries - .filter((summary) => summary.messageCount > 1) - .map((summary) => ({ - id: summary.rootId, + const threads = (await listThreads(tenantId)).slice(0, ROOM_FEED_THREAD_LIMIT); + const items: WorkbenchThreadRow[] = threads + .map((root) => ({ root, members: flattenThread(root) })) + .filter(({ members }) => members.length > 1) + .map(({ root, members }) => ({ + id: String(root.uid), kind: "reply", - parentMessageId: summary.rootId, + parentMessageId: String(root.uid), // Every mail thread this feed lists hangs directly off the root // feed — there is no depth-2 mailbox-native sub-thread concept // (see the fork-affordance note in `use-thread-navigation.ts`) — @@ -248,16 +231,16 @@ export async function loadRoomThreadRows( // (`thread.parentThreadId === rootThreadId`). parentThreadId: ROOM_FEED_ROOT_THREAD_ID, runRef: null, - title: summary.subject ?? null, - createdAt: summary.lastCreatedAt, - replyCount: summary.messageCount - 1, - lastActivityAt: summary.lastCreatedAt, + title: root.envelope.subject.length > 0 ? root.envelope.subject : null, + createdAt: members[members.length - 1]?.envelope.date ?? root.envelope.date, + replyCount: members.length - 1, + lastActivityAt: members[members.length - 1]?.envelope.date ?? root.envelope.date, })); return { rootThreadId: ROOM_FEED_ROOT_THREAD_ID, items }; } /** The mailbox SSE endpoint a room's live-update subscription connects - * to — the same stream `@corbits/inbox`'s Inbox surface reads. */ + * to — the same stream `@corbits/mailbox`'s own inbox surface reads. */ export function roomMailboxEventsUrl(tenantId: string): string { return `${mailboxBasePathFor(tenantId)}/me/inbox/events`; } diff --git a/packages/chat/package.json b/packages/chat/package.json index 73397c6b4e..e861ca72c8 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -45,7 +45,7 @@ "@corbits/connections": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/hub-api-client": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/turn-artifacts": "workspace:*", "@corbits/url-path": "workspace:*", diff --git a/packages/chat/src/mailbox-fanout.ts b/packages/chat/src/mailbox-fanout.ts index 7a76be6ae2..11cf39baec 100644 --- a/packages/chat/src/mailbox-fanout.ts +++ b/packages/chat/src/mailbox-fanout.ts @@ -13,40 +13,33 @@ // The write itself is behind the small `MailboxWriter` port below, not // called against `@corbits/mailbox` directly: `writeChatMailboxFanout`'s // own logic (who gets a row, which direction, what the shared Message-ID -// and refs are) is what this ticket is actually about, and it is -// unit-testable with an in-memory writer that never touches Postgres. -// `createDrizzleMailboxWriter` is the one production implementation, and -// is the only piece that needs a live `@corbits/mailbox` schema. +// is) is what this ticket is actually about, and it is unit-testable with +// an in-memory writer that never touches Postgres. `createDrizzleMailboxWriter` +// is the one production implementation, and is the only piece that needs +// a live `@corbits/mailbox` schema. // // Every recipient's row shares this row's own frame `messageId` — the -// caller-supplied `messageId` on every `writeMailboxMessages` item — so -// the stored frame's `Message-ID:` header, the cached -// `principal_mail.message_id` column, and the value a reply's -// `In-Reply-To` names all agree. Idempotency rides the package's own -// default transport key (`mailboxKey.transport`, direction-scoped) rather -// than a caller-minted one: nothing here has a reason to override it. +// caller-supplied `messageId` on every `writeMailboxMessage` call — so the +// stored frame's `Message-ID:` header, the cached `principal_mail.message_id` +// column, and the value a reply's `In-Reply-To` names all agree. Idempotency +// rides the native store's own messageId-scoped dedupe within a (tenant, +// principal, folder) mailbox. // -// The whole fan-out is ONE batch, written in ONE transaction -// (`@corbits/mailbox`'s `writeMailboxMessages`): a failure partway -// through never leaves some recipients delivered and others missing, and -// a retry after a genuine failure simply re-attempts every row (none of -// which committed). No fallback: a write that fails is reported through -// `reportError` and rethrown — never swallowed into a partially-delivered -// send that looks successful to its caller. A participant address this -// tenant has no principal for is a different, expected case (a stale or -// removed member) and is reported and skipped rather than failing the -// whole send; the sender's OWN principal missing from the row's tenant -// (a share member from another tenant, sending into the owning bench) is -// equally expected and is logged at debug rather than reported — see -// `writeChatMailboxFanout`'s doc comment. +// The native store has no multi-row batch, so each recipient is written +// with its own `writeMailboxMessage` call, in order; a failure partway +// through leaves earlier writes in place rather than rolling the whole +// send back, and a retry re-attempts every item (already-written ones are +// no-ops on the same messageId). No fallback: a write that fails is +// reported through `reportError` and rethrown — never swallowed into a +// partially-delivered send that looks successful to its caller. A +// participant address this tenant has no principal for is a different, +// expected case (a stale or removed member) and is reported and skipped +// rather than failing the whole send; the sender's OWN principal missing +// from the row's tenant (a share member from another tenant, sending into +// the owning bench) is equally expected and is logged at debug rather than +// reported — see `writeChatMailboxFanout`'s doc comment. import { getLogger } from "@intx/log"; -import { - writeMailboxMessages, - type MailboxDb, - type MailboxEventBus, - type MailboxRef, - type WriteMailboxMessagesItem, -} from "@corbits/mailbox"; +import { writeMailboxMessage, type MailboxDb, type MailboxEventBus } from "@corbits/mailbox"; import { reportError } from "@corbits/error-sink"; import { isAgentAddress } from "./mentions"; import type { ParticipantRecord } from "./participants"; @@ -88,7 +81,6 @@ export type MailboxBatchItem = { * stored frame's `References:` header carries. Absent on a root-feed * row, which answers nothing. */ readonly references?: readonly string[]; - readonly refs?: readonly MailboxRef[]; }; export type MailboxBatchResult = { @@ -97,47 +89,54 @@ export type MailboxBatchResult = { }; /** - * The write seam `writeChatMailboxFanout` calls through — an entire - * conversation turn's mailbox rows as ONE transaction. Split from + * The write seam `writeChatMailboxFanout` calls through. Split from * `@corbits/mailbox`'s own call surface so this package's fan-out LOGIC - * (who gets a row, which direction, the shared Message-ID and refs) is + * (who gets a row, which direction, the shared Message-ID) is * unit-testable against an in-memory fake, with only * `createDrizzleMailboxWriter` below needing a live database. */ export interface MailboxWriter { /** - * Writes every item in one transaction — `@corbits/mailbox`'s - * `writeMailboxMessages` semantics: a throw from any item rolls back - * the whole batch, and a per-item `messageKey` collision (retry - * idempotency) is a no-op for that item, not a rollback trigger. - * Returns one result per item, in item order. + * Writes every item, one `@corbits/mailbox` `writeMailboxMessage` append + * at a time (the native store has no multi-row batch) — a throw from any + * item stops the remaining items from being written but does not undo + * ones already appended; the messageId-scoped dedupe still makes a + * retried item a no-op. Returns one result per item written before any + * failure, in item order. */ writeBatch(items: readonly MailboxBatchItem[]): Promise; } /** The production `MailboxWriter`: a thin pass-through onto - * `@corbits/mailbox`'s own `writeMailboxMessages` batch call — no + * `@corbits/mailbox`'s own `writeMailboxMessage`, one append per item — no * hand-rolled inserts against the package's schema. */ export function createDrizzleMailboxWriter(db: MailboxDb, bus?: MailboxEventBus): MailboxWriter { return { async writeBatch(items) { - const batch: WriteMailboxMessagesItem[] = items.map((item) => ({ - scope: { tenantId: item.tenantId, principalId: item.principalId }, - args: { - address: item.address, - fromAddress: item.fromAddress, - subject: item.subject, - body: item.body, - messageId: item.messageId, - direction: item.direction, - ...(item.inReplyTo !== undefined ? { inReplyTo: item.inReplyTo } : {}), - ...(item.references !== undefined && item.references.length > 0 - ? { references: [...item.references] } - : {}), - ...(item.refs !== undefined ? { refs: [...item.refs] } : {}), - }, - })); - return writeMailboxMessages(db, batch, bus !== undefined ? { bus } : {}); + const results: MailboxBatchResult[] = []; + for (const item of items) { + const messageKey = `${item.tenantId}:${item.principalId}:${item.messageId}:${item.direction}`; + const written = await writeMailboxMessage( + db, + { + tenantId: item.tenantId, + principalId: item.principalId, + address: item.address, + fromAddress: item.fromAddress, + subject: item.subject, + body: item.body, + messageId: item.messageId, + folder: item.direction === "outbound" ? "Sent" : "INBOX", + ...(item.inReplyTo !== undefined ? { inReplyTo: item.inReplyTo } : {}), + ...(item.references !== undefined && item.references.length > 0 + ? { references: [...item.references] } + : {}), + }, + bus, + ); + results.push({ messageKey, id: written?.id ?? null }); + } + return results; }, }; } @@ -221,7 +220,6 @@ export async function writeChatMailboxFanout( const known = await deps.resolveKnownPrincipalIds(input.tenantId, candidateList); - const refs: MailboxRef[] = [{ kind: "workbench", id: input.workbenchId }]; const batch: MailboxBatchItem[] = []; for (const principalId of candidateList) { @@ -254,7 +252,6 @@ export async function writeChatMailboxFanout( body: input.body, messageId: input.messageId, direction: principalId === input.senderPrincipalId ? "outbound" : "inbound", - refs, ...(input.inReplyTo !== undefined ? { inReplyTo: input.inReplyTo } : {}), ...(input.references !== undefined && input.references.length > 0 ? { references: input.references } diff --git a/packages/chat/test/mailbox-fanout.drizzle.test.ts b/packages/chat/test/mailbox-fanout.drizzle.test.ts deleted file mode 100644 index 72879105f8..0000000000 --- a/packages/chat/test/mailbox-fanout.drizzle.test.ts +++ /dev/null @@ -1,293 +0,0 @@ -// DB-gated: skipped when no DATABASE_URL is reachable, mirroring -// `threads.drizzle.test.ts`. Runs against its own scratch database with -// a minimal control plane (`tenant`/`principal`) the mailbox FKs need. -// -// Proves `createDrizzleMailboxWriter`'s one write path — a batch through -// `@corbits/mailbox`'s own `writeMailboxMessages` — against a real -// `@corbits/mailbox` schema: an outbound row and an inbound row committed -// together in one transaction, both dedupe on a retried batch, and — the -// critique's frame-Message-ID probe — the stored frame's `Message-ID:` -// header, the row's own cached `message_id` column, and the batch's -// caller-supplied `messageKey` all agree, with `In-Reply-To:` likewise -// matching what the caller passed in. -import { afterAll, beforeAll, expect, test } from "bun:test"; -import postgres from "postgres"; -import { sql } from "drizzle-orm"; -import { createMailboxDb, runMailboxMigrations } from "@corbits/mailbox"; - -import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; -import { dbGate } from "../../../scripts/e2e/db-gate"; -import { createDrizzleMailboxWriter, type MailboxBatchItem } from "../src/mailbox-fanout"; - -function scratchUrlFor(e2eUrl: string): string { - const url = new URL(e2eUrl); - const database = url.pathname.replace(/^\//, ""); - url.pathname = `/${database}_chat_mailbox_fanout_drizzle_test`; - return url.toString(); -} - -const databaseUrl = e2eDatabaseUrl(); -const describeIfDb = dbGate(databaseUrl, import.meta.path); - -const TENANT_ID = "tnt_1"; -const PRINCIPAL_ID = "prn_alice"; -const OTHER_PRINCIPAL_ID = "prn_bob"; -const DOMAIN = "acme.example"; - -describeIfDb("createDrizzleMailboxWriter", () => { - const scratchUrl = scratchUrlFor(databaseUrl ?? "postgres://localhost:5432/unused"); - const scratchTarget = new URL(scratchUrl); - const scratchDatabase = scratchTarget.pathname.replace(/^\//, ""); - - async function freshDb() { - const maintenanceUrl = new URL(scratchUrl); - maintenanceUrl.pathname = "/postgres"; - const maintenance = postgres(maintenanceUrl.toString(), { - max: 1, - onnotice: () => undefined, - }); - try { - await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); - await maintenance.unsafe(`CREATE DATABASE "${scratchDatabase}"`); - } finally { - await maintenance.end(); - } - const { db, close } = createMailboxDb(scratchUrl); - await db.execute(sql` - CREATE TABLE IF NOT EXISTS "tenant" ("id" text PRIMARY KEY) - `); - await db.execute(sql` - CREATE TABLE IF NOT EXISTS "principal" ( - "id" text PRIMARY KEY, - "tenant_id" text NOT NULL REFERENCES "tenant" ("id") ON DELETE CASCADE - ) - `); - await db.execute(sql`INSERT INTO "tenant" ("id") VALUES (${TENANT_ID})`); - await db.execute( - sql`INSERT INTO "principal" ("id", "tenant_id") VALUES (${PRINCIPAL_ID}, ${TENANT_ID}), (${OTHER_PRINCIPAL_ID}, ${TENANT_ID})`, - ); - await runMailboxMigrations(db); - return { db, close }; - } - - beforeAll(async () => { - const maintenanceUrl = new URL(scratchUrl); - maintenanceUrl.pathname = "/postgres"; - const maintenance = postgres(maintenanceUrl.toString(), { - max: 1, - onnotice: () => undefined, - }); - try { - await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); - await maintenance.unsafe(`CREATE DATABASE "${scratchDatabase}"`); - } finally { - await maintenance.end(); - } - }, 20000); - - afterAll(async () => { - const maintenanceUrl = new URL(scratchUrl); - maintenanceUrl.pathname = "/postgres"; - const maintenance = postgres(maintenanceUrl.toString(), { - max: 1, - onnotice: () => undefined, - }); - try { - await maintenance.unsafe(`DROP DATABASE IF EXISTS "${scratchDatabase}"`); - } finally { - await maintenance.end(); - } - }, 20000); - - test("a batch writes an outbound and an inbound row together and dedupes a retried batch", async () => { - const { db, close } = await freshDb(); - try { - const writer = createDrizzleMailboxWriter(db); - const messageId = ""; - - const items: MailboxBatchItem[] = [ - { - tenantId: TENANT_ID, - principalId: PRINCIPAL_ID, - address: `${PRINCIPAL_ID}@${DOMAIN}`, - fromAddress: `${PRINCIPAL_ID}@${DOMAIN}`, - subject: "hello", - body: "hello", - messageId, - direction: "outbound", - }, - { - tenantId: TENANT_ID, - principalId: OTHER_PRINCIPAL_ID, - address: `${OTHER_PRINCIPAL_ID}@${DOMAIN}`, - fromAddress: `${PRINCIPAL_ID}@${DOMAIN}`, - subject: "hello", - body: "hello", - messageId, - direction: "inbound", - }, - ]; - - const results = await writer.writeBatch(items); - expect(results.map((r) => r.id !== null)).toEqual([true, true]); - - const rows = await db.execute<{ - principal_id: string; - direction: string; - message_id: string; - }>( - sql`SELECT principal_id, direction, message_id FROM "mailbox"."principal_mail" WHERE "tenant_id" = ${TENANT_ID} ORDER BY principal_id`, - ); - expect([...rows]).toEqual([ - { - principal_id: PRINCIPAL_ID, - direction: "outbound", - message_id: messageId, - }, - { - principal_id: OTHER_PRINCIPAL_ID, - direction: "inbound", - message_id: messageId, - }, - ]); - - // A retried batch is idempotent — the default transport key dedupes - // both rows without writing a duplicate of either. - const retried = await writer.writeBatch(items); - expect(retried.every((r) => r.id === null)).toBe(true); - - const countRows = await db.execute<{ count: string }>( - sql`SELECT count(*)::text FROM "mailbox"."principal_mail" WHERE "tenant_id" = ${TENANT_ID}`, - ); - expect(countRows[0]?.count).toBe("2"); - } finally { - await close(); - } - }, 20000); - - test("a batch that fails partway writes nothing, and a retry after a real success writes nothing twice", async () => { - const { db, close } = await freshDb(); - try { - const writer = createDrizzleMailboxWriter(db); - const messageId = ""; - - const badItems: MailboxBatchItem[] = [ - { - tenantId: TENANT_ID, - principalId: PRINCIPAL_ID, - address: `${PRINCIPAL_ID}@${DOMAIN}`, - fromAddress: `${PRINCIPAL_ID}@${DOMAIN}`, - subject: "hello", - body: "hello", - messageId, - direction: "outbound", - }, - { - // An unknown principal: the FK refuses this row, and the whole - // batch's transaction must roll back — including the first, - // otherwise-valid item above. - tenantId: TENANT_ID, - principalId: "prn_ghost", - address: `prn_ghost@${DOMAIN}`, - fromAddress: `${PRINCIPAL_ID}@${DOMAIN}`, - subject: "hello", - body: "hello", - messageId, - direction: "inbound", - }, - ]; - - await expect(writer.writeBatch(badItems)).rejects.toThrow(); - - const afterFailure = await db.execute<{ count: string }>( - sql`SELECT count(*)::text FROM "mailbox"."principal_mail" WHERE "tenant_id" = ${TENANT_ID}`, - ); - expect(afterFailure[0]?.count).toBe("0"); - - // Retry with a corrected batch: succeeds and writes exactly the - // rows a first-time success would have. - const goodItems: MailboxBatchItem[] = [ - badItems[0] as MailboxBatchItem, - { - tenantId: TENANT_ID, - principalId: OTHER_PRINCIPAL_ID, - address: `${OTHER_PRINCIPAL_ID}@${DOMAIN}`, - fromAddress: `${PRINCIPAL_ID}@${DOMAIN}`, - subject: "hello", - body: "hello", - messageId, - direction: "inbound", - }, - ]; - const succeeded = await writer.writeBatch(goodItems); - expect(succeeded.every((r) => r.id !== null)).toBe(true); - - // Retrying the now-successful batch again writes nothing more. - const retried = await writer.writeBatch(goodItems); - expect(retried.every((r) => r.id === null)).toBe(true); - - const countRows = await db.execute<{ count: string }>( - sql`SELECT count(*)::text FROM "mailbox"."principal_mail" WHERE "tenant_id" = ${TENANT_ID}`, - ); - expect(countRows[0]?.count).toBe("2"); - } finally { - await close(); - } - }, 20000); - - // Critique probe folded in: the stored frame's `Message-ID:` header, - // the row's cached `message_id` column, and `In-Reply-To:` all agree - // with what the caller passed in — never a separately-minted id. - test("the stored frame's Message-ID and In-Reply-To match the caller's own", async () => { - const { db, close } = await freshDb(); - try { - const writer = createDrizzleMailboxWriter(db); - const messageId = ""; - const inReplyTo = ""; - - await writer.writeBatch([ - { - tenantId: TENANT_ID, - principalId: PRINCIPAL_ID, - address: `${PRINCIPAL_ID}@${DOMAIN}`, - fromAddress: `${PRINCIPAL_ID}@${DOMAIN}`, - subject: "s", - body: "b", - messageId, - direction: "outbound", - inReplyTo, - }, - { - tenantId: TENANT_ID, - principalId: OTHER_PRINCIPAL_ID, - address: `${OTHER_PRINCIPAL_ID}@${DOMAIN}`, - fromAddress: `${PRINCIPAL_ID}@${DOMAIN}`, - subject: "s", - body: "b", - messageId, - direction: "inbound", - inReplyTo, - }, - ]); - - const rows = await db.execute<{ principal_id: string; raw: Buffer }>( - sql`SELECT principal_id, raw FROM "mailbox"."principal_mail" ORDER BY principal_id`, - ); - const headers = [...rows].map((r) => { - const text = Buffer.from(r.raw).toString("utf8"); - const h = (n: string) => text.match(new RegExp(`^${n}:\\s*(.*)$`, "im"))?.[1] ?? null; - return { - principal: r.principal_id, - messageId: h("Message-ID"), - inReplyTo: h("In-Reply-To"), - }; - }); - for (const header of headers) { - expect(header.messageId).toBe(messageId); - expect(header.inReplyTo).toBe(inReplyTo); - } - } finally { - await close(); - } - }, 20000); -}); diff --git a/packages/chat/test/mailbox-fanout.test.ts b/packages/chat/test/mailbox-fanout.test.ts index 3b7f94db46..1c4f727d05 100644 --- a/packages/chat/test/mailbox-fanout.test.ts +++ b/packages/chat/test/mailbox-fanout.test.ts @@ -1,11 +1,11 @@ // CL-7450: a sent human message lands in every human participant's -// mailbox — an "outbound" copy in the sender's own, "inbound" in every -// other human's — all sharing the row's own RFC 5322 Message-ID, all in -// ONE batch (one `@corbits/mailbox` transaction), and the same default -// transport idempotency key makes a retried send a no-op rather than a -// duplicate. Exercised against an in-memory `MailboxWriter`, never a -// live `@corbits/mailbox` schema — see `../src/mailbox-fanout.ts`'s own -// doc comment for why the write is behind that seam. +// mailbox — an "outbound" copy in the sender's own Sent folder, "inbound" +// in every other human's INBOX — all sharing the row's own RFC 5322 +// Message-ID, and the native store's own messageId-scoped dedupe makes a +// retried send a no-op rather than a duplicate. Exercised against an +// in-memory `MailboxWriter`, never a live `@corbits/mailbox` schema — see +// `../src/mailbox-fanout.ts`'s own doc comment for why the write is behind +// that seam. import { describe, expect, test } from "bun:test"; import { writeChatMailboxFanout, @@ -125,7 +125,6 @@ describe("writeChatMailboxFanout (CL-7450)", () => { expect(byPrincipal.has(AGENT_ADDRESS)).toBe(false); for (const row of rows) { - expect(row.refs).toEqual([{ kind: "workbench", id: WORKBENCH_ID }]); expect(row.address).toBe(`${row.principalId}@${DOMAIN}`); } }); diff --git a/packages/inbox/package.json b/packages/inbox/package.json index b5a2e55136..78e7678d02 100644 --- a/packages/inbox/package.json +++ b/packages/inbox/package.json @@ -2,12 +2,11 @@ "name": "@corbits/inbox", "version": "0.0.1", "private": true, - "description": "Workbench product inbox over @corbits/mailbox: three groups, mark-all-read, clear-done", + "description": "The @corbits/notify -> @corbits/mailbox delivery adapter, plus this package's own migration ledger", "license": "LGPL-2.1-or-later", "type": "module", "exports": { ".": "./src/index.ts", - "./client": "./src/client.ts", "./migrations": "./src/migrations.ts" }, "scripts": { @@ -15,14 +14,9 @@ "test": "bun test" }, "dependencies": { - "@corbits/error-sink": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/notify": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/log": "0.3.0", - "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "catalog:", "postgres": "catalog:" }, "devDependencies": { diff --git a/packages/inbox/src/bulk-run.ts b/packages/inbox/src/bulk-run.ts deleted file mode 100644 index 83781106bb..0000000000 --- a/packages/inbox/src/bulk-run.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Server-only per-item bulk runner. Lives apart from `./bulk` so the -// eligibility helpers stay browser-safe (`./client` re-exports them) -// while this catch path can call `reportError` without pulling -// `@intx/log` into a browser bundle. - -import { reportError } from "@corbits/error-sink"; - -export interface BulkOperationResult { - readonly succeeded: number; - readonly failed: number; -} - -/** Optional reportError context and per-item hook for a bulk failure. */ -export type BulkOperationOptions = { - readonly onError?: (item: T, error: unknown) => void; - readonly operation?: string; - readonly tenantId?: string; - readonly extra?: Record; - readonly extraFor?: (item: T) => Record; -}; - -/** - * Apply `apply` to every item, one at a time, never letting one item's - * failure abort the rest (CL-7207). Previously `mark-all-read` and - * `clear-done` ran their per-item writes in a plain loop with no - * per-item try/catch: a throw on item N left the handler throwing out of - * the whole request — a 500 with items before N already mutated, item N - * left in a new inconsistent state, and everything after N untouched, with - * no way for the caller to tell how far it got. Catching per item instead - * means a transient failure on one row costs that one row, not the rest of - * the inbox, and the caller gets back exactly how many succeeded and how - * many didn't rather than an opaque 500. - */ -export async function runBulkOperation( - items: readonly T[], - apply: (item: T) => Promise, - options?: BulkOperationOptions, -): Promise { - let succeeded = 0; - let failed = 0; - for (const item of items) { - try { - await apply(item); - succeeded += 1; - } catch (error) { - failed += 1; - const extra = options?.extraFor?.(item) ?? options?.extra; - reportError(error, { - operation: options?.operation ?? "inbox.bulk", - ...(options?.tenantId !== undefined ? { tenantId: options.tenantId } : {}), - ...(extra !== undefined ? { extra } : {}), - }); - options?.onError?.(item, error); - } - } - return { succeeded, failed }; -} diff --git a/packages/inbox/src/bulk.ts b/packages/inbox/src/bulk.ts deleted file mode 100644 index e78a452da4..0000000000 --- a/packages/inbox/src/bulk.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Pure product rules for bulk inbox ops. Kept free of the DB so the -// mark-all-read / clear-done contracts are unit-testable without the -// platform tenant/principal tables the mailbox FKs require. Also kept -// free of `@corbits/error-sink` so `./client` can re-export the -// eligibility helpers without pulling `@intx/log` into a browser bundle. - -import type { InboxItem } from "./project"; - -/** - * Mentions and deliveries that are still open — never action items. - * Action rows require an explicit decision and must not be bulk-cleared. - */ -export function itemsEligibleForMarkAllRead(items: readonly InboxItem[]): InboxItem[] { - return items.filter((item) => item.group !== "action" && item.status === "open"); -} - -/** Rows whose status is `done` — open rows stay. */ -export function itemsEligibleForClearDone(items: readonly InboxItem[]): InboxItem[] { - return items.filter((item) => item.status === "done"); -} diff --git a/packages/inbox/src/client.test.ts b/packages/inbox/src/client.test.ts deleted file mode 100644 index cc8ac770de..0000000000 --- a/packages/inbox/src/client.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { type } from "arktype"; - -import { - INBOX_GROUPS, - InboxCountsSchema, - InboxItemDetailSchema, - InboxItemSchema, - classificationFromRefs, - inboxGroupOf, - isInboxGroup, - itemsEligibleForClearDone, - itemsEligibleForMarkAllRead, - projectInboxItem, - type InboxItem, -} from "./client"; - -describe("wire schemas", () => { - test("InboxItemSchema parses a minimal open item", () => { - const out = InboxItemSchema({ - id: "msg_1", - group: "delivery", - from: "routine:morning-brief", - date: "2026-01-01T00:00:00.000Z", - read: false, - status: "open", - }); - expect(out instanceof type.errors).toBe(false); - }); - - test("InboxItemDetailSchema requires a body", () => { - const out = InboxItemDetailSchema({ - id: "msg_1", - group: "mention", - from: "chat:acme", - date: "2026-01-01T00:00:00.000Z", - read: true, - status: "done", - body: "You were mentioned in #general.", - }); - expect(out instanceof type.errors).toBe(false); - }); - - test("InboxCountsSchema parses the three group counts plus open", () => { - const out = InboxCountsSchema({ - action: 1, - mention: 2, - delivery: 3, - open: 6, - }); - expect(out instanceof type.errors).toBe(false); - }); -}); - -describe("group classification", () => { - test("INBOX_GROUPS is the three product groups", () => { - expect(INBOX_GROUPS).toEqual(["action", "mention", "delivery"]); - }); - - test("isInboxGroup rejects an unknown string", () => { - expect(isInboxGroup("action")).toBe(true); - expect(isInboxGroup("archived")).toBe(false); - }); - - test("inboxGroupOf derives action from an approval ref", () => { - expect(inboxGroupOf({ refs: [{ kind: "approval" }] })).toBe("action"); - }); - - test("classificationFromRefs mirrors inboxGroupOf", () => { - expect(classificationFromRefs([{ kind: "thread" }])).toBe("mention"); - }); -}); - -describe("bulk eligibility", () => { - const items: InboxItem[] = [ - { - id: "1", - group: "action", - from: "a", - date: "d", - read: false, - status: "open", - }, - { - id: "2", - group: "mention", - from: "b", - date: "d", - read: false, - status: "open", - }, - { - id: "3", - group: "delivery", - from: "c", - date: "d", - read: true, - status: "done", - }, - ]; - - test("mark-all-read skips action items", () => { - expect(itemsEligibleForMarkAllRead(items).map((i) => i.id)).toEqual(["2"]); - }); - - test("clear-done only takes done items", () => { - expect(itemsEligibleForClearDone(items).map((i) => i.id)).toEqual(["3"]); - }); -}); - -describe("projectInboxItem", () => { - test("projects a mailbox message into the product InboxItem shape", () => { - const item = projectInboxItem({ - id: "msg_1", - from: "routine:morning-brief", - to: ["tenant"], - messageId: "msg_1@mailbox", - date: "2026-01-01T00:00:00.000Z", - read: false, - status: "open", - }); - expect(item.group).toBe("delivery"); - }); -}); diff --git a/packages/inbox/src/client.ts b/packages/inbox/src/client.ts deleted file mode 100644 index 9ab565d879..0000000000 --- a/packages/inbox/src/client.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Browser-safe Inbox domain surface: the wire schemas/types a UI parses -// responses with, group classification, the pure bulk-op eligibility rules, -// and the mailbox thread-read fetch client. Re-exported from their owning -// modules rather than redeclared — `./project`, `./group`, and `./bulk` -// import nothing but arktype and (type-only) `@corbits/mailbox`; `./thread- -// client` imports arktype and `@corbits/mailbox`'s own thread schemas -// (a value import, but `@corbits/mailbox` is an external git dependency the -// browser-safe-subpaths check treats as an opaque leaf, not something it -// walks into) — so this subpath adds only `fetch` composition, no server- -// only dependency. The root export (".") also carries `./routes` (hono, -// `@intx/hub-api`), `./delivery` (`@corbits/notify/mailbox`), and -// `./migrations` (`@corbits/mailbox`'s Postgres migration runner) — none of -// which belong in a browser bundle — which is why a UI imports this subpath -// instead of the root. Enforced by `bun run check:browser-safe-subpaths` -// (scripts/checks/browser-safe-subpaths.ts), not just by convention. - -export { itemsEligibleForClearDone, itemsEligibleForMarkAllRead } from "./bulk"; -export { - INBOX_GROUPS, - classificationFromRefs, - inboxGroupOf, - isInboxGroup, - type InboxGroup, -} from "./group"; -export { - InboxCountsSchema, - InboxItemDetailSchema, - InboxItemSchema, - projectInboxItem, - projectInboxItemDetail, - type InboxCounts, - type InboxItem, - type InboxItemDetail, -} from "./project"; -export { - MailboxThreadFetchError, - listMailboxThreadsClient, - readMailboxThreadClient, - type MailboxThreadListResult, - type MailboxThreadResult, -} from "./thread-client"; diff --git a/packages/inbox/src/cursor.ts b/packages/inbox/src/cursor.ts deleted file mode 100644 index a8903158d6..0000000000 --- a/packages/inbox/src/cursor.ts +++ /dev/null @@ -1,41 +0,0 @@ -// A decoded pagination cursor is only meaningful against the exact -// view/sort/filter it was minted under — `@corbits/mailbox` embeds all -// three in every cursor it mints precisely so a caller can reject a -// cross-set replay (paging `?group=action`, then reusing that cursor -// under `?group=mention`) instead of silently seeking into the wrong -// result set. `decodeMailboxListCursor` only checks the cursor is -// well-formed; this is the cross-check `@corbits/mailbox`'s own -// `mount.ts` route performs and every other caller must perform itself. -import { - canonicalMailboxFilter, - type MailboxFilter, - type MailboxInboxView, - type MailboxListCursor, - type MailboxSort, -} from "@corbits/mailbox"; - -export type CursorScope = { - view: MailboxInboxView; - sort: MailboxSort; - filter: MailboxFilter; -}; - -export type CursorMismatch = "view" | "sort" | "filter" | null; - -/** - * Which field of `cursor` disagrees with `expected`, or `null` when the - * cursor was minted for exactly this scope. Checked in the same order - * `@corbits/mailbox`'s `mount.ts` checks it, so a caller can reuse its - * exact error strings (`cursor does not match inbox `). - */ -export function cursorScopeMismatch( - cursor: MailboxListCursor, - expected: CursorScope, -): CursorMismatch { - if (cursor.view !== expected.view) return "view"; - if (cursor.sort !== expected.sort) return "sort"; - if (cursor.filter !== canonicalMailboxFilter(expected.filter)) { - return "filter"; - } - return null; -} diff --git a/packages/inbox/src/delivery.ts b/packages/inbox/src/delivery.ts index 4c79c5d814..29329d8327 100644 --- a/packages/inbox/src/delivery.ts +++ b/packages/inbox/src/delivery.ts @@ -1,14 +1,15 @@ // Workbench adapter that turns `@corbits/notify`'s MailboxDelivery seam into -// a real write against `@corbits/mailbox`. Stamps classification (product -// group) and status so list filters and the three-column UI work without -// re-deriving on every read. +// a real write against `@corbits/mailbox`'s native mailbox store. Refs and +// classification are gone with the retired triage layer (CL-8174, native +// `@corbits/mailbox` 1.0) — a notify item lands as a plain mailbox message, +// and the inbox reads it back through the library's own `/me/inbox` routes. -import { and, eq, or } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { deliverInboxItems, - mailboxKey, principalMail, type DeliverInboxItemsOpts, + type InboxItem, type MailboxDb, type MailboxEventBus, } from "@corbits/mailbox"; @@ -18,47 +19,39 @@ import type { ResolveExistingMailIds, } from "@corbits/notify/mailbox"; -import { classificationFromRefs } from "./group"; - export interface CreateWorkbenchMailboxDeliveryOpts { db: MailboxDb; /** When set, each newly written row publishes a mailbox event for SSE. */ bus?: MailboxEventBus; } +/** The same deterministic id `@corbits/mailbox`'s `deliverInboxItems` mints + * for an item that carries no `messageId` of its own — kept here only for + * `createResolveExistingMailIds`'s read-back, which needs to name the exact + * row a redelivery reports as pre-existing. */ +function inboxMessageIdFor(item: { source: string; externalId: string }): string { + return ``; +} + /** - * Build the `mail` callback `@corbits/notify` needs. Every item is written - * with `status: "open"` and a classification derived from its refs so the - * product groups (action / mention / delivery) are filterable at the store. + * Build the `mail` callback `@corbits/notify` needs: a thin pass-through + * onto `deliverInboxItems`. */ export function createWorkbenchMailboxDelivery( opts: CreateWorkbenchMailboxDeliveryOpts, ): MailboxDelivery { const { db, bus } = opts; return async (items, deliverOpts) => { - const stamped = items.map((item: NotifyInboxItem) => { - const refs = item.refs ?? []; - return { - tenantId: item.tenantId, - principalId: item.principalId, - address: item.address, - fromAddress: item.fromAddress, - subject: item.subject, - body: item.body, - source: item.source, - externalId: item.externalId, - refs: refs.map((ref) => ({ - kind: ref.kind, - id: ref.id, - // A ref's display label (an artifact's title, say) rides - // along so the inbox detail can render a chip without a - // second lookup — dropped only when the writer had none. - label: ref.label, - })), - classification: classificationFromRefs(refs), - status: "open", - }; - }); + const stamped: InboxItem[] = items.map((item: NotifyInboxItem) => ({ + tenantId: item.tenantId, + principalId: item.principalId, + address: item.address, + fromAddress: item.fromAddress, + subject: item.subject, + body: item.body, + source: item.source, + externalId: item.externalId, + })); const hostEnqueue = deliverOpts?.enqueue; const writeOpts: DeliverInboxItemsOpts = {}; @@ -78,7 +71,11 @@ export function createWorkbenchMailboxDelivery( }; } - return deliverInboxItems(db, stamped, writeOpts); + const delivered = await deliverInboxItems(db, stamped, writeOpts); + return delivered.map((result, index) => ({ + messageKey: inboxMessageIdFor(items[index] as NotifyInboxItem), + id: result.id, + })); }; } @@ -86,45 +83,32 @@ export function createWorkbenchMailboxDelivery( * CL-7238 read-back for `@corbits/notify`'s crash window: a redelivery * reports an already-committed row as pre-existing (`id: null`), so the * host resolves it to its row id here and the missing dispatch rows get - * repaired instead of lost. Served from the mailbox unique mail key — - * the same `mailboxKey.inbox(source, externalId)` the write dedupes on — - * so the lookup can never disagree with the delivery about which row an - * item belongs to. Positional in, positional out, `null` where no row - * exists; the dispatch store dedupes by (mail row, sink), so a benign - * redelivery's repair never double-queues. + * repaired instead of lost. Served from the mailbox's `message_id` column + * — the same deterministic id `deliverInboxItems` mints from + * `(source, externalId)` when the item carries none of its own — so the + * lookup can never disagree with the delivery about which row an item + * belongs to. Positional in, positional out, `null` where no row exists. */ export function createResolveExistingMailIds(db: MailboxDb): ResolveExistingMailIds { return async (items) => { if (items.length === 0) return []; - const keyed = items.map((item) => ({ - tenantId: item.tenantId, - principalId: item.principalId, - messageKey: mailboxKey.inbox(item.source, item.externalId), - })); - const rows = await db - .select({ - id: principalMail.id, - tenantId: principalMail.tenantId, - principalId: principalMail.principalId, - messageKey: principalMail.messageKey, - }) - .from(principalMail) - .where( - or( - ...keyed.map((key) => + const rows = await Promise.all( + items.map(async (item) => { + const messageId = inboxMessageIdFor(item); + const [row] = await db + .select({ id: principalMail.id }) + .from(principalMail) + .where( and( - eq(principalMail.tenantId, key.tenantId), - eq(principalMail.principalId, key.principalId), - eq(principalMail.messageKey, key.messageKey), + eq(principalMail.tenantId, item.tenantId), + eq(principalMail.principalId, item.principalId), + eq(principalMail.messageId, messageId), ), - ), - ), - ); - const idsByKey = new Map( - rows.map((row) => [`${row.tenantId}\n${row.principalId}\n${row.messageKey}`, row.id]), - ); - return keyed.map( - (key) => idsByKey.get(`${key.tenantId}\n${key.principalId}\n${key.messageKey}`) ?? null, + ) + .limit(1); + return row?.id ?? null; + }), ); + return rows; }; } diff --git a/packages/inbox/src/group.ts b/packages/inbox/src/group.ts deleted file mode 100644 index d972565d68..0000000000 --- a/packages/inbox/src/group.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Three product groups over mailbox messages. Classification is stamped at -// write time when known; otherwise the group is derived from refs so rows -// delivered before this package still land in the right column. - -export const INBOX_GROUPS = ["action", "mention", "delivery"] as const; -export type InboxGroup = (typeof INBOX_GROUPS)[number]; - -export function isInboxGroup(value: string): value is InboxGroup { - return (INBOX_GROUPS as readonly string[]).includes(value); -} - -/** - * Map a message's classification + refs onto one of the three product groups. - * Order is deliberate: an explicit classification wins; otherwise an approval - * or credential ref means "needs a decision" (approve a tool call, reconnect - * an expired credential), a thread ref means "someone mentioned you", and - * everything else is a delivery (routine output, run failure, …). - */ -export function inboxGroupOf(message: { - classification?: string | undefined; - refs?: readonly { kind: string }[] | undefined; -}): InboxGroup { - if ( - message.classification === "action" || - message.classification === "mention" || - message.classification === "delivery" - ) { - return message.classification; - } - const kinds = new Set((message.refs ?? []).map((ref) => ref.kind)); - if (kinds.has("approval") || kinds.has("credential")) return "action"; - if (kinds.has("thread")) return "mention"; - return "delivery"; -} - -/** - * Classification to stamp on a new write from the item's refs, so later - * list filters (`?classification=`) hit the product group without re-deriving. - */ -export function classificationFromRefs(refs: readonly { kind: string }[] | undefined): InboxGroup { - return inboxGroupOf({ refs }); -} diff --git a/packages/inbox/src/index.ts b/packages/inbox/src/index.ts index a9ac883ed5..f1ae7d2f97 100644 --- a/packages/inbox/src/index.ts +++ b/packages/inbox/src/index.ts @@ -1,28 +1,3 @@ -export { itemsEligibleForClearDone, itemsEligibleForMarkAllRead } from "./bulk"; -export { - INBOX_GROUPS, - classificationFromRefs, - inboxGroupOf, - isInboxGroup, - type InboxGroup, -} from "./group"; - -export { - InboxCountsSchema, - InboxItemDetailSchema, - InboxItemSchema, - projectInboxItem, - projectInboxItemDetail, - type InboxCounts, - type InboxItem, - type InboxItemDetail, -} from "./project"; -export { - WORKBENCH_INBOX_PRIORITIES, - WORKBENCH_INBOX_STATUSES, - WORKBENCH_MAILBOX_VOCABULARY, - type WorkbenchInboxStatus, -} from "./vocabulary"; export { createResolveExistingMailIds, createWorkbenchMailboxDelivery, @@ -34,4 +9,3 @@ export { type ApplyInboxMigrationsReport, type ApplyMailboxMigrationsReport, } from "./migrations"; -export { createInboxRoutes, type CreateInboxRoutesDeps } from "./routes"; diff --git a/packages/inbox/src/project.ts b/packages/inbox/src/project.ts deleted file mode 100644 index 37d2c35c6b..0000000000 --- a/packages/inbox/src/project.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Project a mailbox message into the product InboxItem shape the shell and -// mock consume. Keeps the mailbox package's wire shape internal. - -import type { MailboxMessage, MailboxMessageDetail } from "@corbits/mailbox"; -import { type } from "arktype"; - -import { inboxGroupOf, type InboxGroup } from "./group"; - -export const InboxItemSchema = type({ - id: "string", - group: "'action' | 'mention' | 'delivery'", - from: "string", - "fromDisplay?": "string", - "subject?": "string", - date: "string", - read: "boolean", - status: "'open' | 'done'", - "snippet?": "string", - "refs?": type({ - kind: "string", - id: "string", - "label?": "string", - }).array(), - "priority?": "string", - "assignee?": "string", -}); -export type InboxItem = typeof InboxItemSchema.infer; - -export const InboxItemDetailSchema = InboxItemSchema.and({ - body: "string", -}); -export type InboxItemDetail = typeof InboxItemDetailSchema.infer; - -export const InboxCountsSchema = type({ - action: "number", - mention: "number", - delivery: "number", - open: "number", -}); -export type InboxCounts = typeof InboxCountsSchema.infer; - -function projectStatus(status: string | undefined): "open" | "done" { - if (status === "done") return status; - return "open"; -} - -export function projectInboxItem(message: MailboxMessage): InboxItem { - const group: InboxGroup = inboxGroupOf(message); - const item: InboxItem = { - id: message.id, - group, - from: message.from, - date: message.date, - read: message.read, - status: projectStatus(message.status), - }; - if (message.fromDisplay !== undefined) item.fromDisplay = message.fromDisplay; - if (message.subject !== undefined) item.subject = message.subject; - if (message.snippet !== undefined) item.snippet = message.snippet; - if (message.refs !== undefined) { - item.refs = message.refs.map((ref) => { - const projected: { kind: string; id: string; label?: string } = { - kind: ref.kind, - id: ref.id, - }; - if (ref.label !== undefined) projected.label = ref.label; - return projected; - }); - } - if (message.priority !== undefined) item.priority = message.priority; - if (message.assignee !== undefined) item.assignee = message.assignee; - return item; -} - -export function projectInboxItemDetail(message: MailboxMessageDetail): InboxItemDetail { - return { - ...projectInboxItem(message), - body: message.body, - }; -} diff --git a/packages/inbox/src/routes.ts b/packages/inbox/src/routes.ts deleted file mode 100644 index 40264953b2..0000000000 --- a/packages/inbox/src/routes.ts +++ /dev/null @@ -1,351 +0,0 @@ -// Product inbox routes over `@corbits/mailbox`. The three groups, mark-all- -// read (mentions + deliveries only), and clear-done live here — the package -// itself stays group-agnostic. Snooze is dropped (CL-8185): no `/:id/snooze` -// route, no `snoozed` status. - -import { - decodeMailboxListCursor, - enrichMailboxMessage, - getMailboxMessage, - listUserMailbox, - markMailboxMessageRead, - markMailboxMessageUnread, - trashMailboxMessage, - type MailboxDb, - type MailboxEventBus, - type MailboxEventOp, - type MailboxFilter, - type MailboxListCursor, -} from "@corbits/mailbox"; - -import { reportError } from "@corbits/error-sink"; -import type { TenantEnv } from "@intx/hub-api"; -import { getLogger } from "@intx/log"; -import { Hono, type Context } from "hono"; - -import { itemsEligibleForClearDone, itemsEligibleForMarkAllRead } from "./bulk"; -import { runBulkOperation } from "./bulk-run"; -import { cursorScopeMismatch } from "./cursor"; -import { isInboxGroup, type InboxGroup } from "./group"; -import { - projectInboxItem, - projectInboxItemDetail, - type InboxCounts, - type InboxItem, -} from "./project"; -import { WORKBENCH_INBOX_PRIORITIES } from "./vocabulary"; -import { walkAllOpen } from "./walk"; - -// Page size for bulk product ops (mark-all-read, clear-done, counts). Large -// enough that a normal inbox finishes in one round-trip; anything past it -// walks with the package's cursor. -const BULK_PAGE_LIMIT = 100; - -const publishLog = getLogger(["inbox", "publish"]); - -export interface CreateInboxRoutesDeps { - db: MailboxDb; - bus: MailboxEventBus; -} - -function publish( - bus: MailboxEventBus, - scope: { tenantId: string; principalId: string }, - id: string, - op: MailboxEventOp, -): void { - // `publishMailboxEvent` is package-internal and not re-exported; hosts - // publish through the bus surface with the same best-effort contract. - try { - bus.publish(scope, { type: "mailbox", id, op }); - } catch (error) { - publishLog.error( - "mailbox event publish failed for {id} ({op}) on tenant {tenantId}, principal {principalId}: {error}", - { - id, - op, - tenantId: scope.tenantId, - principalId: scope.principalId, - error: error instanceof Error ? error.message : String(error), - }, - ); - } -} - -async function listAllOpen( - db: MailboxDb, - scope: { tenantId: string; principalId: string }, - filter?: MailboxFilter, -): Promise { - return walkAllOpen((page) => { - const listOpts = { - tenantId: scope.tenantId, - principalId: scope.principalId, - // "all" = not trashed, not archived — the product's open inbox. - view: "all" as const, - limit: BULK_PAGE_LIMIT, - priorities: WORKBENCH_INBOX_PRIORITIES, - }; - const listOptsWithCursor = - page.cursor !== undefined ? { ...listOpts, cursor: page.cursor } : listOpts; - return listUserMailbox( - db, - filter !== undefined ? { ...listOptsWithCursor, filter } : listOptsWithCursor, - ); - }); -} - -/** - * Tenant-scoped product inbox. Mount under - * `/api/tenants/:tenantId/inbox` inside the hub's tenant middleware so - * `c.get("principal")` and `c.get("tenant")` are already resolved. - */ -export function createInboxRoutes(deps: CreateInboxRoutesDeps): Hono { - const { db, bus } = deps; - const app = new Hono(); - - // A failed inbox walk (an undecodable cursor mid-walk, a cursor that - // never advances, or a pathologically large inbox — see walk.ts) must be - // loud, not a silently truncated count or bulk op. Reported through - // @corbits/error-sink and answered as a 500 with the refId a person can - // quote back, rather than a falsely-complete 200 (CL-7207). - async function listAllOpenOrReport( - c: Context, - tenantId: string, - operation: string, - scope: { tenantId: string; principalId: string }, - filter?: MailboxFilter, - ): Promise { - try { - return await listAllOpen(db, scope, filter); - } catch (cause) { - const refId = reportError(cause, { operation, tenantId }); - return c.json({ error: "could not list the inbox", refId }, 500); - } - } - - // Static path segments first so `/:id` never captures them. - app.get("/counts", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const items = await listAllOpenOrReport(c, tenant.id, "inbox_counts_walk", { - tenantId: tenant.id, - principalId: principal.id, - }); - if (items instanceof Response) return items; - const counts: InboxCounts = { - action: 0, - mention: 0, - delivery: 0, - open: 0, - }; - for (const item of items) { - if (item.status !== "open") continue; - counts[item.group] += 1; - counts.open += 1; - } - return c.json(counts); - }); - - app.post("/mark-all-read", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const scope = { tenantId: tenant.id, principalId: principal.id }; - const items = await listAllOpenOrReport(c, tenant.id, "inbox_mark_all_read_walk", scope); - if (items instanceof Response) return items; - const { succeeded: marked, failed } = await runBulkOperation( - itemsEligibleForMarkAllRead(items), - async (item) => { - // Atomic: a throw between the status flip and the read flip must - // never leave a row done-but-unread (CL-7207). - await db.transaction(async (tx) => { - await enrichMailboxMessage(tx, { ...scope, id: item.id }, { status: "done" }); - await markMailboxMessageRead(tx, { ...scope, id: item.id }); - }); - publish(bus, scope, item.id, "mark_read"); - }, - { - operation: "inbox_mark_all_read_item", - tenantId: tenant.id, - extraFor: (item) => ({ id: item.id }), - }, - ); - // A 200 must mean "every eligible item was marked" — a partial result - // is reported as 207 so a caller that only checks the status code (not - // the body) can't mistake "half the inbox" for "success" (CL-7207). - return c.json({ marked, failed, complete: failed === 0 }, failed === 0 ? 200 : 207); - }); - - app.post("/clear-done", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const scope = { tenantId: tenant.id, principalId: principal.id }; - const items = await listAllOpenOrReport(c, tenant.id, "inbox_clear_done_walk", scope); - if (items instanceof Response) return items; - const { succeeded: cleared, failed } = await runBulkOperation( - itemsEligibleForClearDone(items), - async (item) => { - const ok = await trashMailboxMessage(db, { ...scope, id: item.id }); - if (!ok) throw new Error(`message ${item.id} not found to trash`); - publish(bus, scope, item.id, "trash"); - }, - { - operation: "inbox_clear_done_item", - tenantId: tenant.id, - extraFor: (item) => ({ id: item.id }), - }, - ); - // Same partial-vs-complete signal as mark-all-read: 207 whenever any - // item failed, so a status-code-only caller can't read it as success. - return c.json({ cleared, failed, complete: failed === 0 }, failed === 0 ? 200 : 207); - }); - - app.get("/", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const groupParam = c.req.query("group"); - const statusParam = c.req.query("status"); - const limitRaw = c.req.query("limit"); - const rawCursor = c.req.query("cursor"); - - let group: InboxGroup | undefined; - if (groupParam !== undefined) { - if (!isInboxGroup(groupParam)) { - return c.json({ error: "invalid group" }, 400); - } - group = groupParam; - } - - let limit = 50; - if (limitRaw !== undefined) { - const parsed = Number(limitRaw); - if (!Number.isInteger(parsed) || parsed < 1 || parsed > BULK_PAGE_LIMIT) { - return c.json({ error: "invalid limit" }, 400); - } - limit = parsed; - } - - const filter: MailboxFilter = {}; - if (group !== undefined) filter.classification = group; - if (statusParam === "open" || statusParam === "done") { - filter.status = statusParam; - } - - let cursor: MailboxListCursor | undefined; - if (rawCursor !== undefined) { - const decoded = decodeMailboxListCursor(rawCursor); - if (decoded === null) { - return c.json({ error: "malformed cursor" }, 400); - } - // A cursor is only meaningful against the exact view/sort/filter it - // was minted under (CL-7206) — paging `?group=action` then replaying - // that cursor under `?group=mention` must be rejected, not silently - // seek into the wrong result set. Same error vocabulary as - // `@corbits/mailbox`'s own `mount.ts` cross-check. - const mismatch = cursorScopeMismatch(decoded, { - view: "all", - sort: "date", - filter, - }); - if (mismatch !== null) { - return c.json({ error: `cursor does not match inbox ${mismatch}` }, 400); - } - cursor = decoded; - } - - const listMailboxOpts = { - tenantId: tenant.id, - principalId: principal.id, - view: "all" as const, - sort: "date" as const, - limit, - priorities: WORKBENCH_INBOX_PRIORITIES, - }; - const listMailboxOptsWithCursor = - cursor !== undefined ? { ...listMailboxOpts, cursor } : listMailboxOpts; - const page = await listUserMailbox( - db, - Object.keys(filter).length > 0 - ? { ...listMailboxOptsWithCursor, filter } - : listMailboxOptsWithCursor, - ); - - return c.json({ - items: page.items.map(projectInboxItem), - nextCursor: page.nextCursor, - }); - }); - - app.get("/:id", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const id = c.req.param("id"); - const message = await getMailboxMessage(db, { - tenantId: tenant.id, - principalId: principal.id, - id, - }); - if (message === null) return c.json({ error: "not found" }, 404); - return c.json(projectInboxItemDetail(message)); - }); - - app.post("/:id/read", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const id = c.req.param("id"); - const ok = await markMailboxMessageRead(db, { - tenantId: tenant.id, - principalId: principal.id, - id, - }); - if (!ok) return c.json({ error: "not found" }, 404); - publish(bus, { tenantId: tenant.id, principalId: principal.id }, id, "mark_read"); - return c.json({ ok: true }); - }); - - app.post("/:id/unread", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const id = c.req.param("id"); - const ok = await markMailboxMessageUnread(db, { - tenantId: tenant.id, - principalId: principal.id, - id, - }); - if (!ok) return c.json({ error: "not found" }, 404); - publish(bus, { tenantId: tenant.id, principalId: principal.id }, id, "mark_unread"); - return c.json({ ok: true }); - }); - - app.post("/:id/done", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const id = c.req.param("id"); - const scope = { - tenantId: tenant.id, - principalId: principal.id, - id, - }; - const ok = await enrichMailboxMessage(db, scope, { status: "done" }); - if (!ok) return c.json({ error: "not found" }, 404); - await markMailboxMessageRead(db, scope); - publish(bus, { tenantId: tenant.id, principalId: principal.id }, id, "enrich"); - return c.json({ ok: true }); - }); - - app.post("/:id/reopen", async (c) => { - const tenant = c.get("tenant"); - const principal = c.get("principal"); - const id = c.req.param("id"); - const ok = await enrichMailboxMessage( - db, - { tenantId: tenant.id, principalId: principal.id, id }, - { status: "open" }, - ); - if (!ok) return c.json({ error: "not found" }, 404); - publish(bus, { tenantId: tenant.id, principalId: principal.id }, id, "enrich"); - return c.json({ ok: true }); - }); - - return app; -} diff --git a/packages/inbox/src/thread-client.test.ts b/packages/inbox/src/thread-client.test.ts deleted file mode 100644 index 78341798fc..0000000000 --- a/packages/inbox/src/thread-client.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - MailboxThreadFetchError, - listMailboxThreadsClient, - readMailboxThreadClient, -} from "./thread-client"; - -function fetchStub(body: unknown, status = 200): typeof fetch { - return (async () => { - return { - ok: status >= 200 && status < 300, - status, - json: async () => body, - } as Response; - }) as unknown as typeof fetch; -} - -describe("listMailboxThreadsClient", () => { - test("parses a page of thread summaries", async () => { - const page = await listMailboxThreadsClient( - "/api/tenants/t1/mailbox", - { refs: [{ kind: "workbench", id: "w1" }], limit: 10 }, - fetchStub({ - threads: [ - { - rootId: "m1", - rootMessageId: "m1@mailbox", - messageCount: 2, - unreadCount: 1, - lastMessageId: "m2@mailbox", - lastFromAddress: "routine:x", - lastCreatedAt: "2026-01-01T00:00:00.000000Z", - }, - ], - nextCursor: "cur1", - }), - ); - expect(page.threads).toHaveLength(1); - expect(page.nextCursor).toBe("cur1"); - }); - - test("throws MailboxThreadFetchError on a malformed body", async () => { - await expect( - listMailboxThreadsClient( - "/api/tenants/t1/mailbox", - {}, - fetchStub({ threads: "not-an-array" }), - ), - ).rejects.toBeInstanceOf(MailboxThreadFetchError); - }); - - test("throws MailboxThreadFetchError on a non-ok response", async () => { - await expect( - listMailboxThreadsClient( - "/api/tenants/t1/mailbox", - {}, - fetchStub({ error: "bad refs" }, 400), - ), - ).rejects.toBeInstanceOf(MailboxThreadFetchError); - }); -}); - -describe("readMailboxThreadClient", () => { - test("parses a thread's messages oldest-first", async () => { - const page = await readMailboxThreadClient( - "/api/tenants/t1/mailbox", - "root@mailbox", - {}, - fetchStub({ - messages: [ - { - id: "m1", - messageId: "m1@mailbox", - references: [], - fromAddress: "routine:x", - createdAt: "2026-01-01T00:00:00.000000Z", - read: true, - archived: false, - parentId: null, - body: "hello", - }, - ], - }), - ); - expect(page.messages).toHaveLength(1); - expect(page.nextCursor).toBeUndefined(); - }); - - test("404 surfaces as MailboxThreadFetchError", async () => { - await expect( - readMailboxThreadClient( - "/api/tenants/t1/mailbox", - "unknown@mailbox", - {}, - fetchStub({ error: "Thread not found" }, 404), - ), - ).rejects.toBeInstanceOf(MailboxThreadFetchError); - }); -}); diff --git a/packages/inbox/src/thread-client.ts b/packages/inbox/src/thread-client.ts deleted file mode 100644 index eba941db8c..0000000000 --- a/packages/inbox/src/thread-client.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Typed client for the two `@corbits/mailbox` thread-read routes -// (`GET /me/threads`, `GET /me/threads/:rootMessageId`), mounted by -// `apps/hub` under `${TENANT_PREFIX}/mailbox` (see apps/hub/src/index.ts). -// Fetch composition only — the wire schemas and result types are -// `@corbits/mailbox`'s own, reused rather than redeclared so this checkout -// and the pinned mailbox version can never silently drift apart. -// -// `@corbits/mailbox` is an external git dependency, so importing it here -// (even its runtime arktype schemas) is not a browser-safe-subpath -// violation: the check treats an unowned package as an opaque leaf. - -import { type } from "arktype"; -import { - MailboxThreadListResponseSchema, - MailboxThreadResponseSchema, - type MailboxRef, -} from "@corbits/mailbox"; - -export class MailboxThreadFetchError extends Error { - constructor( - message: string, - readonly status: number | undefined, - readonly path: string, - ) { - super(message); - this.name = "MailboxThreadFetchError"; - } -} - -export type MailboxThreadListResult = typeof MailboxThreadListResponseSchema.infer; - -export type MailboxThreadResult = typeof MailboxThreadResponseSchema.infer; - -async function requestMailboxThreadJSON( - path: string, - schema: (data: unknown) => T | type.errors, - fetchImpl: typeof fetch, -): Promise { - let response: Response; - try { - response = await fetchImpl(path, { - headers: { accept: "application/json" }, - }); - } catch (cause) { - throw new MailboxThreadFetchError( - cause instanceof Error ? cause.message : String(cause), - undefined, - path, - ); - } - if (!response.ok) { - const detail = await response - .json() - .then((body: unknown) => - typeof body === "object" && body !== null && "error" in body - ? String((body as { error: unknown }).error) - : "", - ) - .catch(() => ""); - throw new MailboxThreadFetchError( - detail === "" ? `the server answered ${response.status}` : detail, - response.status, - path, - ); - } - const body: unknown = await response.json().catch(() => undefined); - const parsed = schema(body); - if (parsed instanceof type.errors) { - throw new MailboxThreadFetchError( - `unexpected response shape from ${path}: ${parsed.summary}`, - response.status, - path, - ); - } - return parsed; -} - -/** - * List the caller's conversations, newest activity first. `refs` (optional) - * is JSON-encoded the same way the route documents — an array of - * `{kind, id}` pairs OR'd together to scope the listing. - */ -export function listMailboxThreadsClient( - mailboxBasePath: string, - args: { refs?: MailboxRef[]; cursor?: string; limit?: number } = {}, - fetchImpl: typeof fetch = fetch, -): Promise { - const params = new URLSearchParams(); - if (args.refs !== undefined) params.set("refs", JSON.stringify(args.refs)); - if (args.cursor !== undefined) params.set("cursor", args.cursor); - if (args.limit !== undefined) params.set("limit", String(args.limit)); - const query = params.toString(); - const path = `${mailboxBasePath}/me/threads${query === "" ? "" : `?${query}`}`; - return requestMailboxThreadJSON(path, MailboxThreadListResponseSchema, fetchImpl); -} - -/** - * Read one conversation by its root Message-ID, oldest message first. - */ -export function readMailboxThreadClient( - mailboxBasePath: string, - rootMessageId: string, - args: { cursor?: string; limit?: number } = {}, - fetchImpl: typeof fetch = fetch, -): Promise { - const params = new URLSearchParams(); - if (args.cursor !== undefined) params.set("cursor", args.cursor); - if (args.limit !== undefined) params.set("limit", String(args.limit)); - const query = params.toString(); - const path = `${mailboxBasePath}/me/threads/${encodeURIComponent(rootMessageId)}${ - query === "" ? "" : `?${query}` - }`; - return requestMailboxThreadJSON(path, MailboxThreadResponseSchema, fetchImpl); -} diff --git a/packages/inbox/src/vocabulary.ts b/packages/inbox/src/vocabulary.ts deleted file mode 100644 index e064b56c09..0000000000 --- a/packages/inbox/src/vocabulary.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Host vocabulary for `@corbits/mailbox`. The package has none of its own — -// every mount must hand priorities and statuses in. Workbench's three-group -// product layer uses `status` for open/done (snooze is dropped, CL-8185) -// and leaves `priority` as a free ranking the write path may set later. - -import type { MailboxVocabulary } from "@corbits/mailbox"; - -export const WORKBENCH_INBOX_PRIORITIES = ["urgent", "high", "normal", "low"] as const; - -export const WORKBENCH_INBOX_STATUSES = ["open", "done"] as const; - -export type WorkbenchInboxStatus = (typeof WORKBENCH_INBOX_STATUSES)[number]; - -export const WORKBENCH_MAILBOX_VOCABULARY: MailboxVocabulary = { - priorities: [...WORKBENCH_INBOX_PRIORITIES], - statuses: [...WORKBENCH_INBOX_STATUSES], -}; diff --git a/packages/inbox/src/walk.ts b/packages/inbox/src/walk.ts deleted file mode 100644 index 743bb71080..0000000000 --- a/packages/inbox/src/walk.ts +++ /dev/null @@ -1,61 +0,0 @@ -// The bulk product ops (`/counts`, `mark-all-read`, `clear-done`) all walk -// the caller's entire open inbox before acting. `walkAllOpen` is that walk, -// pulled out from `routes.ts` and driven through an injected `listPage` so -// its failure modes — an undecodable cursor, a cursor that never advances, -// an inbox large enough to need an explicit cap — are unit-testable without -// a live Postgres (CL-7207). -// -// Previously this loop treated `decodeMailboxListCursor` returning `null` -// as "done" and silently `break`-ed: a rolling deploy that lands two hub -// versions against `@corbits/mailbox` mid-rollout could change a cursor's -// shape under a long-running walk, and the caller would report success -// having silently stopped partway through the inbox. Throwing instead -// turns that into a loud, reported failure — see routes.ts's callers, -// which report it through `@corbits/error-sink` and answer 500 rather than -// a falsely-complete 200. -import { - decodeMailboxListCursor, - type MailboxListCursor, - type MailboxPage, -} from "@corbits/mailbox"; - -import { projectInboxItem } from "./project"; -import type { InboxItem } from "./project"; - -// 1000 pages * 100 rows/page = 100k rows — generous for any real inbox, -// tight enough that a cursor bug loops a bounded number of times instead -// of forever. -export const MAX_WALK_PAGES = 1000; - -export type ListPageFn = (opts: { cursor?: MailboxListCursor }) => Promise; - -/** The walk stopped before covering the whole inbox. `message` says why. */ -export class IncompleteWalkError extends Error {} - -export async function walkAllOpen( - listPage: ListPageFn, - opts: { maxPages?: number } = {}, -): Promise { - const maxPages = opts.maxPages ?? MAX_WALK_PAGES; - const out: InboxItem[] = []; - let cursor: MailboxListCursor | undefined; - let pages = 0; - for (;;) { - pages += 1; - if (pages > maxPages) { - throw new IncompleteWalkError(`inbox walk exceeded ${maxPages} pages without finishing`); - } - const page = await listPage(cursor !== undefined ? { cursor } : {}); - for (const message of page.items) out.push(projectInboxItem(message)); - if (page.nextCursor === undefined) break; - const next = decodeMailboxListCursor(page.nextCursor); - if (next === null) { - throw new IncompleteWalkError("inbox walk received an undecodable cursor mid-walk"); - } - if (cursor !== undefined && next.createdAt === cursor.createdAt && next.id === cursor.id) { - throw new IncompleteWalkError("inbox walk cursor did not advance"); - } - cursor = next; - } - return out; -} diff --git a/packages/inbox/test/bulk-report-error.test.ts b/packages/inbox/test/bulk-report-error.test.ts deleted file mode 100644 index c7e37bf58f..0000000000 --- a/packages/inbox/test/bulk-report-error.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Per-item bulk failures must reach reportError even when the caller -// omits onError — otherwise mark-all-read / clear-done swallow a row -// with no refId (the catch used to only bump `failed`). -import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"; - -const reportErrorCalls: { - error: unknown; - context: Record; -}[] = []; - -mock.module("@corbits/error-sink", () => ({ - reportError: (error: unknown, context: Record) => { - reportErrorCalls.push({ error, context }); - return "ref_test"; - }, - generateRefId: () => "ref_test", - makeErrorEnvelope: (args: { code: string; userMessage: string; refId?: string }) => ({ - error: { - code: args.code, - message: args.userMessage, - refId: args.refId ?? "ref_test", - }, - }), - parseErrorEnvelope: () => null, -})); - -const { runBulkOperation } = await import("../src/bulk-run"); - -beforeEach(() => { - reportErrorCalls.length = 0; -}); -afterAll(() => { - mock.restore(); -}); - -describe("runBulkOperation reportError", () => { - test("a thrown item is reported even when onError is omitted", async () => { - const boom = new Error("transient write failure"); - const result = await runBulkOperation(["a", "b"], async (id) => { - if (id === "b") throw boom; - }); - - expect(result).toEqual({ succeeded: 1, failed: 1 }); - expect(reportErrorCalls).toHaveLength(1); - expect(reportErrorCalls[0]?.error).toBe(boom); - expect(reportErrorCalls[0]?.context).toEqual({ - operation: "inbox.bulk", - }); - }); - - test("caller-supplied operation and tenantId ride on the report", async () => { - const boom = new Error("row locked"); - await runBulkOperation( - [{ id: "msg_1" }], - async () => { - throw boom; - }, - { - operation: "inbox_mark_all_read_item", - tenantId: "tnt_1", - extraFor: (item) => ({ id: item.id }), - }, - ); - - expect(reportErrorCalls).toHaveLength(1); - expect(reportErrorCalls[0]?.error).toBe(boom); - expect(reportErrorCalls[0]?.context).toEqual({ - operation: "inbox_mark_all_read_item", - tenantId: "tnt_1", - extra: { id: "msg_1" }, - }); - }); -}); diff --git a/packages/inbox/test/bulk.test.ts b/packages/inbox/test/bulk.test.ts deleted file mode 100644 index 5b6a693678..0000000000 --- a/packages/inbox/test/bulk.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { itemsEligibleForClearDone, itemsEligibleForMarkAllRead } from "../src/bulk"; -import { runBulkOperation } from "../src/bulk-run"; -import type { InboxItem } from "../src/project"; - -function item(over: Partial & Pick): InboxItem { - return { - from: "agent:ops", - date: "2026-08-10T12:00:00.000Z", - read: false, - status: "open", - ...over, - }; -} - -describe("itemsEligibleForMarkAllRead", () => { - test("skips action items and non-open rows", () => { - const items = [ - item({ id: "a", group: "action" }), - item({ id: "m", group: "mention" }), - item({ id: "d", group: "delivery" }), - item({ id: "done-m", group: "mention", status: "done" }), - ]; - const eligible = itemsEligibleForMarkAllRead(items); - expect(eligible.map((i) => i.id)).toEqual(["m", "d"]); - }); -}); - -describe("itemsEligibleForClearDone", () => { - test("selects only done rows", () => { - const items = [ - item({ id: "a", group: "action" }), - item({ id: "m", group: "mention", status: "done" }), - item({ id: "d", group: "delivery", status: "open" }), - ]; - expect(itemsEligibleForClearDone(items).map((i) => i.id)).toEqual(["m"]); - }); -}); - -describe("runBulkOperation", () => { - test("a thrown error on one item does not stop the rest", async () => { - const applied: string[] = []; - const failures: { id: string; error: unknown }[] = []; - const result = await runBulkOperation( - ["a", "b", "c"], - async (id) => { - if (id === "b") throw new Error("transient write failure"); - applied.push(id); - }, - { onError: (id, error) => failures.push({ id, error }) }, - ); - - // Both non-failing items still ran, despite "b" throwing between them — - // the caller can tell exactly how far the operation got. - expect(applied).toEqual(["a", "c"]); - expect(result).toEqual({ succeeded: 2, failed: 1 }); - expect(failures).toHaveLength(1); - expect(failures[0]?.id).toBe("b"); - expect((failures[0]?.error as Error).message).toBe("transient write failure"); - }); - - test("every item succeeding reports zero failures", async () => { - const result = await runBulkOperation(["a", "b"], async () => {}); - expect(result).toEqual({ succeeded: 2, failed: 0 }); - }); - - test("onError is optional", async () => { - const result = await runBulkOperation(["a"], async () => { - throw new Error("boom"); - }); - expect(result).toEqual({ succeeded: 0, failed: 1 }); - }); -}); diff --git a/packages/inbox/test/cursor.test.ts b/packages/inbox/test/cursor.test.ts deleted file mode 100644 index 3edd0b583c..0000000000 --- a/packages/inbox/test/cursor.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { MailboxListCursor } from "@corbits/mailbox"; -import { cursorScopeMismatch } from "../src/cursor"; - -function cursor(overrides: Partial = {}): MailboxListCursor { - return { - createdAt: "2026-01-01T00:00:00.000000Z", - id: "msg_1", - view: "all", - sort: "date", - filter: "classification=action", - ...overrides, - }; -} - -describe("cursorScopeMismatch", () => { - test("matching cursor: no mismatch", () => { - const mismatch = cursorScopeMismatch(cursor(), { - view: "all", - sort: "date", - filter: { classification: "action" }, - }); - expect(mismatch).toBeNull(); - }); - - test("cursor minted under a different group filter is rejected", () => { - // Paged `?group=action`, then replayed that cursor under `?group=mention`. - const actionCursor = cursor({ filter: "classification=action" }); - const mismatch = cursorScopeMismatch(actionCursor, { - view: "all", - sort: "date", - filter: { classification: "mention" }, - }); - expect(mismatch).toBe("filter"); - }); - - test("cursor minted under a different view is rejected", () => { - const mismatch = cursorScopeMismatch(cursor({ view: "unread" }), { - view: "all", - sort: "date", - filter: {}, - }); - expect(mismatch).toBe("view"); - }); - - test("cursor minted under a different sort is rejected", () => { - const mismatch = cursorScopeMismatch(cursor({ sort: "priority" }), { - view: "all", - sort: "date", - filter: {}, - }); - expect(mismatch).toBe("sort"); - }); - - test("view is checked before sort and filter", () => { - const mismatch = cursorScopeMismatch( - cursor({ view: "unread", sort: "priority", filter: "status=done" }), - { view: "all", sort: "date", filter: {} }, - ); - expect(mismatch).toBe("view"); - }); -}); diff --git a/packages/inbox/test/delivery.test.ts b/packages/inbox/test/delivery.test.ts index 4c10887b7c..1c5e89c5c9 100644 --- a/packages/inbox/test/delivery.test.ts +++ b/packages/inbox/test/delivery.test.ts @@ -3,16 +3,18 @@ // scripts/db-setup.ts (the hub's standard migration path), each of the four // kinds `@corbits/notify` defines writes a real row through // `createWorkbenchMailboxDelivery`, and that row renders back out through -// `@corbits/mailbox`'s own read path (`listUserMailbox`) — the same query -// the inbox/bell UI drives, grouped the same way `inboxGroupOf` groups it. -// Runs against its own scratch database, never the developer's or the -// walking-skeleton suite's. +// `@corbits/mailbox`'s own native store (CL-8174: the triage/priority/ +// classification vocabulary this test used to assert on is gone with the +// retired product-inbox layer — a notify item is a plain mailbox message +// now, read back the same way `/me/inbox` does). Runs against its own +// scratch database, never the developer's or the walking-skeleton suite's. import { afterAll, beforeAll, expect, test } from "bun:test"; import postgres from "postgres"; import { createDB, schema } from "@intx/db"; import { generateId } from "@intx/hub-common"; -import { createMailboxDb, listUserMailbox } from "@corbits/mailbox"; +import { and, eq, like } from "drizzle-orm"; +import { createMailboxDb, openNativeMailboxStore, principalMail } from "@corbits/mailbox"; import { createInMemoryNotifyDispatchStore, createSinkRegistry, @@ -29,8 +31,6 @@ import { import { setupDatabase } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../scripts/e2e/database-url"; import { createResolveExistingMailIds, createWorkbenchMailboxDelivery } from "../src/delivery"; -import { inboxGroupOf } from "../src/group"; -import { WORKBENCH_INBOX_PRIORITIES } from "../src/vocabulary"; import { dbGate } from "../../../scripts/e2e/db-gate"; function scratchUrlFor(e2eUrl: string): string { @@ -182,26 +182,19 @@ describeIfDb("notify delivery writes a real mailbox row for every notification k expect(report.deliveredMailboxRowIds).toHaveLength(1); } - const page = await listUserMailbox(mailboxDb.db, { + const store = await openNativeMailboxStore(mailboxDb.db, { tenantId, principalId, - limit: 10, - view: "all", - priorities: WORKBENCH_INBOX_PRIORITIES, + folder: "INBOX", }); - expect(page.items).toHaveLength(4); + expect(store.messages).toHaveLength(4); + const subjects = store.messages.map((message) => message.envelope.subject); + expect(subjects.some((subject) => subject.includes("delete_repo"))).toBe(true); + expect(subjects.some((subject) => subject.includes("failed"))).toBe(true); + expect(subjects.some((subject) => subject.includes("mentioned you"))).toBe(true); + expect(subjects.some((subject) => subject.includes("Reconnect"))).toBe(true); - const groupBySubject = (needle: string): string | undefined => { - const item = page.items.find((i) => (i.subject ?? "").includes(needle)); - expect(item).toBeDefined(); - return item === undefined ? undefined : inboxGroupOf(item); - }; - expect(groupBySubject("delete_repo")).toBe("action"); - expect(groupBySubject("failed")).toBe("delivery"); - expect(groupBySubject("mentioned you")).toBe("mention"); - expect(groupBySubject("Reconnect")).toBe("action"); - - expect(page.items.every((item) => item.read === false)).toBe(true); + expect(store.messages.every((message) => !message.flags.has("\\Seen"))).toBe(true); } finally { await mailboxDb.close(); } @@ -262,20 +255,28 @@ describeIfDb("notify delivery writes a real mailbox row for every notification k expect(redelivery.deliveredMailboxRowIds).toEqual([]); expect(redelivery.queuedDispatchCount).toBe(1); - const page = await listUserMailbox(mailboxDb.db, { + const store = await openNativeMailboxStore(mailboxDb.db, { tenantId, principalId, - limit: 10, - view: "all", - priorities: WORKBENCH_INBOX_PRIORITIES, + folder: "INBOX", }); // Exactly one mail row for the redelivered event — the crash and // both redeliveries deduped onto the first attempt's row. - const matching = page.items.filter((item) => - (item.subject ?? "").includes("quarantine_token"), + const matching = store.messages.filter((message) => + message.envelope.subject.includes("quarantine_token"), ); expect(matching).toHaveLength(1); - const mailId = matching[0]?.id; + const rows = await mailboxDb.db + .select({ id: principalMail.id }) + .from(principalMail) + .where( + and( + eq(principalMail.tenantId, tenantId), + eq(principalMail.principalId, principalId), + like(principalMail.subject, "%quarantine_token%"), + ), + ); + const mailId = rows[0]?.id; if (mailId === undefined) throw new Error("expected the redelivered mail row to list"); expect(await dispatch.listFor(mailId)).toHaveLength(1); diff --git a/packages/inbox/test/group.test.ts b/packages/inbox/test/group.test.ts deleted file mode 100644 index 7cb38235c8..0000000000 --- a/packages/inbox/test/group.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { classificationFromRefs, inboxGroupOf, isInboxGroup } from "../src/group"; - -describe("inboxGroupOf", () => { - test("honors an explicit classification", () => { - expect( - inboxGroupOf({ - classification: "mention", - refs: [{ kind: "approval" }], - }), - ).toBe("mention"); - }); - - test("derives action from an approval ref", () => { - expect(inboxGroupOf({ refs: [{ kind: "approval" }] })).toBe("action"); - }); - - test("derives action from a credential ref", () => { - expect(inboxGroupOf({ refs: [{ kind: "credential" }] })).toBe("action"); - }); - - test("derives mention from a thread ref", () => { - expect(inboxGroupOf({ refs: [{ kind: "thread" }] })).toBe("mention"); - }); - - test("defaults to delivery", () => { - expect(inboxGroupOf({ refs: [{ kind: "run" }] })).toBe("delivery"); - expect(inboxGroupOf({})).toBe("delivery"); - }); -}); - -describe("classificationFromRefs", () => { - test("matches the group derivation", () => { - expect(classificationFromRefs([{ kind: "approval" }])).toBe("action"); - expect(classificationFromRefs([{ kind: "thread" }])).toBe("mention"); - expect(classificationFromRefs([])).toBe("delivery"); - }); -}); - -describe("isInboxGroup", () => { - test("accepts the three product groups only", () => { - expect(isInboxGroup("action")).toBe(true); - expect(isInboxGroup("mention")).toBe(true); - expect(isInboxGroup("delivery")).toBe(true); - expect(isInboxGroup("other")).toBe(false); - }); -}); diff --git a/packages/inbox/test/project.test.ts b/packages/inbox/test/project.test.ts deleted file mode 100644 index df87d6a3db..0000000000 --- a/packages/inbox/test/project.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import type { MailboxMessage } from "@corbits/mailbox"; - -import { projectInboxItem } from "../src/project"; - -function message(over: Partial = {}): MailboxMessage { - return { - id: "m1", - from: "agent:ops", - to: ["user:u1"], - date: "2026-08-10T12:00:00.000Z", - messageId: "", - read: false, - ...over, - }; -} - -describe("projectInboxItem", () => { - test("maps an approval-ref message into the action group", () => { - const item = projectInboxItem( - message({ - subject: "Approve deploy", - status: "open", - refs: [{ kind: "approval", id: "appr-1", label: "Deploy" }], - }), - ); - expect(item.group).toBe("action"); - expect(item.status).toBe("open"); - expect(item.subject).toBe("Approve deploy"); - expect(item.refs?.[0]?.kind).toBe("approval"); - }); - - test("maps classification delivery + open status", () => { - const item = projectInboxItem(message({ classification: "delivery", status: "open" })); - expect(item.group).toBe("delivery"); - expect(item.status).toBe("open"); - }); - - test("unknown status falls back to open", () => { - const item = projectInboxItem(message({ status: "weird" })); - expect(item.status).toBe("open"); - }); -}); diff --git a/packages/inbox/test/routes.test.ts b/packages/inbox/test/routes.test.ts deleted file mode 100644 index 483b7c8ee7..0000000000 --- a/packages/inbox/test/routes.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Route-level tests for the parts of `GET /` that reject before touching the -// database — the cursor/filter cross-check (CL-7206) chiefly. `db` is a -// stub that throws on any call, which is itself the assertion that a -// rejected request never reaches the mailbox store. -import { describe, expect, test } from "bun:test"; -import { Hono } from "hono"; -import type { TenantEnv } from "@intx/hub-api"; -import { createInMemoryMailboxEventBus, type MailboxDb } from "@corbits/mailbox"; -import { createInboxRoutes } from "../src/routes"; - -const TENANT = { id: "tnt_1" }; -const PRINCIPAL = { id: "prn_1" }; - -function neverCalledDb(): MailboxDb { - return new Proxy( - {}, - { - get() { - throw new Error("db should not be reached on a rejected request"); - }, - }, - ) as MailboxDb; -} - -function throwingDb(message: string): MailboxDb { - return new Proxy( - {}, - { - get() { - throw new Error(message); - }, - }, - ) as MailboxDb; -} - -function mount(db: MailboxDb = neverCalledDb()): Hono { - const routes = createInboxRoutes({ - db, - bus: createInMemoryMailboxEventBus(), - }); - const app = new Hono(); - app.use("*", async (c, next) => { - c.set("tenant", TENANT as never); - c.set("principal", PRINCIPAL as never); - await next(); - }); - app.route("/", routes); - return app; -} - -describe("GET / cursor/filter cross-check", () => { - test("a cursor minted under ?group=action is rejected when replayed under ?group=mention", async () => { - const app = mount(); - // Minted the same way `listUserMailbox` mints one: base64url JSON with - // view/sort/filter embedded, filter canonicalized to `classification=action`. - const payload = { - createdAt: "2026-01-01T00:00:00.000000Z", - id: "msg_1", - view: "all", - sort: "date", - filter: "classification=action", - }; - const cursor = Buffer.from(JSON.stringify(payload)).toString("base64url"); - - const response = await app.request(`/?group=mention&cursor=${encodeURIComponent(cursor)}`); - - expect(response.status).toBe(400); - const body = (await response.json()) as { error: string }; - expect(body.error).toBe("cursor does not match inbox filter"); - }); - - test("a well-formed cursor with no query params is malformed-rejected without a filter mismatch masking it", async () => { - const app = mount(); - const response = await app.request("/?cursor=not-valid-base64url!!!"); - expect(response.status).toBe(400); - const body = (await response.json()) as { error: string }; - expect(body.error).toBe("malformed cursor"); - }); -}); - -describe("bulk ops surface a failed inbox walk instead of silently truncating it (CL-7207)", () => { - test("GET /counts reports and 500s rather than swallowing a walk failure", async () => { - const app = mount(throwingDb("connection reset mid-walk")); - const response = await app.request("/counts"); - expect(response.status).toBe(500); - const body = (await response.json()) as { error: string; refId: string }; - expect(body.error).toBe("could not list the inbox"); - expect(typeof body.refId).toBe("string"); - expect(body.refId.length).toBeGreaterThan(0); - }); - - test("POST /mark-all-read reports and 500s rather than silently marking nothing", async () => { - const app = mount(throwingDb("connection reset mid-walk")); - const response = await app.request("/mark-all-read", { method: "POST" }); - expect(response.status).toBe(500); - const body = (await response.json()) as { error: string; refId: string }; - expect(body.error).toBe("could not list the inbox"); - expect(typeof body.refId).toBe("string"); - }); - - test("POST /clear-done reports and 500s rather than silently clearing nothing", async () => { - const app = mount(throwingDb("connection reset mid-walk")); - const response = await app.request("/clear-done", { method: "POST" }); - expect(response.status).toBe(500); - const body = (await response.json()) as { error: string; refId: string }; - expect(body.error).toBe("could not list the inbox"); - expect(typeof body.refId).toBe("string"); - }); -}); diff --git a/packages/inbox/test/walk.test.ts b/packages/inbox/test/walk.test.ts deleted file mode 100644 index 3111eb6ff8..0000000000 --- a/packages/inbox/test/walk.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { MailboxPage } from "@corbits/mailbox"; -import { encodeMailboxListCursor } from "@corbits/mailbox"; -import { IncompleteWalkError, walkAllOpen } from "../src/walk"; - -function message(id: string, createdAt: string) { - return { - id, - messageId: `${id}@test`, - from: "routine:test", - to: ["prn_1@inbox.test"], - date: createdAt, - read: false, - status: "open" as const, - createdAt, - }; -} - -const SHAPE = { view: "all" as const, sort: "date" as const, filter: "" }; - -describe("walkAllOpen", () => { - test("walks every page and stops once nextCursor is undefined", async () => { - let calls = 0; - const page: MailboxPage = { - items: [message("msg_1", "2026-01-02T00:00:00.000000Z")], - }; - const items = await walkAllOpen(async () => { - calls += 1; - return page; - }); - expect(items.map((item) => item.id)).toEqual(["msg_1"]); - expect(calls).toBe(1); - }); - - test("follows nextCursor across multiple pages", async () => { - const cursorFor = (id: string, createdAt: string) => - encodeMailboxListCursor({ createdAt, id }, SHAPE); - const pages: MailboxPage[] = [ - { - items: [message("msg_1", "2026-01-03T00:00:00.000000Z")], - nextCursor: cursorFor("msg_1", "2026-01-03T00:00:00.000000Z"), - }, - { items: [message("msg_2", "2026-01-02T00:00:00.000000Z")] }, - ]; - let call = 0; - const items = await walkAllOpen(async () => { - const page = pages[call]; - call += 1; - if (page === undefined) throw new Error("listPage called too many times"); - return page; - }); - expect(items.map((item) => item.id)).toEqual(["msg_1", "msg_2"]); - expect(call).toBe(2); - }); - - test("throws IncompleteWalkError when the next cursor is undecodable", async () => { - const page: MailboxPage = { - items: [message("msg_1", "2026-01-01T00:00:00.000000Z")], - nextCursor: "not-a-valid-cursor", - }; - await expect(walkAllOpen(async () => page)).rejects.toBeInstanceOf(IncompleteWalkError); - }); - - test("throws IncompleteWalkError when the cursor does not advance", async () => { - const stuckCursor = encodeMailboxListCursor( - { createdAt: "2026-01-01T00:00:00.000000Z", id: "msg_1" }, - SHAPE, - ); - const page: MailboxPage = { - items: [message("msg_1", "2026-01-01T00:00:00.000000Z")], - nextCursor: stuckCursor, - }; - // Every call returns the exact same page/cursor — an infinite loop - // without the advance guard. - await expect(walkAllOpen(async () => page)).rejects.toBeInstanceOf(IncompleteWalkError); - }); - - test("throws IncompleteWalkError once maxPages is exceeded", async () => { - let call = 0; - const listPage = async (): Promise => { - call += 1; - return { - items: [message(`msg_${call}`, `2026-01-01T00:00:0${call}.000000Z`)], - nextCursor: encodeMailboxListCursor( - { createdAt: `2026-01-01T00:00:0${call}.000000Z`, id: `msg_${call}` }, - SHAPE, - ), - }; - }; - await expect(walkAllOpen(listPage, { maxPages: 2 })).rejects.toBeInstanceOf( - IncompleteWalkError, - ); - expect(call).toBe(2); - }); -}); diff --git a/packages/inbox/tsconfig.json b/packages/inbox/tsconfig.json index 4afc205a2c..6113c6e904 100644 --- a/packages/inbox/tsconfig.json +++ b/packages/inbox/tsconfig.json @@ -13,12 +13,6 @@ "include": ["src", "test"], "exclude": [], "references": [ - { - "path": "../../vendor/intx/hub-api/tsconfig.src.json" - }, - { - "path": "../error-sink/tsconfig.src.json" - }, { "path": "../notify/tsconfig.src.json" } diff --git a/packages/inbox/tsconfig.src.json b/packages/inbox/tsconfig.src.json index 149acb99d7..b7a6e26dc0 100644 --- a/packages/inbox/tsconfig.src.json +++ b/packages/inbox/tsconfig.src.json @@ -11,12 +11,6 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ - { - "path": "../../vendor/intx/hub-api/tsconfig.src.json" - }, - { - "path": "../error-sink/tsconfig.src.json" - }, { "path": "../notify/tsconfig.src.json" } diff --git a/scripts/checks/browser-safe-subpaths.ts b/scripts/checks/browser-safe-subpaths.ts index e6c372ad27..04b444fa5e 100644 --- a/scripts/checks/browser-safe-subpaths.ts +++ b/scripts/checks/browser-safe-subpaths.ts @@ -1,16 +1,15 @@ // check:browser-safe-subpaths — a package that declares a "browser-safe" -// subpath (`@corbits/inbox/client`, `@corbits/workflows/client`, …) is -// making a promise: nothing reachable from that one entry point pulls in -// a server-only dependency. Comments have said as much for a while; this -// check is what actually makes it true, by walking the real transitive -// import graph — relative imports and `@corbits/*` workspace subpath -// imports — starting at each declared entry, and failing if it ever -// reaches `postgres`, `drizzle-orm`, `hono`, or any `@intx/*` import. +// subpath (`@corbits/workflows/client`, `@corbits/agent-directory/client`, +// …) is making a promise: nothing reachable from that one entry point +// pulls in a server-only dependency. Comments have said as much for a +// while; this check is what actually makes it true, by walking the real +// transitive import graph — relative imports and `@corbits/*` workspace +// subpath imports — starting at each declared entry, and failing if it +// ever reaches `postgres`, `drizzle-orm`, `hono`, or any `@intx/*` import. // // `import type` / `export type` statements are skipped: a type-only -// import is erased at compile time and carries no runtime dependency -// (see e.g. packages/inbox/src/project.ts's type-only `@corbits/mailbox` -// import). A package this repo doesn't own (an external git dependency +// import is erased at compile time and carries no runtime dependency. A +// package this repo doesn't own (an external git dependency // like `@corbits/mailbox` or `@corbits/react-ui`) is an opaque leaf — its // own source isn't in this tree to walk, so it can't be statically // checked here and is trusted the same way a `node_modules` import @@ -45,7 +44,6 @@ export interface BrowserSafeEntry { * only reaches this entry point — this is what backs that claim. */ export const ENTRIES: readonly BrowserSafeEntry[] = [ - { package: "@corbits/inbox", subpath: "./client" }, { package: "@corbits/agent-directory", subpath: "./client" }, { package: "@corbits/api-query", subpath: "." }, // The envelope alone (UnauthenticatedError, ApiQueryError, toAPIQuery) has diff --git a/scripts/checks/report-error-baseline.txt b/scripts/checks/report-error-baseline.txt index b75f7b9269..a362be5de7 100644 --- a/scripts/checks/report-error-baseline.txt +++ b/scripts/checks/report-error-baseline.txt @@ -77,9 +77,17 @@ apps/web/src/session.ts 3 return { apps/web/src/shell/context-menu/items.tsx 1 toast("Couldn't copy the link"); apps/web/src/shell/workbench-list.tsx 1 // Revert the optimistic title on failure; the list will refetch on apps/web/src/shell/workbench-list.tsx 1 setPinned(!next); +packages/agent-directory-tools/src/tool.ts 1 // The agent was genuinely created — that half-success must never +packages/agent-directory-tools/src/tool.ts 1 if (err instanceof CreateAgentDefinitionError) { +packages/agent-directory-tools/src/tool.ts 1 return errorResult(call.id, err); packages/agent-directory/src/definition-history.ts 1 return null; packages/approvals/src/allowance.ts 1 deps.log( packages/approvals/src/allowance.ts 1 return { outcome: "park", reason: "classification_failed" }; +packages/capability-tools/src/tool.ts 1 if (err instanceof CapabilityOutOfInventoryError) { +packages/capability-tools/src/tool.ts 1 return errorResult(call.id, err); +packages/catalog-tools/src/tool.ts 1 return errorResult(call.id, err); +packages/catalog-tools/src/tool.ts 2 return errorResult(call.id, err); +packages/catalog-tools/src/tool.ts 3 return errorResult(call.id, err); packages/chat-ui/src/composer.tsx 1 if (attachGenerationRef.current !== generation) return; packages/chat-ui/src/default-agent-workbench.ts 1 return { packages/chat-ui/src/invite-agent-dialog.tsx 1 setInviteError( @@ -103,6 +111,10 @@ packages/code-review/src/review-run.ts 1 return { reviewer, ok: false, reason: r packages/command-palette/src/recents.ts 1 // A full or disabled store (private browsing, quota) loses packages/command-palette/src/recents.ts 1 // As above — loses persistence, not function. packages/command-palette/src/recents.ts 1 return []; +packages/connections-tools/src/tool.ts 1 if (err instanceof NoOwnRoomError) { +packages/connections-tools/src/tool.ts 1 return errorResult(call.id, err); +packages/connections-tools/src/tool.ts 2 return errorResult(call.id, err); +packages/connections-tools/src/tool.ts 3 return errorResult(call.id, err); packages/connections/src/connect-github-routes.ts 1 // A repoId `state.repos` doesn't carry means the card's own packages/connections/src/connect-github-routes.ts 1 // The GitHub client's own errors (transport, HTTP status, shape packages/connections/src/credential-test.ts 1 // Not JSON, or didn't match either shape — fall through to the @@ -110,20 +122,60 @@ packages/connections/src/credential-test.ts 1 return []; packages/connections/src/credential-test.ts 1 return undefined; packages/connections/src/credential-test.ts 1 return { packages/error-sink/src/index.ts 1 return context.refId ?? UNKNOWN_OPERATION; -packages/inbox/src/routes.ts 1 publishLog.error( +packages/github-tools/src/pull-request-tools.ts 1 return errorResult(call.id, failureMessage(err)); +packages/github-tools/src/pull-request-tools.ts 1 return null; +packages/github-tools/src/pull-request-tools.ts 2 return errorResult(call.id, failureMessage(err)); +packages/github-tools/src/tool.ts 1 return null; +packages/github-tools/src/tool.ts 1 return { +packages/granola-tools/src/tool.ts 1 return null; +packages/granola-tools/src/tool.ts 1 return { +packages/granola-tools/src/tool.ts 2 return { +packages/jimmy-agent/src/gif-search-tool.ts 1 return false; +packages/jimmy-agent/src/gif-search-tool.ts 1 return null; +packages/jimmy-agent/src/gif-search-tool.ts 1 return { +packages/linear-tools/src/tool.ts 1 return null; +packages/linear-tools/src/tool.ts 1 return { +packages/manus-tools/src/tool.ts 1 return errorResult(call.id, err); +packages/manus-tools/src/tool.ts 1 return null; +packages/manus-tools/src/tool.ts 2 return errorResult(call.id, err); +packages/manus-tools/src/tool.ts 3 return errorResult(call.id, err); +packages/manus-tools/src/tool.ts 4 return errorResult(call.id, err); +packages/manus-tools/src/tool.ts 5 return errorResult(call.id, err); +packages/manus-tools/src/tool.ts 6 return errorResult(call.id, err); +packages/mcp-tools/src/tool.ts 1 return errorResult(call.id, err); +packages/mcp-tools/src/tool.ts 1 return errorResult(call.id, new Error(`"${pattern}" is not a valid regex: ${String(err)}`)); +packages/mcp-tools/src/tool.ts 1 return { +packages/mcp-tools/src/tool.ts 2 return errorResult(call.id, err); +packages/mcp-tools/src/tool.ts 3 return errorResult(call.id, err); +packages/mcp-tools/src/tool.ts 4 return errorResult(call.id, err); packages/mocks/src/ollama/capture.ts 1 return raw; packages/ollama-adapter/src/inline-tool-json.ts 1 return false; packages/ollama-adapter/src/inline-tool-json.ts 1 return { kind: "incomplete" }; packages/onboarding/src/pending-seed.ts 1 return drop(); packages/onboarding/src/routes.ts 1 // Neither `ProvisionError` nor `CliError` messages are safe to show packages/onboarding/src/routes.ts 1 return c.json( +packages/reddit-tools/src/tool.ts 1 return null; +packages/reddit-tools/src/tool.ts 1 return { +packages/reddit-tools/src/tool.ts 2 return { packages/sandbox-sidecar/src/provisioner.ts 1 log.error`failed to sweep obsolete unit ${externalRef} for allocation ${allocationId}: ${ packages/sandbox-sidecar/src/provisioner.ts 1 return rejected(...classify(error, "destroy_unit_failed")); packages/sandbox-sidecar/src/provisioner.ts 1 return rejected(...classify(error, "start_unit_failed")); +packages/scout-agent/src/artifact-tool.ts 1 return { +packages/scout-agent/src/artifact-tool.ts 2 return { packages/settings-ui/src/account-section.tsx 1 toast(SETTINGS_STRINGS.accountEmailCopyError); +packages/skills-tools/src/tool.ts 1 return errorResult(call.id, err); +packages/skills-tools/src/tool.ts 2 return errorResult(call.id, err); +packages/skills-tools/src/tool.ts 3 return errorResult(call.id, err); +packages/skills-tools/src/tool.ts 4 return errorResult(call.id, err); +packages/skills-tools/src/tool.ts 5 return errorResult(call.id, err); packages/tool-registry-publish/src/freshness-check.ts 1 log.error`${err instanceof Error ? err.message : String(err)}`; +packages/tools-skills/src/tool.ts 1 return errorResult(call.id, err); +packages/tools-skills/src/tool.ts 2 return errorResult(call.id, err); +packages/tools-skills/src/tool.ts 3 return errorResult(call.id, err); packages/turn-artifacts/src/index.ts 1 return []; packages/url-path/src/decoded-or-null.ts 1 return null; +packages/web-search-tools/src/tool.ts 1 return null; +packages/web-search-tools/src/tool.ts 1 return { packages/webhook-triggers/src/ingress-routes.ts 1 return c.json( packages/webhook-triggers/src/signature.ts 1 return false; workflows/attio-task-agent/src/finalize-tool.ts 1 return { From 66665f29027f3b303ea84141c0c59de5ff7ff963 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 14:22:47 -0700 Subject: [PATCH 2/2] chore: regenerate bun.lock and report-error baseline after rebase Rebasing onto main's CL-8217 tools/workflows/agents/skills restructuring moved package paths the lockfile and report-error baseline both track. --- bun.lock | 771 +++++++++++------------ scripts/checks/report-error-baseline.txt | 53 -- 2 files changed, 384 insertions(+), 440 deletions(-) diff --git a/bun.lock b/bun.lock index 7c00947a61..d9f3eaa70e 100644 --- a/bun.lock +++ b/bun.lock @@ -29,6 +29,63 @@ "typescript": "6.0.3", }, }, + "agents/assistant": { + "name": "@corbits/assistant-workflow", + "version": "0.0.2", + "dependencies": { + "@corbits/catalog-tools": "0.1.1", + "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", + }, + "devDependencies": { + "@corbits/agent-directory-tools": "workspace:*", + "@corbits/connections": "workspace:*", + "@corbits/connections-tools": "workspace:*", + "@corbits/interaction-tools": "workspace:*", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + "@intx/workflow": "0.3.0", + }, + }, + "agents/jimmy": { + "name": "@corbits/jimmy-agent", + "version": "0.0.3", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, + "agents/scout": { + "name": "@corbits/scout-agent", + "version": "0.0.6", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", + "@corbits/web-search-tools": "workspace:*", + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, + }, "apps/hub": { "name": "@workbench/hub", "version": "0.0.1", @@ -184,26 +241,6 @@ "vite": "^7.1.0", }, }, - "packages/access-tools": { - "name": "@corbits/access-tools", - "version": "0.0.6", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@corbits/agent-directory": "workspace:*", - "@corbits/agent-directory-tools": "workspace:*", - "@corbits/tool-registry-publish": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@corbits/error-sink": "0.0.2", - "@intx/agent": "0.3.0", - "@intx/authz": "0.3.0", - "@intx/types": "0.3.0", - }, - }, "packages/agent-directory": { "name": "@corbits/agent-directory", "version": "0.0.1", @@ -238,21 +275,6 @@ "typescript": "catalog:", }, }, - "packages/agent-directory-tools": { - "name": "@corbits/agent-directory-tools", - "version": "0.0.7", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, "packages/agent-events": { "name": "@corbits/agent-events", "version": "0.0.1", @@ -355,37 +377,6 @@ "typescript": "catalog:", }, }, - "packages/capability-tools": { - "name": "@corbits/capability-tools", - "version": "0.0.7", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, - "packages/catalog-tools": { - "name": "@corbits/catalog-tools", - "version": "0.1.1", - "dependencies": { - "@corbits/inference-catalog": "0.1.0", - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, "packages/chat": { "name": "@corbits/chat", "version": "0.0.1", @@ -560,21 +551,6 @@ "typescript": "catalog:", }, }, - "packages/connections-tools": { - "name": "@corbits/connections-tools", - "version": "0.0.12", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, "packages/context-menu": { "name": "@corbits/context-menu", "version": "0.0.1", @@ -678,21 +654,6 @@ "typescript": "catalog:", }, }, - "packages/github-tools": { - "name": "@corbits/github-tools", - "version": "0.0.11", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, "packages/gotenberg-render": { "name": "@corbits/gotenberg-render", "version": "0.0.1", @@ -705,26 +666,6 @@ "typescript": "catalog:", }, }, - "packages/granola-tools": { - "name": "@corbits/granola-tools", - "version": "0.0.5", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@corbits/credential-providers": "workspace:*", - "@intx/authz": "0.3.0", - "@intx/crypto": "0.3.0", - "@intx/db": "workspace:*", - "@intx/harness": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, "packages/hub-api-client": { "name": "@corbits/hub-api-client", "version": "0.0.1", @@ -753,14 +694,9 @@ "name": "@corbits/inbox", "version": "0.0.1", "dependencies": { - "@corbits/error-sink": "workspace:*", "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/notify": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/log": "0.3.0", - "arktype": "catalog:", "drizzle-orm": "catalog:", - "hono": "catalog:", "postgres": "catalog:", }, "devDependencies": { @@ -799,86 +735,6 @@ "typescript": "catalog:", }, }, - "packages/interaction-tools": { - "name": "@corbits/interaction-tools", - "version": "0.0.11", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@corbits/error-sink": "0.0.2", - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, - "packages/jimmy-agent": { - "name": "@corbits/jimmy-agent", - "version": "0.0.3", - "dependencies": { - "@intx/agent": "workspace:*", - "@intx/types": "workspace:*", - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, - "packages/linear-tools": { - "name": "@corbits/linear-tools", - "version": "0.0.5", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@corbits/credential-providers": "workspace:*", - "@intx/authz": "0.3.0", - "@intx/crypto": "0.3.0", - "@intx/db": "workspace:*", - "@intx/harness": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, - "packages/manus-tools": { - "name": "@corbits/manus-tools", - "version": "0.0.12", - "dependencies": { - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, - "packages/mcp-tools": { - "name": "@corbits/mcp-tools", - "version": "0.0.12", - "dependencies": { - "@modelcontextprotocol/sdk": "catalog:", - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - }, - }, "packages/migration-runner": { "name": "@corbits/migration-runner", "version": "0.0.1", @@ -1008,108 +864,277 @@ "typescript": "catalog:", }, }, - "packages/provider-pricing": { - "name": "@corbits/provider-pricing", - "version": "0.0.1", + "packages/provider-pricing": { + "name": "@corbits/provider-pricing", + "version": "0.0.1", + "dependencies": { + "@corbits/connections": "workspace:*", + "@intx/inference-catalog": "0.3.0", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/sandbox-sidecar": { + "name": "@corbits/sandbox-sidecar", + "version": "0.0.1", + "dependencies": { + "@intx/crypto": "0.3.0", + "@intx/hub-sessions": "workspace:*", + "@intx/log": "0.3.0", + "@intx/types": "workspace:*", + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/settings-ui": { + "name": "@corbits/settings-ui", + "version": "0.0.1", + "dependencies": { + "@corbits/api-query": "workspace:*", + "@corbits/bench-ui": "workspace:*", + "@corbits/chat-ui": "workspace:*", + "@corbits/connections": "workspace:*", + "@corbits/error-sink": "workspace:*", + "@corbits/icons": "workspace:*", + "@corbits/inference-settings": "workspace:*", + "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", + "@corbits/workflows": "workspace:*", + "@intx/types": "workspace:*", + "arktype": "catalog:", + "react": "^19.2.0", + "react-dom": "^19.2.0", + }, + "devDependencies": { + "@happy-dom/global-registrator": "^20.11.2", + "@types/bun": "catalog:", + "@types/react": "^19.2.2", + "@types/react-dom": "catalog:", + "react-dom": "^19.2.0", + "typescript": "catalog:", + }, + }, + "packages/shell-layout": { + "name": "@corbits/shell-layout", + "version": "0.0.1", + "dependencies": { + "react": "^19.2.0", + }, + "devDependencies": { + "@happy-dom/global-registrator": "^20.11.2", + "@types/bun": "catalog:", + "@types/react": "^19.2.2", + "@types/react-dom": "catalog:", + "react-dom": "^19.2.0", + "typescript": "catalog:", + }, + }, + "packages/slug": { + "name": "@corbits/slug", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/text-diff": { + "name": "@corbits/text-diff", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/tool-registry-publish": { + "name": "@corbits/tool-registry-publish", + "version": "0.0.1", + "dependencies": { + "@intx/inference": "workspace:*", + "@intx/log": "0.3.0", + "@intx/types": "workspace:*", + "arktype": "catalog:", + "tar": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/turn-artifacts": { + "name": "@corbits/turn-artifacts", + "version": "0.0.1", + "dependencies": { + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/url-path": { + "name": "@corbits/url-path", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/webhook-triggers": { + "name": "@corbits/webhook-triggers", + "version": "0.0.1", + "dependencies": { + "@corbits/agent-runtime": "workspace:*", + "@corbits/error-sink": "workspace:*", + "@corbits/migration-runner": "workspace:*", + "@corbits/workflows": "workspace:*", + "@intx/db": "workspace:*", + "@intx/hub-api": "workspace:*", + "@intx/hub-common": "0.3.0", + "@intx/hub-sessions": "workspace:*", + "@intx/log": "0.3.0", + "@intx/types": "workspace:*", + "arktype": "catalog:", + "drizzle-orm": "catalog:", + "hono": "catalog:", + "postgres": "catalog:", + }, + "devDependencies": { + "@intx/crypto": "0.3.0", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/workflow-schedule": { + "name": "@corbits/workflow-schedule", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/workflows": { + "name": "@corbits/workflows", + "version": "0.0.1", + "dependencies": { + "@corbits/error-sink": "workspace:*", + "@corbits/workflow-schedule": "workspace:*", + "@intx/authz": "0.3.0", + "@intx/db": "workspace:*", + "@intx/hub-api": "workspace:*", + "@intx/hub-common": "0.3.0", + "@intx/hub-sessions": "workspace:*", + "@intx/types": "workspace:*", + "@intx/workflow-deploy": "workspace:*", + "arktype": "catalog:", + "cronstrue": "^3.24.0", + "drizzle-orm": "catalog:", + "hono": "catalog:", + }, + "devDependencies": { + "@corbits/connections": "workspace:*", + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "packages/xai-provider": { + "name": "@corbits/xai-provider", + "version": "0.1.0", + "dependencies": { + "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core#v0.1.1", + "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses#v0.1.0", + "@intx/inference": "workspace:*", + "@intx/types": "workspace:*", + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, + "skills/skills": { + "name": "@corbits/skills", + "version": "0.0.2", "dependencies": { - "@corbits/connections": "workspace:*", - "@intx/inference-catalog": "0.3.0", + "arktype": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, }, - "packages/reddit-tools": { - "name": "@corbits/reddit-tools", - "version": "0.0.2", + "tools/access": { + "name": "@corbits/access-tools", + "version": "0.0.6", "dependencies": { "arktype": "catalog:", }, "devDependencies": { + "@corbits/agent-directory": "workspace:*", + "@corbits/agent-directory-tools": "workspace:*", + "@corbits/tool-registry-publish": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, "peerDependencies": { + "@corbits/error-sink": "0.0.2", "@intx/agent": "0.3.0", + "@intx/authz": "0.3.0", "@intx/types": "0.3.0", }, }, - "packages/sandbox-sidecar": { - "name": "@corbits/sandbox-sidecar", - "version": "0.0.1", + "tools/agent-directory": { + "name": "@corbits/agent-directory-tools", + "version": "0.0.7", "dependencies": { - "@intx/crypto": "0.3.0", - "@intx/hub-sessions": "workspace:*", - "@intx/log": "0.3.0", - "@intx/types": "workspace:*", "arktype": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, - "packages/scout-agent": { - "name": "@corbits/scout-agent", - "version": "0.0.6", + "tools/capability": { + "name": "@corbits/capability-tools", + "version": "0.0.7", "dependencies": { - "@intx/agent": "workspace:*", - "@intx/types": "workspace:*", "arktype": "catalog:", }, "devDependencies": { - "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", - "@corbits/web-search-tools": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, - "packages/settings-ui": { - "name": "@corbits/settings-ui", - "version": "0.0.1", + "tools/catalog": { + "name": "@corbits/catalog-tools", + "version": "0.1.1", "dependencies": { - "@corbits/api-query": "workspace:*", - "@corbits/bench-ui": "workspace:*", - "@corbits/chat-ui": "workspace:*", - "@corbits/connections": "workspace:*", - "@corbits/error-sink": "workspace:*", - "@corbits/icons": "workspace:*", - "@corbits/inference-settings": "workspace:*", - "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", - "@corbits/workflows": "workspace:*", - "@intx/types": "workspace:*", + "@corbits/inference-catalog": "0.1.0", "arktype": "catalog:", - "react": "^19.2.0", - "react-dom": "^19.2.0", }, "devDependencies": { - "@happy-dom/global-registrator": "^20.11.2", "@types/bun": "catalog:", - "@types/react": "^19.2.2", - "@types/react-dom": "catalog:", - "react-dom": "^19.2.0", "typescript": "catalog:", }, - }, - "packages/shell-layout": { - "name": "@corbits/shell-layout", - "version": "0.0.1", - "dependencies": { - "react": "^19.2.0", - }, - "devDependencies": { - "@happy-dom/global-registrator": "^20.11.2", - "@types/bun": "catalog:", - "@types/react": "^19.2.2", - "@types/react-dom": "catalog:", - "react-dom": "^19.2.0", - "typescript": "catalog:", + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", }, }, - "packages/skills": { - "name": "@corbits/skills", - "version": "0.0.2", + "tools/connections": { + "name": "@corbits/connections-tools", + "version": "0.0.12", "dependencies": { "arktype": "catalog:", }, @@ -1117,10 +1142,14 @@ "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, - "packages/skills-tools": { - "name": "@corbits/skills-tools", - "version": "0.0.8", + "tools/github": { + "name": "@corbits/github-tools", + "version": "0.0.11", "dependencies": { "arktype": "catalog:", }, @@ -1133,44 +1162,54 @@ "@intx/types": "0.3.0", }, }, - "packages/slug": { - "name": "@corbits/slug", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", + "tools/granola": { + "name": "@corbits/granola-tools", + "version": "0.0.5", + "dependencies": { + "arktype": "catalog:", }, - }, - "packages/text-diff": { - "name": "@corbits/text-diff", - "version": "0.0.1", "devDependencies": { + "@corbits/credential-providers": "workspace:*", + "@intx/authz": "0.3.0", + "@intx/crypto": "0.3.0", + "@intx/db": "workspace:*", + "@intx/harness": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, - "packages/tool-registry-publish": { - "name": "@corbits/tool-registry-publish", - "version": "0.0.1", + "tools/interaction": { + "name": "@corbits/interaction-tools", + "version": "0.0.11", "dependencies": { - "@intx/inference": "workspace:*", - "@intx/log": "0.3.0", - "@intx/types": "workspace:*", "arktype": "catalog:", - "tar": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@corbits/error-sink": "0.0.2", + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, - "packages/tools-skills": { - "name": "@corbits/tools-skills", - "version": "0.0.4", + "tools/linear": { + "name": "@corbits/linear-tools", + "version": "0.0.5", "dependencies": { "arktype": "catalog:", }, "devDependencies": { + "@corbits/credential-providers": "workspace:*", + "@intx/authz": "0.3.0", + "@intx/crypto": "0.3.0", + "@intx/db": "workspace:*", + "@intx/harness": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, @@ -1179,9 +1218,9 @@ "@intx/types": "0.3.0", }, }, - "packages/turn-artifacts": { - "name": "@corbits/turn-artifacts", - "version": "0.0.1", + "tools/manus": { + "name": "@corbits/manus-tools", + "version": "0.0.12", "dependencies": { "arktype": "catalog:", }, @@ -1189,19 +1228,16 @@ "@types/bun": "catalog:", "typescript": "catalog:", }, - }, - "packages/url-path": { - "name": "@corbits/url-path", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", }, }, - "packages/web-search-tools": { - "name": "@corbits/web-search-tools", - "version": "0.0.4", + "tools/mcp": { + "name": "@corbits/mcp-tools", + "version": "0.0.12", "dependencies": { + "@modelcontextprotocol/sdk": "catalog:", "arktype": "catalog:", }, "devDependencies": { @@ -1213,33 +1249,23 @@ "@intx/types": "0.3.0", }, }, - "packages/webhook-triggers": { - "name": "@corbits/webhook-triggers", - "version": "0.0.1", + "tools/reddit": { + "name": "@corbits/reddit-tools", + "version": "0.0.2", "dependencies": { - "@corbits/agent-runtime": "workspace:*", - "@corbits/error-sink": "workspace:*", - "@corbits/migration-runner": "workspace:*", - "@corbits/workflows": "workspace:*", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/hub-common": "0.3.0", - "@intx/hub-sessions": "workspace:*", - "@intx/log": "0.3.0", - "@intx/types": "workspace:*", "arktype": "catalog:", - "drizzle-orm": "catalog:", - "hono": "catalog:", - "postgres": "catalog:", }, "devDependencies": { - "@intx/crypto": "0.3.0", "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, - "packages/workflow-authoring-tools": { - "name": "@corbits/workflow-authoring-tools", + "tools/skills-tools": { + "name": "@corbits/skills-tools", "version": "0.0.8", "dependencies": { "arktype": "catalog:", @@ -1253,52 +1279,50 @@ "@intx/types": "0.3.0", }, }, - "packages/workflow-schedule": { - "name": "@corbits/workflow-schedule", - "version": "0.0.1", + "tools/tools-skills": { + "name": "@corbits/tools-skills", + "version": "0.0.4", + "dependencies": { + "arktype": "catalog:", + }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, - "packages/workflows": { - "name": "@corbits/workflows", - "version": "0.0.1", + "tools/web-search": { + "name": "@corbits/web-search-tools", + "version": "0.0.4", "dependencies": { - "@corbits/error-sink": "workspace:*", - "@corbits/workflow-schedule": "workspace:*", - "@intx/authz": "0.3.0", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/hub-common": "0.3.0", - "@intx/hub-sessions": "workspace:*", - "@intx/types": "workspace:*", - "@intx/workflow-deploy": "workspace:*", "arktype": "catalog:", - "cronstrue": "^3.24.0", - "drizzle-orm": "catalog:", - "hono": "catalog:", }, "devDependencies": { - "@corbits/connections": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, - "packages/xai-provider": { - "name": "@corbits/xai-provider", - "version": "0.1.0", + "tools/workflow-authoring": { + "name": "@corbits/workflow-authoring-tools", + "version": "0.0.8", "dependencies": { - "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core#v0.1.1", - "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses#v0.1.0", - "@intx/inference": "workspace:*", - "@intx/types": "workspace:*", "arktype": "catalog:", }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", }, + "peerDependencies": { + "@intx/agent": "0.3.0", + "@intx/types": "0.3.0", + }, }, "vendor/intx/agent": { "name": "@intx/agent", @@ -1561,27 +1585,6 @@ "typescript": "catalog:", }, }, - "workflows/assistant": { - "name": "@corbits/assistant-workflow", - "version": "0.0.2", - "dependencies": { - "@corbits/catalog-tools": "0.1.1", - "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", - }, - "devDependencies": { - "@corbits/agent-directory-tools": "workspace:*", - "@corbits/connections": "workspace:*", - "@corbits/connections-tools": "workspace:*", - "@corbits/interaction-tools": "workspace:*", - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0", - "@intx/workflow": "0.3.0", - }, - }, "workflows/attio-task-agent": { "name": "@corbits/attio-task-agent-workflow", "version": "0.0.1", @@ -1927,11 +1930,11 @@ "@connectrpc/connect-web": ["@connectrpc/connect-web@2.2.0", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.7.0", "@connectrpc/connect": "2.2.0" } }, "sha512-uuBUcBTYfb/r1o98LzCCZk1H90yx0vmZ5m21dpw/q4EUwsKwkCDGI8H0aEdjcZld+Q2jJ5A5ADvPqfb6lzQ/sg=="], - "@corbits/access-tools": ["@corbits/access-tools@workspace:packages/access-tools"], + "@corbits/access-tools": ["@corbits/access-tools@workspace:tools/access"], "@corbits/agent-directory": ["@corbits/agent-directory@workspace:packages/agent-directory"], - "@corbits/agent-directory-tools": ["@corbits/agent-directory-tools@workspace:packages/agent-directory-tools"], + "@corbits/agent-directory-tools": ["@corbits/agent-directory-tools@workspace:tools/agent-directory"], "@corbits/agent-events": ["@corbits/agent-events@workspace:packages/agent-events"], @@ -1945,15 +1948,15 @@ "@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#5309a15", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "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-5309a15", "sha512-zGHkq++juclUrOd0O8/Z5ziTldmJZ6gqyH/c9cs5S10UPU4gqn8CHwq174SiJXY0ujJ3cNpJXnrr+0fcqlfQ1w=="], - "@corbits/assistant-workflow": ["@corbits/assistant-workflow@workspace:workflows/assistant"], + "@corbits/assistant-workflow": ["@corbits/assistant-workflow@workspace:agents/assistant"], "@corbits/attio-task-agent-workflow": ["@corbits/attio-task-agent-workflow@workspace:workflows/attio-task-agent"], "@corbits/bench-ui": ["@corbits/bench-ui@workspace:packages/bench-ui"], - "@corbits/capability-tools": ["@corbits/capability-tools@workspace:packages/capability-tools"], + "@corbits/capability-tools": ["@corbits/capability-tools@workspace:tools/capability"], - "@corbits/catalog-tools": ["@corbits/catalog-tools@workspace:packages/catalog-tools"], + "@corbits/catalog-tools": ["@corbits/catalog-tools@workspace:tools/catalog"], "@corbits/chat": ["@corbits/chat@workspace:packages/chat"], @@ -1977,7 +1980,7 @@ "@corbits/connections": ["@corbits/connections@workspace:packages/connections"], - "@corbits/connections-tools": ["@corbits/connections-tools@workspace:packages/connections-tools"], + "@corbits/connections-tools": ["@corbits/connections-tools@workspace:tools/connections"], "@corbits/context-menu": ["@corbits/context-menu@workspace:packages/context-menu"], @@ -1999,13 +2002,13 @@ "@corbits/exa-topic-watch-workflow": ["@corbits/exa-topic-watch-workflow@workspace:workflows/exa-topic-watch"], - "@corbits/github-tools": ["@corbits/github-tools@workspace:packages/github-tools"], + "@corbits/github-tools": ["@corbits/github-tools@workspace:tools/github"], "@corbits/gotenberg-render": ["@corbits/gotenberg-render@workspace:packages/gotenberg-render"], "@corbits/granola-call-workflow": ["@corbits/granola-call-workflow@workspace:workflows/granola-call"], - "@corbits/granola-tools": ["@corbits/granola-tools@workspace:packages/granola-tools"], + "@corbits/granola-tools": ["@corbits/granola-tools@workspace:tools/granola"], "@corbits/heartbeat-workflow": ["@corbits/heartbeat-workflow@workspace:workflows/heartbeat"], @@ -2019,19 +2022,19 @@ "@corbits/inference-settings": ["@corbits/inference-settings@workspace:packages/inference-settings"], - "@corbits/interaction-tools": ["@corbits/interaction-tools@workspace:packages/interaction-tools"], + "@corbits/interaction-tools": ["@corbits/interaction-tools@workspace:tools/interaction"], - "@corbits/jimmy-agent": ["@corbits/jimmy-agent@workspace:packages/jimmy-agent"], + "@corbits/jimmy-agent": ["@corbits/jimmy-agent@workspace:agents/jimmy"], "@corbits/last-30-days-research-workflow": ["@corbits/last-30-days-research-workflow@workspace:workflows/last-30-days-research"], - "@corbits/linear-tools": ["@corbits/linear-tools@workspace:packages/linear-tools"], + "@corbits/linear-tools": ["@corbits/linear-tools@workspace:tools/linear"], "@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#e85b81e", { "dependencies": { "@hono/standard-validator": "0.2.3", "@intx/crypto": "0.3.0", "@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", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-e85b81e", "sha512-ATPP/pll1sYG1DLKV1HvtboK8q1tFkqayQqRXdUmRUIsFJx5wqzd8YA1XELjXzLu1P2f2uCMe2CjsJHGv6E90Q=="], - "@corbits/manus-tools": ["@corbits/manus-tools@workspace:packages/manus-tools"], + "@corbits/manus-tools": ["@corbits/manus-tools@workspace:tools/manus"], - "@corbits/mcp-tools": ["@corbits/mcp-tools@workspace:packages/mcp-tools"], + "@corbits/mcp-tools": ["@corbits/mcp-tools@workspace:tools/mcp"], "@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=="], @@ -2063,19 +2066,19 @@ "@corbits/reddit-opportunity-scanner-workflow": ["@corbits/reddit-opportunity-scanner-workflow@workspace:workflows/reddit-opportunity-scanner"], - "@corbits/reddit-tools": ["@corbits/reddit-tools@workspace:packages/reddit-tools"], + "@corbits/reddit-tools": ["@corbits/reddit-tools@workspace:tools/reddit"], "@corbits/sandbox-sidecar": ["@corbits/sandbox-sidecar@workspace:packages/sandbox-sidecar"], - "@corbits/scout-agent": ["@corbits/scout-agent@workspace:packages/scout-agent"], + "@corbits/scout-agent": ["@corbits/scout-agent@workspace:agents/scout"], "@corbits/settings-ui": ["@corbits/settings-ui@workspace:packages/settings-ui"], "@corbits/shell-layout": ["@corbits/shell-layout@workspace:packages/shell-layout"], - "@corbits/skills": ["@corbits/skills@workspace:packages/skills"], + "@corbits/skills": ["@corbits/skills@workspace:skills/skills"], - "@corbits/skills-tools": ["@corbits/skills-tools@workspace:packages/skills-tools"], + "@corbits/skills-tools": ["@corbits/skills-tools@workspace:tools/skills-tools"], "@corbits/slug": ["@corbits/slug@workspace:packages/slug"], @@ -2083,19 +2086,19 @@ "@corbits/tool-registry-publish": ["@corbits/tool-registry-publish@workspace:packages/tool-registry-publish"], - "@corbits/tools-skills": ["@corbits/tools-skills@workspace:packages/tools-skills"], + "@corbits/tools-skills": ["@corbits/tools-skills@workspace:tools/tools-skills"], "@corbits/turn-artifacts": ["@corbits/turn-artifacts@workspace:packages/turn-artifacts"], "@corbits/url-path": ["@corbits/url-path@workspace:packages/url-path"], - "@corbits/web-search-tools": ["@corbits/web-search-tools@workspace:packages/web-search-tools"], + "@corbits/web-search-tools": ["@corbits/web-search-tools@workspace:tools/web-search"], "@corbits/webhook-triggers": ["@corbits/webhook-triggers@workspace:packages/webhook-triggers"], "@corbits/workbench-digest-workflow": ["@corbits/workbench-digest-workflow@workspace:workflows/workbench-digest"], - "@corbits/workflow-authoring-tools": ["@corbits/workflow-authoring-tools@workspace:packages/workflow-authoring-tools"], + "@corbits/workflow-authoring-tools": ["@corbits/workflow-authoring-tools@workspace:tools/workflow-authoring"], "@corbits/workflow-schedule": ["@corbits/workflow-schedule@workspace:packages/workflow-schedule"], @@ -3275,14 +3278,8 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/artifact-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/assistant-workflow/@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=="], - "@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], - "@corbits/scout-agent/@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=="], - "@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=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], diff --git a/scripts/checks/report-error-baseline.txt b/scripts/checks/report-error-baseline.txt index a362be5de7..e7040dfdce 100644 --- a/scripts/checks/report-error-baseline.txt +++ b/scripts/checks/report-error-baseline.txt @@ -77,17 +77,9 @@ apps/web/src/session.ts 3 return { apps/web/src/shell/context-menu/items.tsx 1 toast("Couldn't copy the link"); apps/web/src/shell/workbench-list.tsx 1 // Revert the optimistic title on failure; the list will refetch on apps/web/src/shell/workbench-list.tsx 1 setPinned(!next); -packages/agent-directory-tools/src/tool.ts 1 // The agent was genuinely created — that half-success must never -packages/agent-directory-tools/src/tool.ts 1 if (err instanceof CreateAgentDefinitionError) { -packages/agent-directory-tools/src/tool.ts 1 return errorResult(call.id, err); packages/agent-directory/src/definition-history.ts 1 return null; packages/approvals/src/allowance.ts 1 deps.log( packages/approvals/src/allowance.ts 1 return { outcome: "park", reason: "classification_failed" }; -packages/capability-tools/src/tool.ts 1 if (err instanceof CapabilityOutOfInventoryError) { -packages/capability-tools/src/tool.ts 1 return errorResult(call.id, err); -packages/catalog-tools/src/tool.ts 1 return errorResult(call.id, err); -packages/catalog-tools/src/tool.ts 2 return errorResult(call.id, err); -packages/catalog-tools/src/tool.ts 3 return errorResult(call.id, err); packages/chat-ui/src/composer.tsx 1 if (attachGenerationRef.current !== generation) return; packages/chat-ui/src/default-agent-workbench.ts 1 return { packages/chat-ui/src/invite-agent-dialog.tsx 1 setInviteError( @@ -111,10 +103,6 @@ packages/code-review/src/review-run.ts 1 return { reviewer, ok: false, reason: r packages/command-palette/src/recents.ts 1 // A full or disabled store (private browsing, quota) loses packages/command-palette/src/recents.ts 1 // As above — loses persistence, not function. packages/command-palette/src/recents.ts 1 return []; -packages/connections-tools/src/tool.ts 1 if (err instanceof NoOwnRoomError) { -packages/connections-tools/src/tool.ts 1 return errorResult(call.id, err); -packages/connections-tools/src/tool.ts 2 return errorResult(call.id, err); -packages/connections-tools/src/tool.ts 3 return errorResult(call.id, err); packages/connections/src/connect-github-routes.ts 1 // A repoId `state.repos` doesn't carry means the card's own packages/connections/src/connect-github-routes.ts 1 // The GitHub client's own errors (transport, HTTP status, shape packages/connections/src/credential-test.ts 1 // Not JSON, or didn't match either shape — fall through to the @@ -122,60 +110,19 @@ packages/connections/src/credential-test.ts 1 return []; packages/connections/src/credential-test.ts 1 return undefined; packages/connections/src/credential-test.ts 1 return { packages/error-sink/src/index.ts 1 return context.refId ?? UNKNOWN_OPERATION; -packages/github-tools/src/pull-request-tools.ts 1 return errorResult(call.id, failureMessage(err)); -packages/github-tools/src/pull-request-tools.ts 1 return null; -packages/github-tools/src/pull-request-tools.ts 2 return errorResult(call.id, failureMessage(err)); -packages/github-tools/src/tool.ts 1 return null; -packages/github-tools/src/tool.ts 1 return { -packages/granola-tools/src/tool.ts 1 return null; -packages/granola-tools/src/tool.ts 1 return { -packages/granola-tools/src/tool.ts 2 return { -packages/jimmy-agent/src/gif-search-tool.ts 1 return false; -packages/jimmy-agent/src/gif-search-tool.ts 1 return null; -packages/jimmy-agent/src/gif-search-tool.ts 1 return { -packages/linear-tools/src/tool.ts 1 return null; -packages/linear-tools/src/tool.ts 1 return { -packages/manus-tools/src/tool.ts 1 return errorResult(call.id, err); -packages/manus-tools/src/tool.ts 1 return null; -packages/manus-tools/src/tool.ts 2 return errorResult(call.id, err); -packages/manus-tools/src/tool.ts 3 return errorResult(call.id, err); -packages/manus-tools/src/tool.ts 4 return errorResult(call.id, err); -packages/manus-tools/src/tool.ts 5 return errorResult(call.id, err); -packages/manus-tools/src/tool.ts 6 return errorResult(call.id, err); -packages/mcp-tools/src/tool.ts 1 return errorResult(call.id, err); -packages/mcp-tools/src/tool.ts 1 return errorResult(call.id, new Error(`"${pattern}" is not a valid regex: ${String(err)}`)); -packages/mcp-tools/src/tool.ts 1 return { -packages/mcp-tools/src/tool.ts 2 return errorResult(call.id, err); -packages/mcp-tools/src/tool.ts 3 return errorResult(call.id, err); -packages/mcp-tools/src/tool.ts 4 return errorResult(call.id, err); packages/mocks/src/ollama/capture.ts 1 return raw; packages/ollama-adapter/src/inline-tool-json.ts 1 return false; packages/ollama-adapter/src/inline-tool-json.ts 1 return { kind: "incomplete" }; packages/onboarding/src/pending-seed.ts 1 return drop(); packages/onboarding/src/routes.ts 1 // Neither `ProvisionError` nor `CliError` messages are safe to show packages/onboarding/src/routes.ts 1 return c.json( -packages/reddit-tools/src/tool.ts 1 return null; -packages/reddit-tools/src/tool.ts 1 return { -packages/reddit-tools/src/tool.ts 2 return { packages/sandbox-sidecar/src/provisioner.ts 1 log.error`failed to sweep obsolete unit ${externalRef} for allocation ${allocationId}: ${ packages/sandbox-sidecar/src/provisioner.ts 1 return rejected(...classify(error, "destroy_unit_failed")); packages/sandbox-sidecar/src/provisioner.ts 1 return rejected(...classify(error, "start_unit_failed")); -packages/scout-agent/src/artifact-tool.ts 1 return { -packages/scout-agent/src/artifact-tool.ts 2 return { packages/settings-ui/src/account-section.tsx 1 toast(SETTINGS_STRINGS.accountEmailCopyError); -packages/skills-tools/src/tool.ts 1 return errorResult(call.id, err); -packages/skills-tools/src/tool.ts 2 return errorResult(call.id, err); -packages/skills-tools/src/tool.ts 3 return errorResult(call.id, err); -packages/skills-tools/src/tool.ts 4 return errorResult(call.id, err); -packages/skills-tools/src/tool.ts 5 return errorResult(call.id, err); packages/tool-registry-publish/src/freshness-check.ts 1 log.error`${err instanceof Error ? err.message : String(err)}`; -packages/tools-skills/src/tool.ts 1 return errorResult(call.id, err); -packages/tools-skills/src/tool.ts 2 return errorResult(call.id, err); -packages/tools-skills/src/tool.ts 3 return errorResult(call.id, err); packages/turn-artifacts/src/index.ts 1 return []; packages/url-path/src/decoded-or-null.ts 1 return null; -packages/web-search-tools/src/tool.ts 1 return null; -packages/web-search-tools/src/tool.ts 1 return { packages/webhook-triggers/src/ingress-routes.ts 1 return c.json( packages/webhook-triggers/src/signature.ts 1 return false; workflows/attio-task-agent/src/finalize-tool.ts 1 return {