diff --git a/apps/hub/package.json b/apps/hub/package.json
index 07de99b61d..e36de88288 100644
--- a/apps/hub/package.json
+++ b/apps/hub/package.json
@@ -25,12 +25,10 @@
"@corbits/e2b-sandbox-sidecar": "workspace:*",
"@corbits/error-sink": "workspace:*",
"@corbits/hub-api-client": "workspace:*",
- "@corbits/inbox": "workspace:*",
"@corbits/inference-catalog": "workspace:*",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38",
"@corbits/mcp-tools": "workspace:*",
"@corbits/memory": "github:corbitsdev/corbits-memory#e74da20f148a302dff5400915fe504ee2395e913",
- "@corbits/notify": "workspace:*",
"@corbits/process-provisioner": "workspace:*",
"@corbits/skills": "workspace:*",
"@corbits/tool-registry-publish": "workspace:*",
diff --git a/apps/web/package.json b/apps/web/package.json
index fa2c4cf205..de02c71498 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -30,7 +30,6 @@
"@corbits/context-menu": "workspace:*",
"@corbits/error-sink": "workspace:*",
"@corbits/icons": "workspace:*",
- "@corbits/inbox": "workspace:*",
"@corbits/inference-settings": "workspace:*",
"@corbits/plugins-ui": "workspace:*",
"@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635",
diff --git a/bun.lock b/bun.lock
index 69552cd6d8..7bb66956f2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -12,6 +12,7 @@
"@corbits/codex-provider": "workspace:*",
"@corbits/connections": "workspace:*",
"@corbits/evals": "workspace:*",
+ "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38",
"@corbits/ollama-adapter": "workspace:*",
"@corbits/webhook-triggers": "workspace:*",
"@corbits/xai-provider": "workspace:*",
@@ -67,12 +68,10 @@
"@corbits/e2b-sandbox-sidecar": "workspace:*",
"@corbits/error-sink": "workspace:*",
"@corbits/hub-api-client": "workspace:*",
- "@corbits/inbox": "workspace:*",
"@corbits/inference-catalog": "workspace:*",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38",
"@corbits/mcp-tools": "workspace:*",
"@corbits/memory": "github:corbitsdev/corbits-memory#e74da20f148a302dff5400915fe504ee2395e913",
- "@corbits/notify": "workspace:*",
"@corbits/process-provisioner": "workspace:*",
"@corbits/skills": "workspace:*",
"@corbits/tool-registry-publish": "workspace:*",
@@ -155,7 +154,6 @@
"@corbits/context-menu": "workspace:*",
"@corbits/error-sink": "workspace:*",
"@corbits/icons": "workspace:*",
- "@corbits/inbox": "workspace:*",
"@corbits/inference-settings": "workspace:*",
"@corbits/plugins-ui": "workspace:*",
"@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635",
@@ -529,18 +527,6 @@
"typescript": "catalog:",
},
},
- "packages/gotenberg-render": {
- "name": "@corbits/gotenberg-render",
- "version": "0.0.1",
- "dependencies": {
- "@corbits/error-sink": "workspace:*",
- "arktype": "catalog:",
- },
- "devDependencies": {
- "@types/bun": "catalog:",
- "typescript": "catalog:",
- },
- },
"packages/hub-api-client": {
"name": "@corbits/hub-api-client",
"version": "0.0.1",
@@ -565,22 +551,6 @@
"typescript": "catalog:",
},
},
- "packages/inbox": {
- "name": "@corbits/inbox",
- "version": "0.0.1",
- "dependencies": {
- "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38",
- "@corbits/notify": "workspace:*",
- "drizzle-orm": "catalog:",
- "postgres": "catalog:",
- },
- "devDependencies": {
- "@intx/db": "workspace:*",
- "@intx/hub-common": "0.3.0",
- "@types/bun": "catalog:",
- "typescript": "catalog:",
- },
- },
"packages/inference-catalog": {
"name": "@corbits/inference-catalog",
"version": "0.1.0",
@@ -622,33 +592,6 @@
"typescript": "catalog:",
},
},
- "packages/mocks": {
- "name": "@corbits/mocks",
- "version": "0.0.1",
- "dependencies": {
- "arktype": "catalog:",
- },
- "devDependencies": {
- "@types/bun": "catalog:",
- "typescript": "catalog:",
- },
- },
- "packages/notify": {
- "name": "@corbits/notify",
- "version": "0.0.1",
- "dependencies": {
- "@intx/authz": "0.3.0",
- "@intx/hub-common": "0.3.0",
- "@intx/types": "workspace:*",
- "arktype": "catalog:",
- "drizzle-orm": "catalog:",
- "postgres": "catalog:",
- },
- "devDependencies": {
- "@types/bun": "catalog:",
- "typescript": "catalog:",
- },
- },
"packages/ollama-adapter": {
"name": "@corbits/ollama-adapter",
"version": "0.0.2",
@@ -700,18 +643,6 @@
"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",
@@ -1576,16 +1507,12 @@
"@corbits/github-tools": ["@corbits/github-tools@workspace:tools/github"],
- "@corbits/gotenberg-render": ["@corbits/gotenberg-render@workspace:packages/gotenberg-render"],
-
"@corbits/granola-tools": ["@corbits/granola-tools@workspace:tools/granola"],
"@corbits/hub-api-client": ["@corbits/hub-api-client@workspace:packages/hub-api-client"],
"@corbits/icons": ["@corbits/icons@workspace:packages/icons"],
- "@corbits/inbox": ["@corbits/inbox@workspace:packages/inbox"],
-
"@corbits/inference-catalog": ["@corbits/inference-catalog@workspace:packages/inference-catalog"],
"@corbits/inference-settings": ["@corbits/inference-settings@workspace:packages/inference-settings"],
@@ -1604,10 +1531,6 @@
"@corbits/migration-runner": ["@corbits/migration-runner@workspace:packages/migration-runner"],
- "@corbits/mocks": ["@corbits/mocks@workspace:packages/mocks"],
-
- "@corbits/notify": ["@corbits/notify@workspace:packages/notify"],
-
"@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=="],
"@corbits/ollama-adapter": ["@corbits/ollama-adapter@workspace:packages/ollama-adapter"],
@@ -1618,8 +1541,6 @@
"@corbits/process-provisioner": ["@corbits/process-provisioner@workspace:packages/process-provisioner"],
- "@corbits/provider-pricing": ["@corbits/provider-pricing@workspace:packages/provider-pricing"],
-
"@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/reddit-tools": ["@corbits/reddit-tools@workspace:tools/reddit"],
@@ -2848,6 +2769,8 @@
"@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/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#e74da20", { "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-e74da20", "sha512-/uA9xTcF7XwMXgnR8OZamierzkLG4g4mhw3u9Fl3Uj6ait8pH3wcoQ6V32on1nMdaqtPRdYVFnxof5O+4ZacGA=="],
"better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="],
diff --git a/package.json b/package.json
index b5b06dd3d8..36131e2763 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"@intx/crypto": "workspace:*",
"@intx/log": "workspace:*",
"@corbits/connections": "workspace:*",
+ "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38",
"@intx/mime": "0.3.0",
"@intx/types": "0.3.0",
"@types/bun": "1.3.14",
diff --git a/packages/chat-ui/src/mailbox-timeline.ts b/packages/chat-ui/src/mailbox-timeline.ts
index 1076ac8be0..22025010c8 100644
--- a/packages/chat-ui/src/mailbox-timeline.ts
+++ b/packages/chat-ui/src/mailbox-timeline.ts
@@ -6,14 +6,14 @@
//
// 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.
+// responses with local arktype schemas rather than a package client,
+// since importing `@corbits/mailbox`'s server-only `migrations.ts` (a
+// `node:crypto` user) at the value level is something 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
diff --git a/packages/gotenberg-render/LICENSE b/packages/gotenberg-render/LICENSE
deleted file mode 100644
index 173c3a21d0..0000000000
--- a/packages/gotenberg-render/LICENSE
+++ /dev/null
@@ -1,176 +0,0 @@
-GNU LESSER GENERAL PUBLIC LICENSE
-
-Version 2.1, February 1999
-
-Copyright (C) 1991, 1999 Free Software Foundation, Inc.
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]
-
-Preamble
-
-The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
-
-This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
-
-When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
-
-To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
-
-For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
-
-We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
-
-To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
-
-Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
-
-Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
-
-When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
-
-We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
-
-For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
-
-In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
-
-Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
-
-The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
-
-GNU LESSER GENERAL PUBLIC LICENSE
-TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
-
-A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
-
-The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
-
-"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
-
-Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
-
-1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
-
-You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
-
-2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
-
- a) The modified work must itself be a software library.
-
- b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
-
- c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
-
- d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
-
-(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
-
-3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
-
-Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
-
-This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
-
-4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
-
-If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
-
-5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
-
-However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
-
-When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
-
-If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
-
-Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
-
-6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
-
-You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
-
- a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
-
- b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
-
- c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
-
- d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
-
- e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
-
-For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
-
-It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
-
-7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
-
- a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
-
- b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
-
-8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
-
-9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
-
-10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
-
-11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
-
-This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
-
-12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
-
-13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
-
-14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
-
-NO WARRANTY
-
-15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-END OF TERMS AND CONDITIONS
-
-How to Apply These Terms to Your New Libraries
-
-If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
-
-To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
-
- one line to give the library's name and an idea of what it does.
- Copyright (C) year name of author
-
- This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License.
-
- This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail.
-
-You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
-
-Yoyodyne, Inc., hereby disclaims all copyright interest in
-the library `Frob' (a library for tweaking knobs) written
-by James Random Hacker.
-
-signature of Ty Coon, 1 April 1990
-Ty Coon, President of Vice
-That's all there is to it!
diff --git a/packages/gotenberg-render/README.md b/packages/gotenberg-render/README.md
deleted file mode 100644
index 59c15695b1..0000000000
--- a/packages/gotenberg-render/README.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# @corbits/gotenberg-render
-
-Renders a Markdown [Library](../artifacts-hub) artifact to PDF through an
-operator-configured [Gotenberg](https://gotenberg.dev) server (CL-6499),
-and hands the resulting bytes to a caller-supplied sink to persist as a
-new Library artifact — no new download mechanism, no artifact-shaped
-document invented along the way.
-
-## What this package does
-
-- `resolveGotenbergConfig(env)` — reads `GOTENBERG_URL` and returns
- `{ baseUrl }`, or `null` when it's unset. `null` means the render
- capability is simply absent for this bench: callers must check for it
- before ever offering a "Download as PDF" action, rather than showing a
- button that errors or no-ops when clicked.
-- `renderMarkdownToPdf(config, { title, markdown })` — POSTs the Markdown
- to Gotenberg's Chromium `convert/markdown` route and returns the PDF as
- `Uint8Array`. Throws `GotenbergRenderError` on any transport or non-2xx
- failure.
-- `renderMarkdownArtifactToPdf(config, source, sink, context)` —
- orchestrates the above end to end: renders, then calls `sink.savePdf`
- to persist the PDF as a new artifact. On any failure it reports through
- `@corbits/error-sink` and returns a plain-language message
- ("Couldn't build the PDF. Try again shortly.") plus a `refId` — never a
- raw HTTP status or internal error to the person waiting on their brief.
-
-`sink` and the Markdown `source` are both injected ports, not a concrete
-artifact store: the caller wires in however it already reads a Library
-artifact's content and however it already persists a new one (the tenant
-Library's multipart `/upload` route being the one that currently accepts
-arbitrary binary content — see "What's still missing" below).
-
-## Turning this on
-
-1. **Run a Gotenberg server.** It's normally a container; the official
- image needs no extra configuration for Markdown-to-PDF:
-
- ```sh
- docker run --rm -p 3000:3000 gotenberg/gotenberg:8
- ```
-
- Point it at wherever you run containers for this bench (a sidecar
- service, a small Railway service, etc.) — Gotenberg is stateless, so it
- scales horizontally with zero shared state.
-
-2. **Set `GOTENBERG_URL`** on the process that calls this package to that
- server's base URL, e.g. `http://gotenberg:3000` or
- `https://gotenberg.internal.example.com`. Leaving it unset (the
- default) keeps the PDF-render capability off entirely — nothing here
- errors at startup either way.
-
-3. **Wire a `PdfArtifactSink`** that persists the returned bytes as a new
- Library artifact with `mimeType: "application/pdf"`, and a source that
- reads the Markdown artifact's content, then call
- `renderMarkdownArtifactToPdf`.
-
-## What's still missing for an end-to-end brief
-
-Rendering itself works against any Markdown + a running Gotenberg
-server. Two integration gaps remain before a real due-diligence brief can
-go artifact-in, PDF-artifact-out with no glue code:
-
-- The workflow-run artifact surface
- (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, the one a
- workflow run authenticates against) has `POST /` (create) and
- `GET /recent` (list) but no `GET /:id` to fetch one artifact's content
- back — so a workflow run can't yet read the Markdown brief it just
- saved in order to hand it to this package.
-- That same workflow surface stores `content` as a JSON string capped at
- 64k characters — it has no path for binary content. The only route
- today that accepts arbitrary bytes (`POST /upload` on the tenant
- Library surface) is authenticated by browser tenant session, not a
- workflow run's sidecar token. Persisting a rendered PDF from inside a
- workflow run needs one of those two surfaces extended; this package's
- `PdfArtifactSink` port is deliberately shaped so that extension is a
- drop-in once it exists.
diff --git a/packages/gotenberg-render/package.json b/packages/gotenberg-render/package.json
deleted file mode 100644
index 88c9ae8d51..0000000000
--- a/packages/gotenberg-render/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "@corbits/gotenberg-render",
- "version": "0.0.1",
- "private": true,
- "description": "Renders a Markdown Library artifact to PDF via an operator-configured Gotenberg server (CL-6499): config resolution that treats an unset endpoint as an absent capability, a thin Gotenberg HTTP client, and an orchestration step that reports failures through @corbits/error-sink",
- "license": "LGPL-2.1-only",
- "type": "module",
- "exports": {
- ".": "./src/index.ts"
- },
- "scripts": {
- "typecheck": "tsc --noEmit",
- "test": "bun test"
- },
- "dependencies": {
- "@corbits/error-sink": "workspace:*",
- "arktype": "catalog:"
- },
- "devDependencies": {
- "@types/bun": "catalog:",
- "typescript": "catalog:"
- }
-}
diff --git a/packages/gotenberg-render/src/client.test.ts b/packages/gotenberg-render/src/client.test.ts
deleted file mode 100644
index 739f081408..0000000000
--- a/packages/gotenberg-render/src/client.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { describe, expect, test } from "bun:test";
-import { GotenbergRenderError, renderMarkdownToPdf } from "./client";
-
-const CONFIG = { baseUrl: "http://gotenberg.internal:3000" };
-
-describe("renderMarkdownToPdf", () => {
- test("posts the markdown and index template, returning the PDF bytes", async () => {
- const pdfBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]);
- let capturedUrl: string | undefined;
- let capturedForm: FormData | undefined;
- const fetchStub = (async (url: string | URL | Request, init?: RequestInit) => {
- capturedUrl = String(url);
- capturedForm = init?.body as FormData;
- return new Response(pdfBytes, { status: 200 });
- }) as unknown as typeof fetch;
-
- const result = await renderMarkdownToPdf(
- CONFIG,
- { title: "Acme Diligence Brief", markdown: "# Hello" },
- fetchStub,
- );
-
- expect(result).toEqual(pdfBytes);
- expect(capturedUrl).toBe("http://gotenberg.internal:3000/forms/chromium/convert/markdown");
- const files = capturedForm?.getAll("files") ?? [];
- expect(files).toHaveLength(2);
- });
-
- test("wraps a network failure in GotenbergRenderError", async () => {
- const fetchStub = (async () => {
- throw new Error("ECONNREFUSED");
- }) as unknown as typeof fetch;
-
- await expect(
- renderMarkdownToPdf(CONFIG, { title: "t", markdown: "m" }, fetchStub),
- ).rejects.toBeInstanceOf(GotenbergRenderError);
- });
-
- test("wraps a non-2xx response in GotenbergRenderError", async () => {
- const fetchStub = (async () =>
- new Response("boom", { status: 500 })) as unknown as typeof fetch;
-
- await expect(
- renderMarkdownToPdf(CONFIG, { title: "t", markdown: "m" }, fetchStub),
- ).rejects.toBeInstanceOf(GotenbergRenderError);
- });
-});
diff --git a/packages/gotenberg-render/src/client.ts b/packages/gotenberg-render/src/client.ts
deleted file mode 100644
index 1d88b7324b..0000000000
--- a/packages/gotenberg-render/src/client.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-// Talks to one Gotenberg server (https://gotenberg.dev) over its Chromium
-// module's Markdown route: POST an `index.html` that inlines a Markdown
-// file via Gotenberg's `toHTML` template helper, get a rendered PDF back.
-// Gotenberg is stateless and holds no workbench data of its own — every
-// call is a one-shot conversion, nothing to provision or migrate.
-export type GotenbergFetch = typeof fetch;
-
-const MARKDOWN_ROUTE = "/forms/chromium/convert/markdown";
-
-function escapeHtml(value: string): string {
- return value
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """);
-}
-
-function indexTemplateFor(title: string): string {
- return (
- `
` +
- `${escapeHtml(title)}` +
- `{{ toHTML "body.md" }}`
- );
-}
-
-export class GotenbergRenderError extends Error {}
-
-/**
- * Converts one Markdown document to PDF bytes via a Gotenberg server.
- * Throws `GotenbergRenderError` on any transport or non-2xx failure —
- * callers report it through `@corbits/error-sink`, never swallow it.
- */
-export async function renderMarkdownToPdf(
- config: { readonly baseUrl: string },
- input: { readonly title: string; readonly markdown: string },
- fetchImpl: GotenbergFetch = fetch,
-): Promise {
- const form = new FormData();
- form.append(
- "files",
- new Blob([indexTemplateFor(input.title)], { type: "text/html" }),
- "index.html",
- );
- form.append("files", new Blob([input.markdown], { type: "text/markdown" }), "body.md");
-
- let response: Response;
- try {
- response = await fetchImpl(`${config.baseUrl}${MARKDOWN_ROUTE}`, {
- method: "POST",
- body: form,
- });
- } catch (err) {
- throw new GotenbergRenderError(
- `Could not reach Gotenberg at ${config.baseUrl}: ${
- err instanceof Error ? err.message : String(err)
- }`,
- );
- }
- if (!response.ok) {
- throw new GotenbergRenderError(
- `Gotenberg rejected the render: ${response.status} ${response.statusText}`,
- );
- }
- return new Uint8Array(await response.arrayBuffer());
-}
diff --git a/packages/gotenberg-render/src/config.test.ts b/packages/gotenberg-render/src/config.test.ts
deleted file mode 100644
index a1fa70dcee..0000000000
--- a/packages/gotenberg-render/src/config.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { describe, expect, test } from "bun:test";
-import { resolveGotenbergConfig } from "./config";
-
-describe("resolveGotenbergConfig", () => {
- test("returns null when GOTENBERG_URL is unset — the capability is absent", () => {
- expect(resolveGotenbergConfig({})).toBeNull();
- });
-
- test("returns null when GOTENBERG_URL is blank", () => {
- expect(resolveGotenbergConfig({ GOTENBERG_URL: " " })).toBeNull();
- });
-
- test("resolves a configured URL, trimming a trailing slash", () => {
- expect(resolveGotenbergConfig({ GOTENBERG_URL: "http://gotenberg:3000/" })).toEqual({
- baseUrl: "http://gotenberg:3000",
- });
- });
-
- test("throws on a value that isn't a URL", () => {
- expect(() => resolveGotenbergConfig({ GOTENBERG_URL: "not-a-url" })).toThrow();
- });
-});
diff --git a/packages/gotenberg-render/src/config.ts b/packages/gotenberg-render/src/config.ts
deleted file mode 100644
index 5eb29b55f0..0000000000
--- a/packages/gotenberg-render/src/config.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// Resolves the operator-configured Gotenberg endpoint (CL-6499). Gotenberg
-// is an optional, self-hosted PDF-rendering service — the operator points
-// us at it with one URL, exactly like any other pluggable external
-// endpoint. Absence is a supported, ordinary state, not a config error: a
-// bench with no `GOTENBERG_URL` set simply never gains the "render to
-// PDF" capability, so callers must treat `null` as "capability absent"
-// rather than retrying or logging a warning.
-import { type } from "arktype";
-
-const GotenbergUrl = type("string.url");
-
-export type GotenbergConfig = {
- readonly baseUrl: string;
-};
-
-/**
- * Reads `GOTENBERG_URL` from the given env map. Returns `null` when unset
- * or blank — the expected shape for a bench that hasn't opted in. Throws
- * only when the operator set the variable to something that isn't a
- * parseable URL, since that's a genuine misconfiguration worth failing on
- * loudly rather than silently treating as "absent".
- */
-export function resolveGotenbergConfig(
- env: Record,
-): GotenbergConfig | null {
- const raw = env.GOTENBERG_URL?.trim();
- if (raw === undefined || raw === "") return null;
- const parsed = GotenbergUrl(raw);
- if (parsed instanceof type.errors) {
- throw new Error(`GOTENBERG_URL is not a valid URL: ${parsed.summary}`);
- }
- return { baseUrl: parsed.replace(/\/+$/, "") };
-}
diff --git a/packages/gotenberg-render/src/index.ts b/packages/gotenberg-render/src/index.ts
deleted file mode 100644
index 218a243d33..0000000000
--- a/packages/gotenberg-render/src/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export { resolveGotenbergConfig, type GotenbergConfig } from "./config";
-export { renderMarkdownToPdf, GotenbergRenderError } from "./client";
-export {
- renderMarkdownArtifactToPdf,
- type MarkdownArtifactSource,
- type PdfArtifactSink,
- type SavedPdfArtifact,
- type RenderContext,
- type RenderBriefResult,
-} from "./render";
diff --git a/packages/gotenberg-render/src/render.test.ts b/packages/gotenberg-render/src/render.test.ts
deleted file mode 100644
index 73f8e85fa4..0000000000
--- a/packages/gotenberg-render/src/render.test.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { describe, expect, test } from "bun:test";
-import { renderMarkdownArtifactToPdf, type PdfArtifactSink } from "./render";
-
-const CONFIG = { baseUrl: "http://gotenberg.internal:3000" };
-const SOURCE = {
- title: "Acme Diligence Brief",
- markdown: "# Acme\n\nFindings.",
-};
-
-describe("renderMarkdownArtifactToPdf", () => {
- test("renders a markdown artifact and hands the PDF bytes to the sink", async () => {
- const fetchStub = (async () =>
- new Response(new Uint8Array([0x25, 0x50, 0x44, 0x46]), {
- status: 200,
- })) as unknown as typeof fetch;
- let savedBytes: Uint8Array | undefined;
- const sink: PdfArtifactSink = {
- async savePdf(input) {
- savedBytes = input.bytes;
- return { id: "artifact-1", version: 1 };
- },
- };
-
- const result = await renderMarkdownArtifactToPdf(
- CONFIG,
- SOURCE,
- sink,
- { operation: "gotenberg.render" },
- fetchStub,
- );
-
- expect(result).toEqual({
- ok: true,
- artifact: { id: "artifact-1", version: 1 },
- });
- expect(savedBytes).toEqual(new Uint8Array([0x25, 0x50, 0x44, 0x46]));
- });
-
- test("surfaces a friendly message and a refId when Gotenberg fails", async () => {
- const fetchStub = (async () =>
- new Response("boom", { status: 503 })) as unknown as typeof fetch;
- const sink: PdfArtifactSink = {
- savePdf: async () => {
- throw new Error("should not be called");
- },
- };
-
- const result = await renderMarkdownArtifactToPdf(
- CONFIG,
- SOURCE,
- sink,
- { operation: "gotenberg.render", tenantId: "tenant-1" },
- fetchStub,
- );
-
- expect(result.ok).toBe(false);
- if (result.ok) throw new Error("expected failure");
- expect(result.message).toBe("Couldn't build the PDF. Try again shortly.");
- expect(result.message).not.toMatch(/503|http|internal/i);
- expect(result.refId.length).toBeGreaterThan(0);
- });
-});
diff --git a/packages/gotenberg-render/src/render.ts b/packages/gotenberg-render/src/render.ts
deleted file mode 100644
index a57e58e48a..0000000000
--- a/packages/gotenberg-render/src/render.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-// Orchestrates one Markdown-artifact-to-PDF-artifact render (CL-6499).
-// Reading the source artifact and persisting the resulting PDF are both
-// injected as ports rather than wired to one concrete artifact store here:
-// the caller already owns a way to read a Library artifact's content and
-// a way to save a new one (the tenant Library upload path, a workflow
-// run's artifact client, or a test double), and this package's job is
-// only the Gotenberg-specific middle step, matching how
-// `@corbits/artifacts-hub` itself injects its store rather than importing
-// a concrete db.
-import { reportError, type ErrorContext } from "@corbits/error-sink";
-import { renderMarkdownToPdf, type GotenbergFetch } from "./client";
-import type { GotenbergConfig } from "./config";
-
-export type MarkdownArtifactSource = {
- readonly title: string;
- readonly markdown: string;
-};
-
-export type SavedPdfArtifact = {
- readonly id: string;
- readonly version: number;
-};
-
-export type PdfArtifactSink = {
- savePdf(input: {
- readonly filename: string;
- readonly bytes: Uint8Array;
- }): Promise;
-};
-
-export type RenderContext = {
- readonly operation: string;
- readonly tenantId?: string;
-};
-
-export type RenderBriefResult =
- | { readonly ok: true; readonly artifact: SavedPdfArtifact }
- | { readonly ok: false; readonly message: string; readonly refId: string };
-
-const FRIENDLY_FAILURE_MESSAGE = "Couldn't build the PDF. Try again shortly.";
-
-function errorContextFor(context: RenderContext): ErrorContext {
- if (context.tenantId === undefined) return { operation: context.operation };
- return { operation: context.operation, tenantId: context.tenantId };
-}
-
-/**
- * Renders a Markdown artifact to PDF via Gotenberg and hands the bytes to
- * `sink` to persist as a new Library artifact. Callers must have already
- * confirmed Gotenberg is configured (`resolveGotenbergConfig` returned
- * non-`null`) before reaching this — it never falls back to a no-op.
- */
-export async function renderMarkdownArtifactToPdf(
- config: GotenbergConfig,
- source: MarkdownArtifactSource,
- sink: PdfArtifactSink,
- context: RenderContext,
- fetchImpl: GotenbergFetch = fetch,
-): Promise {
- try {
- const bytes = await renderMarkdownToPdf(
- config,
- { title: source.title, markdown: source.markdown },
- fetchImpl,
- );
- const artifact = await sink.savePdf({
- filename: `${source.title}.pdf`,
- bytes,
- });
- return { ok: true, artifact };
- } catch (err) {
- const refId = reportError(err, errorContextFor(context));
- return { ok: false, message: FRIENDLY_FAILURE_MESSAGE, refId };
- }
-}
diff --git a/packages/gotenberg-render/tsconfig.json b/packages/gotenberg-render/tsconfig.json
deleted file mode 100644
index 58e347560b..0000000000
--- a/packages/gotenberg-render/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "extends": "./tsconfig.src.json",
- "compilerOptions": {
- "composite": false,
- "noEmit": true,
- "disableSourceOfProjectReferenceRedirect": true,
- "declaration": false,
- "declarationMap": false,
- "emitDeclarationOnly": false,
- "tsBuildInfoFile": "tsconfig.tsbuildinfo",
- "rootDir": "../.."
- },
- "include": ["src"],
- "exclude": [],
- "references": [
- {
- "path": "../error-sink/tsconfig.src.json"
- }
- ]
-}
diff --git a/packages/gotenberg-render/tsconfig.src.json b/packages/gotenberg-render/tsconfig.src.json
deleted file mode 100644
index 36b9e6e9ae..0000000000
--- a/packages/gotenberg-render/tsconfig.src.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "extends": "../../tsconfig.base.json",
- "include": ["src", "package.json", "src/**/*.json"],
- "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"],
- "compilerOptions": {
- "lib": ["ESNext", "DOM", "DOM.Iterable"],
- "types": ["bun"],
- "composite": true,
- "emitDeclarationOnly": true,
- "outDir": "dist",
- "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
- },
- "references": [
- {
- "path": "../error-sink/tsconfig.src.json"
- }
- ]
-}
diff --git a/packages/inbox/LICENSE b/packages/inbox/LICENSE
deleted file mode 100644
index 173c3a21d0..0000000000
--- a/packages/inbox/LICENSE
+++ /dev/null
@@ -1,176 +0,0 @@
-GNU LESSER GENERAL PUBLIC LICENSE
-
-Version 2.1, February 1999
-
-Copyright (C) 1991, 1999 Free Software Foundation, Inc.
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]
-
-Preamble
-
-The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
-
-This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
-
-When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
-
-To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
-
-For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
-
-We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
-
-To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
-
-Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
-
-Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
-
-When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
-
-We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
-
-For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
-
-In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
-
-Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
-
-The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
-
-GNU LESSER GENERAL PUBLIC LICENSE
-TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
-
-A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
-
-The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
-
-"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
-
-Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
-
-1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
-
-You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
-
-2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
-
- a) The modified work must itself be a software library.
-
- b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
-
- c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
-
- d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
-
-(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
-
-3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
-
-Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
-
-This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
-
-4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
-
-If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
-
-5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
-
-However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
-
-When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
-
-If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
-
-Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
-
-6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
-
-You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
-
- a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
-
- b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
-
- c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
-
- d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
-
- e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
-
-For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
-
-It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
-
-7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
-
- a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
-
- b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
-
-8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
-
-9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
-
-10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
-
-11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
-
-This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
-
-12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
-
-13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
-
-14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
-
-NO WARRANTY
-
-15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-END OF TERMS AND CONDITIONS
-
-How to Apply These Terms to Your New Libraries
-
-If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
-
-To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
-
- one line to give the library's name and an idea of what it does.
- Copyright (C) year name of author
-
- This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License.
-
- This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail.
-
-You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
-
-Yoyodyne, Inc., hereby disclaims all copyright interest in
-the library `Frob' (a library for tweaking knobs) written
-by James Random Hacker.
-
-signature of Ty Coon, 1 April 1990
-Ty Coon, President of Vice
-That's all there is to it!
diff --git a/packages/inbox/README.md b/packages/inbox/README.md
deleted file mode 100644
index 98db0ce370..0000000000
--- a/packages/inbox/README.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# @corbits/inbox
-
-Workbench's product inbox over `@corbits/mailbox`: projects raw mailbox
-messages into three product groups (action, mention, delivery), the
-mark-all-read / clear-done bulk rules, and the hub routes
-(`createInboxRoutes`) mounted at `/api/tenants/:tenantId/inbox` — server-side,
-backed by `@corbits/mailbox`, `@corbits/notify`, and `@intx/hub-api`.
-
-## `/client` subpath contract
-
-`@corbits/inbox/client` (`src/client.ts`) is a browser-safe re-export of
-the package root's already-pure slice — `./project`, `./group`, and
-`./bulk` import nothing but arktype and (type-only) `@corbits/mailbox`, so
-this subpath adds no code of its own. It exists because the root export
-(`.`) also carries `./routes` (`hono`, `@intx/hub-api`), `./delivery`
-(`@corbits/notify/mailbox`), and `./migrations` (`@corbits/mailbox`'s
-Postgres migration runner) alongside them, and none of those belong in a
-browser bundle — enforced by `bun run check:browser-safe-subpaths`
-(`scripts/checks/browser-safe-subpaths.ts`), which walks this subpath's
-transitive import graph, not just by convention.
-
-**Owns:**
-
-- `InboxItemSchema`, `InboxItemDetailSchema`, `InboxCountsSchema` — arktype
- wire schemas (and inferred `InboxItem` / `InboxItemDetail` / `InboxCounts`
- types) for the shapes `routes.ts` returns.
-- `projectInboxItem`, `projectInboxItemDetail` — project a raw
- `MailboxMessage(Detail)` into the product `InboxItem(Detail)` shape.
-- `INBOX_GROUPS`, `isInboxGroup`, `inboxGroupOf`, `classificationFromRefs`,
- `InboxGroup` — the three product groups and how a message's refs map onto
- one.
-- `itemsEligibleForMarkAllRead`, `itemsEligibleForClearDone` — the pure
- bulk-op eligibility rules (never touch action rows; only `done` rows
- clear).
-
-**A host injects:** the actual fetch — its own tenant-scoped request
-functions that hit `routes.ts`'s endpoints and parse the response with
-these schemas (see `apps/web/src/inbox-api.ts`), and any UI-only filter
-concept layered on top of `InboxGroup` (e.g. an `"all"` tab).
-
-**Never imports:** no `hono`, `@intx/hub-api`, `@corbits/notify`, or
-`@corbits/mailbox`'s runtime (migration/mount) surface — only its
-type-only `MailboxMessage` / `MailboxMessageDetail` / `MailboxVocabulary`
-shapes, erased at build time.
-
-## Tests
-
-```
-cd packages/inbox && bun test
-```
-
-`test/delivery.test.ts` needs a reachable Postgres via
-`DATABASE_URL=postgres://localhost:5432/workbench_e2e` (parsed the same
-way `apps/hub/src/index.ts` does); the rest are pure unit tests.
diff --git a/packages/inbox/package.json b/packages/inbox/package.json
deleted file mode 100644
index 54cf1f4468..0000000000
--- a/packages/inbox/package.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "@corbits/inbox",
- "version": "0.0.1",
- "private": true,
- "description": "The @corbits/notify -> @corbits/mailbox delivery adapter, plus this package's own migration ledger",
- "license": "LGPL-2.1-only",
- "type": "module",
- "exports": {
- ".": "./src/index.ts",
- "./migrations": "./src/migrations.ts"
- },
- "scripts": {
- "typecheck": "tsc --noEmit",
- "test": "bun test"
- },
- "dependencies": {
- "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38",
- "@corbits/notify": "workspace:*",
- "drizzle-orm": "catalog:",
- "postgres": "catalog:"
- },
- "devDependencies": {
- "@intx/db": "workspace:*",
- "@intx/hub-common": "0.3.0",
- "@types/bun": "catalog:",
- "typescript": "catalog:"
- }
-}
diff --git a/packages/inbox/src/delivery.ts b/packages/inbox/src/delivery.ts
deleted file mode 100644
index 29329d8327..0000000000
--- a/packages/inbox/src/delivery.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-// Workbench adapter that turns `@corbits/notify`'s MailboxDelivery seam into
-// 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 } from "drizzle-orm";
-import {
- deliverInboxItems,
- principalMail,
- type DeliverInboxItemsOpts,
- type InboxItem,
- type MailboxDb,
- type MailboxEventBus,
-} from "@corbits/mailbox";
-import type {
- MailboxDelivery,
- NotifyInboxItem,
- ResolveExistingMailIds,
-} from "@corbits/notify/mailbox";
-
-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: a thin pass-through
- * onto `deliverInboxItems`.
- */
-export function createWorkbenchMailboxDelivery(
- opts: CreateWorkbenchMailboxDeliveryOpts,
-): MailboxDelivery {
- const { db, bus } = opts;
- return async (items, deliverOpts) => {
- 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 = {};
- if (bus !== undefined) writeOpts.bus = bus;
- if (hostEnqueue !== undefined) {
- writeOpts.enqueue = ({ id, item }) => {
- const original = items.find(
- (candidate) =>
- candidate.tenantId === item.tenantId &&
- candidate.principalId === item.principalId &&
- candidate.externalId === item.externalId &&
- candidate.source === item.source,
- );
- if (original !== undefined) {
- hostEnqueue({ id, item: original });
- }
- };
- }
-
- const delivered = await deliverInboxItems(db, stamped, writeOpts);
- return delivered.map((result, index) => ({
- messageKey: inboxMessageIdFor(items[index] as NotifyInboxItem),
- id: result.id,
- }));
- };
-}
-
-/**
- * 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'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 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, item.tenantId),
- eq(principalMail.principalId, item.principalId),
- eq(principalMail.messageId, messageId),
- ),
- )
- .limit(1);
- return row?.id ?? null;
- }),
- );
- return rows;
- };
-}
diff --git a/packages/inbox/src/index.ts b/packages/inbox/src/index.ts
deleted file mode 100644
index b27ab915ee..0000000000
--- a/packages/inbox/src/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export {
- createResolveExistingMailIds,
- createWorkbenchMailboxDelivery,
- type CreateWorkbenchMailboxDeliveryOpts,
-} from "./delivery";
-export { applyMailboxMigrations, type ApplyMailboxMigrationsReport } from "./migrations";
diff --git a/packages/inbox/src/migrations.ts b/packages/inbox/src/migrations.ts
deleted file mode 100644
index f3114ecb99..0000000000
--- a/packages/inbox/src/migrations.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// A URL-shaped wrapper around `@corbits/mailbox`'s own
-// `runMailboxMigrations`, so scripts/db-setup.ts can apply it the same
-// way it applies every other installed package's migrations. That
-// package keeps its own ledger inside the `mailbox` schema.
-import { createMailboxDb, runMailboxMigrations } from "@corbits/mailbox";
-
-export interface ApplyMailboxMigrationsReport {
- applied: string[];
-}
-
-/**
- * Apply the mailbox package's migrations against `databaseUrl`. The package
- * keeps its own ledger inside the `mailbox` schema; this wrapper only opens
- * a short-lived handle, runs the migrator, and closes it.
- *
- * The report's `applied` list is best-effort: the package's runner does not
- * return which rows it just wrote, so a successful call reports a single
- * sentinel so db-setup can log that the step ran without error.
- */
-export async function applyMailboxMigrations(
- databaseUrl: string,
-): Promise {
- const { db, close } = createMailboxDb(databaseUrl);
- try {
- await runMailboxMigrations(db);
- return { applied: ["mailbox"] };
- } finally {
- await close();
- }
-}
diff --git a/packages/inbox/test/delivery.test.ts b/packages/inbox/test/delivery.test.ts
deleted file mode 100644
index 81e38c2003..0000000000
--- a/packages/inbox/test/delivery.test.ts
+++ /dev/null
@@ -1,293 +0,0 @@
-// DB-gated integration test proving CL-6002's actual outcome: once the
-// platform's plus every installed package's migrations are applied through
-// 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 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 { and, eq, like } from "drizzle-orm";
-import { createMailboxDb, openNativeMailboxStore, principalMail } from "@corbits/mailbox";
-import {
- createInMemoryNotifyDispatchStore,
- createSinkRegistry,
- deliverApprovalMail,
- deliverCredentialMail,
- deliverMentionMail,
- deliverRunFailureMail,
- type ApprovalNotification,
- type NotifyDeliveryDeps,
- type NotifyDispatchStore,
- type SinkDeliveryResult,
-} from "@corbits/notify";
-
-import { setupDatabase } from "../../../scripts/db-setup";
-import { e2eDatabaseUrl } from "../../../test/database-url";
-import { createResolveExistingMailIds, createWorkbenchMailboxDelivery } from "../src/delivery";
-import { dbGate } from "../../../test/db-gate";
-
-function scratchUrlFor(e2eUrl: string): string {
- const url = new URL(e2eUrl);
- const database = url.pathname.replace(/^\//, "");
- url.pathname = `/${database}_inbox_delivery_test`;
- return url.toString();
-}
-
-// Parse DATABASE_URL the same way the hub does (apps/hub/src/index.ts): an
-// empty user falls through to the postgres client's OS-username default.
-function dbConfigFromUrl(databaseUrl: string) {
- const url = new URL(databaseUrl);
- return {
- host: url.hostname,
- port: url.port === "" ? 5432 : Number(url.port),
- user: decodeURIComponent(url.username),
- password: decodeURIComponent(url.password),
- database: url.pathname.replace(/^\//, ""),
- };
-}
-
-const databaseUrl = e2eDatabaseUrl();
-const describeIfDb = dbGate(databaseUrl, import.meta.path);
-
-describeIfDb("notify delivery writes a real mailbox row for every notification kind", () => {
- const scratchUrl = scratchUrlFor(databaseUrl ?? "postgres://localhost:5432/unused");
- const scratchDatabase = new URL(scratchUrl).pathname.replace(/^\//, "");
-
- const tenantId = generateId("tenant");
- const principalId = generateId("principal");
-
- 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();
- }
-
- // The same path the hub boots with: the platform's own migrations,
- // then every installed package's, including `@corbits/notify` and
- // `@corbits/mailbox` — the thing CL-6002 makes true.
- await setupDatabase(scratchUrl);
-
- const { db, close } = createDB(dbConfigFromUrl(scratchUrl));
- try {
- await db.insert(schema.tenant).values({
- id: tenantId,
- name: "Delivery Test Bench",
- slug: `delivery-${tenantId}`,
- domain: `delivery-${tenantId}.localhost`,
- });
- await db.insert(schema.principal).values({
- id: principalId,
- tenantId,
- kind: "agent",
- refId: "not-a-real-agent-instance",
- status: "active",
- });
- } finally {
- await close();
- }
- // `setupDatabase` applies the platform's migrations plus every
- // installed package's (six of them); under `bun run test`'s
- // cross-package concurrency that comfortably exceeds bun:test's
- // default 5s hook timeout, so it gets an explicit one here rather
- // than a flaky suite.
- }, 30000);
-
- 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();
- }
- });
-
- test("approval, run-failure, mention, and credential-expired each land a row the inbox read path lists", async () => {
- const mailboxDb = createMailboxDb(scratchUrl);
- const deps: NotifyDeliveryDeps = {
- mail: createWorkbenchMailboxDelivery({ db: mailboxDb.db }),
- addressing: {
- inbox: (recipient) => `${recipient.principalId}@inbox.test`,
- from: (kind) => `${kind}@notify.test`,
- },
- dispatch: createInMemoryNotifyDispatchStore(),
- sinks: createSinkRegistry(),
- };
-
- const createdAt = new Date().toISOString();
- const recipients = [{ tenantId, principalId }];
-
- try {
- const approval = await deliverApprovalMail(deps, {
- kind: "approval",
- approvalId: generateId("approval"),
- tenantId,
- runId: generateId("workflowRun"),
- deploymentId: generateId("workflowRun"),
- toolName: "delete_repo",
- toolArguments: { repo: "corbitsdev/workbench" },
- recipients,
- createdAt,
- });
- const runFailure = await deliverRunFailureMail(deps, {
- kind: "run-failure",
- tenantId,
- runId: generateId("workflowRun"),
- deploymentId: generateId("workflowRun"),
- runLabel: "Nightly sync",
- error: "timed out",
- recipients,
- createdAt,
- });
- const mention = await deliverMentionMail(deps, {
- kind: "mention",
- tenantId,
- threadId: `chn_${crypto.randomUUID()}`,
- threadLabel: "#growth",
- mentionedBy: "prn_someone",
- excerpt: "@you can you take a look?",
- recipients,
- createdAt,
- });
- const credential = await deliverCredentialMail(deps, {
- kind: "credential-expired",
- tenantId,
- credentialId: generateId("credential"),
- providerId: "huggingface",
- providerLabel: "Hugging Face",
- recipients,
- createdAt,
- });
-
- for (const report of [approval, runFailure, mention, credential]) {
- expect(report.deliveredMailboxRowIds).toHaveLength(1);
- }
-
- const store = await openNativeMailboxStore(mailboxDb.db, {
- tenantId,
- principalId,
- folder: "INBOX",
- });
- 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);
-
- expect(store.messages.every((message) => !message.flags.has("\\Seen"))).toBe(true);
- } finally {
- await mailboxDb.close();
- }
- }, 15000);
-
- test("a crash between mail write and dispatch enqueue is repaired on redelivery without doubles (CL-7238)", async () => {
- const mailboxDb = createMailboxDb(scratchUrl);
- try {
- const innerDispatch = createInMemoryNotifyDispatchStore();
- let crashNext = true;
- const dispatch: NotifyDispatchStore = {
- ...innerDispatch,
- enqueue: async (inputs) => {
- if (crashNext) {
- crashNext = false;
- throw new Error("boom: crash between the mail write and the dispatch enqueue");
- }
- await innerDispatch.enqueue(inputs);
- },
- };
- const delivered: SinkDeliveryResult = { status: "delivered" };
- const sinks = createSinkRegistry();
- sinks.register({
- name: "always",
- isEnabledFor: async () => true,
- deliver: async () => delivered,
- });
- const deps: NotifyDeliveryDeps = {
- mail: createWorkbenchMailboxDelivery({ db: mailboxDb.db }),
- addressing: {
- inbox: (recipient) => `${recipient.principalId}@inbox.test`,
- from: (kind) => `${kind}@notify.test`,
- },
- dispatch,
- resolveExistingMailIds: createResolveExistingMailIds(mailboxDb.db),
- sinks,
- };
- const event: ApprovalNotification = {
- kind: "approval",
- approvalId: generateId("approval"),
- tenantId,
- runId: generateId("workflowRun"),
- deploymentId: generateId("workflowRun"),
- toolName: "quarantine_token",
- toolArguments: { repo: "corbitsdev/workbench" },
- recipients: [{ tenantId, principalId }],
- createdAt: new Date().toISOString(),
- };
-
- // The first attempt commits its mail row through the real adapter,
- // then dies before the dispatch enqueue — the crash window.
- await expect(deliverApprovalMail(deps, event)).rejects.toThrow("boom");
-
- // The redelivery dedupes on the mail key (no new row), resolves the
- // committed row's id through the adapter read-back, and queues the
- // dispatch the crash swallowed.
- const redelivery = await deliverApprovalMail(deps, event);
- expect(redelivery.deliveredMailboxRowIds).toEqual([]);
- expect(redelivery.queuedDispatchCount).toBe(1);
-
- const store = await openNativeMailboxStore(mailboxDb.db, {
- tenantId,
- principalId,
- folder: "INBOX",
- });
- // Exactly one mail row for the redelivered event — the crash and
- // both redeliveries deduped onto the first attempt's row.
- const matching = store.messages.filter((message) =>
- message.envelope.subject.includes("quarantine_token"),
- );
- expect(matching).toHaveLength(1);
- 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);
-
- // A further benign redelivery repairs the same row again, and the
- // dispatch store's (mail row, sink) dedupe holds it to one row.
- const third = await deliverApprovalMail(deps, event);
- expect(third.deliveredMailboxRowIds).toEqual([]);
- expect(third.queuedDispatchCount).toBe(1);
- expect(await dispatch.listFor(mailId)).toHaveLength(1);
- } finally {
- await mailboxDb.close();
- }
- }, 15000);
-});
diff --git a/packages/inbox/tsconfig.json b/packages/inbox/tsconfig.json
deleted file mode 100644
index 6113c6e904..0000000000
--- a/packages/inbox/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "extends": "./tsconfig.src.json",
- "compilerOptions": {
- "composite": false,
- "noEmit": true,
- "disableSourceOfProjectReferenceRedirect": true,
- "declaration": false,
- "declarationMap": false,
- "emitDeclarationOnly": false,
- "tsBuildInfoFile": "tsconfig.tsbuildinfo",
- "rootDir": "../.."
- },
- "include": ["src", "test"],
- "exclude": [],
- "references": [
- {
- "path": "../notify/tsconfig.src.json"
- }
- ]
-}
diff --git a/packages/inbox/tsconfig.src.json b/packages/inbox/tsconfig.src.json
deleted file mode 100644
index b7a6e26dc0..0000000000
--- a/packages/inbox/tsconfig.src.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "extends": "../../tsconfig.base.json",
- "include": ["src", "package.json", "src/**/*.json"],
- "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"],
- "compilerOptions": {
- "types": ["bun"],
- "customConditions": ["bun"],
- "composite": true,
- "emitDeclarationOnly": true,
- "outDir": "dist",
- "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
- },
- "references": [
- {
- "path": "../notify/tsconfig.src.json"
- }
- ]
-}
diff --git a/packages/mocks/LICENSE b/packages/mocks/LICENSE
deleted file mode 100644
index 173c3a21d0..0000000000
--- a/packages/mocks/LICENSE
+++ /dev/null
@@ -1,176 +0,0 @@
-GNU LESSER GENERAL PUBLIC LICENSE
-
-Version 2.1, February 1999
-
-Copyright (C) 1991, 1999 Free Software Foundation, Inc.
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]
-
-Preamble
-
-The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
-
-This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
-
-When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
-
-To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
-
-For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
-
-We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
-
-To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
-
-Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
-
-Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
-
-When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
-
-We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
-
-For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
-
-In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
-
-Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
-
-The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
-
-GNU LESSER GENERAL PUBLIC LICENSE
-TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
-
-A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
-
-The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
-
-"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
-
-Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
-
-1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
-
-You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
-
-2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
-
- a) The modified work must itself be a software library.
-
- b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
-
- c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
-
- d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
-
-(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
-
-3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
-
-Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
-
-This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
-
-4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
-
-If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
-
-5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
-
-However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
-
-When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
-
-If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
-
-Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
-
-6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
-
-You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
-
- a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
-
- b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
-
- c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
-
- d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
-
- e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
-
-For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
-
-It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
-
-7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
-
- a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
-
- b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
-
-8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
-
-9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
-
-10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
-
-11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
-
-This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
-
-12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
-
-13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
-
-14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
-
-NO WARRANTY
-
-15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-END OF TERMS AND CONDITIONS
-
-How to Apply These Terms to Your New Libraries
-
-If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
-
-To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
-
- one line to give the library's name and an idea of what it does.
- Copyright (C) year name of author
-
- This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License.
-
- This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail.
-
-You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
-
-Yoyodyne, Inc., hereby disclaims all copyright interest in
-the library `Frob' (a library for tweaking knobs) written
-by James Random Hacker.
-
-signature of Ty Coon, 1 April 1990
-Ty Coon, President of Vice
-That's all there is to it!
diff --git a/packages/mocks/README.md b/packages/mocks/README.md
deleted file mode 100644
index 53ad1e830b..0000000000
--- a/packages/mocks/README.md
+++ /dev/null
@@ -1,206 +0,0 @@
-# @corbits/mocks
-
-Deterministic mocks of the inference boundary — local providers first —
-built to be consumed by our own tests, not adopted off the shelf. Three of
-last night's worst defects (`tools: []` reaching the model with no
-conversation history — CL-6448; a hallucinated tool name bricking a room —
-CL-6478; `embeddinggemma` winning the chat default — CL-6477) lived at this
-boundary and were only found by hand-building a logging proxy in front of
-Ollama at 2am. This package turns each of those into a permanent CI
-regression test with no GPU involved.
-
-## Design bar
-
-- **1:1 with how we actually consume providers**, not a kitchen sink. No
- video/TTS/vector-DB surface we will never call.
-- **Tree-shakeable and pluggable.** Every provider is its own entry point —
- `@corbits/mocks/ollama` today — so a consumer imports only the provider it
- mocks. The package root (`@corbits/mocks`, no subpath) exports nothing —
- it exists only so the workspace's package-hygiene check has a bare
- import to resolve; it is not a barrel and never gains provider imports.
-- **Assert on what we SEND, not just what comes back.** Every request the
- mock receives is captured and exposed through readable assertion methods
- — `expectModel`, `expectToolsDeclared`, `expectMessageRoles`,
- `expectHistoryContains` — instead of a test hand-digging through raw JSON.
-- **Usable in-process and as a server.** `OllamaMock#fetch` is a plain
- Fetch-API handler a unit test wires directly into a `fetchImpl`, with no
- network involved. `OllamaMock#listen()` starts a real HTTP server on an
- ephemeral port for the e2e path, where the stack is pointed at an Ollama
- origin via env.
-
-## Quickstart: `@corbits/mocks/ollama`
-
-```ts
-import { createOllamaMock } from "@corbits/mocks/ollama";
-
-const ollama = createOllamaMock({
- models: [
- { name: "qwen3.8:27b", capabilities: ["completion", "tools"] },
- { name: "embeddinggemma:300m", capabilities: ["embedding"] },
- ],
-});
-
-// In-process: no network, no port. Wire ollama.fetch straight into
-// whatever `fetchImpl` seam your code under test already accepts
-// (testProviderCredential, fetchOllamaModelCatalog, the openai adapter's
-// own fetch, ...).
-const result = await testProviderCredential({
- provider: "ollama",
- apiKey: OLLAMA_PLACEHOLDER_SECRET,
- fetchImpl: ollama.fetch,
-});
-
-// As a server: for e2e, point the stack's OLLAMA_BASE_URL at this instead.
-const server = await ollama.listen();
-process.env.OLLAMA_BASE_URL = server.url;
-// ...run the real stack...
-await server.close();
-```
-
-### Scripting the model catalogue (CL-6477's shape)
-
-`GET /api/tags` and `POST /api/show` are served straight off the
-`models` you hand the mock — no need to pull a real model to test
-model-selection logic:
-
-```ts
-const ollama = createOllamaMock({
- models: [
- { name: "embeddinggemma:300m", capabilities: ["embedding"] },
- { name: "qwen3.8:27b", capabilities: ["completion", "tools"] },
- ],
-});
-
-// preferCompletionCapable / hasCompletionCapableModel now have real,
-// wire-shaped capability data to filter on, with no live Ollama instance.
-```
-
-### Scripting a chat reply
-
-```ts
-ollama.onChat((request) => {
- // `request` is the CapturedChatRequest this exact call received —
- // branch on it if the reply should depend on what was sent.
- return request.messages.some((m) => m.content.includes("book a flight"))
- ? ollama.reply.toolCall("search_flights", { origin: "SFO" })
- : ollama.reply.text("how can I help?");
-});
-```
-
-`ollama.reply` covers the two shapes a real completion-capable model
-returns: `.text(string)` for a plain assistant turn, `.toolCall(name, args)`
-/ `.toolCalls([...])` for the tool-call branch. Both streaming
-(`stream: true`, SSE chunks terminated with `data: [DONE]`) and
-non-streaming requests are answered from the same scripted reply — the
-mock reads `stream` off the request, the test never has to pick a
-different handler for each.
-
-### Asserting on the request (the highest-value half)
-
-```ts
-const request = ollama.requests.last();
-
-// Would have caught CL-6448 D2 outright: tools: [] reaching the model.
-request.expectToolsDeclared(["create_agent", "send_message"]);
-
-// Pins the model actually dialed — catches a silent fallback to the
-// adapter's default instead of the tenant's pinned choice.
-request.expectModel("qwen3.8:27b");
-
-// Would have caught CL-6448 D3: history collapsed to [system, latest user].
-request.expectMessageRoles(["system", "user", "assistant", "user"]);
-
-// Or, when only part of the history matters:
-request.expectHistoryContains([
- { role: "system" },
- { role: "user", content: "book me a flight" },
- { role: "tool" },
-]);
-```
-
-See `src/ollama/cl-6448-demo.test.ts` for the full demonstration: the same
-assertions failing against CL-6448's broken request shape, and passing
-once tools and history are actually sent.
-
-### The adversarial output catalogue (CL-6478's shape)
-
-Real local models misbehave in specific, repeatable ways. Every scenario
-below reproduces one we actually hit — not a hypothetical edge case —
-selectable in one line straight off `ollama.reply`, exactly like `.text`
-and `.toolCall`:
-
-```ts
-ollama.onChat(() => ollama.reply.malformedToolName());
-```
-
-| Scenario | What it reproduces |
-| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- |
-| `.malformedToolName()` | CL-6478's flagship case: `qwen3.8:27b` leaked `\n Promise` handler — the in-process path. |
-| `mock.listen(port?)` | Starts a real Bun HTTP server; returns `{ url, close() }` — the server path. |
-| `mock.setModels(models)` | Rewrites the catalogue mid-test (a connect flow that re-reads it). |
-| `mock.onChat(handler)` | Scripts every subsequent `/v1/chat/completions` reply. |
-| `mock.reply.text` / `.toolCall` / `.toolCalls` | Builds an `OllamaChatReply` for a handler to return. |
-| `mock.requests` | The `CapturedRequestLog` — `.all`, `.count`, `.last()`, `.clear()`. |
-| `CapturedChatRequest` | One captured request: `.model`, `.tools`, `.messages`, plus every `expect*`. |
-| `mock.reply.malformedToolName` / `.toolNameOfLength` / `.textlessToolCall` / `.wrongShapedToolArgs` / `.truncatedToolArgs` / `.refusal` / `.oversized` / `.hallucinatedToolName` | The adversarial output catalogue — see above. |
-| `sequence(replies)` | Scripts one reply per turn for `onChat`; repeats the last once exhausted. |
-
-Routes covered: `GET /api/tags` (native catalogue), `POST /api/show`
-(per-model capability probe), `POST /v1/chat/completions`
-(openai-compatible chat, tool calls, streaming and non-streaming) — the
-exact three endpoints `@corbits/connections`'s `credential-test.ts` and
-the openai adapter's request-building actually call. No `/api/generate`,
-`/api/embeddings`, or anything else Ollama exposes that we don't.
-
-## Roadmap (not in this unit)
-
-- **OpenAI-compatible and Anthropic provider mocks** — `@corbits/mocks/openai`
- and `@corbits/mocks/anthropic`, same request-capture and reply-scripting
- API shape as this one.
-- **Wiring the CL-6478 regression guard through the real turn
- assembler** — `cl-6478-demo.test.ts` demonstrates the shape at this mock
- layer only; an equivalent test exercising `@workbench/hub-sessions`'s
- actual turn assembly (vendored, out of scope for this unit) is next.
-- **Converging the scattered fakes** — `packages/evals`' github MCP fake
- and its stub inference harness should be rebuilt on top of this package
- rather than living on beside it.
diff --git a/packages/mocks/package.json b/packages/mocks/package.json
deleted file mode 100644
index 3380a3ce5e..0000000000
--- a/packages/mocks/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "@corbits/mocks",
- "version": "0.0.1",
- "private": true,
- "description": "Deterministic mocks of the inference boundary — local providers first — for tests that need a scriptable model and a record of exactly what was sent",
- "license": "LGPL-2.1-only",
- "type": "module",
- "exports": {
- ".": "./src/index.ts",
- "./ollama": "./src/ollama/index.ts"
- },
- "scripts": {
- "typecheck": "tsc --noEmit",
- "test": "bun test"
- },
- "dependencies": {
- "arktype": "catalog:"
- },
- "devDependencies": {
- "@types/bun": "catalog:",
- "typescript": "catalog:"
- }
-}
diff --git a/packages/mocks/src/index.ts b/packages/mocks/src/index.ts
deleted file mode 100644
index a006d87422..0000000000
--- a/packages/mocks/src/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// @corbits/mocks deliberately has no barrel export. Each provider mock is
-// its own subpath (`@corbits/mocks/ollama`, with `./openai` and
-// `./anthropic` to follow) so a consumer never pulls in a provider it
-// isn't mocking. This root module exists only so a bare
-// `import("@corbits/mocks")` resolves for tooling that expects every
-// workspace package to have one (`check:packages`); it exports nothing.
-export {};
diff --git a/packages/mocks/src/ollama/capture.ts b/packages/mocks/src/ollama/capture.ts
deleted file mode 100644
index c7cd31d4dc..0000000000
--- a/packages/mocks/src/ollama/capture.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import type {
- CapturedMessage,
- CapturedMessageRole,
- CapturedToolDeclaration,
- ChatCompletionRequestBody,
-} from "./types";
-
-function decodeToolCallArguments(raw: string): unknown {
- try {
- return JSON.parse(raw);
- } catch {
- return raw;
- }
-}
-
-function toCapturedMessage(
- message: ChatCompletionRequestBody["messages"][number],
-): CapturedMessage {
- return {
- role: message.role,
- content: message.content ?? "",
- ...(message.tool_call_id !== undefined ? { toolCallId: message.tool_call_id } : {}),
- ...(message.tool_calls !== undefined
- ? {
- toolCalls: message.tool_calls.map((call) => ({
- name: call.function.name,
- arguments: decodeToolCallArguments(call.function.arguments),
- })),
- }
- : {}),
- };
-}
-
-/**
- * One `/v1/chat/completions` request the mock received, with the
- * assertion helpers a test actually reaches for — the shape of check
- * that would have caught CL-6448 (`tools: []`, no history reaching the
- * model) had it existed. Every `expect*` throws a message naming what
- * was actually sent, not just that the check failed.
- */
-export class CapturedChatRequest {
- readonly model: string;
- readonly stream: boolean;
- readonly tools: readonly CapturedToolDeclaration[];
- readonly messages: readonly CapturedMessage[];
-
- constructor(body: ChatCompletionRequestBody) {
- this.model = body.model;
- this.stream = body.stream ?? false;
- this.tools = (body.tools ?? []).map((t) => ({
- name: t.function.name,
- ...(t.function.description !== undefined ? { description: t.function.description } : {}),
- ...(t.function.parameters !== undefined ? { parameters: t.function.parameters } : {}),
- }));
- this.messages = body.messages.map(toCapturedMessage);
- }
-
- toolNames(): readonly string[] {
- return this.tools.map((t) => t.name);
- }
-
- roles(): readonly CapturedMessageRole[] {
- return this.messages.map((m) => m.role);
- }
-
- /** Throws unless the request was pinned to exactly this model — catches
- * a request silently falling back to whatever the adapter defaults to. */
- expectModel(model: string): void {
- if (this.model !== model) {
- throw new Error(`expected request to be pinned to model "${model}", got "${this.model}"`);
- }
- }
-
- /**
- * Throws unless at least one tool was declared — the direct CL-6448
- * regression check (`tools: []` reaching the model). Pass `names` to
- * additionally require exactly that set of tool names, independent of
- * declaration order.
- */
- expectToolsDeclared(names?: readonly string[]): void {
- if (this.tools.length === 0) {
- throw new Error(
- "expected request to declare at least one tool, but tools was empty or missing (this is the CL-6448 regression shape)",
- );
- }
- if (names === undefined) return;
- const actual = new Set(this.toolNames());
- const expected = new Set(names);
- const missing = names.filter((name) => !actual.has(name));
- const extra = this.toolNames().filter((name) => !expected.has(name));
- if (missing.length > 0 || extra.length > 0) {
- throw new Error(
- `expected declared tools ${JSON.stringify(names)}, got ${JSON.stringify(this.toolNames())}` +
- (missing.length > 0 ? ` (missing: ${JSON.stringify(missing)})` : "") +
- (extra.length > 0 ? ` (unexpected: ${JSON.stringify(extra)})` : ""),
- );
- }
- }
-
- /** Throws unless the message roles, in order, exactly match `roles` —
- * catches history collapsing to just `[system, user]` on every turn
- * (CL-6448's other half). */
- expectMessageRoles(roles: readonly CapturedMessageRole[]): void {
- const actual = this.roles();
- const matches = actual.length === roles.length && actual.every((role, i) => role === roles[i]);
- if (!matches) {
- throw new Error(
- `expected message roles ${JSON.stringify(roles)}, got ${JSON.stringify(actual)}`,
- );
- }
- }
-
- /**
- * Throws unless `expected` appears, in order, as a subsequence of the
- * captured messages — each entry matched on `role` and, when given, a
- * `content` substring. Use this over `expectMessageRoles` when only
- * part of the history matters (e.g. "the user's original ask is still
- * in there somewhere after three tool round-trips").
- */
- expectHistoryContains(
- expected: readonly { role: CapturedMessageRole; content?: string }[],
- ): void {
- let cursor = 0;
- for (const want of expected) {
- const foundAt = this.messages.findIndex((message, i) => {
- if (i < cursor) return false;
- if (message.role !== want.role) return false;
- if (want.content !== undefined && !message.content.includes(want.content)) {
- return false;
- }
- return true;
- });
- if (foundAt === -1) {
- throw new Error(
- `expected history to contain ${JSON.stringify(want)} after position ${cursor}, but it did not appear in ${JSON.stringify(this.messages)}`,
- );
- }
- cursor = foundAt + 1;
- }
- }
-}
-
-/** The mock's record of every chat request it has received, oldest first. */
-export class CapturedRequestLog {
- private readonly captured: CapturedChatRequest[] = [];
-
- push(request: CapturedChatRequest): void {
- this.captured.push(request);
- }
-
- get all(): readonly CapturedChatRequest[] {
- return this.captured;
- }
-
- get count(): number {
- return this.captured.length;
- }
-
- /** The most recent request. Throws if the mock has received none yet —
- * a test asserting on `.last()` with no prior call is a test bug, not a
- * "no requests" state worth a silent `undefined`. */
- last(): CapturedChatRequest {
- const request = this.captured.at(-1);
- if (request === undefined) {
- throw new Error("no chat request has been captured yet");
- }
- return request;
- }
-
- clear(): void {
- this.captured.length = 0;
- }
-}
diff --git a/packages/mocks/src/ollama/cl-6448-demo.test.ts b/packages/mocks/src/ollama/cl-6448-demo.test.ts
deleted file mode 100644
index dd696da845..0000000000
--- a/packages/mocks/src/ollama/cl-6448-demo.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-// Demonstrates the exact shape of test that would have caught CL-6448
-// before it shipped: the openai-compatible inference path sent
-// `tools: []` and only `[system, latest user message]` — no history —
-// on every turn, wire-proven by a logging proxy at 2am. No test anywhere
-// asserted on the outgoing request shape; this is what one looks like.
-import { describe, expect, test } from "bun:test";
-import { createOllamaMock } from "./index";
-
-// Stands in for the real call site (`@intx/inference`'s openai adapter,
-// invoked through workbench's turn assembly) — a launch/turn assembler
-// that is SUPPOSED to forward the agent's declared tools and the full
-// conversation history, but has regressed to CL-6448's broken shape.
-async function assembleAndSendBrokenTurn(
- fetchImpl: (req: Request) => Promise,
- baseURL: string,
-): Promise {
- await fetchImpl(
- new Request(`${baseURL}/v1/chat/completions`, {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({
- model: "qwen3.8:27b",
- // CL-6448 D2: tools declared upstream, but an empty list reaches
- // the wire.
- tools: [],
- // CL-6448 D3: history collapsed to just the latest turn — no
- // system prompt, no prior assistant/tool turns.
- messages: [{ role: "user", content: "this is turn 17" }],
- stream: false,
- }),
- }),
- );
-}
-
-async function assembleAndSendCorrectTurn(
- fetchImpl: (req: Request) => Promise,
- baseURL: string,
-): Promise {
- await fetchImpl(
- new Request(`${baseURL}/v1/chat/completions`, {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({
- model: "qwen3.8:27b",
- tools: [
- { type: "function", function: { name: "create_agent" } },
- { type: "function", function: { name: "send_message" } },
- ],
- messages: [
- { role: "system", content: "6.5KB of tool doctrine" },
- { role: "user", content: "turn 1" },
- { role: "assistant", content: "reply 1" },
- { role: "user", content: "this is turn 17" },
- ],
- stream: false,
- }),
- }),
- );
-}
-
-describe("CL-6448 regression shape", () => {
- test("expectToolsDeclared catches an empty tools array reaching the model", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("[Called create_agent: ...]"));
-
- await assembleAndSendBrokenTurn(ollama.fetch, "http://mock-ollama");
-
- expect(() => ollama.requests.last().expectToolsDeclared()).toThrow(
- /tools was empty or missing/,
- );
- });
-
- test("expectHistoryContains catches a turn with no conversation history", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("this is the first message"));
-
- await assembleAndSendBrokenTurn(ollama.fetch, "http://mock-ollama");
-
- expect(() =>
- ollama.requests
- .last()
- .expectHistoryContains([{ role: "system" }, { role: "user", content: "turn 1" }]),
- ).toThrow(/expected history to contain/);
- });
-
- test("the same assertions pass once tools and history are actually sent", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.toolCall("create_agent", {}));
-
- await assembleAndSendCorrectTurn(ollama.fetch, "http://mock-ollama");
-
- const request = ollama.requests.last();
- expect(() => request.expectToolsDeclared(["create_agent", "send_message"])).not.toThrow();
- expect(() =>
- request.expectHistoryContains([
- { role: "system" },
- { role: "user", content: "turn 1" },
- { role: "assistant" },
- { role: "user", content: "turn 17" },
- ]),
- ).not.toThrow();
- });
-});
diff --git a/packages/mocks/src/ollama/cl-6478-demo.test.ts b/packages/mocks/src/ollama/cl-6478-demo.test.ts
deleted file mode 100644
index b3b92c227f..0000000000
--- a/packages/mocks/src/ollama/cl-6478-demo.test.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-// Demonstrates the exact shape of test that would have caught CL-6478
-// before it shipped: qwen3.8:27b emitted `\n Promise,
- body: unknown,
-): Promise {
- return fetchImpl(
- new Request("http://mock-ollama/v1/chat/completions", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify(body),
- }),
- );
-}
-
-// Stands in for hub-sessions's turn assembler persisting the PRECEDING
-// assistant reply -- tool_calls included -- into the next turn's history
-// before sending. This is the persistence step CL-6478's fix guards.
-async function assembleAndSendNextTurn(
- fetchImpl: (req: Request) => Promise,
- priorAssistantMessage: unknown,
- nextTurnContent: string,
-): Promise {
- return chat(fetchImpl, {
- model: "qwen3.8:27b",
- tools: [{ type: "function", function: { name: "run_shell" } }],
- messages: [
- { role: "system", content: "doctrine" },
- { role: "user", content: "run the sidecar bundle" },
- priorAssistantMessage,
- { role: "user", content: nextTurnContent },
- ],
- stream: false,
- });
-}
-
-describe("CL-6478 regression shape", () => {
- test("a turn assembler that persists the malformed name unchanged carries it into every following turn's history", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(
- sequence([ollama.reply.malformedToolName(), ollama.reply.text("turn 2, room still alive")]),
- );
-
- const turn1Response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- tools: [{ type: "function", function: { name: "run_shell" } }],
- messages: [
- { role: "system", content: "doctrine" },
- { role: "user", content: "run the sidecar bundle" },
- ],
- stream: false,
- });
- const turn1Body = (await turn1Response.json()) as ChatCompletionBody;
- const brokenToolCall = turn1Body.choices[0]?.message.tool_calls?.[0];
- expect(brokenToolCall?.function.name).toContain("\n
- message.toolCalls?.some((call) => call.name.includes("\n {
- const ollama = createOllamaMock();
- ollama.onChat(
- sequence([ollama.reply.malformedToolName(), ollama.reply.text("turn 2, room still alive")]),
- );
-
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- tools: [{ type: "function", function: { name: "run_shell" } }],
- messages: [
- { role: "system", content: "doctrine" },
- { role: "user", content: "run the sidecar bundle" },
- ],
- stream: false,
- });
-
- // A correct assembler runs sanitizeToolNameForPersistence (or
- // equivalent) before persisting -- the malformed fragment never
- // reaches history. Simulated here as the placeholder name the real
- // sanitizer collapses onto (MALFORMED_TOOL_NAME in
- // vendor/intx/hub-sessions/src/sanitize-tool-name.ts).
- const turn2Response = await assembleAndSendNextTurn(
- ollama.fetch,
- {
- role: "assistant",
- content: null,
- tool_calls: [
- {
- id: "call_0",
- type: "function",
- function: { name: "malformed_tool_call", arguments: "{}" },
- },
- ],
- },
- "did that work?",
- );
- const turn2Body = (await turn2Response.json()) as ChatCompletionBody;
-
- expect(turn2Body.choices[0]?.message.content).toBe("turn 2, room still alive");
- expect(
- ollama.requests
- .last()
- .messages.some((message) =>
- message.toolCalls?.some((call) => call.name.includes("\n Promise` shape every real caller
-// (`testProviderCredential`, `fetchOllamaModelCatalog`,
-// `fetchOllamaModelCapabilities`) actually uses. A cast past a signature
-// mismatch here would compile and then silently return `undefined` at
-// runtime, since these functions swallow thrown errors as "unreachable" —
-// exactly the failure mode a reviewer using this package as a real
-// consumer hit.
-import { describe, expect, test } from "bun:test";
-import {
- fetchOllamaModelCatalog,
- testProviderCredential,
- OLLAMA_PLACEHOLDER_SECRET,
-} from "@corbits/connections/credential-test";
-import { preferCompletionCapable } from "@corbits/connections/model-capability";
-import { createOllamaMock } from "./index";
-
-const BASE_URL = "http://mock-ollama";
-
-describe("OllamaMock as a real connections FetchLike consumer", () => {
- test("testProviderCredential proves reachability through the mock with no cast", async () => {
- const ollama = createOllamaMock({ models: [{ name: "qwen3.8:27b" }] });
-
- const result = await testProviderCredential({
- provider: "ollama",
- apiKey: OLLAMA_PLACEHOLDER_SECRET,
- baseURL: BASE_URL,
- fetchImpl: ollama.fetch,
- });
-
- expect(result).toEqual({ ok: true });
- });
-
- // The CL-6477 scenario: a fresh Ollama connect whose only pulled model
- // sorts first alphabetically is an embedding model. Without real,
- // wire-observed capability data, default-model resolution
- // (`preferCompletionCapable`) has nothing to filter on and an embedding
- // model wins the chat default. `fetchOllamaModelCatalog` probes each
- // model's capabilities (`/api/show`) the same way a live instance would.
- test("fetchOllamaModelCatalog + preferCompletionCapable never let embeddinggemma win the chat default", async () => {
- const ollama = createOllamaMock({
- models: [
- { name: "embeddinggemma:300m", capabilities: ["embedding"] },
- { name: "qwen3.8:27b", capabilities: ["completion", "tools"] },
- ],
- });
-
- const catalog = await fetchOllamaModelCatalog(BASE_URL, ollama.fetch);
- expect(catalog).toBeDefined();
- expect(catalog?.map((m) => m.canonicalName)).toEqual(["embeddinggemma:300m", "qwen3.8:27b"]);
-
- const chatCapable = preferCompletionCapable(
- catalog ?? [],
- (model) => model.capabilities,
- (model) => model.canonicalName,
- );
-
- expect(chatCapable.map((m) => m.canonicalName)).toEqual(["qwen3.8:27b"]);
- });
-});
diff --git a/packages/mocks/src/ollama/index.ts b/packages/mocks/src/ollama/index.ts
deleted file mode 100644
index 35e281b9b7..0000000000
--- a/packages/mocks/src/ollama/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export { createOllamaMock, OllamaMock } from "./mock";
-export type { OllamaChatHandler, CreateOllamaMockOptions, OllamaMockServer } from "./mock";
-export { sequence } from "./scenarios";
-export type { AdversarialReplies } from "./scenarios";
-export { CapturedChatRequest, CapturedRequestLog } from "./capture";
-export type {
- OllamaCapability,
- OllamaCatalogEntry,
- OllamaChatReply,
- OllamaToolCall,
- CapturedToolDeclaration,
- CapturedMessage,
- CapturedMessageRole,
-} from "./types";
diff --git a/packages/mocks/src/ollama/mock.test.ts b/packages/mocks/src/ollama/mock.test.ts
deleted file mode 100644
index b4d7458bbd..0000000000
--- a/packages/mocks/src/ollama/mock.test.ts
+++ /dev/null
@@ -1,235 +0,0 @@
-import { describe, expect, test } from "bun:test";
-import { createOllamaMock } from "./index";
-
-async function chat(
- fetchImpl: (req: Request) => Promise,
- body: unknown,
-): Promise {
- return fetchImpl(
- new Request("http://mock-ollama/v1/chat/completions", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify(body),
- }),
- );
-}
-
-describe("createOllamaMock: model catalogue", () => {
- test("serves a scripted /api/tags with no models pulled", async () => {
- const ollama = createOllamaMock({
- models: [
- { name: "qwen3.8:27b", capabilities: ["completion", "tools"] },
- { name: "embeddinggemma:300m", capabilities: ["embedding"] },
- ],
- });
-
- const response = await ollama.fetch(new Request("http://mock-ollama/api/tags"));
- const body = (await response.json()) as { models: { name: string }[] };
-
- expect(response.status).toBe(200);
- expect(body.models.map((m) => m.name)).toEqual(["qwen3.8:27b", "embeddinggemma:300m"]);
- });
-
- test("setModels updates the catalogue a test connects against mid-run", async () => {
- const ollama = createOllamaMock({ models: [] });
- ollama.setModels([{ name: "qwen3.8:27b" }]);
-
- const response = await ollama.fetch(new Request("http://mock-ollama/api/tags"));
- const body = (await response.json()) as { models: { name: string }[] };
- expect(body.models).toHaveLength(1);
- });
-
- test("/api/show answers each model's own scripted capabilities", async () => {
- const ollama = createOllamaMock({
- models: [{ name: "embeddinggemma:300m", capabilities: ["embedding"] }],
- });
-
- const response = await ollama.fetch(
- new Request("http://mock-ollama/api/show", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ model: "embeddinggemma:300m" }),
- }),
- );
- const body = (await response.json()) as { capabilities: string[] };
- expect(body.capabilities).toEqual(["embedding"]);
- });
-});
-
-describe("createOllamaMock: chat completions", () => {
- test("non-streaming reply carries the scripted text", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("hi there"));
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "hello" }],
- stream: false,
- });
- const body = (await response.json()) as {
- choices: { message: { content: string }; finish_reason: string }[];
- };
-
- expect(body.choices[0]?.message.content).toBe("hi there");
- expect(body.choices[0]?.finish_reason).toBe("stop");
- });
-
- test("streaming reply is SSE, terminated with [DONE]", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("streamed"));
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "hello" }],
- stream: true,
- });
- const text = await response.text();
-
- expect(response.headers.get("content-type")).toBe("text/event-stream");
- expect(text).toContain('"content":"streamed"');
- expect(text.trim().endsWith("data: [DONE]")).toBe(true);
- });
-
- test("tool-call reply carries function name and JSON-encoded arguments", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.toolCall("create_agent", { name: "researcher" }));
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "make me an agent" }],
- tools: [{ type: "function", function: { name: "create_agent" } }],
- stream: false,
- });
- const body = (await response.json()) as {
- choices: {
- message: {
- tool_calls?: { function: { name: string; arguments: string } }[];
- };
- finish_reason: string;
- }[];
- };
- const call = body.choices[0]?.message.tool_calls?.[0];
-
- expect(call?.function.name).toBe("create_agent");
- expect(JSON.parse(call?.function.arguments ?? "{}")).toEqual({
- name: "researcher",
- });
- expect(body.choices[0]?.finish_reason).toBe("tool_calls");
- });
-
- test("rejects a malformed request body instead of crashing", async () => {
- const ollama = createOllamaMock();
- const response = await chat(ollama.fetch, { model: 123, messages: "nope" });
- expect(response.status).toBe(400);
- });
-});
-
-describe("createOllamaMock: request capture", () => {
- test("records every chat request in order", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("ok"));
-
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "first" }],
- });
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "second" }],
- });
-
- expect(ollama.requests.count).toBe(2);
- expect(ollama.requests.all[0]?.messages[0]?.content).toBe("first");
- expect(ollama.requests.last().messages[0]?.content).toBe("second");
- });
-
- test("last() throws when nothing has been captured yet", () => {
- const ollama = createOllamaMock();
- expect(() => ollama.requests.last()).toThrow(/no chat request/i);
- });
-});
-
-describe("CapturedChatRequest assertions", () => {
- test("expectModel throws with the actual model on mismatch", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("ok"));
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "hi" }],
- });
-
- expect(() => ollama.requests.last().expectModel("qwen3.8:27b")).not.toThrow();
- expect(() => ollama.requests.last().expectModel("other-model")).toThrow(/qwen3\.8:27b/);
- });
-
- test("expectToolsDeclared(names) enforces the exact declared set", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("ok"));
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "hi" }],
- tools: [{ type: "function", function: { name: "send_message" } }],
- });
-
- const request = ollama.requests.last();
- expect(() => request.expectToolsDeclared(["send_message"])).not.toThrow();
- expect(() => request.expectToolsDeclared(["create_agent"])).toThrow();
- });
-
- test("expectMessageRoles catches history collapsing to just [system, user]", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("ok"));
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [
- { role: "system", content: "doctrine" },
- { role: "user", content: "turn 1" },
- { role: "assistant", content: "reply 1" },
- { role: "user", content: "turn 17" },
- ],
- });
-
- const request = ollama.requests.last();
- expect(() => request.expectMessageRoles(["system", "user", "assistant", "user"])).not.toThrow();
- expect(() => request.expectMessageRoles(["system", "user"])).toThrow();
- });
-
- test("expectHistoryContains finds an ordered subsequence", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.text("ok"));
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [
- { role: "system", content: "doctrine" },
- { role: "user", content: "book me a flight" },
- { role: "assistant", content: "calling search_flights" },
- { role: "tool", content: "3 results", tool_call_id: "call_0" },
- ],
- });
-
- const request = ollama.requests.last();
- expect(() =>
- request.expectHistoryContains([
- { role: "user", content: "book me a flight" },
- { role: "tool" },
- ]),
- ).not.toThrow();
- expect(() =>
- request.expectHistoryContains([{ role: "user", content: "never sent" }]),
- ).toThrow();
- });
-});
-
-describe("createOllamaMock: as a server", () => {
- test("listen() serves the same routes over real HTTP", async () => {
- const ollama = createOllamaMock({ models: [{ name: "qwen3.8:27b" }] });
- const server = await ollama.listen();
- try {
- const response = await fetch(`${server.url}/api/tags`);
- const body = (await response.json()) as { models: { name: string }[] };
- expect(body.models[0]?.name).toBe("qwen3.8:27b");
- } finally {
- await server.close();
- }
- });
-});
diff --git a/packages/mocks/src/ollama/mock.ts b/packages/mocks/src/ollama/mock.ts
deleted file mode 100644
index f152d4db49..0000000000
--- a/packages/mocks/src/ollama/mock.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-import { type } from "arktype";
-import { CapturedChatRequest, CapturedRequestLog } from "./capture";
-import { createAdversarialReplies } from "./scenarios";
-import {
- ChatCompletionRequestBody,
- type OllamaCatalogEntry,
- type OllamaChatReply,
- type OllamaToolCall,
-} from "./types";
-
-export type OllamaChatHandler = (request: CapturedChatRequest) => OllamaChatReply;
-
-export type CreateOllamaMockOptions = {
- /** The `/api/tags` catalogue served before any `onChat` handler runs.
- * Change it mid-test with `setModels` to exercise a connect flow that
- * (re)reads the catalogue. */
- readonly models?: readonly OllamaCatalogEntry[];
-};
-
-export type OllamaMockServer = {
- readonly url: string;
- close(): Promise;
-};
-
-function defaultReply(): OllamaChatReply {
- return { text: "mock reply", finishReason: "stop" };
-}
-
-function toolCallToWire(call: OllamaToolCall, index: number) {
- return {
- id: `call_${index}`,
- type: "function" as const,
- function: {
- name: call.name,
- arguments: call.rawArguments ?? JSON.stringify(call.arguments),
- },
- };
-}
-
-function replyMessage(reply: OllamaChatReply) {
- const toolCalls = reply.toolCalls?.map(toolCallToWire);
- return {
- role: "assistant" as const,
- content: reply.text ?? null,
- ...(toolCalls !== undefined ? { tool_calls: toolCalls } : {}),
- };
-}
-
-function finishReasonOf(reply: OllamaChatReply): string {
- return (
- reply.finishReason ??
- (reply.toolCalls !== undefined && reply.toolCalls.length > 0 ? "tool_calls" : "stop")
- );
-}
-
-function nonStreamingResponse(model: string, reply: OllamaChatReply): Response {
- const body = {
- id: "chatcmpl-mock",
- object: "chat.completion",
- created: Math.floor(Date.now() / 1000),
- model,
- choices: [
- {
- index: 0,
- message: replyMessage(reply),
- finish_reason: finishReasonOf(reply),
- },
- ],
- };
- return new Response(JSON.stringify(body), {
- status: 200,
- headers: { "content-type": "application/json" },
- });
-}
-
-// A single-chunk SSE stream carrying the whole reply, then `[DONE]` — real
-// Ollama and OpenAI both emit the assistant content across many deltas,
-// but this mock's contract is the request it received and the shape of
-// the reply, not reproducing token-by-token pacing.
-function streamingResponse(model: string, reply: OllamaChatReply): Response {
- const chunk = {
- id: "chatcmpl-mock",
- object: "chat.completion.chunk",
- created: Math.floor(Date.now() / 1000),
- model,
- choices: [
- {
- index: 0,
- delta: {
- role: "assistant",
- content: reply.text ?? null,
- ...(reply.toolCalls !== undefined
- ? { tool_calls: reply.toolCalls.map(toolCallToWire) }
- : {}),
- },
- finish_reason: null,
- },
- ],
- };
- const doneChunk = {
- id: "chatcmpl-mock",
- object: "chat.completion.chunk",
- created: Math.floor(Date.now() / 1000),
- model,
- choices: [{ index: 0, delta: {}, finish_reason: finishReasonOf(reply) }],
- };
- const body =
- `data: ${JSON.stringify(chunk)}\n\n` +
- `data: ${JSON.stringify(doneChunk)}\n\n` +
- `data: [DONE]\n\n`;
- return new Response(body, {
- status: 200,
- headers: { "content-type": "text/event-stream" },
- });
-}
-
-export class OllamaMock {
- readonly requests = new CapturedRequestLog();
- private models: OllamaCatalogEntry[];
- private handler: OllamaChatHandler = defaultReply;
-
- constructor(options: CreateOllamaMockOptions = {}) {
- this.models = [...(options.models ?? [])];
- }
-
- setModels(models: readonly OllamaCatalogEntry[]): void {
- this.models = [...models];
- }
-
- /** Scripts the reply to every `/v1/chat/completions` request from now
- * on. Called once per request, so a handler can vary its reply by
- * turn count (`this.requests.count`) or by inspecting the captured
- * request it's handed. */
- onChat(handler: OllamaChatHandler): void {
- this.handler = handler;
- }
-
- reply = {
- text: (text: string): OllamaChatReply => ({ text, finishReason: "stop" }),
- toolCall: (name: string, args: unknown): OllamaChatReply => ({
- toolCalls: [{ name, arguments: args }],
- finishReason: "tool_calls",
- }),
- toolCalls: (calls: readonly OllamaToolCall[]): OllamaChatReply => ({
- toolCalls: calls,
- finishReason: "tool_calls",
- }),
- ...createAdversarialReplies(),
- };
-
- /**
- * The whole mock as a `fetch`-shaped function — `(input, init?)`, the
- * same two-argument shape as global `fetch` and, load-bearingly,
- * `@corbits/connections`'s `FetchLike` (the seam every real caller —
- * `testProviderCredential`, `fetchOllamaModelCatalog`,
- * `fetchOllamaModelCapabilities` — actually threads a `fetchImpl`
- * through as). A caller passing a bare `Request`
- * (`ollama.fetch(new Request(...))`) still works; the common case is a
- * URL string plus an init object, exactly what those functions build.
- */
- fetch = async (input: string | Request, init?: RequestInit): Promise => {
- const request =
- input instanceof Request
- ? init === undefined
- ? input
- : new Request(input, init)
- : new Request(input, init);
- return this.route(request);
- };
-
- private async route(request: Request): Promise {
- const url = new URL(request.url);
- if (request.method === "GET" && url.pathname === "/api/tags") {
- return this.handleTags();
- }
- if (request.method === "POST" && url.pathname === "/api/show") {
- return this.handleShow(await request.json());
- }
- if (request.method === "POST" && url.pathname === "/v1/chat/completions") {
- return this.handleChatCompletions(await request.json());
- }
- return new Response(
- JSON.stringify({
- error: `mock ollama has no route for ${request.method} ${url.pathname}`,
- }),
- { status: 404, headers: { "content-type": "application/json" } },
- );
- }
-
- private handleTags(): Response {
- const body = {
- models: this.models.map((m) => ({
- name: m.name,
- digest: m.digest ?? `sha256:mock-${m.name}`,
- size: m.size ?? 0,
- })),
- };
- return new Response(JSON.stringify(body), {
- status: 200,
- headers: { "content-type": "application/json" },
- });
- }
-
- private handleShow(rawBody: unknown): Response {
- const ShowRequest = type({ model: "string" });
- const parsed = ShowRequest(rawBody);
- if (parsed instanceof type.errors) {
- return new Response(JSON.stringify({ error: parsed.summary }), {
- status: 400,
- headers: { "content-type": "application/json" },
- });
- }
- const entry = this.models.find((m) => m.name === parsed.model);
- return new Response(JSON.stringify({ capabilities: entry?.capabilities ?? [] }), {
- status: 200,
- headers: { "content-type": "application/json" },
- });
- }
-
- private handleChatCompletions(rawBody: unknown): Response {
- const parsed = ChatCompletionRequestBody(rawBody);
- if (parsed instanceof type.errors) {
- return new Response(
- JSON.stringify({
- error: `malformed chat-completions request: ${parsed.summary}`,
- }),
- { status: 400, headers: { "content-type": "application/json" } },
- );
- }
- const captured = new CapturedChatRequest(parsed);
- this.requests.push(captured);
- const reply = this.handler(captured);
- return parsed.stream
- ? streamingResponse(parsed.model, reply)
- : nonStreamingResponse(parsed.model, reply);
- }
-
- /** Starts a real HTTP server (Bun) and returns its URL — for the e2e
- * path, where the stack is pointed at an Ollama origin via env rather
- * than an in-process fetch. */
- async listen(port = 0): Promise {
- const server = Bun.serve({ port, fetch: (request) => this.route(request) });
- return {
- url: `http://localhost:${server.port}`,
- close: async () => {
- server.stop(true);
- },
- };
- }
-}
-
-export function createOllamaMock(options?: CreateOllamaMockOptions): OllamaMock {
- return new OllamaMock(options);
-}
diff --git a/packages/mocks/src/ollama/scenarios.test.ts b/packages/mocks/src/ollama/scenarios.test.ts
deleted file mode 100644
index f12b5b3c5d..0000000000
--- a/packages/mocks/src/ollama/scenarios.test.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-// Each of these reproduces one specific, observed way a real local model
-// misbehaved -- every one cost us a bug the night this catalogue was
-// built. They are asserted here as plain reply builders; cl-6478-demo.test.ts
-// demonstrates the flagship case (malformedToolName) wired through a
-// multi-turn sequence.
-import { describe, expect, test } from "bun:test";
-import { createOllamaMock, sequence } from "./index";
-
-async function chat(
- fetchImpl: (req: Request) => Promise,
- body: unknown,
-): Promise {
- return fetchImpl(
- new Request("http://mock-ollama/v1/chat/completions", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify(body),
- }),
- );
-}
-
-type ChatCompletionBody = {
- choices: {
- message: {
- content: string | null;
- tool_calls?: { function: { name: string; arguments: string } }[];
- };
- finish_reason: string;
- }[];
-};
-
-describe("ollama.reply adversarial scenarios", () => {
- test("malformedToolName carries CL-6478's exact leaked fragment", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.malformedToolName());
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "run the sidecar bundle" }],
- tools: [{ type: "function", function: { name: "run_shell" } }],
- });
- const body = (await response.json()) as ChatCompletionBody;
-
- const name = body.choices[0]?.message.tool_calls?.[0]?.function.name;
- expect(name).toContain("\n {
- const ollama = createOllamaMock();
- for (const length of [63, 64, 65]) {
- ollama.onChat(() => ollama.reply.toolNameOfLength(length));
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "go" }],
- });
- const body = (await response.json()) as ChatCompletionBody;
- const name = body.choices[0]?.message.tool_calls?.[0]?.function.name;
- expect(name).toHaveLength(length);
- }
- });
-
- test("textlessToolCall carries no text, only a tool call -- a tool-only round, not an empty reply", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.textlessToolCall("create_agent", { name: "researcher" }));
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "make me an agent" }],
- });
- const body = (await response.json()) as ChatCompletionBody;
-
- expect(body.choices[0]?.message.content).toBeNull();
- expect(body.choices[0]?.message.tool_calls).toHaveLength(1);
- expect(body.choices[0]?.finish_reason).toBe("tool_calls");
- });
-
- test("wrongShapedToolArgs is valid JSON that does not match the declared schema", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() =>
- ollama.reply.wrongShapedToolArgs("search_flights", {
- destination_typo: "SFO",
- passengers: "two",
- }),
- );
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "book me a flight" }],
- });
- const body = (await response.json()) as ChatCompletionBody;
- const rawArgs = body.choices[0]?.message.tool_calls?.[0]?.function.arguments;
-
- expect(() => JSON.parse(rawArgs ?? "")).not.toThrow();
- expect(JSON.parse(rawArgs ?? "{}")).toEqual({
- destination_typo: "SFO",
- passengers: "two",
- });
- });
-
- test("truncatedToolArgs reaches the wire byte-for-byte and is NOT valid JSON", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.truncatedToolArgs("search_flights", '{"origin": "SFO"'));
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "book me a flight" }],
- });
- const body = (await response.json()) as ChatCompletionBody;
- const rawArgs = body.choices[0]?.message.tool_calls?.[0]?.function.arguments;
-
- expect(rawArgs).toBe('{"origin": "SFO"');
- expect(() => JSON.parse(rawArgs ?? "")).toThrow();
- });
-
- test("refusal returns plain declining text with a normal stop reason", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.refusal("I can't help with that."));
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "do something unsafe" }],
- });
- const body = (await response.json()) as ChatCompletionBody;
-
- expect(body.choices[0]?.message.content).toBe("I can't help with that.");
- expect(body.choices[0]?.finish_reason).toBe("stop");
- });
-
- test("oversized produces a large text blob with a length finish reason", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.oversized(50_000));
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "write me a very long story" }],
- });
- const body = (await response.json()) as ChatCompletionBody;
-
- expect(body.choices[0]?.message.content).toHaveLength(50_000);
- expect(body.choices[0]?.finish_reason).toBe("length");
- });
-
- test("hallucinatedToolName calls load_skill instead of skills_load", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(() => ollama.reply.hallucinatedToolName());
-
- const response = await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "load the git skill" }],
- tools: [{ type: "function", function: { name: "skills_load" } }],
- });
- const body = (await response.json()) as ChatCompletionBody;
-
- const name = body.choices[0]?.message.tool_calls?.[0]?.function.name;
- expect(name).toBe("load_skill");
- expect(name).not.toBe("skills_load");
- });
-});
-
-describe("sequence", () => {
- test("scripts one reply per turn, then repeats the last one", async () => {
- const ollama = createOllamaMock();
- ollama.onChat(sequence([ollama.reply.malformedToolName(), ollama.reply.text("turn 2")]));
-
- const turn1 = (await (
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "1" }],
- })
- ).json()) as ChatCompletionBody;
- const turn2 = (await (
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "2" }],
- })
- ).json()) as ChatCompletionBody;
- const turn3 = (await (
- await chat(ollama.fetch, {
- model: "qwen3.8:27b",
- messages: [{ role: "user", content: "3" }],
- })
- ).json()) as ChatCompletionBody;
-
- expect(turn1.choices[0]?.message.tool_calls?.[0]?.function.name).toContain("\n {
- expect(() => sequence([])).toThrow(/at least one reply/);
- });
-});
diff --git a/packages/mocks/src/ollama/scenarios.ts b/packages/mocks/src/ollama/scenarios.ts
deleted file mode 100644
index ab38874c9d..0000000000
--- a/packages/mocks/src/ollama/scenarios.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import type { CapturedChatRequest } from "./capture";
-import type { OllamaChatReply } from "./types";
-
-// CL-6478's exact wire value: qwen3.8:27b leaked a stray `\n ({
- toolCalls: [{ name: CL_6478_MALFORMED_TOOL_NAME, arguments: {} }],
- finishReason: "tool_calls",
- }),
- toolNameOfLength: (length) => ({
- toolCalls: [{ name: nameOfExactLength(length), arguments: {} }],
- finishReason: "tool_calls",
- }),
- textlessToolCall: (name, args = {}) => ({
- toolCalls: [{ name, arguments: args }],
- finishReason: "tool_calls",
- }),
- wrongShapedToolArgs: (name, args) => ({
- toolCalls: [{ name, arguments: args }],
- finishReason: "tool_calls",
- }),
- truncatedToolArgs: (name, rawArguments) => ({
- toolCalls: [{ name, arguments: undefined, rawArguments }],
- finishReason: "tool_calls",
- }),
- refusal: (text = "I can't help with that request.") => ({
- text,
- finishReason: "stop",
- }),
- oversized: (approxChars = 200_000) => ({
- text: "x".repeat(approxChars),
- finishReason: "length",
- }),
- hallucinatedToolName: () => ({
- toolCalls: [{ name: HALLUCINATED_TOOL_NAME, arguments: {} }],
- finishReason: "tool_calls",
- }),
- };
-}
-
-/**
- * Scripts one reply per turn -- turn 1 gets `replies[0]`, turn 2 gets
- * `replies[1]`, and so on; once `replies` runs out, every later turn
- * repeats the last one. Pass straight to `onChat`:
- *
- * ```ts
- * ollama.onChat(sequence([
- * ollama.reply.malformedToolName(),
- * ollama.reply.text("turn 2 -- does the room survive?"),
- * ]));
- * ```
- *
- * This is the shape CL-6478's "does the room survive the next turn?"
- * contract needs: script the bad turn once, then assert the room is still
- * alive on the turn after.
- */
-export function sequence(
- replies: readonly OllamaChatReply[],
-): (request: CapturedChatRequest) => OllamaChatReply {
- if (replies.length === 0) {
- throw new Error("sequence() needs at least one reply");
- }
- let turn = 0;
- return () => {
- const index = Math.min(turn, replies.length - 1);
- turn += 1;
- const reply = replies[index];
- if (reply === undefined) {
- throw new Error("unreachable: sequence index out of bounds");
- }
- return reply;
- };
-}
diff --git a/packages/mocks/src/ollama/types.ts b/packages/mocks/src/ollama/types.ts
deleted file mode 100644
index a29db24c51..0000000000
--- a/packages/mocks/src/ollama/types.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { type } from "arktype";
-
-// Ollama's own `/api/show` capability vocabulary — the same strings
-// `@corbits/connections`'s `OLLAMA_CAPABILITY_MAP` translates from. A
-// catalogue entry scripts these directly so model-selection logic
-// (`preferCompletionCapable`, capability probing) is testable against the
-// exact values the real instance would answer, with no translation layer
-// duplicated in the mock.
-export type OllamaCapability = "completion" | "embedding" | "tools" | "vision" | "insert";
-
-export type OllamaCatalogEntry = {
- readonly name: string;
- readonly capabilities?: readonly OllamaCapability[];
- readonly digest?: string;
- readonly size?: number;
-};
-
-export type OllamaToolCall = {
- readonly name: string;
- readonly arguments: unknown;
- /** Wire-level override: when set, this exact string is sent as the
- * `function.arguments` field instead of `JSON.stringify(arguments)` --
- * the only way to script arguments that are NOT valid JSON (a truncated
- * or otherwise unparseable tool-call payload, the shape a mid-stream
- * cutoff produces on a real model). Leave `arguments` as `undefined`
- * when this is set. */
- readonly rawArguments?: string;
-};
-
-/**
- * What a scripted `onChat` handler hands back. `text` alone is a plain
- * assistant reply; `toolCalls` puts the model in the tool-call branch a
- * real completion-capable model takes when its declared tools cover the
- * turn. Both may be set (a model that narrates before calling a tool).
- */
-export type OllamaChatReply = {
- readonly text?: string;
- readonly toolCalls?: readonly OllamaToolCall[];
- readonly finishReason?: "stop" | "tool_calls" | "length";
-};
-
-export type CapturedToolDeclaration = {
- readonly name: string;
- readonly description?: string;
- readonly parameters?: unknown;
-};
-
-export type CapturedMessageRole = "system" | "user" | "assistant" | "tool";
-
-export type CapturedMessage = {
- readonly role: CapturedMessageRole;
- readonly content: string;
- readonly toolCallId?: string;
- readonly toolCalls?: readonly OllamaToolCall[];
-};
-
-// The OpenAI-compatible chat-completions request body, as `@intx/inference`'s
-// openai adapter actually builds it (`buildRequest` in
-// `providers/openai.js`) and Ollama's `/v1/chat/completions` route
-// receives it. Parsed at the boundary — this is the mock server reading
-// a request it did not construct — so a caller sending a malformed body
-// surfaces a clear parse error instead of a downstream `undefined` crash.
-const ChatCompletionTool = type({
- type: "'function'",
- function: {
- name: "string",
- "description?": "string",
- "parameters?": "unknown",
- },
-});
-
-const ChatCompletionToolCall = type({
- id: "string",
- type: "'function'",
- function: { name: "string", arguments: "string" },
-});
-
-const ChatCompletionMessage = type({
- role: "'system' | 'user' | 'assistant' | 'tool'",
- "content?": "string | null",
- "tool_call_id?": "string",
- "tool_calls?": ChatCompletionToolCall.array(),
-});
-
-export const ChatCompletionRequestBody = type({
- model: "string",
- messages: ChatCompletionMessage.array(),
- "tools?": ChatCompletionTool.array(),
- "stream?": "boolean",
- "temperature?": "number",
-});
-
-export type ChatCompletionRequestBody = typeof ChatCompletionRequestBody.infer;
diff --git a/packages/mocks/tsconfig.json b/packages/mocks/tsconfig.json
deleted file mode 100644
index e125e90614..0000000000
--- a/packages/mocks/tsconfig.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "extends": "./tsconfig.src.json",
- "compilerOptions": {
- "composite": false,
- "noEmit": true,
- "disableSourceOfProjectReferenceRedirect": true,
- "declaration": false,
- "declarationMap": false,
- "emitDeclarationOnly": false,
- "tsBuildInfoFile": "tsconfig.tsbuildinfo",
- "rootDir": "../.."
- },
- "include": ["src"],
- "exclude": []
-}
diff --git a/packages/mocks/tsconfig.src.json b/packages/mocks/tsconfig.src.json
deleted file mode 100644
index fc8724305f..0000000000
--- a/packages/mocks/tsconfig.src.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "extends": "../../tsconfig.base.json",
- "include": ["src", "package.json", "src/**/*.json"],
- "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"],
- "compilerOptions": {
- "types": ["bun"],
- "composite": true,
- "emitDeclarationOnly": true,
- "outDir": "dist",
- "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
- }
-}
diff --git a/packages/notify/LICENSE b/packages/notify/LICENSE
deleted file mode 100644
index 173c3a21d0..0000000000
--- a/packages/notify/LICENSE
+++ /dev/null
@@ -1,176 +0,0 @@
-GNU LESSER GENERAL PUBLIC LICENSE
-
-Version 2.1, February 1999
-
-Copyright (C) 1991, 1999 Free Software Foundation, Inc.
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]
-
-Preamble
-
-The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
-
-This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
-
-When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
-
-To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
-
-For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
-
-We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
-
-To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
-
-Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
-
-Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
-
-When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
-
-We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
-
-For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
-
-In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
-
-Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
-
-The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
-
-GNU LESSER GENERAL PUBLIC LICENSE
-TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
-
-A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
-
-The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
-
-"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
-
-Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
-
-1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
-
-You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
-
-2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
-
- a) The modified work must itself be a software library.
-
- b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
-
- c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
-
- d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
-
-(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
-
-3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
-
-Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
-
-This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
-
-4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
-
-If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
-
-5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
-
-However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
-
-When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
-
-If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
-
-Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
-
-6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
-
-You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
-
- a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
-
- b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
-
- c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
-
- d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
-
- e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
-
-For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
-
-It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
-
-7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
-
- a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
-
- b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
-
-8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
-
-9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
-
-10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
-
-11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
-
-This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
-
-12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
-
-13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
-
-14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
-
-NO WARRANTY
-
-15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-END OF TERMS AND CONDITIONS
-
-How to Apply These Terms to Your New Libraries
-
-If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
-
-To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
-
- one line to give the library's name and an idea of what it does.
- Copyright (C) year name of author
-
- This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License.
-
- This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail.
-
-You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
-
-Yoyodyne, Inc., hereby disclaims all copyright interest in
-the library `Frob' (a library for tweaking knobs) written
-by James Random Hacker.
-
-signature of Ty Coon, 1 April 1990
-Ty Coon, President of Vice
-That's all there is to it!
diff --git a/packages/notify/README.md b/packages/notify/README.md
deleted file mode 100644
index 616cb96656..0000000000
--- a/packages/notify/README.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# @corbits/notify
-
-Turns things that need a human — a parked approval, a mention, a failed
-run, a credential about to expire, a task result — into mail in the
-recipient's mailbox, and fans that mail out to any external sinks
-(Slack, email, ...) a principal has turned on. The mailbox row is always
-the durable record; a sink delivery is a copy queued strictly after the
-mail commits, never the source of truth.
-
-## Composition
-
-- Delivers into a mailbox substrate this package names but does not
- import directly — `mailbox.ts` documents the seam
- (`@corbits/mailbox`'s `deliverInboxItems` shape: same fields, same
- dedupe contract, same post-commit `enqueue` callback) so `@corbits/notify`
- stays free of a hard dependency on any one mailbox build. `@corbits/inbox`
- is the host that wires this package's `./mailbox` types to the real
- `@corbits/mailbox`.
-- Sinks plug in through `sinks.ts`'s `NotificationSinkPlugin` contract —
- shaped like `@corbits/commands`' command-plugin contract: named factory
- exports, a registry, one explicit `register(...)` call in the host's
- composition root. No dispatch table, no switch statement.
-- `context.ts`'s `resolveNotifyContext` authorizes emit-time (may this
- install push a principal's notification to an external place at all)
- via `@intx/authz` and `@intx/types/authz`; read-time authorization needs
- nothing extra since a mailbox is already scoped to one principal.
-- `store.ts` uses `@intx/hub-common`'s `generateId` and Drizzle
- (`drizzle-orm/postgres-js`) against the package-owned `notify` Postgres
- schema (`schema.ts`), fully siloed from the platform's `public` schema.
-
-## Key modules
-
-- `events.ts` — `NotificationEvent` union (approval, mention, run
- failure, credential-expired) parsed at the boundary via arktype.
-- `deliver.ts` — `deliverNotification` and per-kind `deliver*Mail`
- helpers: writes the mailbox row, then queues one dispatch row per
- enabled sink.
-- `dispatcher.ts` — `createNotifyDispatcher`: the worker that carries a
- due dispatch row out to its sink; finds nothing due until an operator
- registers a sink.
-- `approval-bridge.ts` — `createApprovalNotificationBridge`: reads a
- parked approval back (keyed on `approval.id`, also the mailbox dedupe
- key, so a redelivered register frame mails once) and delivers it.
- Registers nothing itself.
-- `credential-expiry.ts` — pure decision layer (`findDueCredentialExpiries`)
- for which stored OAuth credentials just crossed their `expiresAt` line;
- touches no database, sends no mail.
-- `render.ts` — renders a `NotificationEvent` to subject/body/refs for a
- human reader; identifiers travel only in `refs`, never in display text.
-- `migrations.ts` / `schema.ts` — the one product table (`notify_dispatch`):
- one attempt stream per (mail row, sink).
-
-## Tests
-
-```
-cd packages/notify && bun test
-```
-
-`test/migrations.test.ts` is DB-gated against its own scratch database via
-`e2e/harness.ts`'s `e2eDatabaseUrl` — set
-`DATABASE_URL=postgres://localhost:5432/workbench_e2e` or it skips.
diff --git a/packages/notify/package.json b/packages/notify/package.json
deleted file mode 100644
index 2fbc55cefc..0000000000
--- a/packages/notify/package.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "name": "@corbits/notify",
- "version": "0.0.1",
- "private": true,
- "description": "Turns things that need a human — a parked approval, a failed run, a mention — into mail in the recipient's mailbox, and fans that mail out to registered sinks",
- "license": "LGPL-2.1-only",
- "type": "module",
- "exports": {
- ".": "./src/index.ts",
- "./mailbox": "./src/mailbox.ts",
- "./migrations": "./src/migrations.ts"
- },
- "scripts": {
- "typecheck": "tsc --noEmit",
- "test": "bun test"
- },
- "dependencies": {
- "@intx/authz": "0.3.0",
- "@intx/hub-common": "0.3.0",
- "@intx/types": "workspace:*",
- "arktype": "catalog:",
- "drizzle-orm": "catalog:",
- "postgres": "catalog:"
- },
- "devDependencies": {
- "@types/bun": "catalog:",
- "typescript": "catalog:"
- }
-}
diff --git a/packages/notify/src/approval-bridge.ts b/packages/notify/src/approval-bridge.ts
deleted file mode 100644
index 7dbf1a2946..0000000000
--- a/packages/notify/src/approval-bridge.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-// The step the platform is missing: an approval exists, therefore somebody
-// has mail. The approval and its signal correlation are already written by
-// the platform's own register co-write; this reads that pair back and
-// delivers it, registering nothing and widening nothing.
-//
-// Delivery keys off `approval.id`, which is also the mailbox dedupe key, so a
-// redelivered register frame — sidecar reconnect, log replay, supervisor
-// restart — mails once and only once.
-import { deliverApprovalMail, type NotifyDeliveryDeps } from "./deliver";
-import type { NotifyRecipient } from "./events";
-
-export interface ParkedApproval {
- readonly approvalId: string;
- readonly tenantId: string;
- readonly runId: string;
- readonly deploymentId: string;
- /** The tool the run is asking to call, named the way a person would read it. */
- readonly toolName: string;
- readonly toolArguments: object;
- readonly createdAt: Date;
-}
-
-export interface ApprovalNotificationBridgeDeps {
- readonly delivery: NotifyDeliveryDeps;
- readonly findParkedApproval: (correlationId: string) => Promise;
- /** Who may resolve this approval, and therefore who should hear about it. */
- readonly listApprovers: (approval: ParkedApproval) => Promise;
-}
-
-export type ApprovalNotificationBridge = (correlationId: string) => Promise;
-
-/**
- * Build the "an approval was created, so mail it" step. A correlation with no
- * approval row, or an approval nobody can resolve, delivers nothing rather
- * than mailing into the void.
- */
-export function createApprovalNotificationBridge(
- deps: ApprovalNotificationBridgeDeps,
-): ApprovalNotificationBridge {
- return async (correlationId) => {
- const approval = await deps.findParkedApproval(correlationId);
- if (approval === null) return;
- const recipients = await deps.listApprovers(approval);
- if (recipients.length === 0) return;
- await deliverApprovalMail(deps.delivery, {
- kind: "approval",
- approvalId: approval.approvalId,
- tenantId: approval.tenantId,
- runId: approval.runId,
- deploymentId: approval.deploymentId,
- toolName: approval.toolName,
- toolArguments: approval.toolArguments,
- recipients: [...recipients],
- createdAt: approval.createdAt.toISOString(),
- });
- };
-}
diff --git a/packages/notify/src/context.ts b/packages/notify/src/context.ts
deleted file mode 100644
index 931cd36366..0000000000
--- a/packages/notify/src/context.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-// Emit-time authorization and credential resolution for one sink, in one
-// place. Read-time authorization needs nothing here: a mailbox is scoped to a
-// single principal by construction, so there is no cross-principal read to
-// guard. What has to be checked is the other direction — whether this install
-// may push a principal's notification out to an external place at all.
-import { authorize } from "@intx/authz";
-import type { ConditionRegistry, GrantStore } from "@intx/types/authz";
-
-export type NotifyCredential = {
- readonly id: string;
- readonly name: string;
- readonly kind: string;
-};
-
-export type NotifyContext = {
- readonly tenantId: string;
- readonly principalId: string;
- readonly sinkName: string;
- readonly credential: NotifyCredential;
-};
-
-export class NotifyGrantMissingError extends Error {
- constructor(sinkName: string, tenantId: string) {
- super(
- `No grant allows delivering notifications through the ${JSON.stringify(sinkName)} ` +
- `sink in this workspace. Grant "notify:${sinkName}" in ${tenantId} first.`,
- );
- this.name = "NotifyGrantMissingError";
- }
-}
-
-export class NotifySinkNotConfiguredError extends Error {
- constructor(sinkName: string) {
- super(
- `The ${JSON.stringify(sinkName)} notification sink has no credential configured ` +
- "in this workspace, so there is nothing to deliver through.",
- );
- this.name = "NotifySinkNotConfiguredError";
- }
-}
-
-export class NotifySinkCredentialInvalidError extends Error {
- constructor(sinkName: string, expectedKind: string, actualKind: string) {
- super(
- `The credential configured for the ${JSON.stringify(sinkName)} notification sink is a ` +
- `${JSON.stringify(actualKind)} credential, but the sink needs a ${JSON.stringify(expectedKind)} one.`,
- );
- this.name = "NotifySinkCredentialInvalidError";
- }
-}
-
-export const NOTIFY_DELIVER_ACTION = "deliver";
-
-export interface ResolveNotifyContextDeps {
- readonly grantStore: GrantStore;
- readonly conditionRegistry?: ConditionRegistry;
- /** The credential an operator configured for this sink in this workspace, if any. */
- readonly findSinkCredential: (args: {
- tenantId: string;
- sinkName: string;
- }) => Promise;
-}
-
-export interface ResolveNotifyContextArgs {
- readonly tenantId: string;
- readonly principalId: string;
- readonly sinkName: string;
- readonly credentialKind: string;
-}
-
-/**
- * Resolve the grant and credential one sink delivery needs, or throw a named
- * error saying exactly which of the three is missing. Grants are the platform's
- * own — the resource string is `notify:`, matched by `@intx/authz`
- * the same way `approval:` is.
- */
-export async function resolveNotifyContext(
- deps: ResolveNotifyContextDeps,
- args: ResolveNotifyContextArgs,
-): Promise {
- const result = await authorize(
- deps.grantStore,
- args.principalId,
- args.tenantId,
- `notify:${args.sinkName}`,
- NOTIFY_DELIVER_ACTION,
- deps.conditionRegistry,
- );
- if (result.effect !== "allow") {
- throw new NotifyGrantMissingError(args.sinkName, args.tenantId);
- }
- const credential = await deps.findSinkCredential({
- tenantId: args.tenantId,
- sinkName: args.sinkName,
- });
- if (credential === null) {
- throw new NotifySinkNotConfiguredError(args.sinkName);
- }
- if (credential.kind !== args.credentialKind) {
- throw new NotifySinkCredentialInvalidError(args.sinkName, args.credentialKind, credential.kind);
- }
- return {
- tenantId: args.tenantId,
- principalId: args.principalId,
- sinkName: args.sinkName,
- credential,
- };
-}
diff --git a/packages/notify/src/deliver.ts b/packages/notify/src/deliver.ts
deleted file mode 100644
index b0246bd0e2..0000000000
--- a/packages/notify/src/deliver.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-// The one delivery step this package adds to the platform: something needs a
-// human, so it becomes mail in that human's mailbox. Nothing else here is new
-// state — an approval's parked run already lives in `signal_correlation` and
-// `approval`, and this never registers a correlation of its own.
-//
-// Fan-out to external sinks is queued strictly after the mail commits, one
-// dispatch row per (mail row, enabled sink). A sink is never called from this
-// path: the mail is the durable record, and a copy of it is the worker's job.
-//
-// That queuing step is NOT atomic with the mail write: a crash or a
-// throw between them leaves a mail row committed with no dispatch row
-// queued (CL-7238). The mail delivery reports such rows with a `null`
-// id, so a redelivery resolves their ids through the host's
-// `resolveExistingMailIds` read-back and repairs the missing dispatch
-// rows; the dispatch store dedupes by (mail row, sink), so the repair
-// never double-queues.
-import {
- parseNotificationEvent,
- type ApprovalNotification,
- type CredentialExpiredNotification,
- type MentionNotification,
- type NotificationEvent,
- type RunFailureNotification,
-} from "./events";
-import type {
- MailboxDelivery,
- NotifyAddressing,
- NotifyInboxItem,
- ResolveExistingMailIds,
-} from "./mailbox";
-import { notificationExternalId, renderNotification } from "./render";
-import type { SinkRegistry } from "./sinks";
-import type { EnqueueDispatchInput, NotifyDispatchStore } from "./store";
-
-/** Every notification is mail from the same place, so a mailbox can group it. */
-export const NOTIFY_MAIL_SOURCE = "notify";
-
-export interface NotifyDeliveryDeps {
- readonly mail: MailboxDelivery;
- readonly addressing: NotifyAddressing;
- readonly dispatch: NotifyDispatchStore;
- readonly sinks: SinkRegistry;
- /**
- * Read-back for the CL-7238 crash window: when the mail delivery
- * reports an item as pre-existing (`id: null`), the row was committed
- * by an earlier attempt that died before its dispatch enqueue.
- * Resolving those items to their row ids lets this call repair the
- * missing dispatch rows; without it a redelivery after such a crash
- * still loses the dispatch. The host owns this wiring.
- */
- readonly resolveExistingMailIds?: ResolveExistingMailIds;
-}
-
-export interface NotifyDeliveryReport {
- /** Mail rows newly written by this call; a deduped recipient contributes none. */
- readonly deliveredMailboxRowIds: readonly string[];
- /** Dispatch rows queued for external sinks; zero until a sink is registered. */
- readonly queuedDispatchCount: number;
-}
-
-function toInboxItems(event: NotificationEvent, addressing: NotifyAddressing): NotifyInboxItem[] {
- const rendered = renderNotification(event);
- const externalId = notificationExternalId(event);
- return event.recipients.map((recipient) => ({
- tenantId: recipient.tenantId,
- principalId: recipient.principalId,
- address: addressing.inbox(recipient),
- fromAddress: addressing.from(event.kind),
- subject: rendered.subject,
- body: rendered.body,
- source: NOTIFY_MAIL_SOURCE,
- externalId,
- refs: rendered.refs,
- }));
-}
-
-/**
- * Parse, write mail, then queue sink fan-out. The event is parsed here and
- * nowhere else, so no caller can push an unvalidated shape into a mailbox.
- */
-export async function deliverNotification(
- deps: NotifyDeliveryDeps,
- input: unknown,
-): Promise {
- const event = parseNotificationEvent(input);
- const items = toInboxItems(event, deps.addressing);
- const written: { id: string; tenantId: string; principalId: string }[] = [];
- const results = await deps.mail(items, {
- enqueue: ({ id, item }) => {
- written.push({
- id,
- tenantId: item.tenantId,
- principalId: item.principalId,
- });
- },
- });
-
- // CL-7238: a `null` id marks a row committed by an earlier attempt
- // that died before its dispatch enqueue (or a benign redelivery).
- // Resolve those ids so the enqueue below repairs the missing rows;
- // without the read-back there is nothing to enqueue for them.
- const deduped = items.filter((_, index) => (results[index]?.id ?? null) === null);
- const repaired: { id: string; tenantId: string; principalId: string }[] = [];
- if (deduped.length > 0 && deps.resolveExistingMailIds !== undefined) {
- const resolved = await deps.resolveExistingMailIds(deduped);
- deduped.forEach((item, index) => {
- const id = resolved[index];
- if (id !== undefined && id !== null) {
- repaired.push({
- id,
- tenantId: item.tenantId,
- principalId: item.principalId,
- });
- }
- });
- }
-
- const queued: EnqueueDispatchInput[] = [];
- for (const row of [...written, ...repaired]) {
- const enabled = await deps.sinks.listEnabledFor({
- tenantId: row.tenantId,
- principalId: row.principalId,
- });
- for (const sink of enabled) {
- queued.push({
- mailboxRowId: row.id,
- tenantId: row.tenantId,
- principalId: row.principalId,
- sinkName: sink.name,
- });
- }
- }
- await deps.dispatch.enqueue(queued);
-
- return {
- deliveredMailboxRowIds: written.map((row) => row.id),
- queuedDispatchCount: queued.length,
- };
-}
-
-/** A workflow parked on an approval, delivered to the people who can resolve it. */
-export function deliverApprovalMail(
- deps: NotifyDeliveryDeps,
- event: ApprovalNotification,
-): Promise {
- return deliverNotification(deps, event);
-}
-
-export function deliverRunFailureMail(
- deps: NotifyDeliveryDeps,
- event: RunFailureNotification,
-): Promise {
- return deliverNotification(deps, event);
-}
-
-export function deliverMentionMail(
- deps: NotifyDeliveryDeps,
- event: MentionNotification,
-): Promise {
- return deliverNotification(deps, event);
-}
-
-/** A stored credential's token expired, mailed to whoever can reconnect it. */
-export function deliverCredentialMail(
- deps: NotifyDeliveryDeps,
- event: CredentialExpiredNotification,
-): Promise {
- return deliverNotification(deps, event);
-}
diff --git a/packages/notify/src/dispatcher.ts b/packages/notify/src/dispatcher.ts
deleted file mode 100644
index ba870c3e40..0000000000
--- a/packages/notify/src/dispatcher.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-// The worker that carries a mail row out to whatever external places a
-// principal has turned on. With no sink registered it has nothing to carry and
-// finds nothing due, which is the correct steady state of a fresh install — a
-// sink only exists once an operator adds one to the composition root.
-import type { NotificationEvent } from "./events";
-import type { SinkRegistry } from "./sinks";
-import type { NotifyDispatchRow, NotifyDispatchStore } from "./store";
-
-export interface NotifyDispatchLogger {
- warn(message: string): void;
- error(message: string): void;
-}
-
-export interface NotifyDispatcherDeps {
- readonly store: NotifyDispatchStore;
- readonly sinks: SinkRegistry;
- readonly log: NotifyDispatchLogger;
- /** Rebuilds the event a mail row came from, so a sink renders the same thing the mailbox shows. */
- readonly loadEvent: (row: NotifyDispatchRow) => Promise;
- readonly tickIntervalMs: number;
- readonly batchSize: number;
- readonly maxAttempts: number;
- readonly retryBackoffMs: number;
-}
-
-export interface NotifyDispatcher {
- /** Claim everything due once and deliver it; returns how many rows were settled. */
- runOnce(now: Date): Promise;
- start(): void;
- stop(): void;
-}
-
-function backoffFrom(now: Date, attempts: number, baseMs: number): Date {
- return new Date(now.getTime() + baseMs * 2 ** (attempts - 1));
-}
-
-export function createNotifyDispatcher(deps: NotifyDispatcherDeps): NotifyDispatcher {
- let timer: ReturnType | undefined;
-
- async function settleOne(row: NotifyDispatchRow, now: Date): Promise {
- const attempts = row.attempts + 1;
- const sink = deps.sinks.get(row.sinkName);
- if (sink === undefined) {
- // The sink was removed from the composition root while rows were still
- // queued for it. Nothing can ever carry them, so they stop rather than
- // spinning forever.
- deps.log.warn(
- `Notification sink ${JSON.stringify(row.sinkName)} is no longer registered; ` +
- "its queued deliveries are closed out.",
- );
- await deps.store.settle({
- id: row.id,
- status: "dead",
- attempts,
- lastError: "sink is not registered",
- nextAttemptAt: now,
- });
- return;
- }
-
- const event = await deps.loadEvent(row);
- if (event === null) {
- await deps.store.settle({
- id: row.id,
- status: "dead",
- attempts,
- lastError: "the notification this delivery belongs to is gone",
- nextAttemptAt: now,
- });
- return;
- }
-
- const result = await sink
- .deliver({
- tenantId: row.tenantId,
- principalId: row.principalId,
- mailboxRowId: row.mailboxRowId,
- event,
- })
- .catch((error: unknown) => ({
- status: "failed" as const,
- error: error instanceof Error ? error.message : String(error),
- retryable: true,
- }));
-
- if (result.status === "delivered") {
- await deps.store.settle({
- id: row.id,
- status: "delivered",
- attempts,
- lastError: null,
- nextAttemptAt: now,
- });
- return;
- }
- if (result.status === "skipped") {
- await deps.store.settle({
- id: row.id,
- status: "delivered",
- attempts,
- lastError: result.reason,
- nextAttemptAt: now,
- });
- return;
- }
- const exhausted = !result.retryable || attempts >= deps.maxAttempts;
- await deps.store.settle({
- id: row.id,
- status: exhausted ? "dead" : "failed",
- attempts,
- lastError: result.error,
- nextAttemptAt: exhausted ? now : backoffFrom(now, attempts, deps.retryBackoffMs),
- });
- }
-
- async function runOnce(now: Date): Promise {
- const due = await deps.store.findDue(now, deps.batchSize);
- for (const row of due) await settleOne(row, now);
- return due.length;
- }
-
- return {
- runOnce,
- start() {
- if (timer !== undefined) return;
- timer = setInterval(() => {
- void runOnce(new Date()).catch((error: unknown) => {
- deps.log.error(
- `Notification dispatch tick failed: ${error instanceof Error ? error.message : String(error)}`,
- );
- });
- }, deps.tickIntervalMs);
- },
- stop() {
- if (timer === undefined) return;
- clearInterval(timer);
- timer = undefined;
- },
- };
-}
diff --git a/packages/notify/src/events.ts b/packages/notify/src/events.ts
deleted file mode 100644
index 1ca47968a5..0000000000
--- a/packages/notify/src/events.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-// The three things that can need a human's attention, parsed at the
-// boundary before anything is written. An approval carries no new state
-// of its own: the platform's `signal_correlation` + `approval` row pair
-// already IS the parked run, and these events only describe the delivery
-// of that fact into somebody's mail.
-import { type } from "arktype";
-
-export const NotifyRecipient = type({
- tenantId: "string > 0",
- principalId: "string > 0",
-});
-
-export const ApprovalNotification = type({
- kind: '"approval"',
- approvalId: "string > 0",
- tenantId: "string > 0",
- runId: "string > 0",
- deploymentId: "string > 0",
- toolName: "string > 0",
- toolArguments: "object",
- recipients: NotifyRecipient.array(),
- createdAt: "string.date.iso",
-});
-
-export const RunFailureNotification = type({
- kind: '"run-failure"',
- tenantId: "string > 0",
- runId: "string > 0",
- deploymentId: "string > 0",
- runLabel: "string > 0",
- error: "string",
- recipients: NotifyRecipient.array(),
- createdAt: "string.date.iso",
-});
-
-export const MentionNotification = type({
- kind: '"mention"',
- tenantId: "string > 0",
- threadId: "string > 0",
- threadLabel: "string > 0",
- mentionedBy: "string > 0",
- excerpt: "string",
- recipients: NotifyRecipient.array(),
- createdAt: "string.date.iso",
-});
-
-/** A stored credential's token expired — not a run event, but the same
- * shape of "a human needs to act" the other three kinds carry.
- * `providerId` is the credential's provider id (e.g. `huggingface`);
- * `providerLabel` is what a person calls it. Keyed for dedupe on
- * `credentialId` alone (see `notificationExternalId`), not per-sweep-tick,
- * so a still-expired credential is only ever mailed once until it's fixed. */
-export const CredentialExpiredNotification = type({
- kind: '"credential-expired"',
- tenantId: "string > 0",
- credentialId: "string > 0",
- providerId: "string > 0",
- providerLabel: "string > 0",
- recipients: NotifyRecipient.array(),
- createdAt: "string.date.iso",
-});
-
-export const NotificationEvent = ApprovalNotification.or(RunFailureNotification)
- .or(MentionNotification)
- .or(CredentialExpiredNotification);
-
-export type NotifyRecipient = typeof NotifyRecipient.infer;
-export type ApprovalNotification = typeof ApprovalNotification.infer;
-export type RunFailureNotification = typeof RunFailureNotification.infer;
-export type MentionNotification = typeof MentionNotification.infer;
-export type CredentialExpiredNotification = typeof CredentialExpiredNotification.infer;
-export type NotificationEvent = typeof NotificationEvent.infer;
-
-export class InvalidNotificationEventError extends Error {
- constructor(summary: string) {
- super(`Notification event does not parse: ${summary}`);
- this.name = "InvalidNotificationEventError";
- }
-}
-
-/** Parse-or-throw at the delivery boundary; nothing downstream casts. */
-export function parseNotificationEvent(input: unknown): NotificationEvent {
- const parsed = NotificationEvent(input);
- if (parsed instanceof type.errors) {
- throw new InvalidNotificationEventError(parsed.summary);
- }
- return parsed;
-}
diff --git a/packages/notify/src/index.ts b/packages/notify/src/index.ts
deleted file mode 100644
index b502bf171e..0000000000
--- a/packages/notify/src/index.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-export {
- createApprovalNotificationBridge,
- type ApprovalNotificationBridge,
- type ApprovalNotificationBridgeDeps,
- type ParkedApproval,
-} from "./approval-bridge";
-export {
- ApprovalNotification,
- CredentialExpiredNotification,
- InvalidNotificationEventError,
- MentionNotification,
- NotificationEvent,
- NotifyRecipient,
- RunFailureNotification,
- parseNotificationEvent,
-} from "./events";
-export {
- applyNotifyMigrations,
- notifyMigrations,
- type ApplyNotifyMigrationsReport,
- type NotifyMigration,
-} from "./migrations";
-export { notifyDispatch } from "./schema";
-export {
- createDrizzleNotifyDispatchStore,
- createInMemoryNotifyDispatchStore,
- type EnqueueDispatchInput,
- type NotifyDb,
- type NotifyDispatchRow,
- type NotifyDispatchStatus,
- type NotifyDispatchStore,
- type SettleDispatchInput,
-} from "./store";
-export {
- createSinkRegistry,
- DuplicateSinkNameError,
- type NotificationSinkPlugin,
- type SinkDeliveryContext,
- type SinkDeliveryResult,
- type SinkRegistry,
- type SinkScope,
-} from "./sinks";
-export {
- NOTIFY_MAIL_SOURCE,
- deliverApprovalMail,
- deliverCredentialMail,
- deliverMentionMail,
- deliverNotification,
- deliverRunFailureMail,
- type NotifyDeliveryDeps,
- type NotifyDeliveryReport,
-} from "./deliver";
-export {
- createNotifyDispatcher,
- type NotifyDispatcher,
- type NotifyDispatcherDeps,
- type NotifyDispatchLogger,
-} from "./dispatcher";
-export {
- NOTIFY_DELIVER_ACTION,
- NotifyGrantMissingError,
- NotifySinkCredentialInvalidError,
- NotifySinkNotConfiguredError,
- resolveNotifyContext,
- type NotifyContext,
- type NotifyCredential,
- type ResolveNotifyContextArgs,
- type ResolveNotifyContextDeps,
-} from "./context";
-export { notificationExternalId, renderNotification, type RenderedNotification } from "./render";
-export type {
- MailboxDelivery,
- NotifyAddressing,
- NotifyDeliveredItem,
- NotifyDeliverOpts,
- NotifyInboxItem,
- NotifyMailRef,
- ResolveExistingMailIds,
-} from "./mailbox";
diff --git a/packages/notify/src/mailbox.ts b/packages/notify/src/mailbox.ts
deleted file mode 100644
index 556225e9b4..0000000000
--- a/packages/notify/src/mailbox.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-// The mail substrate this package writes through, expressed as the one
-// function it calls. The shape is `@corbits/mailbox`'s `deliverInboxItems`
-// bound to a database handle: same fields, same dedupe contract, same
-// post-commit `enqueue` callback. Naming it here is what keeps `@corbits/notify`
-// free of a direct dependency on any one mailbox build while still refusing to
-// invent a second delivery mechanism — there is exactly one seam, and it is mail.
-export type NotifyMailRef = {
- readonly kind: string;
- readonly id: string;
- /** A human label for the ref, when the caller already has one to
- * hand (e.g. an artifact's title) — optional, since most refs are
- * navigational only (see `render.ts`'s own doc note) and a reader
- * resolves a display label from the id itself. */
- readonly label?: string;
-};
-
-export type NotifyInboxItem = {
- readonly tenantId: string;
- readonly principalId: string;
- readonly address: string;
- readonly fromAddress: string;
- readonly subject: string;
- readonly body: string;
- readonly source: string;
- readonly externalId: string;
- readonly refs?: readonly NotifyMailRef[];
-};
-
-/** `id` is null exactly when the item deduped and no row was written. */
-export type NotifyDeliveredItem = {
- readonly messageKey: string;
- readonly id: string | null;
-};
-
-export type NotifyDeliverOpts = {
- /** Called once per newly written row, strictly after the batch commits. */
- readonly enqueue?: (delivered: { id: string; item: NotifyInboxItem }) => void;
-};
-
-/** Results align positionally with `items`, mirroring the vendor delivery. */
-export type MailboxDelivery = (
- items: NotifyInboxItem[],
- opts?: NotifyDeliverOpts,
-) => Promise;
-
-/**
- * Read-back for mail rows an earlier delivery committed before dying
- * ahead of its dispatch enqueue (CL-7238). The input lists the items
- * the mail delivery just reported as pre-existing (`id: null`); the
- * result aligns positionally with the input, with `null` where no row
- * exists. The host serves this from the mailbox's unique mail key — a
- * lookup the seam doesn't expose today, so the host owns the wiring.
- */
-export type ResolveExistingMailIds = (
- items: readonly NotifyInboxItem[],
-) => Promise;
-
-/**
- * How a notification addresses mail. The host owns its own address space —
- * a mailbox address is a deployment fact, not a product rule — so both halves
- * are supplied rather than assembled from a hardcoded domain here.
- */
-export type NotifyAddressing = {
- inbox(recipient: { tenantId: string; principalId: string }): string;
- from(kind: string): string;
-};
diff --git a/packages/notify/src/migrations.ts b/packages/notify/src/migrations.ts
deleted file mode 100644
index d3f3ad9e1e..0000000000
--- a/packages/notify/src/migrations.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-// Package-owned migrations for `@corbits/notify`'s one product table,
-// following the same ledger pattern as `@corbits/schedules`: bookkeeping in
-// this package's own table so its history stays extractable on its own.
-// One migration, in final shape — the table is new, so there is nothing to
-// alter and nothing to backfill.
-import postgres from "postgres";
-
-export interface NotifyMigration {
- name: string;
- sql: string;
-}
-
-export const notifyMigrations: readonly NotifyMigration[] = [
- {
- name: "0001_notify_dispatch",
- sql: `
- CREATE TABLE IF NOT EXISTS "notify"."notify_dispatch" (
- "id" text PRIMARY KEY,
- "mailbox_row_id" text NOT NULL,
- "tenant_id" text NOT NULL REFERENCES "public"."tenant" ("id") ON DELETE CASCADE,
- "principal_id" text NOT NULL REFERENCES "public"."principal" ("id") ON DELETE CASCADE,
- "sink_name" text NOT NULL,
- "status" text NOT NULL,
- "attempts" integer NOT NULL DEFAULT 0,
- "last_error" text,
- "next_attempt_at" timestamptz NOT NULL DEFAULT now(),
- "created_at" timestamptz NOT NULL DEFAULT now(),
- "updated_at" timestamptz NOT NULL DEFAULT now(),
- CONSTRAINT "notify_dispatch_row_sink_unique"
- UNIQUE ("mailbox_row_id", "sink_name")
- );
- CREATE INDEX IF NOT EXISTS "notify_dispatch_due_idx"
- ON "notify"."notify_dispatch" ("status", "next_attempt_at");
- `,
- },
-];
-
-const SCHEMA = "notify";
-const LEDGER_TABLE = "notify_migrations";
-
-function quoteIdentifier(name: string): string {
- return `"${name.replace(/"/g, '""')}"`;
-}
-
-function quoteQualified(schema: string, name: string): string {
- return `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
-}
-
-export interface ApplyNotifyMigrationsReport {
- applied: string[];
- alreadyApplied: string[];
-}
-
-/**
- * Apply `notifyMigrations` against `databaseUrl`, idempotently. Failures name
- * the migration and the underlying error, so a partial apply is loud here
- * rather than silent at the next boot.
- */
-export async function applyNotifyMigrations(
- databaseUrl: string,
-): Promise {
- const sql = postgres(databaseUrl, { max: 1, onnotice: () => undefined });
- try {
- await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(SCHEMA)}`);
-
- await sql.unsafe(
- `CREATE TABLE IF NOT EXISTS ${quoteQualified(SCHEMA, LEDGER_TABLE)} (` +
- `name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`,
- );
- const rows = await sql.unsafe(`SELECT name FROM ${quoteQualified(SCHEMA, LEDGER_TABLE)}`);
- const alreadyApplied = new Set(rows.map((row) => String(row["name"])));
- const applied: string[] = [];
- for (const migration of notifyMigrations) {
- if (alreadyApplied.has(migration.name)) continue;
- try {
- await sql.begin(async (tx) => {
- await tx.unsafe(migration.sql);
- await tx.unsafe(
- `INSERT INTO ${quoteQualified(SCHEMA, LEDGER_TABLE)} (name) VALUES ($1)`,
- [migration.name],
- );
- });
- applied.push(migration.name);
- } catch (error) {
- throw new Error(
- `@corbits/notify migration ${JSON.stringify(migration.name)} failed: ` +
- `${error instanceof Error ? error.message : String(error)}`,
- { cause: error },
- );
- }
- }
- return { applied, alreadyApplied: [...alreadyApplied] };
- } finally {
- await sql.end();
- }
-}
diff --git a/packages/notify/src/render.ts b/packages/notify/src/render.ts
deleted file mode 100644
index 4794fda707..0000000000
--- a/packages/notify/src/render.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-// What a notification reads like in a mailbox. Every line here is written for
-// a person: tool names, run labels, thread titles and handles — never an
-// identifier. The identifiers travel in `refs`, where the interface uses them
-// to navigate and never to display.
-import type { NotificationEvent } from "./events";
-import type { NotifyMailRef } from "./mailbox";
-
-export type RenderedNotification = {
- readonly subject: string;
- readonly body: string;
- readonly refs: readonly NotifyMailRef[];
-};
-
-const MAX_ARGUMENT_LINES = 12;
-
-function describeToolArguments(args: object): string {
- const entries = Object.entries(args as Record);
- if (entries.length === 0) return "It takes no arguments.";
- const shown = entries.slice(0, MAX_ARGUMENT_LINES);
- const lines = shown.map(([key, value]) => ` ${key}: ${format(value)}`);
- if (entries.length > shown.length) {
- lines.push(` …and ${entries.length - shown.length} more`);
- }
- return ["With:", ...lines].join("\n");
-}
-
-function format(value: unknown): string {
- if (typeof value === "string") return value;
- return JSON.stringify(value) ?? String(value);
-}
-
-export function renderNotification(event: NotificationEvent): RenderedNotification {
- if (event.kind === "approval") {
- return {
- subject: `Approve “${event.toolName}”?`,
- body: [
- `A workflow is paused and waiting on you before it runs “${event.toolName}”.`,
- describeToolArguments(event.toolArguments),
- "Approve or decline it from your inbox to let the workflow continue.",
- ].join("\n\n"),
- refs: [
- { kind: "approval", id: event.approvalId },
- { kind: "run", id: event.runId },
- ],
- };
- }
- if (event.kind === "run-failure") {
- return {
- subject: `“${event.runLabel}” failed`,
- body: [
- `The run “${event.runLabel}” stopped with an error.`,
- event.error === "" ? "No error detail was reported." : event.error,
- ].join("\n\n"),
- refs: [{ kind: "run", id: event.runId }],
- };
- }
- if (event.kind === "credential-expired") {
- return {
- subject: `Reconnect ${event.providerLabel} — your token expired`,
- body: [
- `Your ${event.providerLabel} connection expired, so agents and routines using it can no longer run inference through it. Anything else on your bench keeps working.`,
- `Reconnect from the same connect card in Settings or onboarding to pick up right where you left off.`,
- `For a connection that doesn't expire, use a fine-grained personal access token instead of reconnecting — see ${event.providerLabel}'s token settings.`,
- ].join("\n\n"),
- refs: [{ kind: "credential", id: event.credentialId }],
- };
- }
- return {
- subject: `${event.mentionedBy} mentioned you in “${event.threadLabel}”`,
- body: [
- `${event.mentionedBy} mentioned you in “${event.threadLabel}”.`,
- event.excerpt === "" ? "The message has no text." : event.excerpt,
- ].join("\n\n"),
- refs: [{ kind: "thread", id: event.threadId }],
- };
-}
-
-/**
- * The stable external identity of a notification, which is also its dedupe key
- * inside the mailbox. An approval keys off the approval itself, so a redelivered
- * register frame never mails twice; the other kinds key off the run or thread
- * plus the moment they happened, since a run can fail once per attempt and a
- * thread can mention someone repeatedly.
- */
-export function notificationExternalId(event: NotificationEvent): string {
- if (event.kind === "approval") return event.approvalId;
- if (event.kind === "run-failure") return `${event.runId}:${event.createdAt}`;
- if (event.kind === "credential-expired") return event.credentialId;
- return `${event.threadId}:${event.createdAt}`;
-}
diff --git a/packages/notify/src/schema.ts b/packages/notify/src/schema.ts
deleted file mode 100644
index 219fcaed0f..0000000000
--- a/packages/notify/src/schema.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// The one product table this package owns: bookkeeping for one attempt
-// stream per (mail row, sink). It is deliberately not an event bus — the
-// durable fact is always the mail row itself, and a row here only records
-// whether a copy of that mail made it to one external place yet. Lives
-// in its own `notify` Postgres schema, fully siloed from the platform's
-// `public` schema — see docs/package-migrations.md.
-//
-// `tenant_id`/`principal_id` are hard foreign keys into Interchange's own
-// `tenant`/`principal` tables (CL-8210): a dispatch row can never outlive
-// the tenant or principal it was queued for. `hostTenant`/`hostPrincipal`
-// below are declared, never migrated, just far enough to carry the FK —
-// same pattern as `@corbits/artifacts`'s `src/schema.ts`.
-import { index, integer, pgSchema, pgTable, text, timestamp } from "drizzle-orm/pg-core";
-
-export const notifySchema = pgSchema("notify");
-
-const hostTenant = pgTable("tenant", { id: text("id").primaryKey() });
-const hostPrincipal = pgTable("principal", { id: text("id").primaryKey() });
-
-export const notifyDispatch = notifySchema.table(
- "notify_dispatch",
- {
- id: text("id").primaryKey(),
- mailboxRowId: text("mailbox_row_id").notNull(),
- tenantId: text("tenant_id")
- .notNull()
- .references(() => hostTenant.id, { onDelete: "cascade" }),
- principalId: text("principal_id")
- .notNull()
- .references(() => hostPrincipal.id, { onDelete: "cascade" }),
- sinkName: text("sink_name").notNull(),
- status: text("status", {
- enum: ["pending", "delivered", "failed", "dead"],
- }).notNull(),
- attempts: integer("attempts").notNull().default(0),
- lastError: text("last_error"),
- nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).notNull().defaultNow(),
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
- },
- (t) => [index("notify_dispatch_due_idx").on(t.status, t.nextAttemptAt)],
-);
diff --git a/packages/notify/src/sinks.ts b/packages/notify/src/sinks.ts
deleted file mode 100644
index 2647a9f9b6..0000000000
--- a/packages/notify/src/sinks.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-// The seam a notification sink plugs into, shaped exactly like
-// `@corbits/commands`' command-plugin contract: a set of named factory
-// exports, a registry that holds plugins by name, and an explicit
-// registration call in the host's composition root. There is no dispatch
-// table and no switch — adding Slack, email or anything else is one
-// package plus one `register(...)` line, and nothing in this file changes.
-import type { NotificationEvent } from "./events";
-
-export type SinkScope = {
- readonly tenantId: string;
- readonly principalId: string;
-};
-
-export type SinkDeliveryContext = {
- readonly tenantId: string;
- readonly principalId: string;
- /** The durable mail row this delivery is a fan-out of; the mail is the record, the sink is a copy. */
- readonly mailboxRowId: string;
- readonly event: NotificationEvent;
-};
-
-export type SinkDeliveryResult =
- | { readonly status: "delivered" }
- | { readonly status: "skipped"; readonly reason: string }
- | {
- readonly status: "failed";
- readonly error: string;
- readonly retryable: boolean;
- };
-
-export type NotificationSinkPlugin = {
- readonly name: string;
- isEnabledFor(scope: SinkScope): Promise;
- deliver(ctx: SinkDeliveryContext): Promise;
-};
-
-export type SinkRegistry = {
- register(plugin: NotificationSinkPlugin): void;
- get(name: string): NotificationSinkPlugin | undefined;
- list(): readonly NotificationSinkPlugin[];
- listEnabledFor(scope: SinkScope): Promise;
-};
-
-export class DuplicateSinkNameError extends Error {
- constructor(name: string) {
- super(
- `A notification sink named ${JSON.stringify(name)} is already registered; ` +
- "each sink owns its name outright so a delivery row can always name one plugin.",
- );
- this.name = "DuplicateSinkNameError";
- }
-}
-
-export function createSinkRegistry(): SinkRegistry {
- const plugins = new Map();
- return {
- register(plugin) {
- if (plugins.has(plugin.name)) throw new DuplicateSinkNameError(plugin.name);
- plugins.set(plugin.name, plugin);
- },
- get(name) {
- return plugins.get(name);
- },
- list() {
- return [...plugins.values()];
- },
- async listEnabledFor(scope) {
- const enabled: NotificationSinkPlugin[] = [];
- for (const plugin of plugins.values()) {
- if (await plugin.isEnabledFor(scope)) enabled.push(plugin);
- }
- return enabled;
- },
- };
-}
diff --git a/packages/notify/src/store.ts b/packages/notify/src/store.ts
deleted file mode 100644
index ccc0d3fa2d..0000000000
--- a/packages/notify/src/store.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-// Persistence for the one dispatch table, kept apart from delivery and the
-// worker so neither touches drizzle directly. `createInMemoryNotifyDispatchStore`
-// is the fake this package's own tests drive the worker through.
-import { and, asc, eq, inArray, lte } from "drizzle-orm";
-import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
-import { generateId } from "@intx/hub-common";
-
-import { notifyDispatch } from "./schema";
-
-export type NotifyDb = Record> =
- PostgresJsDatabase;
-
-export type NotifyDispatchStatus = "pending" | "delivered" | "failed" | "dead";
-
-export interface NotifyDispatchRow {
- readonly id: string;
- readonly mailboxRowId: string;
- readonly tenantId: string;
- readonly principalId: string;
- readonly sinkName: string;
- readonly status: NotifyDispatchStatus;
- readonly attempts: number;
- readonly lastError: string | null;
- readonly nextAttemptAt: Date;
-}
-
-export interface EnqueueDispatchInput {
- readonly mailboxRowId: string;
- readonly tenantId: string;
- readonly principalId: string;
- readonly sinkName: string;
-}
-
-export interface SettleDispatchInput {
- readonly id: string;
- readonly status: NotifyDispatchStatus;
- readonly attempts: number;
- readonly lastError: string | null;
- readonly nextAttemptAt: Date;
-}
-
-export interface NotifyDispatchStore {
- /** One row per (mail row, sink); a redelivered notification never queues a second copy. */
- enqueue(inputs: readonly EnqueueDispatchInput[]): Promise;
- /** Rows whose next attempt is due, oldest first, across every tenant. */
- findDue(now: Date, limit: number): Promise;
- settle(input: SettleDispatchInput): Promise;
- listFor(mailboxRowId: string): Promise;
-}
-
-const ACTIVE_STATUSES: NotifyDispatchStatus[] = ["pending", "failed"];
-
-export function createDrizzleNotifyDispatchStore(db: NotifyDb): NotifyDispatchStore {
- return {
- async enqueue(inputs) {
- if (inputs.length === 0) return;
- await db
- .insert(notifyDispatch)
- .values(
- inputs.map((input) => ({
- id: generateId("signal"),
- mailboxRowId: input.mailboxRowId,
- tenantId: input.tenantId,
- principalId: input.principalId,
- sinkName: input.sinkName,
- status: "pending" as const,
- })),
- )
- .onConflictDoNothing();
- },
-
- async findDue(now, limit) {
- const rows = await db
- .select()
- .from(notifyDispatch)
- .where(
- and(
- inArray(notifyDispatch.status, ACTIVE_STATUSES),
- lte(notifyDispatch.nextAttemptAt, now),
- ),
- )
- .orderBy(asc(notifyDispatch.nextAttemptAt))
- .limit(limit);
- return rows.map(toRow);
- },
-
- async settle(input) {
- await db
- .update(notifyDispatch)
- .set({
- status: input.status,
- attempts: input.attempts,
- lastError: input.lastError,
- nextAttemptAt: input.nextAttemptAt,
- updatedAt: new Date(),
- })
- .where(eq(notifyDispatch.id, input.id));
- },
-
- async listFor(mailboxRowId) {
- const rows = await db
- .select()
- .from(notifyDispatch)
- .where(eq(notifyDispatch.mailboxRowId, mailboxRowId));
- return rows.map(toRow);
- },
- };
-}
-
-function toRow(row: typeof notifyDispatch.$inferSelect): NotifyDispatchRow {
- return {
- id: row.id,
- mailboxRowId: row.mailboxRowId,
- tenantId: row.tenantId,
- principalId: row.principalId,
- sinkName: row.sinkName,
- status: row.status,
- attempts: row.attempts,
- lastError: row.lastError,
- nextAttemptAt: row.nextAttemptAt,
- };
-}
-
-export function createInMemoryNotifyDispatchStore(): NotifyDispatchStore {
- const rows = new Map();
- const keyOf = (mailboxRowId: string, sinkName: string) => `${mailboxRowId}\u0000${sinkName}`;
- return {
- async enqueue(inputs) {
- for (const input of inputs) {
- const key = keyOf(input.mailboxRowId, input.sinkName);
- if (rows.has(key)) continue;
- rows.set(key, {
- id: generateId("signal"),
- mailboxRowId: input.mailboxRowId,
- tenantId: input.tenantId,
- principalId: input.principalId,
- sinkName: input.sinkName,
- status: "pending",
- attempts: 0,
- lastError: null,
- nextAttemptAt: new Date(0),
- });
- }
- },
- async findDue(now, limit) {
- return [...rows.values()]
- .filter((row) => ACTIVE_STATUSES.includes(row.status) && row.nextAttemptAt <= now)
- .sort((a, b) => a.nextAttemptAt.getTime() - b.nextAttemptAt.getTime())
- .slice(0, limit);
- },
- async settle(input) {
- for (const [key, row] of rows) {
- if (row.id !== input.id) continue;
- rows.set(key, {
- id: row.id,
- mailboxRowId: row.mailboxRowId,
- tenantId: row.tenantId,
- principalId: row.principalId,
- sinkName: row.sinkName,
- status: input.status,
- attempts: input.attempts,
- lastError: input.lastError,
- nextAttemptAt: input.nextAttemptAt,
- });
- }
- },
- async listFor(mailboxRowId) {
- return [...rows.values()].filter((row) => row.mailboxRowId === mailboxRowId);
- },
- };
-}
diff --git a/packages/notify/test/approval-bridge.test.ts b/packages/notify/test/approval-bridge.test.ts
deleted file mode 100644
index c424878354..0000000000
--- a/packages/notify/test/approval-bridge.test.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { describe, expect, test } from "bun:test";
-
-import {
- createApprovalNotificationBridge,
- createInMemoryNotifyDispatchStore,
- createSinkRegistry,
- type MailboxDelivery,
- type NotifyAddressing,
- type NotifyInboxItem,
- type ParkedApproval,
-} from "../src/index";
-
-const addressing: NotifyAddressing = {
- inbox: (recipient) => `${recipient.principalId}@bench.invalid`,
- from: (kind) => `${kind}@notify.invalid`,
-};
-
-const parked: ParkedApproval = {
- approvalId: "apr_7",
- tenantId: "tnt_1",
- runId: "run_1",
- deploymentId: "dep_1",
- toolName: "post_to_slack",
- toolArguments: { channel: "#general" },
- createdAt: new Date("2026-08-08T09:00:00.000Z"),
-};
-
-function bridgeOver(
- findParkedApproval: (id: string) => Promise,
- approvers: { tenantId: string; principalId: string }[],
-): { run: (id: string) => Promise; written: NotifyInboxItem[] } {
- const written: NotifyInboxItem[] = [];
- const seen = new Set();
- const mail: MailboxDelivery = async (items, opts) =>
- items.map((item) => {
- const deduped = seen.has(item.externalId + item.principalId);
- seen.add(item.externalId + item.principalId);
- if (deduped) return { messageKey: item.externalId, id: null };
- written.push(item);
- const id = `mail-${written.length}`;
- opts?.enqueue?.({ id, item });
- return { messageKey: item.externalId, id };
- });
- const run = createApprovalNotificationBridge({
- delivery: {
- mail,
- addressing,
- dispatch: createInMemoryNotifyDispatchStore(),
- sinks: createSinkRegistry(),
- },
- findParkedApproval,
- listApprovers: async () => approvers,
- });
- return { run, written };
-}
-
-describe("createApprovalNotificationBridge", () => {
- test("mails every approver once, even when the register frame is redelivered", async () => {
- const { run, written } = bridgeOver(
- async () => parked,
- [
- { tenantId: "tnt_1", principalId: "prn_1" },
- { tenantId: "tnt_1", principalId: "prn_2" },
- ],
- );
- await run("cor_1");
- await run("cor_1");
- expect(written).toHaveLength(2);
- expect(written.map((item) => item.principalId)).toEqual(["prn_1", "prn_2"]);
- expect(written[0]?.externalId).toBe("apr_7");
- expect(written[0]?.subject).toBe("Approve “post_to_slack”?");
- });
-
- test("mails nobody when the correlation has no approval", async () => {
- const { run, written } = bridgeOver(
- async () => null,
- [{ tenantId: "tnt_1", principalId: "prn_1" }],
- );
- await run("cor_missing");
- expect(written).toHaveLength(0);
- });
-
- test("mails nobody when no one can resolve the approval", async () => {
- const { run, written } = bridgeOver(async () => parked, []);
- await run("cor_1");
- expect(written).toHaveLength(0);
- });
-});
diff --git a/packages/notify/test/context.test.ts b/packages/notify/test/context.test.ts
deleted file mode 100644
index eb8b9eb24b..0000000000
--- a/packages/notify/test/context.test.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { describe, expect, test } from "bun:test";
-import type { GrantRule, GrantStore } from "@intx/types/authz";
-
-import {
- NotifyGrantMissingError,
- NotifySinkCredentialInvalidError,
- NotifySinkNotConfiguredError,
- resolveNotifyContext,
- type NotifyCredential,
-} from "../src/index";
-
-function grantStoreWith(grants: GrantRule[]): GrantStore {
- return {
- collectGrants: async () => grants,
- collectGrantsInChain: async () => grants,
- };
-}
-
-const slackCredential: NotifyCredential = {
- id: "crd_1",
- name: "team-slack",
- kind: "slack-bot-token",
-};
-
-function deliverGrant(resource: string): GrantRule {
- return {
- id: "grt_1",
- resource,
- action: "deliver",
- effect: "allow",
- origin: "role",
- conditions: null,
- expiresAt: null,
- roleId: null,
- principalId: "prn_1",
- };
-}
-
-describe("resolveNotifyContext", () => {
- test("resolves the credential when a matching grant allows delivery", async () => {
- const context = await resolveNotifyContext(
- {
- grantStore: grantStoreWith([deliverGrant("notify:slack")]),
- findSinkCredential: async () => slackCredential,
- },
- {
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "slack",
- credentialKind: "slack-bot-token",
- },
- );
- expect(context.credential.name).toBe("team-slack");
- expect(context.sinkName).toBe("slack");
- });
-
- test("fails closed when no grant covers the sink", async () => {
- await expect(
- resolveNotifyContext(
- {
- grantStore: grantStoreWith([deliverGrant("notify:email")]),
- findSinkCredential: async () => slackCredential,
- },
- {
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "slack",
- credentialKind: "slack-bot-token",
- },
- ),
- ).rejects.toBeInstanceOf(NotifyGrantMissingError);
- });
-
- test("names the missing configuration when no credential is set up", async () => {
- await expect(
- resolveNotifyContext(
- {
- grantStore: grantStoreWith([deliverGrant("notify:slack")]),
- findSinkCredential: async () => null,
- },
- {
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "slack",
- credentialKind: "slack-bot-token",
- },
- ),
- ).rejects.toBeInstanceOf(NotifySinkNotConfiguredError);
- });
-
- test("rejects a credential of the wrong kind rather than trying it", async () => {
- await expect(
- resolveNotifyContext(
- {
- grantStore: grantStoreWith([deliverGrant("notify:slack")]),
- findSinkCredential: async () => slackCredential,
- },
- {
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "slack",
- credentialKind: "smtp",
- },
- ),
- ).rejects.toBeInstanceOf(NotifySinkCredentialInvalidError);
- });
-});
diff --git a/packages/notify/test/deliver.test.ts b/packages/notify/test/deliver.test.ts
deleted file mode 100644
index ef6edf77bf..0000000000
--- a/packages/notify/test/deliver.test.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-import { describe, expect, test } from "bun:test";
-
-import {
- createInMemoryNotifyDispatchStore,
- createSinkRegistry,
- deliverApprovalMail,
- deliverCredentialMail,
- deliverMentionMail,
- deliverNotification,
- deliverRunFailureMail,
- InvalidNotificationEventError,
- NOTIFY_MAIL_SOURCE,
- type MailboxDelivery,
- type NotifyAddressing,
- type NotifyDeliveryDeps,
- type NotifyDispatchStore,
- type NotifyInboxItem,
- type ResolveExistingMailIds,
- type SinkDeliveryResult,
-} from "../src/index";
-
-const addressing: NotifyAddressing = {
- inbox: (recipient) => `${recipient.principalId}@bench.invalid`,
- from: (kind) => `${kind}@notify.invalid`,
-};
-
-function recordingMailbox(): {
- mail: MailboxDelivery;
- written: NotifyInboxItem[];
-} {
- const written: NotifyInboxItem[] = [];
- let next = 0;
- const mail: MailboxDelivery = async (items, opts) => {
- return items.map((item) => {
- written.push(item);
- next += 1;
- const id = `mail-${next}`;
- opts?.enqueue?.({ id, item });
- return { messageKey: `key-${id}`, id };
- });
- };
- return { mail, written };
-}
-
-function dedupingMailbox(): MailboxDelivery {
- return async (items) => items.map((item) => ({ messageKey: item.externalId, id: null }));
-}
-
-function depsWith(mail: MailboxDelivery): NotifyDeliveryDeps {
- return {
- mail,
- addressing,
- dispatch: createInMemoryNotifyDispatchStore(),
- sinks: createSinkRegistry(),
- };
-}
-
-const approval = {
- kind: "approval",
- approvalId: "apr_1",
- tenantId: "tnt_1",
- runId: "run_1",
- deploymentId: "dep_1",
- toolName: "send_invoice",
- toolArguments: { amount: 4200, to: "Acme" },
- recipients: [{ tenantId: "tnt_1", principalId: "prn_1" }],
- createdAt: "2026-08-08T10:00:00.000Z",
-} as const;
-
-describe("deliverNotification", () => {
- test("writes one mail per recipient, keyed on the approval itself", async () => {
- const { mail, written } = recordingMailbox();
- const deps = depsWith(mail);
- const report = await deliverApprovalMail(deps, {
- kind: "approval",
- approvalId: approval.approvalId,
- tenantId: approval.tenantId,
- runId: approval.runId,
- deploymentId: approval.deploymentId,
- toolName: approval.toolName,
- toolArguments: approval.toolArguments,
- recipients: [
- { tenantId: "tnt_1", principalId: "prn_1" },
- { tenantId: "tnt_1", principalId: "prn_2" },
- ],
- createdAt: approval.createdAt,
- });
-
- expect(report.deliveredMailboxRowIds).toEqual(["mail-1", "mail-2"]);
- expect(written).toHaveLength(2);
- expect(written[0]?.source).toBe(NOTIFY_MAIL_SOURCE);
- expect(written[0]?.externalId).toBe("apr_1");
- expect(written[0]?.address).toBe("prn_1@bench.invalid");
- expect(written[1]?.address).toBe("prn_2@bench.invalid");
- expect(written[0]?.subject).toContain("send_invoice");
- expect(written[0]?.subject).not.toContain("apr_1");
- expect(written[0]?.refs).toContainEqual({ kind: "approval", id: "apr_1" });
- });
-
- test("queues nothing when a recipient already had this mail", async () => {
- const deps = depsWith(dedupingMailbox());
- const report = await deliverNotification(deps, approval);
- expect(report.deliveredMailboxRowIds).toEqual([]);
- expect(report.queuedDispatchCount).toBe(0);
- });
-
- test("refuses an event that does not parse instead of writing mail", async () => {
- const { mail, written } = recordingMailbox();
- const deps = depsWith(mail);
- await expect(
- deliverNotification(deps, {
- kind: "approval",
- approvalId: "",
- tenantId: "tnt_1",
- runId: "run_1",
- deploymentId: "dep_1",
- toolName: "x",
- toolArguments: {},
- recipients: [],
- createdAt: "2026-08-08T10:00:00.000Z",
- }),
- ).rejects.toBeInstanceOf(InvalidNotificationEventError);
- expect(written).toHaveLength(0);
- });
-
- test("queues one dispatch row per enabled sink and none for a disabled one", async () => {
- const { mail } = recordingMailbox();
- const dispatch = createInMemoryNotifyDispatchStore();
- const sinks = createSinkRegistry();
- const delivered: SinkDeliveryResult = { status: "delivered" };
- sinks.register({
- name: "always",
- isEnabledFor: async () => true,
- deliver: async () => delivered,
- });
- sinks.register({
- name: "never",
- isEnabledFor: async () => false,
- deliver: async () => delivered,
- });
-
- const report = await deliverNotification({ mail, addressing, dispatch, sinks }, approval);
-
- expect(report.queuedDispatchCount).toBe(1);
- const rows = await dispatch.listFor("mail-1");
- expect(rows.map((row) => row.sinkName)).toEqual(["always"]);
- expect(rows[0]?.status).toBe("pending");
- expect(rows[0]?.id.startsWith("sig_")).toBe(true);
- });
-
- test("run failures and mentions read as plain sentences about named things", async () => {
- const { mail, written } = recordingMailbox();
- const deps = depsWith(mail);
- await deliverRunFailureMail(deps, {
- kind: "run-failure",
- tenantId: "tnt_1",
- runId: "run_9",
- deploymentId: "dep_9",
- runLabel: "Nightly digest",
- error: "upstream timed out",
- recipients: [{ tenantId: "tnt_1", principalId: "prn_1" }],
- createdAt: "2026-08-08T11:00:00.000Z",
- });
- await deliverMentionMail(deps, {
- kind: "mention",
- tenantId: "tnt_1",
- threadId: "thr_3",
- threadLabel: "Launch plan",
- mentionedBy: "Noor",
- excerpt: "can you take this one?",
- recipients: [{ tenantId: "tnt_1", principalId: "prn_1" }],
- createdAt: "2026-08-08T12:00:00.000Z",
- });
-
- expect(written[0]?.subject).toBe("“Nightly digest” failed");
- expect(written[0]?.externalId).toBe("run_9:2026-08-08T11:00:00.000Z");
- expect(written[1]?.subject).toBe("Noor mentioned you in “Launch plan”");
- for (const item of written) {
- expect(item.subject).not.toContain("_");
- }
- });
-
- test("a credential-expired event mails a reconnect nudge naming the durable PAT alternative", async () => {
- const { mail, written } = recordingMailbox();
- const deps = depsWith(mail);
- await deliverCredentialMail(deps, {
- kind: "credential-expired",
- tenantId: "tnt_1",
- credentialId: "cred_hf_1",
- providerId: "huggingface",
- providerLabel: "Hugging Face",
- recipients: [{ tenantId: "tnt_1", principalId: "prn_1" }],
- createdAt: "2026-08-13T09:00:00.000Z",
- });
-
- expect(written[0]?.subject).toBe("Reconnect Hugging Face — your token expired");
- expect(written[0]?.body).toContain("personal access token");
- expect(written[0]?.externalId).toBe("cred_hf_1");
- expect(written[0]?.refs).toContainEqual({
- kind: "credential",
- id: "cred_hf_1",
- });
- });
-
- test("re-notifying the same still-expired credential dedupes on the credential, not the tick", async () => {
- const dispatch = createInMemoryNotifyDispatchStore();
- const sinks = createSinkRegistry();
- const seen = new Set();
- const mail: MailboxDelivery = async (items, opts) =>
- items.map((item) => {
- if (seen.has(item.externalId)) {
- return { messageKey: item.externalId, id: null };
- }
- seen.add(item.externalId);
- const id = `mail-${item.externalId}`;
- opts?.enqueue?.({ id, item });
- return { messageKey: item.externalId, id };
- });
-
- const first = await deliverCredentialMail(
- { mail, addressing, dispatch, sinks },
- {
- kind: "credential-expired",
- tenantId: "tnt_1",
- credentialId: "cred_hf_1",
- providerId: "huggingface",
- providerLabel: "Hugging Face",
- recipients: [{ tenantId: "tnt_1", principalId: "prn_1" }],
- createdAt: "2026-08-13T09:00:00.000Z",
- },
- );
- const second = await deliverCredentialMail(
- { mail, addressing, dispatch, sinks },
- {
- kind: "credential-expired",
- tenantId: "tnt_1",
- credentialId: "cred_hf_1",
- providerId: "huggingface",
- providerLabel: "Hugging Face",
- // A later sweep tick, same still-unfixed credential.
- recipients: [{ tenantId: "tnt_1", principalId: "prn_1" }],
- createdAt: "2026-08-13T09:30:00.000Z",
- },
- );
-
- expect(first.deliveredMailboxRowIds).toEqual(["mail-cred_hf_1"]);
- expect(second.deliveredMailboxRowIds).toEqual([]);
- });
-});
-
-describe("deliverNotification crash window (CL-7238)", () => {
- function durableMailbox(): {
- mail: MailboxDelivery;
- resolveExistingMailIds: ResolveExistingMailIds;
- } {
- const rows = new Map();
- let next = 0;
- const keyOf = (item: NotifyInboxItem) =>
- `${item.tenantId}\n${item.principalId}\n${item.externalId}`;
- const mail: MailboxDelivery = async (items, opts) =>
- items.map((item) => {
- const existing = rows.get(keyOf(item));
- if (existing !== undefined) return { messageKey: item.externalId, id: null };
- next += 1;
- const id = `mail-${next}`;
- rows.set(keyOf(item), id);
- opts?.enqueue?.({ id, item });
- return { messageKey: item.externalId, id };
- });
- const resolveExistingMailIds: ResolveExistingMailIds = async (items) =>
- items.map((item) => rows.get(keyOf(item)) ?? null);
- return { mail, resolveExistingMailIds };
- }
-
- function crashableDispatch(): {
- dispatch: NotifyDispatchStore;
- armCrash: () => void;
- } {
- const inner = createInMemoryNotifyDispatchStore();
- let crashNext = false;
- const dispatch: NotifyDispatchStore = {
- ...inner,
- enqueue: async (inputs) => {
- if (crashNext) {
- crashNext = false;
- throw new Error("boom: crash between the mail write and the dispatch enqueue");
- }
- await inner.enqueue(inputs);
- },
- };
- return {
- dispatch,
- armCrash: () => {
- crashNext = true;
- },
- };
- }
-
- function crashDeps(
- mail: MailboxDelivery,
- dispatch: NotifyDispatchStore,
- resolveExistingMailIds?: ResolveExistingMailIds,
- ): NotifyDeliveryDeps {
- const sinks = createSinkRegistry();
- const delivered: SinkDeliveryResult = { status: "delivered" };
- sinks.register({
- name: "always",
- isEnabledFor: async () => true,
- deliver: async () => delivered,
- });
- const deps: NotifyDeliveryDeps = { mail, addressing, dispatch, sinks };
- return resolveExistingMailIds === undefined ? deps : { ...deps, resolveExistingMailIds };
- }
-
- test("a redelivery repairs the crash window without double-queueing", async () => {
- const { mail, resolveExistingMailIds } = durableMailbox();
- const { dispatch, armCrash } = crashableDispatch();
- const deps = crashDeps(mail, dispatch, resolveExistingMailIds);
-
- armCrash();
- await expect(deliverNotification(deps, approval)).rejects.toThrow("boom");
-
- const redelivery = await deliverNotification(deps, approval);
- expect(redelivery.deliveredMailboxRowIds).toEqual([]);
- expect(redelivery.queuedDispatchCount).toBe(1);
- expect(await dispatch.listFor("mail-1")).toHaveLength(1);
-
- const third = await deliverNotification(deps, approval);
- expect(third.queuedDispatchCount).toBe(1);
- expect(await dispatch.listFor("mail-1")).toHaveLength(1);
- });
-
- test("a redelivery without the read-back still loses the dispatch", async () => {
- const { mail } = durableMailbox();
- const { dispatch, armCrash } = crashableDispatch();
- const deps = crashDeps(mail, dispatch);
-
- armCrash();
- await expect(deliverNotification(deps, approval)).rejects.toThrow("boom");
-
- const redelivery = await deliverNotification(deps, approval);
- expect(redelivery).toEqual({
- deliveredMailboxRowIds: [],
- queuedDispatchCount: 0,
- });
- expect(await dispatch.listFor("mail-1")).toHaveLength(0);
- });
-
- test("a mixed batch dispatches fresh and repaired rows together", async () => {
- const { mail, resolveExistingMailIds } = durableMailbox();
- const { dispatch } = crashableDispatch();
- const deps = crashDeps(mail, dispatch, resolveExistingMailIds);
-
- await deliverNotification(deps, approval);
- const mixed = await deliverNotification(deps, {
- ...approval,
- recipients: [
- { tenantId: "tnt_1", principalId: "prn_1" },
- { tenantId: "tnt_1", principalId: "prn_2" },
- ],
- });
-
- expect(mixed.deliveredMailboxRowIds).toEqual(["mail-2"]);
- expect(mixed.queuedDispatchCount).toBe(2);
- expect(await dispatch.listFor("mail-1")).toHaveLength(1);
- expect(await dispatch.listFor("mail-2")).toHaveLength(1);
- });
-});
diff --git a/packages/notify/test/dispatcher.test.ts b/packages/notify/test/dispatcher.test.ts
deleted file mode 100644
index a9a1421a3c..0000000000
--- a/packages/notify/test/dispatcher.test.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-import { describe, expect, test } from "bun:test";
-
-import {
- createInMemoryNotifyDispatchStore,
- createNotifyDispatcher,
- createSinkRegistry,
- DuplicateSinkNameError,
- type NotificationEvent,
- type NotificationSinkPlugin,
- type NotifyDispatchLogger,
- type SinkDeliveryResult,
-} from "../src/index";
-
-const event: NotificationEvent = {
- kind: "approval",
- approvalId: "apr_1",
- tenantId: "tnt_1",
- runId: "run_1",
- deploymentId: "dep_1",
- toolName: "send_invoice",
- toolArguments: {},
- recipients: [{ tenantId: "tnt_1", principalId: "prn_1" }],
- createdAt: "2026-08-08T10:00:00.000Z",
-};
-
-const silent: NotifyDispatchLogger = {
- warn: () => undefined,
- error: () => undefined,
-};
-
-function fakeSink(
- name: string,
- results: SinkDeliveryResult[],
-): { plugin: NotificationSinkPlugin; calls: number[] } {
- const calls: number[] = [];
- let index = 0;
- const plugin: NotificationSinkPlugin = {
- name,
- isEnabledFor: async () => true,
- deliver: async () => {
- calls.push(index);
- const result = results[Math.min(index, results.length - 1)];
- index += 1;
- return result ?? { status: "delivered" };
- },
- };
- return { plugin, calls };
-}
-
-function dispatcherOver(
- sinks: ReturnType,
- store: ReturnType,
-) {
- return createNotifyDispatcher({
- store,
- sinks,
- log: silent,
- loadEvent: async () => event,
- tickIntervalMs: 60_000,
- batchSize: 25,
- maxAttempts: 3,
- retryBackoffMs: 1_000,
- });
-}
-
-describe("createNotifyDispatcher", () => {
- test("does nothing, and does not throw, with no sink registered", async () => {
- const store = createInMemoryNotifyDispatchStore();
- const dispatcher = dispatcherOver(createSinkRegistry(), store);
- expect(await dispatcher.runOnce(new Date())).toBe(0);
- });
-
- test("marks a delivered row delivered and stops retrying it", async () => {
- const store = createInMemoryNotifyDispatchStore();
- const sinks = createSinkRegistry();
- const { plugin, calls } = fakeSink("test", [{ status: "delivered" }]);
- sinks.register(plugin);
- await store.enqueue([
- {
- mailboxRowId: "mail-1",
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "test",
- },
- ]);
- const dispatcher = dispatcherOver(sinks, store);
-
- expect(await dispatcher.runOnce(new Date())).toBe(1);
- const [row] = await store.listFor("mail-1");
- expect(row?.status).toBe("delivered");
- expect(row?.attempts).toBe(1);
- expect(await dispatcher.runOnce(new Date())).toBe(0);
- expect(calls).toHaveLength(1);
- });
-
- test("backs a retryable failure off, then gives up at the attempt ceiling", async () => {
- const store = createInMemoryNotifyDispatchStore();
- const sinks = createSinkRegistry();
- const { plugin } = fakeSink("test", [
- { status: "failed", error: "rate limited", retryable: true },
- ]);
- sinks.register(plugin);
- await store.enqueue([
- {
- mailboxRowId: "mail-1",
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "test",
- },
- ]);
- const dispatcher = dispatcherOver(sinks, store);
-
- const first = new Date("2026-08-08T10:00:00.000Z");
- await dispatcher.runOnce(first);
- let [row] = await store.listFor("mail-1");
- expect(row?.status).toBe("failed");
- expect(row?.lastError).toBe("rate limited");
- expect(row?.nextAttemptAt.getTime()).toBe(first.getTime() + 1_000);
-
- // Nothing is due until the backoff elapses.
- expect(await dispatcher.runOnce(first)).toBe(0);
-
- await dispatcher.runOnce(new Date(first.getTime() + 10_000));
- await dispatcher.runOnce(new Date(first.getTime() + 100_000));
- [row] = await store.listFor("mail-1");
- expect(row?.attempts).toBe(3);
- expect(row?.status).toBe("dead");
- });
-
- test("a non-retryable failure dies on its first attempt", async () => {
- const store = createInMemoryNotifyDispatchStore();
- const sinks = createSinkRegistry();
- const { plugin } = fakeSink("test", [
- { status: "failed", error: "no such channel", retryable: false },
- ]);
- sinks.register(plugin);
- await store.enqueue([
- {
- mailboxRowId: "mail-1",
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "test",
- },
- ]);
- await dispatcherOver(sinks, store).runOnce(new Date());
- const [row] = await store.listFor("mail-1");
- expect(row?.status).toBe("dead");
- expect(row?.attempts).toBe(1);
- });
-
- test("a throwing sink is treated as a retryable failure, never a crash", async () => {
- const store = createInMemoryNotifyDispatchStore();
- const sinks = createSinkRegistry();
- sinks.register({
- name: "test",
- isEnabledFor: async () => true,
- deliver: async () => {
- throw new Error("socket hang up");
- },
- });
- await store.enqueue([
- {
- mailboxRowId: "mail-1",
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "test",
- },
- ]);
- await dispatcherOver(sinks, store).runOnce(new Date());
- const [row] = await store.listFor("mail-1");
- expect(row?.status).toBe("failed");
- expect(row?.lastError).toBe("socket hang up");
- });
-
- test("rows left behind by an unregistered sink are closed out", async () => {
- const store = createInMemoryNotifyDispatchStore();
- await store.enqueue([
- {
- mailboxRowId: "mail-1",
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "removed",
- },
- ]);
- await dispatcherOver(createSinkRegistry(), store).runOnce(new Date());
- const [row] = await store.listFor("mail-1");
- expect(row?.status).toBe("dead");
- });
-});
-
-describe("createSinkRegistry", () => {
- test("refuses two sinks with the same name", () => {
- const sinks = createSinkRegistry();
- const { plugin } = fakeSink("test", [{ status: "delivered" }]);
- sinks.register(plugin);
- expect(() => sinks.register(plugin)).toThrow(DuplicateSinkNameError);
- });
-
- test("lists only the sinks enabled for a principal", async () => {
- const sinks = createSinkRegistry();
- sinks.register({
- name: "on",
- isEnabledFor: async (scope) => scope.principalId === "prn_1",
- deliver: async () => ({ status: "delivered" }),
- });
- sinks.register({
- name: "off",
- isEnabledFor: async () => false,
- deliver: async () => ({ status: "delivered" }),
- });
- const enabled = await sinks.listEnabledFor({
- tenantId: "tnt_1",
- principalId: "prn_1",
- });
- expect(enabled.map((sink) => sink.name)).toEqual(["on"]);
- expect(sinks.list()).toHaveLength(2);
- });
-});
diff --git a/packages/notify/test/migrations.test.ts b/packages/notify/test/migrations.test.ts
deleted file mode 100644
index f5a9570181..0000000000
--- a/packages/notify/test/migrations.test.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-// DB-gated: runs against its own scratch database, never the developer's or
-// the walking-skeleton suite's, mirroring `packages/schedules/test/migrations.test.ts`.
-import { afterAll, beforeAll, expect, test } from "bun:test";
-import { drizzle } from "drizzle-orm/postgres-js";
-import postgres from "postgres";
-
-import { e2eDatabaseUrl } from "../../../test/database-url";
-import { applyNotifyMigrations } from "../src/migrations";
-import { createDrizzleNotifyDispatchStore } from "../src/store";
-import { dbGate } from "../../../test/db-gate";
-
-function scratchUrlFor(e2eUrl: string): string {
- const url = new URL(e2eUrl);
- const database = url.pathname.replace(/^\//, "");
- url.pathname = `/${database}_notify_migrations_test`;
- return url.toString();
-}
-
-const databaseUrl = e2eDatabaseUrl();
-const describeIfDb = dbGate(databaseUrl, import.meta.path);
-
-describeIfDb("applyNotifyMigrations", () => {
- const scratchUrl = scratchUrlFor(databaseUrl ?? "postgres://localhost:5432/unused");
- const scratchDatabase = new URL(scratchUrl).pathname.replace(/^\//, "");
-
- 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);
-
- const migrationNames = ["0001_notify_dispatch"];
-
- test("creates the dispatch table in its own schema once and is a no-op on a re-run", async () => {
- const first = await applyNotifyMigrations(scratchUrl);
- expect(first.applied).toEqual(migrationNames);
- const second = await applyNotifyMigrations(scratchUrl);
- expect(second.applied).toEqual([]);
- expect(second.alreadyApplied.sort()).toEqual([...migrationNames].sort());
-
- const sql = postgres(scratchUrl, { max: 1, onnotice: () => undefined });
- try {
- const tables = await sql.unsafe(
- `SELECT table_name FROM information_schema.tables ` +
- `WHERE table_schema = 'notify' AND table_name = 'notify_dispatch'`,
- );
- expect(tables).toHaveLength(1);
-
- const inPublic = await sql.unsafe(
- `SELECT 1 FROM information_schema.tables ` +
- `WHERE table_schema = 'public' AND table_name = 'notify_dispatch'`,
- );
- expect(inPublic).toHaveLength(0);
- } finally {
- await sql.end();
- }
- });
-
- test("queues one row per sink, dedupes a redelivery, and settles it", async () => {
- await applyNotifyMigrations(scratchUrl);
- const client = postgres(scratchUrl, { max: 1, onnotice: () => undefined });
- try {
- const store = createDrizzleNotifyDispatchStore(drizzle(client));
- const queued = {
- mailboxRowId: "mail-1",
- tenantId: "tnt_1",
- principalId: "prn_1",
- sinkName: "slack",
- };
- await store.enqueue([queued]);
- await store.enqueue([queued]);
- const rows = await store.listFor("mail-1");
- expect(rows).toHaveLength(1);
- expect(rows[0]?.status).toBe("pending");
- expect(rows[0]?.id.startsWith("sig_")).toBe(true);
-
- const due = await store.findDue(new Date(), 10);
- expect(due.map((row) => row.id)).toEqual([rows[0]?.id ?? ""]);
-
- await store.settle({
- id: rows[0]?.id ?? "",
- status: "delivered",
- attempts: 1,
- lastError: null,
- nextAttemptAt: new Date(),
- });
- expect(await store.findDue(new Date(), 10)).toEqual([]);
- } finally {
- await client.end();
- }
- });
-});
diff --git a/packages/notify/tsconfig.json b/packages/notify/tsconfig.json
deleted file mode 100644
index a4adb265e4..0000000000
--- a/packages/notify/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "extends": "./tsconfig.src.json",
- "compilerOptions": {
- "composite": false,
- "noEmit": true,
- "disableSourceOfProjectReferenceRedirect": true,
- "declaration": false,
- "declarationMap": false,
- "emitDeclarationOnly": false,
- "tsBuildInfoFile": "tsconfig.tsbuildinfo",
- "rootDir": "../.."
- },
- "include": ["src", "test"],
- "exclude": [],
- "references": [
- {
- "path": "../../vendor/intx/types/tsconfig.src.json"
- }
- ]
-}
diff --git a/packages/notify/tsconfig.src.json b/packages/notify/tsconfig.src.json
deleted file mode 100644
index fb751701ae..0000000000
--- a/packages/notify/tsconfig.src.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "extends": "../../tsconfig.base.json",
- "include": ["src", "package.json", "src/**/*.json"],
- "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"],
- "compilerOptions": {
- "types": ["bun"],
- "composite": true,
- "emitDeclarationOnly": true,
- "outDir": "dist",
- "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
- },
- "references": [
- {
- "path": "../../vendor/intx/types/tsconfig.src.json"
- }
- ]
-}
diff --git a/packages/provider-pricing/LICENSE b/packages/provider-pricing/LICENSE
deleted file mode 100644
index 173c3a21d0..0000000000
--- a/packages/provider-pricing/LICENSE
+++ /dev/null
@@ -1,176 +0,0 @@
-GNU LESSER GENERAL PUBLIC LICENSE
-
-Version 2.1, February 1999
-
-Copyright (C) 1991, 1999 Free Software Foundation, Inc.
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.]
-
-Preamble
-
-The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
-
-This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
-
-When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
-
-To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
-
-For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
-
-We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
-
-To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
-
-Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
-
-Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
-
-When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
-
-We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
-
-For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
-
-In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
-
-Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
-
-The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
-
-GNU LESSER GENERAL PUBLIC LICENSE
-TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
-
-A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
-
-The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
-
-"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
-
-Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
-
-1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
-
-You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
-
-2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
-
- a) The modified work must itself be a software library.
-
- b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
-
- c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
-
- d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
-
-(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
-
-3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
-
-Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
-
-This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
-
-4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
-
-If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
-
-5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
-
-However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
-
-When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
-
-If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
-
-Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
-
-6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
-
-You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
-
- a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
-
- b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
-
- c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
-
- d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
-
- e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
-
-For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
-
-It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
-
-7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
-
- a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
-
- b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
-
-8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
-
-9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
-
-10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
-
-11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
-
-This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
-
-12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
-
-13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
-
-14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
-
-NO WARRANTY
-
-15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-END OF TERMS AND CONDITIONS
-
-How to Apply These Terms to Your New Libraries
-
-If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
-
-To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
-
- one line to give the library's name and an idea of what it does.
- Copyright (C) year name of author
-
- This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License.
-
- This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
-
- You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail.
-
-You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
-
-Yoyodyne, Inc., hereby disclaims all copyright interest in
-the library `Frob' (a library for tweaking knobs) written
-by James Random Hacker.
-
-signature of Ty Coon, 1 April 1990
-Ty Coon, President of Vice
-That's all there is to it!
diff --git a/packages/provider-pricing/README.md b/packages/provider-pricing/README.md
deleted file mode 100644
index bf8320ddc2..0000000000
--- a/packages/provider-pricing/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# @corbits/provider-pricing
-
-Static token rates for the workbench catalog seed set, keyed by
-`(provider, model)`. Insights uses this at query time when a usage turn
-did not carry a provider-reported dollar cost.
-
-Rates are a models.dev snapshot from 2026-04-18,
-mapped onto workbench provider names (`google-genai`, `opencode-zen`,
-…). Some seed models have a higher-context tier; `lookupRates` picks
-the highest tier whose `minContextTokens` the turn's prompt-side tokens
-meet. A model with no row here is an honest miss — callers must not
-invent `$0`.
-
-Ollama is priced at zero (local). Models with no public list price are
-omitted, not guessed.
-
-## Key modules
-
-- `src/rates.ts` — the table
-- `src/lookup.ts` — `(provider, model, contextTokens) → rates | null`
-- `src/cost.ts` — `(tokens / 1e6) * rate` per class; missing rate + tokens → null
diff --git a/packages/provider-pricing/package.json b/packages/provider-pricing/package.json
deleted file mode 100644
index b1a063fd6a..0000000000
--- a/packages/provider-pricing/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "@corbits/provider-pricing",
- "version": "0.0.1",
- "private": true,
- "description": "Static (provider, model) token rates and cost math for the catalog seed set. Insights looks up a turn here when the provider did not report a dollar cost.",
- "license": "LGPL-2.1-only",
- "type": "module",
- "exports": {
- ".": "./src/index.ts"
- },
- "scripts": {
- "typecheck": "tsc --noEmit",
- "test": "bun test"
- },
- "dependencies": {
- "@corbits/connections": "workspace:*",
- "@intx/inference-catalog": "0.3.0"
- },
- "devDependencies": {
- "@types/bun": "catalog:",
- "typescript": "catalog:"
- }
-}
diff --git a/packages/provider-pricing/src/catalog-models.test.ts b/packages/provider-pricing/src/catalog-models.test.ts
deleted file mode 100644
index d65ce18e04..0000000000
--- a/packages/provider-pricing/src/catalog-models.test.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { catalogModels } from "@intx/inference-catalog/models";
-import { describe, expect, test } from "bun:test";
-
-import { CATALOG_MODEL_NAMES } from "./catalog-models";
-
-describe("CATALOG_MODEL_NAMES", () => {
- test("matches @intx/inference-catalog's catalogModels exactly", () => {
- const fromCatalog = new Set(catalogModels.map((m) => m.canonicalName));
- const hardListed = new Set(CATALOG_MODEL_NAMES);
- expect(hardListed.size).toBe(CATALOG_MODEL_NAMES.length);
- expect([...fromCatalog].sort()).toEqual([...hardListed].sort());
- });
-});
diff --git a/packages/provider-pricing/src/catalog-models.ts b/packages/provider-pricing/src/catalog-models.ts
deleted file mode 100644
index 93ca54106d..0000000000
--- a/packages/provider-pricing/src/catalog-models.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * Literal union of `@intx/inference-catalog`'s `catalogModels` canonical
- * names. The catalog exports that list as a plain (non-const) array, so
- * TypeScript can't derive a literal type from it directly — this union is
- * hand-listed and kept honest by `catalog-models.test.ts`, which fails the
- * moment it drifts from the live `catalogModels` export. Assigning a typo'd
- * or made-up model id to a `CatalogModelName`-typed field is a compile
- * error, not a silent `lookupRates` miss.
- */
-export type CatalogModelName =
- | "claude-sonnet-5"
- | "claude-opus-5"
- | "claude-haiku-4-5-20251001"
- | "gpt-5.5"
- | "gpt-5.6-sol"
- | "gemini-2.5-pro"
- | "gemini-3.6-flash"
- | "kimi-k3"
- | "kimi-k2.7-code"
- | "claude-fable-5"
- | "claude-opus-4-8"
- | "claude-opus-4-5-20251101"
- | "claude-opus-4-6"
- | "claude-opus-4-7"
- | "claude-sonnet-4-5-20250929"
- | "claude-sonnet-4-6"
- | "gpt-5.6-terra"
- | "gpt-5.6-luna"
- | "gemini-2.5-flash"
- | "gemini-3.5-flash"
- | "gemini-2.5-flash-image"
- | "gemini-3.1-flash-image"
- | "gemini-3-flash-preview"
- | "gemini-3.1-pro-preview"
- | "kimi-k2.6"
- | "qwen3.7-plus"
- | "mimo-v2.5"
- | "glm-5.2"
- | "gpt-5.4-mini"
- | "deepseek-v4-pro"
- | "deepseek-v4-flash"
- | "glm-5"
- | "glm-5.1"
- | "hy3"
- | "kimi-k2.5"
- | "mimo-v2.5-pro"
- | "minimax-m2.5"
- | "minimax-m2.7"
- | "minimax-m3"
- | "qwen3.5-plus"
- | "qwen3.6-plus"
- | "qwen3.7-max"
- | "qwen3.8-max"
- | "gpt-5"
- | "gpt-5-mini"
- | "gpt-5-nano"
- | "gpt-5.1"
- | "gpt-5.2"
- | "gpt-5.4"
- | "gpt-5.4-nano"
- | "o1"
- | "o3"
- | "o3-mini"
- | "o4-mini"
- | "gpt-4.1"
- | "gpt-4.1-mini"
- | "gpt-4.1-nano"
- | "gpt-4o"
- | "gpt-4o-mini"
- | "gpt-4-turbo"
- | "gpt-4"
- | "grok-4.20-0309-non-reasoning"
- | "grok-4.20-0309-reasoning"
- | "grok-4.3"
- | "grok-4.5"
- | "grok-4.6"
- | "grok-build-0.1";
-
-export const CATALOG_MODEL_NAMES: readonly CatalogModelName[] = [
- "claude-sonnet-5",
- "claude-opus-5",
- "claude-haiku-4-5-20251001",
- "gpt-5.5",
- "gpt-5.6-sol",
- "gemini-2.5-pro",
- "gemini-3.6-flash",
- "kimi-k3",
- "kimi-k2.7-code",
- "claude-fable-5",
- "claude-opus-4-8",
- "claude-opus-4-5-20251101",
- "claude-opus-4-6",
- "claude-opus-4-7",
- "claude-sonnet-4-5-20250929",
- "claude-sonnet-4-6",
- "gpt-5.6-terra",
- "gpt-5.6-luna",
- "gemini-2.5-flash",
- "gemini-3.5-flash",
- "gemini-2.5-flash-image",
- "gemini-3.1-flash-image",
- "gemini-3-flash-preview",
- "gemini-3.1-pro-preview",
- "kimi-k2.6",
- "qwen3.7-plus",
- "mimo-v2.5",
- "glm-5.2",
- "gpt-5.4-mini",
- "deepseek-v4-pro",
- "deepseek-v4-flash",
- "glm-5",
- "glm-5.1",
- "hy3",
- "kimi-k2.5",
- "mimo-v2.5-pro",
- "minimax-m2.5",
- "minimax-m2.7",
- "minimax-m3",
- "qwen3.5-plus",
- "qwen3.6-plus",
- "qwen3.7-max",
- "qwen3.8-max",
- "gpt-5",
- "gpt-5-mini",
- "gpt-5-nano",
- "gpt-5.1",
- "gpt-5.2",
- "gpt-5.4",
- "gpt-5.4-nano",
- "o1",
- "o3",
- "o3-mini",
- "o4-mini",
- "gpt-4.1",
- "gpt-4.1-mini",
- "gpt-4.1-nano",
- "gpt-4o",
- "gpt-4o-mini",
- "gpt-4-turbo",
- "gpt-4",
- "grok-4.20-0309-non-reasoning",
- "grok-4.20-0309-reasoning",
- "grok-4.3",
- "grok-4.5",
- "grok-4.6",
- "grok-build-0.1",
-];
diff --git a/packages/provider-pricing/src/cost.test.ts b/packages/provider-pricing/src/cost.test.ts
deleted file mode 100644
index f3ad4d77f0..0000000000
--- a/packages/provider-pricing/src/cost.test.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { describe, expect, test } from "bun:test";
-
-import { computeCost, totalTokens } from "./cost";
-
-describe("computeCost", () => {
- test("sums known rates into a total", () => {
- const result = computeCost(
- {
- input: 1_000_000,
- cacheRead: 0,
- cacheWrite: 0,
- output: 500_000,
- thinking: 0,
- },
- {
- inputPerMTok: 3,
- cacheReadPerMTok: 0.3,
- cacheWritePerMTok: 3.75,
- outputPerMTok: 15,
- thinkingPerMTok: 15,
- },
- );
- expect(result.totalUsd).toBe(3 + 7.5);
- expect(result.byClass.input).toBe(3);
- expect(result.byClass.output).toBe(7.5);
- });
-
- test("zero-token classes contribute 0 even without a rate", () => {
- const result = computeCost(
- { input: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 0, thinking: 0 },
- {
- inputPerMTok: 2,
- cacheReadPerMTok: null,
- cacheWritePerMTok: null,
- outputPerMTok: null,
- thinkingPerMTok: null,
- },
- );
- expect(result.totalUsd).toBe(2);
- expect(result.byClass.cacheRead).toBe(0);
- });
-
- test("tokens with a missing rate make the total null", () => {
- const result = computeCost(
- { input: 100, cacheRead: 0, cacheWrite: 0, output: 10, thinking: 0 },
- {
- inputPerMTok: 2,
- cacheReadPerMTok: null,
- cacheWritePerMTok: null,
- outputPerMTok: null,
- thinkingPerMTok: null,
- },
- );
- expect(result.totalUsd).toBeNull();
- expect(result.byClass.output).toBeNull();
- });
-});
-
-describe("totalTokens", () => {
- test("sums every class", () => {
- expect(
- totalTokens({
- input: 1,
- cacheRead: 2,
- cacheWrite: 3,
- output: 4,
- thinking: 5,
- }),
- ).toBe(15);
- });
-});
diff --git a/packages/provider-pricing/src/cost.ts b/packages/provider-pricing/src/cost.ts
deleted file mode 100644
index e138f84c24..0000000000
--- a/packages/provider-pricing/src/cost.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { CostBreakdown, TokenClasses, TokenRates } from "./types";
-
-/**
- * Compute cost for one turn. A class with zero tokens contributes 0
- * even without a rate. A class with tokens > 0 and a null rate makes
- * that class — and therefore the total — absent (null). Never invents
- * a zero cost for an unknown rate.
- */
-export function computeCost(tokens: TokenClasses, rates: TokenRates): CostBreakdown {
- const input = classCost(tokens.input, rates.inputPerMTok);
- const cacheRead = classCost(tokens.cacheRead, rates.cacheReadPerMTok);
- const cacheWrite = classCost(tokens.cacheWrite, rates.cacheWritePerMTok);
- const output = classCost(tokens.output, rates.outputPerMTok);
- const thinking = classCost(tokens.thinking, rates.thinkingPerMTok);
-
- const parts = [input, cacheRead, cacheWrite, output, thinking];
- const totalUsd = parts.some((p) => p === null)
- ? null
- : parts.reduce((sum, p) => sum + (p as number), 0);
-
- return {
- totalUsd,
- byClass: { input, cacheRead, cacheWrite, output, thinking },
- };
-}
-
-function classCost(tokens: number, ratePerMTok: number | null): number | null {
- if (tokens === 0) return 0;
- if (ratePerMTok === null) return null;
- return (tokens / 1_000_000) * ratePerMTok;
-}
-
-/** Sum token classes from a row-shaped object. */
-export function totalTokens(tokens: TokenClasses): number {
- return tokens.input + tokens.cacheRead + tokens.cacheWrite + tokens.output + tokens.thinking;
-}
-
-/** Prompt-side tokens used to pick a context-size rate tier. */
-export function contextTokens(tokens: TokenClasses): number {
- return tokens.input + tokens.cacheRead + tokens.cacheWrite;
-}
diff --git a/packages/provider-pricing/src/index.ts b/packages/provider-pricing/src/index.ts
deleted file mode 100644
index b5985adc80..0000000000
--- a/packages/provider-pricing/src/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export { CATALOG_MODEL_NAMES } from "./catalog-models";
-export type { CatalogModelName } from "./catalog-models";
-export { computeCost, contextTokens, totalTokens } from "./cost";
-export { costUsd, lookupRates } from "./lookup";
-export { PROVIDER_PRICES } from "./rates";
-export type {
- CostBreakdown,
- LookupRatesInput,
- ModelPrice,
- RateTier,
- TokenClasses,
- TokenRates,
-} from "./types";
diff --git a/packages/provider-pricing/src/lookup.test.ts b/packages/provider-pricing/src/lookup.test.ts
deleted file mode 100644
index 5b8aed6908..0000000000
--- a/packages/provider-pricing/src/lookup.test.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { describe, expect, test } from "bun:test";
-
-import { costUsd, lookupRates } from "./lookup";
-
-const millionInput = {
- input: 1_000_000,
- cacheRead: 0,
- cacheWrite: 0,
- output: 0,
- thinking: 0,
-};
-
-describe("lookupRates", () => {
- test("anthropic claude-sonnet-5 is not zen claude-sonnet-5", () => {
- const anthropic = lookupRates({
- provider: "anthropic",
- model: "claude-sonnet-5",
- contextTokens: 1_000,
- });
- const zen = lookupRates({
- provider: "opencode-zen",
- model: "claude-sonnet-5",
- contextTokens: 1_000,
- });
- expect(anthropic?.inputPerMTok).toBe(2);
- expect(zen?.inputPerMTok).toBe(1.7);
- });
-
- test("unknown pair is null", () => {
- expect(
- lookupRates({
- provider: "anthropic",
- model: "mystery-model",
- contextTokens: 1,
- }),
- ).toBeNull();
- });
-
- test("catalog model with no price row is null, not a guess", () => {
- // "gpt-5" is a real @intx/inference-catalog model served by OpenAI
- // Direct, but workbench has no priced row for it — an unpriced
- // catalog model is an expected, honest absence, not a silent gap.
- expect(
- lookupRates({
- provider: "openai",
- model: "gpt-5",
- contextTokens: 1_000,
- }),
- ).toBeNull();
- });
-
- test("openai gpt-5.6-terra steps up at 272k context", () => {
- const below = lookupRates({
- provider: "openai",
- model: "gpt-5.6-terra",
- contextTokens: 271_999,
- });
- const at = lookupRates({
- provider: "openai",
- model: "gpt-5.6-terra",
- contextTokens: 272_000,
- });
- expect(below?.inputPerMTok).toBe(2);
- expect(at?.inputPerMTok).toBe(4);
- });
-
- test("xai grok-4.6 steps up at 200k context", () => {
- const below = lookupRates({
- provider: "xai",
- model: "grok-4.6",
- contextTokens: 199_999,
- });
- const at = lookupRates({
- provider: "xai",
- model: "grok-4.6",
- contextTokens: 200_000,
- });
- expect(below?.outputPerMTok).toBe(6);
- expect(at?.outputPerMTok).toBe(12);
- });
-});
-
-describe("costUsd", () => {
- test("1M anthropic sonnet-5 input is $2", () => {
- expect(
- costUsd({
- provider: "anthropic",
- model: "claude-sonnet-5",
- tokens: millionInput,
- }),
- ).toBe(2);
- });
-
- test("unknown model with tokens is null, not zero", () => {
- expect(
- costUsd({
- provider: "anthropic",
- model: "mystery-model",
- tokens: millionInput,
- }),
- ).toBeNull();
- });
-
- test("unknown model with zero tokens is $0", () => {
- expect(
- costUsd({
- provider: "anthropic",
- model: "mystery-model",
- tokens: {
- input: 0,
- cacheRead: 0,
- cacheWrite: 0,
- output: 0,
- thinking: 0,
- },
- }),
- ).toBe(0);
- });
-
- test("ollama is $0", () => {
- expect(
- costUsd({
- provider: "ollama",
- model: "qwen3.8:27b",
- tokens: millionInput,
- }),
- ).toBe(0);
- });
-});
diff --git a/packages/provider-pricing/src/lookup.ts b/packages/provider-pricing/src/lookup.ts
deleted file mode 100644
index 93aee88f84..0000000000
--- a/packages/provider-pricing/src/lookup.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { computeCost, contextTokens, totalTokens } from "./cost";
-import { PROVIDER_PRICES } from "./rates";
-import type { LookupRatesInput, TokenClasses, TokenRates } from "./types";
-
-const BY_PROVIDER_MODEL = new Map(
- PROVIDER_PRICES.map((price) => [`${price.provider}\0${price.model}`, price]),
-);
-
-/**
- * Rates for one `(provider, model)` at a given prompt-side context size.
- * Unknown pair → null. Same model id on two providers is two rows.
- */
-export function lookupRates(input: LookupRatesInput): TokenRates | null {
- const price = BY_PROVIDER_MODEL.get(`${input.provider}\0${input.model}`);
- if (price === undefined) return null;
- let chosen = price.tiers[0];
- if (chosen === undefined) return null;
- for (const tier of price.tiers) {
- if (input.contextTokens >= tier.minContextTokens) chosen = tier;
- }
- return chosen.rates;
-}
-
-/**
- * Look up rates and multiply. Unknown pair with tokens → null, not $0.
- * Zero-token turns cost $0 even without a row.
- */
-export function costUsd(input: {
- readonly provider: string;
- readonly model: string;
- readonly tokens: TokenClasses;
-}): number | null {
- const rates = lookupRates({
- provider: input.provider,
- model: input.model,
- contextTokens: contextTokens(input.tokens),
- });
- if (rates === null) return totalTokens(input.tokens) === 0 ? 0 : null;
- return computeCost(input.tokens, rates).totalUsd;
-}
diff --git a/packages/provider-pricing/src/rates.ts b/packages/provider-pricing/src/rates.ts
deleted file mode 100644
index 53336e6111..0000000000
--- a/packages/provider-pricing/src/rates.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import type { SupportedCredentialProvider } from "@corbits/connections/credential-test";
-
-import type { CatalogModelName } from "./catalog-models";
-import type { ModelPrice, RateTier, TokenRates } from "./types";
-
-function usd(
- input: number,
- output: number,
- cacheRead: number | null,
- cacheWrite: number | null,
- thinking: number | null = output,
-): TokenRates {
- return {
- inputPerMTok: input,
- outputPerMTok: output,
- cacheReadPerMTok: cacheRead,
- cacheWritePerMTok: cacheWrite,
- thinkingPerMTok: thinking,
- };
-}
-
-/** A priced row whose wire model id is a real `@intx/inference-catalog` model. */
-function priced(
- provider: SupportedCredentialProvider,
- model: CatalogModelName,
- base: TokenRates,
- higher?: readonly RateTier[],
-): ModelPrice {
- return {
- provider,
- model,
- catalogModel: model,
- tiers: [{ minContextTokens: 0, rates: base }, ...(higher ?? [])],
- };
-}
-
-/**
- * A priced row whose wire model id the pinned catalog does not carry —
- * an OpenRouter/Hugging Face namespaced id, an Ollama tag, or a model the
- * catalog hasn't onboarded. `reason` is required so the gap is a
- * documented decision, not a silent omission.
- */
-function unpriced(
- provider: SupportedCredentialProvider,
- model: string,
- reason: string,
- base: TokenRates,
- higher?: readonly RateTier[],
-): ModelPrice {
- return {
- provider,
- model,
- catalogModel: null,
- uncatalogedReason: reason,
- tiers: [{ minContextTokens: 0, rates: base }, ...(higher ?? [])],
- };
-}
-
-const FREE = usd(0, 0, 0, 0, 0);
-
-const OPENROUTER_NAMESPACED =
- "OpenRouter namespaces this wire id with its own vendor prefix; the pinned catalog's canonical id carries no such prefix, so this exact string is not a catalog member.";
-
-const NOT_IN_PINNED_CATALOG =
- "Not present in the pinned @intx/inference-catalog snapshot — the catalog has not onboarded this model/deployment yet.";
-
-const HUGGINGFACE_REPO_ID =
- "Hugging Face bills by its own `org/Repo-Name` inference-endpoint id, which the catalog does not carry — it only tracks each model's first-party canonical name.";
-
-const OLLAMA_LOCAL_TAG =
- "Ollama tags a locally pulled model with its own size/quant suffix, which the catalog does not carry — it only tracks each model's first-party canonical name.";
-
-/**
- * Catalog-seed prices from models.dev (2026-04-18), keyed by workbench
- * provider name + the wire model id that lands on a usage turn. Thinking
- * is billed at the provider's `reasoning` rate when listed, otherwise at
- * the output rate (the usual "thinking is output" rule).
- *
- * Every row's `provider` is a `SupportedCredentialProvider` — a
- * misspelled or unsupported provider is a compile error. Every row's
- * `model` is billed as the real wire id; `priced()` additionally proves
- * that id is a genuine `@intx/inference-catalog` model (also a compile
- * error to get wrong), while `unpriced()` marks a wire id the pinned
- * catalog does not carry, with a named reason instead of a silent gap.
- * The reverse case — a catalog model with no price row here, e.g. `gpt-5`
- * or `o3` — is not a gap either: `lookupRates`/`costUsd` return `null` for
- * any (provider, model) this list doesn't cover, catalog member or not
- * (see `lookup.test.ts`'s "catalog model with no price row" case).
- */
-export const PROVIDER_PRICES: readonly ModelPrice[] = [
- priced("anthropic", "claude-sonnet-5", usd(2, 10, 0.2, 2.5)),
-
- priced("openai", "gpt-5.6-terra", usd(2, 12, 0.2, 2.5), [
- { minContextTokens: 272_000, rates: usd(4, 18, 0.4, 5) },
- ]),
- priced("openai", "gpt-4o-mini", usd(0.15, 0.6, 0.075, null)),
-
- unpriced(
- "google-genai",
- "gemini-3.7-flash",
- NOT_IN_PINNED_CATALOG,
- usd(0.1, 0.4, 0.01, 0.125, 0.4),
- ),
- priced("google-genai", "gemini-2.5-flash", usd(0.3, 2.5, 0.03, 0.38333, 2.5)),
-
- priced("xai", "grok-4.6", usd(2, 6, 0.2, 2.5), [
- { minContextTokens: 200_000, rates: usd(4, 12, 0.4, 5) },
- ]),
- priced("xai", "grok-4.5", usd(2, 6, 0.2, 2.5), [
- { minContextTokens: 200_000, rates: usd(4, 12, 0.4, 5) },
- ]),
-
- unpriced("openrouter", "qwen/qwen3.8-27b", NOT_IN_PINNED_CATALOG, usd(0.09, 0.29, 0.018, 0.1125)),
- unpriced(
- "openrouter",
- "anthropic/claude-sonnet-5",
- OPENROUTER_NAMESPACED,
- usd(2, 10, 0.2, 3.125),
- ),
- unpriced("openrouter", "openai/gpt-5.6-sol", OPENROUTER_NAMESPACED, usd(2, 12, 0.2, 2.5), [
- { minContextTokens: 272_000, rates: usd(4, 18, 0.4, 5) },
- ]),
- unpriced(
- "openrouter",
- "meta-llama/llama-3.3-70b-instruct",
- NOT_IN_PINNED_CATALOG,
- usd(0.1, 0.32, 0.02, 0.125),
- ),
- unpriced("openrouter", "google/gemma-4-26b-a4b-it:free", NOT_IN_PINNED_CATALOG, FREE),
-
- priced("opencode-zen", "qwen3.7-plus", usd(0.32, 1.28, 0.064, 0.4), [
- { minContextTokens: 256_000, rates: usd(0.96, 3.84, 0.192, 1.2) },
- ]),
- priced("opencode-zen", "claude-sonnet-5", usd(1.7, 8.5, 0.17, 2.125)),
- priced("opencode-zen", "gpt-5.4-mini", usd(0.4, 1.6, 0.04, 0.5)),
- priced("opencode-zen", "deepseek-v4-flash", usd(0.14, 0.28, 0.028, 0.175)),
- priced("opencode-zen", "kimi-k2.6", usd(0.5, 2.8, 0.1, 0.625)),
-
- unpriced("groq", "llama-3.3-70b-versatile", NOT_IN_PINNED_CATALOG, usd(0.59, 0.79, null, null)),
- unpriced("groq", "llama-3.1-8b-instant", NOT_IN_PINNED_CATALOG, usd(0.05, 0.08, null, null)),
- unpriced("groq", "openai/gpt-oss-120b", NOT_IN_PINNED_CATALOG, usd(0.15, 0.6, 0.075, null)),
-
- priced("deepseek", "deepseek-v4-flash", usd(0.14, 0.28, 0.028, 0.175)),
- priced("deepseek", "deepseek-v4-pro", usd(1.74, 3.48, 0.145, 2.175)),
-
- unpriced("mistral", "mistral-small-2603", NOT_IN_PINNED_CATALOG, usd(0.15, 0.6, 0.03, 0.1875)),
- unpriced("mistral", "mistral-large-2512", NOT_IN_PINNED_CATALOG, usd(0.5, 1.5, 0.05, 0.625, 1.5)),
- unpriced("mistral", "codestral-2508", NOT_IN_PINNED_CATALOG, usd(0.3, 0.9, null, null)),
-
- unpriced(
- "huggingface",
- "deepseek-ai/DeepSeek-V4-Flash",
- HUGGINGFACE_REPO_ID,
- usd(0.14, 0.28, 0.028, 0.175),
- ),
- unpriced(
- "huggingface",
- "meta-llama/Llama-3.3-70B-Instruct",
- HUGGINGFACE_REPO_ID,
- usd(0.1, 0.32, 0.03, 0.1),
- ),
- unpriced(
- "huggingface",
- "Qwen/Qwen3-Coder-30B-A3B-Instruct",
- HUGGINGFACE_REPO_ID,
- usd(0.07, 0.27, 0.014, 0.0875),
- ),
-
- unpriced("ollama", "qwen3.8:27b", OLLAMA_LOCAL_TAG, FREE),
- unpriced("ollama", "qwen3.5:9b-mlx", OLLAMA_LOCAL_TAG, FREE),
-];
diff --git a/packages/provider-pricing/src/types.ts b/packages/provider-pricing/src/types.ts
deleted file mode 100644
index a081bcf4ee..0000000000
--- a/packages/provider-pricing/src/types.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import type { SupportedCredentialProvider } from "@corbits/connections/credential-test";
-
-import type { CatalogModelName } from "./catalog-models";
-
-/**
- * Token classes a turn may report. Cost is the sum of each class's
- * (tokens / 1e6) * rate-per-MTok when that rate is known.
- */
-export type TokenClasses = {
- readonly input: number;
- readonly cacheRead: number;
- readonly cacheWrite: number;
- readonly output: number;
- readonly thinking: number;
-};
-
-export type TokenRates = {
- readonly inputPerMTok: number | null;
- readonly cacheReadPerMTok: number | null;
- readonly cacheWritePerMTok: number | null;
- readonly outputPerMTok: number | null;
- readonly thinkingPerMTok: number | null;
-};
-
-export type RateTier = {
- /** Inclusive lower bound on prompt-side tokens (input + cache). */
- readonly minContextTokens: number;
- readonly rates: TokenRates;
-};
-
-/**
- * A priced (provider, model) pair. `provider` is typed against
- * `SupportedCredentialProvider` — the same provider identity workbench
- * already uses for credentials and the catalog's adapter plugin mapping
- * (see `@corbits/connections/credential-test`) — so a misspelled or
- * unsupported provider is a compile error.
- *
- * `model` is the actual wire id billed on a usage turn, which is not
- * always a catalog canonical name (OpenRouter and Hugging Face namespace
- * their ids, Ollama tags carry a size/quant suffix, and several relays
- * serve models the catalog hasn't onboarded). Where the wire id *is* a
- * catalog canonical name, `catalogModel` repeats it as a
- * `CatalogModelName` — a compile-time check that the id is real — and
- * where it isn't, `catalogModel` is explicitly `null` with a documented
- * reason, never a silent gap.
- */
-export type ModelPrice = {
- readonly provider: SupportedCredentialProvider;
- readonly model: string;
- readonly tiers: readonly RateTier[];
-} & (
- | { readonly catalogModel: CatalogModelName }
- | { readonly catalogModel: null; readonly uncatalogedReason: string }
-);
-
-export type LookupRatesInput = {
- readonly provider: string;
- readonly model: string;
- readonly contextTokens: number;
-};
-
-export type CostBreakdown = {
- /** USD total, or null when any class with tokens > 0 lacks a rate. */
- readonly totalUsd: number | null;
- readonly byClass: {
- readonly input: number | null;
- readonly cacheRead: number | null;
- readonly cacheWrite: number | null;
- readonly output: number | null;
- readonly thinking: number | null;
- };
-};
diff --git a/packages/provider-pricing/tsconfig.json b/packages/provider-pricing/tsconfig.json
deleted file mode 100644
index d7611c1229..0000000000
--- a/packages/provider-pricing/tsconfig.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "extends": "../../tsconfig.base.json",
- "include": ["src", "test"],
- "compilerOptions": {
- "types": ["bun"],
- "noEmit": true
- }
-}
diff --git a/scripts/db-setup.ts b/scripts/db-setup.ts
index 8b6cd323a3..794e87c70b 100644
--- a/scripts/db-setup.ts
+++ b/scripts/db-setup.ts
@@ -29,9 +29,23 @@ import path from "node:path";
import { readdir } from "node:fs/promises";
import { applyWebhookTriggersMigrations } from "../packages/webhook-triggers/src/migrations";
-import { applyNotifyMigrations } from "../packages/notify/src/migrations";
-import { applyMailboxMigrations } from "../packages/inbox/src/migrations";
import { applyCronMigrations } from "../packages/cron/src/migrations";
+import { createMailboxDb, runMailboxMigrations } from "@corbits/mailbox";
+
+/**
+ * Apply `@corbits/mailbox`'s own migrations against `databaseUrl`. The
+ * package keeps its own ledger inside the `mailbox` schema; this wrapper
+ * only opens a short-lived handle, runs the migrator, and closes it.
+ */
+async function applyMailboxMigrations(databaseUrl: string): Promise<{ applied: string[] }> {
+ const { db, close } = createMailboxDb(databaseUrl);
+ try {
+ await runMailboxMigrations(db);
+ return { applied: ["mailbox"] };
+ } finally {
+ await close();
+ }
+}
const repoRoot = path.resolve(import.meta.dir, "..");
const HUB_DIR = path.join(repoRoot, "apps", "hub");
@@ -49,7 +63,6 @@ const INSTALLED_PACKAGE_MIGRATIONS: readonly {
apply: (databaseUrl: string) => Promise<{ applied: string[] }>;
}[] = [
{ name: "@corbits/webhook-triggers", apply: applyWebhookTriggersMigrations },
- { name: "@corbits/notify", apply: applyNotifyMigrations },
{ name: "@corbits/mailbox", apply: applyMailboxMigrations },
{ name: "@corbits/cron", apply: applyCronMigrations },
];
@@ -518,7 +531,6 @@ const PACKAGE_SCHEMAS = [
"mailbox",
"routines",
"insights",
- "notify",
"webhook_triggers",
"access_policy",
"bench",
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 78cca2625f..868b08b5dd 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -55,30 +55,18 @@
{
"path": "./packages/error-sink/tsconfig.src.json"
},
- {
- "path": "./packages/gotenberg-render/tsconfig.src.json"
- },
{
"path": "./packages/hub-api-client/tsconfig.src.json"
},
{
"path": "./packages/icons/tsconfig.src.json"
},
- {
- "path": "./packages/inbox/tsconfig.src.json"
- },
{
"path": "./packages/inference-catalog/tsconfig.src.json"
},
{
"path": "./packages/migration-runner/tsconfig.src.json"
},
- {
- "path": "./packages/mocks/tsconfig.src.json"
- },
- {
- "path": "./packages/notify/tsconfig.src.json"
- },
{
"path": "./packages/ollama-adapter/tsconfig.src.json"
},