diff --git a/.github/workflows/backup-checkpoint-qualification.yml b/.github/workflows/backup-checkpoint-qualification.yml index 3b90802cc13..d325d927e89 100644 --- a/.github/workflows/backup-checkpoint-qualification.yml +++ b/.github/workflows/backup-checkpoint-qualification.yml @@ -46,6 +46,8 @@ jobs: with: node-version: '24' cache: pnpm + - name: Resolve dependency manifests for this run + run: pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile - run: pnpm install --frozen-lockfile --ignore-scripts - name: Build workspace import prerequisites run: | diff --git a/doc/DATABASE.md b/doc/DATABASE.md index c5822df0f02..b231152f6d4 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -1,5 +1,28 @@ # Database +### Better Auth 1.7.16 upgrade + +Better Auth 1.7.3 and later identify accounts by `provider_id` and `account_id`. +They no longer write the `issuer` column that 1.7.0–1.7.2 required. Apply the +schema migration before starting the updated server. It makes `issuer` nullable, +keeps its existing values, removes the old issuer index, and adds a unique index +on the provider/account pair. This permits new credential signups and preserves +existing accounts. + +Before the upgrade, check for duplicate provider/account pairs: + +```sql +SELECT provider_id, account_id, count(*) +FROM account +GROUP BY provider_id, account_id +HAVING count(*) > 1; +``` + +Resolve any duplicates before migration. Keep different users separate. A duplicate +causes index creation to fail rather than leaving an ambiguous login identity. +Do not restore the issuer `NOT NULL` constraint on rollback while new rows have +null issuers. See the [Better Auth upgrade guide](https://better-auth.com/docs/guides/1-7-upgrade-guide). + ### Durable native provider backups `native_harness_backups` journals encrypted provider-session archives in object diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 1023f2f1414..494ef747545 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -1443,6 +1443,11 @@ The server typecheck builds only the Runner's TypeScript output. Use `pnpm --filter @paperclipai/paperclip-runner build` when a local check needs the Runner binary. +If a compiler child terminates on a signal, the wrapper reports the signal, +layer, command, and working directory on stderr, then stops with a nonzero exit. +Compiler stdout and stderr remain visible. A signal such as `SIGKILL` alone does +not establish that the process ran out of memory. + `src/__tests__/tsconfig.json` holds the options for the test-tree layers, so an editor and the typecheck report the same errors. Server tests reach Daytona plugin source, but the root install does not install that plugin's dependencies. So this diff --git a/doc/connections/SLACK-TASK-TOOLS.md b/doc/connections/SLACK-TASK-TOOLS.md index 351cc8ef79e..68c4e573ed9 100644 --- a/doc/connections/SLACK-TASK-TOOLS.md +++ b/doc/connections/SLACK-TASK-TOOLS.md @@ -207,18 +207,18 @@ ordinary task finals are not automatically broadcast to Slack. ## Require an at-mention for inbound channel messages -Slack app settings include **Require at-mention** directly below **Allow direct -messages**. It defaults to on for new Slack connections. Existing connections +Slack app settings include **Require at-mention in channels** directly below +**Allow direct messages**. It defaults to on for new Slack connections. Existing connections keep their saved setting; the setting applies only to channels. -When enabled, each inbound channel message must explicitly @mention the connected -bot, including replies in existing channel threads. Direct messages never require +When enabled, each top-level channel message must explicitly @mention the connected +bot. Replies in existing channel threads do not require an @mention. Direct messages never require an @mention, but must be enabled with **Allow direct messages** and pass the usual sender authorization checks. Top-level DMs remain a linear conversation with top-level replies; use a private channel for conversations organized into threads. -Unmentioned channel messages do not create or continue a task. Queued messages +Unmentioned top-level channel messages do not create a task. Queued messages are checked again before starting agent work. If a queued run combines several -channel messages, an unmentioned input cancels that queued run when this setting -is enabled. Send a new mention to start it again. Unmentioned DM inputs do not -trigger this cancellation. +channel messages, an unmentioned top-level input cancels that queued run when this setting +is enabled. Send a new mention to start it again. Unmentioned thread replies and +DM inputs do not trigger this cancellation. This setting does not cancel work that has already started or revoke its Slack tool access. diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md index 1812b0839ae..1f0004da3a9 100644 --- a/doc/execution-semantics.md +++ b/doc/execution-semantics.md @@ -3029,6 +3029,24 @@ ends with `native_provider_state_incompatible`, and Retry or the next message continues in a fresh provider session in the same workspace. A session with work in flight keeps the unclassified behavior. +A follow-up after a governed wait (`allow_replacement_after_governed_wait`) +normally requires the same provider session. For this one refusal it continues +on its own in a fresh provider session in the same run, without a Retry, when +all of these hold: the driver reports the Runner's typed missing-state refusal +for a session with nothing in flight; the reconciled checkpoint is the untouched +rebind of the previous run (no cursor progress, turn, runtime request, proposed +result, terminal, or governed wait of the follow-up run, and the at-most-once +recovery marker present); and the control plane proves from durable rows, +across every runner instance, that the follow-up run has no provider event, +source-sequenced runner event, tool invocation, or accepted result. The follow-up +has not started a turn, so a fresh session cannot repeat provider work or a tool +side effect; only the earlier conversation context is lost. The fresh session +gets the full task prompt, the interaction responses, the stored fresh-session +handoff, and queued input, as any replacement does, and the run log records the +existing provider session continuity break with the reason. Any other +incompatible state, a control plane without that proof, or a checkpoint saved +without those fields keeps `native_provider_state_incompatible` and its Retry. + Bootstrap retries, exact-checkpoint resumes, and fresh replacement sessions share three total provider attempts, including the original attempt. Linked run IDs, controller restarts, and duplicate wakes do not reset this budget. A resume after a graceful shutdown drain suspended the run for reattachment does not consume an attempt, because the run did not cause that restart. A crash restart (no drain record) consumes an attempt only when the attempt it interrupted spent provider work without progress. This is one rule over recorded outcomes, not over intents. The attempt counts as started only when the provider accepted a turn: a `turn.accepted` event or the provider's own `turn.started`. A startup still marked `beforeSpawn`, a runner that spawned but was never admitted, and a `turn.start` the provider did not accept are not charged. Progress means that an accepted turn then completed work: a tool execution or a completed item other than a usage report. An accepted turn with only usage, a failure, or no events is charged, so a resume that fails each time still uses up the budget. The first restart judges the original execution. Each later restart claim records the run's event-log position, and the next claim judges only the attempt recorded after it; a normal retry in between starts a new attempt (at its `native.process_start_requested`), so earlier work does not count for it. A reattachment adopts a provider that already started, so for it only progress counts. A resume that launches a new runner and that the controller died before starting used no provider work, so the next claim replaces it and the attempt ends at the attempt that resume was to run as. Uncharged restarts stop after 12 controller generations, so a crash loop that never starts the provider, or that makes progress and then crashes the controller each time, is still bounded. Legacy runs follow the same rule: a preparation interrupted before provider work started (`providerWorkStarted: false`) is retried in its own `interrupted_preparation` lane, which never spends the failure retry budget and is bounded at 12 retries. This holds for a hard kill as well as a graceful drain: once the killed controller's recorded process is proven gone, restart recovery fails the run as `process_lost` with the bootstrap certificate and schedules it in that lane. A run saved by an older release without a controller process stamp cannot be proven and stays in the charged lane. Exact-checkpoint resumes retain the 30-second delay. Replacement scheduling and predecessor lineage commit together, with one successor per predecessor and admission through the normal task locks, authorization, pause, approval, and budget gates. A due failed remote retry can retain its provider PID after heartbeat completion diff --git a/packages/db/src/better-auth-account-issuer-migration.test.ts b/packages/db/src/better-auth-account-issuer-migration.test.ts index a8264d99ec7..ed260aa9a15 100644 --- a/packages/db/src/better-auth-account-issuer-migration.test.ts +++ b/packages/db/src/better-auth-account-issuer-migration.test.ts @@ -1,17 +1,8 @@ -/** - * The `account.issuer` backfill has to be total. The column lands NOT NULL, so - * a row the UPDATE misses aborts the whole migration and leaves an upgraded - * install with no working auth at all. This suite rewinds the migration, - * seeds the pre-upgrade rows an existing deployment would have, and re-applies - * it — a backfill narrowed to one provider fails here. - */ - -import { createHash, randomUUID } from "node:crypto"; +import { randomUUID } from "node:crypto"; import fs from "node:fs"; import { getTableConfig } from "drizzle-orm/pg-core"; import postgres from "postgres"; import { afterEach, describe, expect, it } from "vitest"; -import { applyPendingMigrations } from "./client.js"; import { authAccounts } from "./schema/auth.js"; import { getEmbeddedPostgresTestSupport, @@ -21,10 +12,6 @@ import { const MIGRATION_FILE = "0230_better_auth_account_issuer.sql"; const UNIQUE_INDEX = "account_issuer_account_id_uq"; -// Better Auth's own issuer helpers: `createLocalAccountIssuer("credential")` -// for email/password and `createOAuthAccountIssuer(providerId)` for a social -// provider that declares no issuer of its own. Sign-in looks the credential -// account up by this exact value. const CREDENTIAL_ISSUER = "local:credential"; const OAUTH_ISSUER_PREFIX = "local:oauth:"; @@ -32,33 +19,32 @@ const cleanups: Array<() => Promise> = []; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; -async function migrationHash() { - const content = await fs.promises.readFile( - new URL(`./migrations/${MIGRATION_FILE}`, import.meta.url), - "utf8", - ); - return createHash("sha256").update(content).digest("hex"); +async function applySqlMigration(sql: postgres.Sql, file: string) { + const content = await fs.promises.readFile(new URL(`./migrations/${file}`, import.meta.url), "utf8"); + await sql.begin(async (transaction) => { + for (const statement of content.split("--> statement-breakpoint")) { + if (statement.trim()) await transaction.unsafe(statement); + } + }); } describe("account issuer schema", () => { - it("declares the column Better Auth requires and the key it indexes on", () => { + it("keeps issuer nullable and identifies accounts by provider and account ID", () => { const config = getTableConfig(authAccounts); const issuer = config.columns.find((column) => column.name === "issuer"); expect(issuer).toBeDefined(); - expect(issuer?.notNull).toBe(true); + expect(issuer?.notNull).toBe(false); - // Better Auth declares `(issuer, accountId)` unique on the account model - // and resolves accounts by that pair. - const unique = config.indexes.find((index) => index.config.name === UNIQUE_INDEX); + const unique = config.indexes.find((index) => index.config.name === "account_provider_account_id_uq"); expect(unique?.config.unique).toBe(true); expect(unique?.config.columns.map((column) => (column as { name: string }).name)).toEqual([ - "issuer", + "provider_id", "account_id", ]); }); }); -describeEmbeddedPostgres("account issuer migration", () => { +describeEmbeddedPostgres("historical issuer migration and patched provider identity upgrade", () => { // Reverse registration order, one at a time. The raw `postgres` client is not // registered with the module's client registry, so the cluster teardown does // not close it. Stopping the cluster while that client is still draining kills @@ -75,8 +61,7 @@ describeEmbeddedPostgres("account issuer migration", () => { const sql = postgres(database.connectionString, { max: 1 }); cleanups.push(async () => sql.end()); - // Rewind to the pre-upgrade shape: no issuer column, no unique index. - await sql`DELETE FROM "drizzle"."__drizzle_migrations" WHERE "hash" = ${await migrationHash()}`; + await sql`DROP INDEX IF EXISTS "account_provider_account_id_uq"`; await sql`DROP INDEX IF EXISTS ${sql(UNIQUE_INDEX)}`; await sql`ALTER TABLE "account" DROP COLUMN IF EXISTS "issuer"`; @@ -101,7 +86,7 @@ describeEmbeddedPostgres("account issuer migration", () => { (${googleAccountId}, 'google-subject-1', 'google', ${socialUserId}, NULL, now(), now()) `; - await applyPendingMigrations(database.connectionString); + await applySqlMigration(sql, MIGRATION_FILE); const rows = await sql<{ id: string; issuer: string }[]>` SELECT "id", "issuer" FROM "account" ORDER BY "id" @@ -133,5 +118,50 @@ describeEmbeddedPostgres("account issuer migration", () => { VALUES (${`account-${randomUUID()}`}, ${CREDENTIAL_ISSUER}, ${credentialUserId}, 'credential', ${credentialUserId}, now(), now()) `, ).rejects.toMatchObject({ code: "23505", constraint_name: UNIQUE_INDEX }); + + await applySqlMigration(sql, "0465_better_auth_account_identity.sql"); + const preserved = await sql`SELECT "id", "issuer" FROM "account" ORDER BY "id"`; + expect(preserved).toEqual(rows); + const [nullable] = await sql` + SELECT "is_nullable" FROM "information_schema"."columns" + WHERE "table_schema" = 'public' AND "table_name" = 'account' AND "column_name" = 'issuer' + `; + expect(nullable?.is_nullable).toBe("YES"); + await expect(sql` + INSERT INTO "account" ("id", "account_id", "provider_id", "user_id", "created_at", "updated_at") + VALUES (${randomUUID()}, ${credentialUserId}, 'credential', ${credentialUserId}, now(), now()) + `).rejects.toMatchObject({ code: "23505", constraint_name: "account_provider_account_id_uq" }); + await sql` + INSERT INTO "account" ("id", "account_id", "provider_id", "user_id", "created_at", "updated_at") + VALUES (${randomUUID()}, 'new-subject', 'credential', ${credentialUserId}, now(), now()) + `; + }, 30_000); +}); + +describeEmbeddedPostgres("provider identity migration with duplicate legacy keys", () => { + it("rejects ambiguous provider/account keys without changing legacy rows", async () => { + const database = await startEmbeddedPostgresTestDatabase("paperclip-account-duplicates-"); + const sql = postgres(database.connectionString, { max: 1 }); + try { + await sql`DROP INDEX "account_provider_account_id_uq"`; + await sql`ALTER TABLE "account" ALTER COLUMN "issuer" SET NOT NULL`; + await sql`CREATE UNIQUE INDEX "account_issuer_account_id_uq" ON "account" ("issuer", "account_id")`; + const userId = randomUUID(); + await sql`INSERT INTO "user" ("id", "name", "email", "created_at", "updated_at") + VALUES (${userId}, 'Legacy User', 'legacy@example.com', now(), now())`; + await sql`INSERT INTO "account" ("id", "issuer", "account_id", "provider_id", "user_id", "created_at", "updated_at") + VALUES (${randomUUID()}, 'issuer-a', 'same-subject', 'credential', ${userId}, now(), now()), + (${randomUUID()}, 'issuer-b', 'same-subject', 'credential', ${userId}, now(), now())`; + const before = await sql`SELECT "id", "issuer" FROM "account" ORDER BY "id"`; + await expect(applySqlMigration(sql, "0465_better_auth_account_identity.sql")) + .rejects.toMatchObject({ code: "23505" }); + expect(await sql`SELECT "id", "issuer" FROM "account" ORDER BY "id"`).toEqual(before); + const [column] = await sql`SELECT "is_nullable" FROM "information_schema"."columns" + WHERE "table_schema" = 'public' AND "table_name" = 'account' AND "column_name" = 'issuer'`; + expect(column?.is_nullable).toBe("NO"); + } finally { + await sql.end(); + await database.cleanup(); + } }, 30_000); }); diff --git a/packages/db/src/migrations/0465_better_auth_account_identity.sql b/packages/db/src/migrations/0465_better_auth_account_identity.sql new file mode 100644 index 00000000000..5145e3b237f --- /dev/null +++ b/packages/db/src/migrations/0465_better_auth_account_identity.sql @@ -0,0 +1,3 @@ +DROP INDEX "account_issuer_account_id_uq";--> statement-breakpoint +ALTER TABLE "account" ALTER COLUMN "issuer" DROP NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "account_provider_account_id_uq" ON "account" USING btree ("provider_id","account_id"); \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0460_snapshot.json b/packages/db/src/migrations/meta/0465_snapshot.json similarity index 99% rename from packages/db/src/migrations/meta/0460_snapshot.json rename to packages/db/src/migrations/meta/0465_snapshot.json index 38297d445c0..951e3c6deea 100644 --- a/packages/db/src/migrations/meta/0460_snapshot.json +++ b/packages/db/src/migrations/meta/0465_snapshot.json @@ -1,6 +1,6 @@ { - "id": "f93ccf18-7e86-4788-be77-b17430c86b2f", - "prevId": "e070677d-cfb8-4438-847c-29c4da7ca8c5", + "id": "38dbf2b9-da26-4c3a-b813-f37d0c19cba2", + "prevId": "e2a678da-05cc-4316-a40e-d85a69be01e3", "version": "7", "dialect": "postgresql", "tables": { @@ -6233,7 +6233,7 @@ "name": "issuer", "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, "account_id": { "name": "account_id", @@ -6309,11 +6309,11 @@ } }, "indexes": { - "account_issuer_account_id_uq": { - "name": "account_issuer_account_id_uq", + "account_provider_account_id_uq": { + "name": "account_provider_account_id_uq", "columns": [ { - "expression": "issuer", + "expression": "provider_id", "isExpression": false, "asc": true, "nulls": "last" @@ -43580,6 +43580,28 @@ "method": "btree", "with": {} }, + "issues_company_legacy_origin_run_idx": { + "name": "issues_company_legacy_origin_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"issues\".\"origin_issue_id\" is null and \"issues\".\"origin_run_id\" is not null", + "concurrently": true, + "method": "btree", + "with": {} + }, "issues_company_project_workspace_idx": { "name": "issues_company_project_workspace_idx", "columns": [ @@ -49085,6 +49107,93 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.plugin_lifecycle_resource_cursors": { + "name": "plugin_lifecycle_resource_cursors", + "schema": "", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "acknowledged_through_id": { + "name": "acknowledged_through_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "plugin_lifecycle_resource_cursors_plugin_id_plugins_id_fk": { + "name": "plugin_lifecycle_resource_cursors_plugin_id_plugins_id_fk", + "tableFrom": "plugin_lifecycle_resource_cursors", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_lifecycle_resource_cursors_company_id_companies_id_fk": { + "name": "plugin_lifecycle_resource_cursors_company_id_companies_id_fk", + "tableFrom": "plugin_lifecycle_resource_cursors", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_lifecycle_resource_cursors_pk": { + "name": "plugin_lifecycle_resource_cursors_pk", + "columns": [ + "plugin_id", + "company_id", + "resource_type", + "resource_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.plugin_logs": { "name": "plugin_logs", "schema": "", @@ -50949,6 +51058,12 @@ "primaryKey": false, "notNull": false }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "git_authorization_grant_id": { "name": "git_authorization_grant_id", "type": "uuid", @@ -51100,6 +51215,19 @@ "onDelete": "no action", "onUpdate": "no action" }, + "projects_environment_id_environments_id_fk": { + "name": "projects_environment_id_environments_id_fk", + "tableFrom": "projects", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, "projects_git_authorization_grant_id_connection_grants_id_fk": { "name": "projects_git_authorization_grant_id_connection_grants_id_fk", "tableFrom": "projects", @@ -59494,6 +59622,13 @@ "notNull": true, "default": "''" }, + "pending_reveal": { + "name": "pending_reveal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, "allowed_actions": { "name": "allowed_actions", "type": "jsonb", diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 1deaa2d7742..efa85154e34 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -3242,6 +3242,13 @@ "when": 1794346237220, "tag": "0464_project_environment", "breakpoints": true + }, + { + "idx": 465, + "version": "7", + "when": 1794400732201, + "tag": "0465_better_auth_account_identity", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/auth.ts b/packages/db/src/schema/auth.ts index 1e82b8a1256..4679accedbb 100644 --- a/packages/db/src/schema/auth.ts +++ b/packages/db/src/schema/auth.ts @@ -21,25 +21,11 @@ export const authSessions = pgTable("session", { userId: text("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }), }); -/** - * Better Auth's `account` model. Every column here has to exist for the - * Drizzle adapter to accept the table at all: the adapter validates the - * model's fields against the schema on each write and throws - * `The field "" does not exist in the "account" Drizzle schema` - * when one is missing, which surfaces as a 500 on sign-up. - * - * `issuer` is the account namespace Better Auth 1.7 added. It is - * `local:credential` for email/password accounts and - * `local:oauth:` for social accounts that do not declare an - * issuer of their own, and it pairs with `account_id` as the stable - * provider-side key — hence the unique index, which mirrors the - * `(issuer, accountId)` unique index Better Auth declares on the model. - */ export const authAccounts = pgTable( "account", { id: text("id").primaryKey(), - issuer: text("issuer").notNull(), + issuer: text("issuer"), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }), @@ -54,7 +40,7 @@ export const authAccounts = pgTable( updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(), }, (table) => ({ - issuerAccountIdUq: uniqueIndex("account_issuer_account_id_uq").on(table.issuer, table.accountId), + providerAccountIdUq: uniqueIndex("account_provider_account_id_uq").on(table.providerId, table.accountId), }), ); diff --git a/packages/paperclip-runner/src/backends/harness-driver-backend.ts b/packages/paperclip-runner/src/backends/harness-driver-backend.ts index dfaee9efff3..e6623853776 100644 --- a/packages/paperclip-runner/src/backends/harness-driver-backend.ts +++ b/packages/paperclip-runner/src/backends/harness-driver-backend.ts @@ -167,6 +167,7 @@ export class HarnessDriverBackend implements NativeSessionBackend { recovered: false, reason: recovered.reason, ...(recovered.failureClass ? { failureClass: recovered.failureClass } : {}), + ...(recovered.resumeStateUnavailable ? { resumeStateUnavailable: true as const } : {}), }; } try { diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts index 8ccefae294d..6886e845d0a 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -488,6 +488,7 @@ describe("native backend factory", () => { recovered: false, reason: expect.stringContaining("PRP provider resume state is unavailable"), failureClass: PROVIDER_STATE_INCOMPATIBLE, + resumeStateUnavailable: true, }); }); @@ -499,6 +500,7 @@ describe("native backend factory", () => { const recovered = await backend.recoverSession!({ ...snapshot, ...inFlight } as never, { signal: new AbortController().signal }); expect(recovered).toMatchObject({ recovered: false, reason: expect.stringContaining("PRP provider resume state is unavailable") }); expect(recovered).not.toHaveProperty("failureClass"); + expect(recovered).not.toHaveProperty("resumeStateUnavailable"); }); }); diff --git a/packages/paperclip-runner/src/contracts/control-plane-port.ts b/packages/paperclip-runner/src/contracts/control-plane-port.ts index b6e3aa685ea..6b91bfd4492 100644 --- a/packages/paperclip-runner/src/contracts/control-plane-port.ts +++ b/packages/paperclip-runner/src/contracts/control-plane-port.ts @@ -99,4 +99,11 @@ export interface ControlPlanePort { result: NativeRunResult | CompleteControlPlaneRunInput, options?: FinalizeControlPlaneOperationOptions, ): Promise; + /** + * Durable proof, across every runner instance, that this run has recorded no + * provider event, tool invocation, or accepted result. True only when the + * control plane can prove it; a port without this proof keeps every + * recovery refusal unchanged. + */ + provesNoRunProviderWork?(options: FinalizeControlPlaneOperationOptions): Promise; } diff --git a/packages/paperclip-runner/src/contracts/harness-driver.ts b/packages/paperclip-runner/src/contracts/harness-driver.ts index b8903443c8c..7a9396eac64 100644 --- a/packages/paperclip-runner/src/contracts/harness-driver.ts +++ b/packages/paperclip-runner/src/contracts/harness-driver.ts @@ -557,6 +557,10 @@ export interface HarnessSessionRecoveryResult { reason?: string; /** Closed runner class of a deterministic recovery failure. */ failureClass?: RunnerRecoveryFailureClass; + /** Set only with PROVIDER_STATE_INCOMPATIBLE: the Runner proved it holds no + * saved state for this session (NativeRunnerResumeStateUnavailableError) and + * the checkpoint had no turn or runtime request in flight. */ + resumeStateUnavailable?: true; } /** Reads the closed class of an authenticated runner command failure, or of a diff --git a/packages/paperclip-runner/src/contracts/native-session-backend.ts b/packages/paperclip-runner/src/contracts/native-session-backend.ts index 2931aa8b1e5..b8e9d775fec 100644 --- a/packages/paperclip-runner/src/contracts/native-session-backend.ts +++ b/packages/paperclip-runner/src/contracts/native-session-backend.ts @@ -86,6 +86,7 @@ export interface NativeSessionRecoveryResult { session?: NativeSession; reason?: string; failureClass?: HarnessSessionRecoveryResult["failureClass"]; + resumeStateUnavailable?: HarnessSessionRecoveryResult["resumeStateUnavailable"]; } /** Runner-owned cause: the process was restored, but its active turn was lost. */ diff --git a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts index 726196c3244..633616a30eb 100644 --- a/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts +++ b/packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts @@ -774,13 +774,15 @@ export class CodexAppServerDriver implements HarnessDriver { // resume it here. With nothing in flight, the runtime's recovery policy // decides, as for a moved workspace root. An in-flight session keeps the // unclassified failure. - const failureClass = runnerRecoveryFailureClass(error) - ?? (isRunnerResumeStateUnavailable(error) && !persistedSessionInFlight(snapshot) - ? PROVIDER_STATE_INCOMPATIBLE : undefined); + const classified = runnerRecoveryFailureClass(error); + const resumeStateUnavailable = !classified && isRunnerResumeStateUnavailable(error) + && !persistedSessionInFlight(snapshot); + const failureClass = classified ?? (resumeStateUnavailable ? PROVIDER_STATE_INCOMPATIBLE : undefined); return { recovered: false, reason: redactCodexDiagnostic(String(error)), ...(failureClass ? { failureClass } : {}), + ...(resumeStateUnavailable ? { resumeStateUnavailable: true as const } : {}), }; } finally { cancellation.detach(); diff --git a/packages/paperclip-runner/src/native-session-runtime.test.ts b/packages/paperclip-runner/src/native-session-runtime.test.ts index 0f30e117452..ba31e5da9b8 100644 --- a/packages/paperclip-runner/src/native-session-runtime.test.ts +++ b/packages/paperclip-runner/src/native-session-runtime.test.ts @@ -17,6 +17,7 @@ import type { NativeRunIdentity } from "./contracts/types.js"; import type { NativeSession, NativeSessionBackend, + NativeSessionRecoveryResult, PersistedNativeSession, } from "./contracts/native-session-backend.js"; import { @@ -6664,6 +6665,171 @@ describe("executeNativeSession recovery", () => { } }); + // Staging class P after #20757: a follow-up after a governed (approval) wait + // rebinds the previous run's session, but the new sandbox holds no saved + // Runner state for it. The follow-up has not started a turn, so a fresh + // provider session cannot repeat provider work or a tool side effect; only + // the earlier conversation context is lost. The run continues in a fresh + // session with the full task, the answer and the stored handoff. + describe("a governed-wait follow-up whose saved provider state is gone", () => { + const approval = { interactionId: "interaction-approval", kind: "request_confirmation" as const, + response: { outcome: "accepted", note: "APPROVED_BY_BOARD" } }; + const followUpInput = { + ...input, + task: { ...input.task, prompt: "FULL_TASK_CONTEXT\nAPPROVAL_FOLLOW_UP" }, + interactionResponses: [approval], + }; + const reboundCheckpoint = (): PersistedNativeSession => ({ + backendKind: "mock", driverKind: "codex_app_server", sessionId: "driver-lost", identity, + providerSessionId: "provider-lost", providerRecoveryPolicy: "allow_replacement_after_governed_wait", + cursor: null, semanticResult: null, terminal: null, activeTurnId: null, terminalTurns: [], + pendingRuntimeRequests: [], dispositionOnlyRecoveryConsumed: false, dispositionOnlyRecoveryTurnId: null, + lineage: [], + }); + const stateUnavailable: NativeSessionRecoveryResult = { + recovered: false, failureClass: PROVIDER_STATE_INCOMPATIBLE, resumeStateUnavailable: true, + reason: "Error: NativeRunnerResumeStateUnavailableError: PRP provider resume state is unavailable", + }; + function harness(options: { + checkpoint?: PersistedNativeSession; + recovery?: Awaited>>; + proof?: boolean | null | "rejects" | "hangs"; + replay?: PrpEvent[]; + } = {}) { + const startTurn = vi.fn(async () => ({ turnId: "turn-fresh" })); + const replacementSnapshot: PersistedNativeSession = { + ...reboundCheckpoint(), sessionId: "driver-fresh", providerSessionId: "provider-fresh", + }; + const replacementSession: NativeSession = { + identity: () => identity, + capabilities: async () => ({ resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }), + async *events() { yield runnerEvent(1, "turn.completed"); }, + startTurn, + result: async () => ({ result, terminal, turnId: "turn-fresh" }), + snapshot: async () => structuredClone(replacementSnapshot), + close: async () => {}, + }; + const openReplacementSession = vi.fn(async () => replacementSession); + const openSession = vi.fn(async () => { throw new Error("replacement seam must be used"); }); + const recoverSession = vi.fn(async () => options.recovery ?? stateUnavailable); + const backend: NativeSessionBackend = { + descriptor: async () => ({ kind: "mock", name: "governed-follow-up", version: "1", + capabilities: { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true } }), + openSession, openReplacementSession, recoverSession, + }; + const events: PrpEvent[] = []; + const provesNoRunProviderWork = vi.fn(async ({ signal }: { signal: AbortSignal }) => { + if (options.proof === "rejects") throw new Error("database unavailable"); + if (options.proof === "hangs") return await new Promise((_resolve, reject) => + signal.addEventListener("abort", () => reject(signal.reason), { once: true })); + return options.proof === true; + }); + const openRun = vi.fn(async () => undefined); + const replay = options.replay ?? []; + const controlPlane: ControlPlanePort = { + openRun, + loadSessionCheckpoint: async () => structuredClone(options.checkpoint ?? reboundCheckpoint()), + checkpointSession: async () => {}, + async appendEvent(event) { + events.push(structuredClone(event as PrpEvent)); + return { cursor: events.length, highestContiguousSourceSeq: highestContiguous(events), disposition: "committed" }; + }, + replayEvents: async ({ afterSourceSeq }) => { + const page = replay.filter(event => event.sourceSeq > afterSourceSeq); + return { events: page, highestContiguousSourceSeq: highestContiguous(page) }; + }, + completeRun: async () => {}, + ...(options.proof === null ? {} : { provesNoRunProviderWork }), + }; + const onContinuityBreak = vi.fn(async () => undefined); + const getFreshSessionHandoff = vi.fn(async () => "FRESH_HANDOFF: prior conversation summary"); + const run = () => executeNativeSession({ + input: followUpInput, backend, controlPlane, getFreshSessionHandoff, onContinuityBreak, + runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", + }); + return { run, startTurn, openReplacementSession, openSession, recoverSession, provesNoRunProviderWork, + openRun, onContinuityBreak, getFreshSessionHandoff }; + } + + it("continues in a fresh provider session with the task, the answer and the stored handoff", async () => { + const h = harness({ proof: true }); + await expect(h.run()).resolves.toMatchObject({ providerSessionId: "provider-fresh" }); + expect(h.recoverSession).toHaveBeenCalledOnce(); + expect(h.provesNoRunProviderWork).toHaveBeenCalledOnce(); + expect(h.openReplacementSession).toHaveBeenCalledOnce(); + expect(h.openSession).not.toHaveBeenCalled(); + expect(h.startTurn).toHaveBeenCalledOnce(); + const envelope = JSON.parse(h.startTurn.mock.calls[0]![0].message.text) as { + task: { prompt: string }; interactionResponses: unknown[]; + }; + expect(envelope.task.prompt).toBe("FRESH_HANDOFF: prior conversation summary\n\nFULL_TASK_CONTEXT\nAPPROVAL_FOLLOW_UP"); + expect(envelope.interactionResponses).toEqual([approval]); + expect(h.startTurn.mock.calls[0]![0]).not.toHaveProperty("continuation"); + // The user sees that the earlier conversation context was reset. + expect(h.onContinuityBreak).toHaveBeenCalledWith({ + reason: expect.stringContaining("saved provider state is unavailable, so the follow-up continues in a fresh provider session without the earlier conversation context"), + previousDriverSessionId: "driver-lost", previousProviderSessionId: "provider-lost", + replacementDriverSessionId: "driver-fresh", replacementProviderSessionId: "provider-fresh", + }); + }); + + it.each([ + ["the control plane cannot prove the run started no provider work", { proof: false }], + ["the control plane predates the provider-work proof", { proof: null }], + ["the provider-work proof fails", { proof: "rejects" }], + ["the saved state is incompatible for another reason", { proof: true, recovery: { + recovered: false as const, failureClass: PROVIDER_STATE_INCOMPATIBLE, reason: "saved launch profile changed" } }], + ["the checkpoint requires the same session", { proof: true, + checkpoint: { ...reboundCheckpoint(), providerRecoveryPolicy: "same_session_only" as const } }], + ["this run already completed a turn", { proof: true, + checkpoint: { ...reboundCheckpoint(), cursor: "2", terminalTurns: [{ turnId: "turn-own", fingerprint: "completed" }] } }], + ["this run already advanced the durable cursor", { proof: true, checkpoint: { ...reboundCheckpoint(), cursor: "3" } }], + ["this run already recorded a provider event", { proof: true, replay: [runnerEvent(1, "turn.started")] }], + ["this run already proposed a result", { proof: true, checkpoint: { ...reboundCheckpoint(), semanticResult: result } }], + ["the checkpoint was saved without the at-most-once recovery marker", { proof: true, + checkpoint: (() => { const { dispositionOnlyRecoveryConsumed: _omitted, ...older } = reboundCheckpoint(); return older; })() }], + ] as const)("keeps the named refusal and a working Retry when %s", async (_label, options) => { + const h = harness(options as Parameters[0]); + const failure = await h.run().catch((error: unknown) => error); + expect(failure).toBeInstanceOf(NativeProviderStateIncompatibleError); + expect((failure as NativeProviderStateIncompatibleError).code).toBe("native_provider_state_incompatible"); + expect(h.openReplacementSession).not.toHaveBeenCalled(); + expect(h.openSession).not.toHaveBeenCalled(); + expect(h.openRun).not.toHaveBeenCalled(); + expect(h.startTurn).not.toHaveBeenCalled(); + }); + + it("never replaces a session whose checkpoint records this run's own governed wait", async () => { + const h = harness({ proof: true, checkpoint: { ...reboundCheckpoint(), + governedWait: { sourceEvent: runnerEvent(1, "turn.started"), result: yieldedResult } } }); + await expect(h.run()).rejects.toThrow(); + expect(h.openReplacementSession).not.toHaveBeenCalled(); + expect(h.startTurn).not.toHaveBeenCalled(); + }); + + it("keeps the named refusal when the provider-work proof does not answer in time", async () => { + vi.useFakeTimers(); + try { + const h = harness({ proof: "hangs" }); + const failure = h.run().catch((error: unknown) => error); + await vi.advanceTimersByTimeAsync(30_001); + expect(await failure).toBeInstanceOf(NativeProviderStateIncompatibleError); + expect(h.provesNoRunProviderWork).toHaveBeenCalledOnce(); + expect(h.openReplacementSession).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + + it("stops by name when the follow-up's saved session still owns a runtime request", async () => { + const h = harness({ proof: true, recovery: { recovered: false as const, failureClass: PROVIDER_SIDE_EFFECT_PENDING, + reason: "native_runner_source_runtime_reconciliation_required" } }); + await expect(h.run()).rejects.toBeInstanceOf(NativeProviderSideEffectPendingError); + expect(h.provesNoRunProviderWork).not.toHaveBeenCalled(); + expect(h.openReplacementSession).not.toHaveBeenCalled(); + }); + }); + // A remote workspace epoch was abandoned, so the run was assigned a new // sibling root while its checkpoint still names the old one. A checkpoint // that requires the same session (an unfinished goal) ends with the diff --git a/packages/paperclip-runner/src/native-session-runtime.ts b/packages/paperclip-runner/src/native-session-runtime.ts index 7b9d0971362..1d480fb524f 100644 --- a/packages/paperclip-runner/src/native-session-runtime.ts +++ b/packages/paperclip-runner/src/native-session-runtime.ts @@ -18,7 +18,7 @@ import type { NativeSession, NativeSessionBackend, } from "./contracts/native-session-backend.js"; -import type { PersistedNativeSession } from "./contracts/native-session-backend.js"; +import type { NativeSessionRecoveryResult, PersistedNativeSession } from "./contracts/native-session-backend.js"; import { PROVIDER_ATTACH_REFUSED, PROVIDER_SIDE_EFFECT_PENDING, @@ -1917,6 +1917,61 @@ function checkpointCursor(cursor: string | null | undefined): number { return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0; } +/** + * A follow-up after a governed wait normally requires the same provider + * session. When the Runner proved the saved session state is gone, and nothing + * proves this run started provider work of its own, a fresh session cannot + * repeat a turn or a tool side effect: only the earlier conversation context is + * lost. Every other case keeps the named refusal. The reconciled checkpoint must + * be the untouched rebind of the previous run (no cursor progress, turn, + * runtime request, result, terminal or governed wait of this run), and the + * control plane must prove durably, across runner instances, that the run has + * no provider event, tool invocation or accepted result. A control plane + * without that proof, or a checkpoint missing those fields, keeps the refusal. + */ +const GOVERNED_WAIT_PROVIDER_WORK_PROOF_TIMEOUT_MS = 30_000; + +async function governedWaitFollowUpMayStartFresh(input: { + input: NativeExecutionInput; + checkpoint: PersistedNativeSession; + recovery: NativeSessionRecoveryResult; + restartInterruption: boolean; + controlPlane: ControlPlanePort; + timeoutMs: number; +}): Promise { + const { checkpoint, recovery } = input; + if ( + checkpoint.providerRecoveryPolicy !== "allow_replacement_after_governed_wait" || + ("homeMode" in input.input && input.input.homeMode === "preconfigured") || + recovery.failureClass !== PROVIDER_STATE_INCOMPATIBLE || + recovery.resumeStateUnavailable !== true || + input.restartInterruption || + checkpoint.identity.runId !== input.input.binding.runId || + !(checkpoint.cursor === null || checkpoint.cursor === "0") || + checkpoint.activeTurnId !== null || + !Array.isArray(checkpoint.terminalTurns) || checkpoint.terminalTurns.length > 0 || + !Array.isArray(checkpoint.pendingRuntimeRequests) || checkpoint.pendingRuntimeRequests.length > 0 || + checkpoint.semanticResult !== null || + checkpoint.terminal !== null || + checkpoint.governedWait !== undefined || + checkpoint.dispositionOnlyRecoveryConsumed !== false || + !input.controlPlane.provesNoRunProviderWork + ) return false; + const controlPlane = input.controlPlane; + // A proof that fails or expires is no proof: keep the named refusal and its + // Retry rather than surfacing a generic interruption. + const timeoutMs = Math.min(input.timeoutMs, GOVERNED_WAIT_PROVIDER_WORK_PROOF_TIMEOUT_MS); + try { + return await runAbortableOperationWithin({ + timeoutMs, + timeoutMessage: `native session provider-work proof timed out after ${timeoutMs}ms`, + operation: (signal) => controlPlane.provesNoRunProviderWork!({ signal }), + }) === true; + } catch { + return false; + } +} + async function reconcileRecoveryCheckpoint(input: { controlPlane: ControlPlanePort; checkpoint: PersistedNativeSession; @@ -2520,7 +2575,15 @@ export async function executeNativeSession( if (recovery.failureClass === PROVIDER_ATTACH_REFUSED) { throw new NativeProviderStateIncompatibleError(recovery.reason ?? "unknown"); } - if (!replacementAllowed) { + const governedWaitFollowUpLostState = !replacementAllowed && await governedWaitFollowUpMayStartFresh({ + input, + checkpoint: providerRecoveryCheckpoint, + recovery, + restartInterruption: Boolean(checkpointedInterruption), + controlPlane: options.controlPlane, + timeoutMs: recoveryTimeoutMs, + }); + if (!replacementAllowed && !governedWaitFollowUpLostState) { if (recovery.failureClass === PROVIDER_STATE_INCOMPATIBLE) { throw new NativeProviderStateIncompatibleError(recovery.reason ?? "unknown"); } @@ -2529,7 +2592,9 @@ export async function executeNativeSession( ); } continuityBreak = { - reason: recovery.reason ?? "provider session is no longer recoverable", + reason: governedWaitFollowUpLostState + ? `saved provider state is unavailable, so the follow-up continues in a fresh provider session without the earlier conversation context (${recovery.reason ?? "unknown"})` + : recovery.reason ?? "provider session is no longer recoverable", previousDriverSessionId: providerRecoveryCheckpoint.sessionId, previousProviderSessionId: providerRecoveryCheckpoint.providerSessionId ?? null, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d825f9785f..d24edac1c10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1015,7 +1015,7 @@ importers: version: 2.2.0(@grpc/grpc-js@1.14.5)(nice-grpc-common@2.0.4)(nice-grpc@2.1.17) '@vercel/connect': specifier: 2.3.3 - version: 2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))) + version: 2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))) acorn: specifier: 8.18.0 version: 8.18.0 @@ -1032,8 +1032,8 @@ importers: specifier: ^3.0.1 version: 3.0.1(ajv@8.20.0) better-auth: - specifier: 1.7.2 - version: 1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) + specifier: 1.7.16 + version: 1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) chat: specifier: 4.39.0 version: 4.39.0(ai@7.0.130(zod@4.6.7))(zod@4.6.7) @@ -1804,64 +1804,61 @@ packages: '@types/react': optional: true - '@better-auth/core@1.7.2': - resolution: {integrity: sha512-j0nM4ygsWbF/fcYRoKtDn8gn8uLXkmC+075HqSqsJEAV828cJR9bvYBCUQ1zmxNyRBk6Iz/qXsA0Zm2oksiOTg==} + '@better-auth/core@1.7.16': + resolution: {integrity: sha512-dwVAtEAEpjPsCV/TqOKRHreWyuiqvja+TnTTAuVbi3P0afM0+b1+ga+RTXNjVE86Q4HRwRg45Ek3txFrY8/1DA==} peerDependencies: - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - '@cloudflare/workers-types': '>=4' + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@better-fetch/fetch': 1.3.3 '@opentelemetry/api': ^1.9.0 - better-call: 1.4.0 + better-call: 1.4.5 jose: ^6.1.0 kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true '@opentelemetry/api': optional: true - '@better-auth/drizzle-adapter@1.7.2': - resolution: {integrity: sha512-A5wE10PIv3aS5LGePecEHntQylKy6OOF17B4dqlE0DwJeqU/IOBSd7/LZhMop9cNJ3WFjKMpazVSf91yYM/NFg==} + '@better-auth/drizzle-adapter@1.7.16': + resolution: {integrity: sha512-8cgtcWkCmJal+ecrl681Y+SKvMDy5Vg2ydCOPE7X8Mcx1Ud7Mqve3pASG0I2JWMJtrge1m8YbpNmCxhVGh+gpQ==} peerDependencies: - '@better-auth/core': ^1.7.2 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.16 + '@better-auth/utils': ^0.4.2 || ^0.5.0 drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.7.2': - resolution: {integrity: sha512-LYdSRLOvZiF+6S0UThu+wE/Qxsq9P2jQs7ZKkY6BIBJqUjYyxVDmi8HFcantBvWWW1/BeQCSsD7YVDG4gICMIQ==} + '@better-auth/kysely-adapter@1.7.16': + resolution: {integrity: sha512-MkLZwHxhYbVr1QGGDJDZXMT1g54Q5yucDMdLGaz3Y5P8ERRfaqVEe+3MCVCb8XxJ7Rh+VNJKUix4VOBq7vXimg==} peerDependencies: - '@better-auth/core': ^1.7.2 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.16 + '@better-auth/utils': ^0.4.2 || ^0.5.0 kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.7.2': - resolution: {integrity: sha512-0q1SXMzm5esH9L0xVuM6IxCk59E4G+3HySX4My9gvEwqtmUobykn+iuc/si3Y4xwUO7JODqQ5o+/pPcLDDMIrA==} + '@better-auth/memory-adapter@1.7.16': + resolution: {integrity: sha512-b0uUE3mhQJDu2QqLDN3XVHhXUhpr6zK+UhjL7IdNteTJJaAMhCcXfpLc7+kpOcNVEEBdRiFXBwsEtxRjcB3rLQ==} peerDependencies: - '@better-auth/core': ^1.7.2 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.16 + '@better-auth/utils': ^0.4.2 || ^0.5.0 - '@better-auth/mongo-adapter@1.7.2': - resolution: {integrity: sha512-4879SmUWHUs0OYlvHoCFbycZ7i1bqytkcgAUdt9RLQMvZ5H3LRMTgax2YVlGZEXgwNjY/X7xAoXOecWLhlQWeA==} + '@better-auth/mongo-adapter@1.7.16': + resolution: {integrity: sha512-uZG9DhCHv7QcF8HtEIgYEL7Lawu0/1Pcp6ABe4ImYQdLV3HNRaAuQU/PxVxFQFAMSpnCVi4Rwg8+Qk8qVn2YMg==} peerDependencies: - '@better-auth/core': ^1.7.2 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.16 + '@better-auth/utils': ^0.4.2 || ^0.5.0 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/prisma-adapter@1.7.2': - resolution: {integrity: sha512-mXTr/83WrNWLrvzIjtgDgdu9iXhOcSG1+qBQOAKlbGSFiOB+z4IMRneQ2wmMOiB8mKY9qGkClVUjKRFXqtHnFQ==} + '@better-auth/prisma-adapter@1.7.16': + resolution: {integrity: sha512-rSVMa4sWxvEURqVR+o4SZvq6dosCvD58nLw2LOw7woz791/CKOMxhrqrjiHLLzx+OfzGO0Ak9x7lrFTUwYO/KQ==} peerDependencies: - '@better-auth/core': ^1.7.2 - '@better-auth/utils': 0.4.2 + '@better-auth/core': ^1.7.16 + '@better-auth/utils': ^0.4.2 || ^0.5.0 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: @@ -1870,21 +1867,18 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.7.2': - resolution: {integrity: sha512-LcWu+O0zrxYDQj8E36vfkJwGPW4k9ZDA/rCo0zST6ihzL+juR7pBowoZIM9E6tK0Vit52mf6412bGT4XM4eTjQ==} + '@better-auth/telemetry@1.7.16': + resolution: {integrity: sha512-mwQC0xB8KO636CjBxMZCNgglaIwh18DOemBJyNCy7DBOngkjPdNggPWYB8BpJ8wMqcn3SaKchdWZrNNqLRZV4Q==} peerDependencies: - '@better-auth/core': ^1.7.2 - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-auth/core': ^1.7.16 + '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@better-fetch/fetch': 1.3.3 - '@better-auth/utils@0.4.2': - resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} + '@better-auth/utils@0.5.1': + resolution: {integrity: sha512-WETqw5vQ52uQ/xwWR4E8rTgT6fDEr/wnS+otDNkBCa/69281FCF32OKDdQUcahMCnjsvqXBNiEvAQHFB0EVRRg==} - '@better-auth/utils@0.5.0': - resolution: {integrity: sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==} - - '@better-fetch/fetch@1.3.1': - resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@better-fetch/fetch@1.3.3': + resolution: {integrity: sha512-U6SlDt/GUWtK2JAxS/NhdoYTEim+EL1G5byz2TUj83KjLZBUpYzb5SmX0aXaCnbGkAjFKnV/o/0+cyXMbEcqqg==} '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -6058,15 +6052,14 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - better-auth@1.7.2: - resolution: {integrity: sha512-gKapKBEvYIGcMxi74RjQ7EbFLiqyQt58vdoJmL1qAlWSkY1Bc2Vqshl524/3u1NxauiOU03M/Ebh762Brmac9A==} + better-auth@1.7.16: + resolution: {integrity: sha512-ur6xj8jeUC9lcH8ni9LrwB4FiFMPDTWFJmb89yZ2SHsFSUobYgGMFsMq6aOorlVtqRr9ckTemRunCR6vzsO65Q==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 - '@sveltejs/kit': ^2.0.0 + '@sveltejs/kit': ^2.0.0 || ^3.0.0 '@tanstack/react-start': ^1.0.0 '@tanstack/solid-start': ^1.0.0 - better-sqlite3: ^12.0.0 drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 mongodb: ^6.0.0 || ^7.0.0 @@ -6078,7 +6071,7 @@ packages: react-dom: ^19.2.8 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 - vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': @@ -6091,8 +6084,6 @@ packages: optional: true '@tanstack/solid-start': optional: true - better-sqlite3: - optional: true drizzle-kit: optional: true drizzle-orm: @@ -6120,8 +6111,8 @@ packages: vue: optional: true - better-call@1.4.0: - resolution: {integrity: sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==} + better-call@1.4.5: + resolution: {integrity: sha512-GVOWfn3pjEfUXn6GrfGf2h7tPAqj1oMLFBWBEswJrSkDKcLk6TIcjV0zexibcZZ0c5N1mkLMsPFopXWzWj/gDQ==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -7301,9 +7292,6 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.2.10: - resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} - jose@6.2.12: resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} @@ -9938,64 +9926,59 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - '@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3)': + '@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)': dependencies: - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - '@opentelemetry/semantic-conventions': 1.43.0 + '@better-auth/utils': 0.5.1 + '@better-fetch/fetch': 1.3.3 '@standard-schema/spec': 1.1.0 - better-call: 1.4.0(zod@4.6.7) - jose: 6.2.10 + better-call: 1.4.5(zod@4.6.7) + jose: 6.2.12 kysely: 0.29.5 nanostores: 1.5.3 zod: 4.6.7 optionalDependencies: '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))': + '@better-auth/drizzle-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) + '@better-auth/utils': 0.5.1 optionalDependencies: drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)) - '@better-auth/kysely-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(kysely@0.29.5)': + '@better-auth/kysely-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(kysely@0.29.5)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) + '@better-auth/utils': 0.5.1 optionalDependencies: kysely: 0.29.5 - '@better-auth/memory-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)': + '@better-auth/memory-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) + '@better-auth/utils': 0.5.1 - '@better-auth/mongo-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)': + '@better-auth/mongo-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) + '@better-auth/utils': 0.5.1 - '@better-auth/prisma-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)': + '@better-auth/prisma-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) - '@better-auth/utils': 0.4.2 + '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) + '@better-auth/utils': 0.5.1 - '@better-auth/telemetry@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + '@better-auth/telemetry@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)': dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) + '@better-auth/utils': 0.5.1 + '@better-fetch/fetch': 1.3.3 - '@better-auth/utils@0.4.2': + '@better-auth/utils@0.5.1': dependencies: '@noble/hashes': 2.4.0 - '@better-auth/utils@0.5.0': - dependencies: - '@noble/hashes': 2.4.0 - - '@better-fetch/fetch@1.3.1': {} + '@better-fetch/fetch@1.3.3': {} '@braintree/sanitize-url@7.1.2': {} @@ -13803,14 +13786,14 @@ snapshots: dependencies: execa: 5.1.1 - '@vercel/connect@2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))))': + '@vercel/connect@2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))))': dependencies: '@vercel/oidc': 3.8.9 optionalDependencies: '@chat-adapter/slack': 4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7) '@modelcontextprotocol/sdk': 1.31.0(zod@4.6.7) ai: 7.0.130(zod@4.6.7) - better-auth: 1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) + better-auth: 1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) '@vercel/oidc@3.2.0': {} @@ -14079,22 +14062,22 @@ snapshots: before-after-hook@4.0.0: {} - better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))): + better-auth@1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))): dependencies: - '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) - '@better-auth/drizzle-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda))) - '@better-auth/kysely-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(kysely@0.29.5) - '@better-auth/memory-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2) - '@better-auth/mongo-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2) - '@better-auth/prisma-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2) - '@better-auth/telemetry': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) - '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) + '@better-auth/drizzle-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda))) + '@better-auth/kysely-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(kysely@0.29.5) + '@better-auth/memory-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1) + '@better-auth/mongo-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1) + '@better-auth/prisma-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1) + '@better-auth/telemetry': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3) + '@better-auth/utils': 0.5.1 + '@better-fetch/fetch': 1.3.3 '@noble/ciphers': 2.4.0 '@noble/hashes': 2.4.0 - better-call: 1.4.0(zod@4.6.7) + better-call: 1.4.5(zod@4.6.7) defu: 6.1.7 - jose: 6.2.10 + jose: 6.2.12 kysely: 0.29.5 nanostores: 1.5.3 zod: 4.6.7 @@ -14106,13 +14089,12 @@ snapshots: react-dom: 19.2.8(react@19.2.8) vitest: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) transitivePeerDependencies: - - '@cloudflare/workers-types' - '@opentelemetry/api' - better-call@1.4.0(zod@4.6.7): + better-call@1.4.5(zod@4.6.7): dependencies: - '@better-auth/utils': 0.5.0 - '@better-fetch/fetch': 1.3.1 + '@better-auth/utils': 0.5.1 + '@better-fetch/fetch': 1.3.3 rou3: 0.9.2 set-cookie-parser: 3.1.2 optionalDependencies: @@ -15327,8 +15309,6 @@ snapshots: jose@5.10.0: {} - jose@6.2.10: {} - jose@6.2.12: {} joycon@3.1.1: {} diff --git a/server/package.json b/server/package.json index f6d4ab6491c..24877a9bd28 100644 --- a/server/package.json +++ b/server/package.json @@ -90,7 +90,7 @@ "ai": "7.0.130", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", - "better-auth": "1.7.2", + "better-auth": "1.7.16", "chat": "4.39.0", "chokidar": "^5.0.0", "compression": "^1.8.2", diff --git a/server/scripts/typecheck-layers.mjs b/server/scripts/typecheck-layers.mjs index 7791e3671f6..ea775d40e67 100644 --- a/server/scripts/typecheck-layers.mjs +++ b/server/scripts/typecheck-layers.mjs @@ -288,7 +288,12 @@ export function typecheckLayers({ const config = path.join(OUTPUT_DIRECTORY, `tsconfig.${layer.name}.json`); const { status, signal } = run(["-p", config], { cwd: serverRoot }); // A killed process leaves no declarations, so later layers would only add noise. - if (signal) return status || 1; + if (signal) { + console.error( + `typecheck:layers: ${layer.name}: compiler terminated by ${signal} (tsc -p ${config}, cwd: ${serverRoot})`, + ); + return status || 1; + } // Later layers still run after type errors. Each file is checked in one // layer, so this reports everything a single tsc run would. if (status !== 0) exitCode = status; diff --git a/server/scripts/typecheck-layers.test.mjs b/server/scripts/typecheck-layers.test.mjs index 97ee651b6ce..8239fea0c4d 100644 --- a/server/scripts/typecheck-layers.test.mjs +++ b/server/scripts/typecheck-layers.test.mjs @@ -319,6 +319,38 @@ describe("typecheckLayers", () => { expect(status).toBe(1); expect(configs).toHaveLength(1); }); + + it.each([ + ["SIGTERM", "below"], + ["SIGKILL", "cycle"], + ])("reports %s and the affected %s layer from a real compiler child", (signal, layer) => { + const root = serverFixture(); + const bin = path.join(root, "node_modules", "typescript", "bin"); + mkdirSync(bin, { recursive: true }); + writeFileSync(path.join(bin, "tsc"), ` + const fs = require("node:fs"); + fs.writeSync(1, "compiler stdout\\n"); + fs.writeSync(2, "compiler stderr\\n"); + if (process.argv[3].endsWith("tsconfig.${layer}.json")) { + process.kill(process.pid, ${JSON.stringify(signal)}); + } + `); + const wrapper = new URL("./typecheck-layers.mjs", import.meta.url).href; + + const result = spawnSync(process.execPath, ["--input-type=module", "-e", ` + import { typecheckLayers } from ${JSON.stringify(wrapper)}; + process.exitCode = typecheckLayers({ serverRoot: ${JSON.stringify(root)} }); + `], { encoding: "utf8", timeout: 10_000 }); + + expect(result.error).toBeUndefined(); + expect(result.signal).toBeNull(); + expect(result.status).toBe(1); + expect(result.stdout).toContain("compiler stdout"); + expect(result.stderr).toContain("compiler stderr"); + expect(result.stderr).toContain(`typecheck:layers: ${layer}: compiler terminated by ${signal}`); + expect(result.stderr).toContain(`tsconfig.${layer}.json`); + expect(result.stdout).not.toContain("typecheck:layers: above"); + }); }); describe("typecheckLayers with the real compiler", () => { diff --git a/server/src/__tests__/better-auth-credential-signup.integration.test.ts b/server/src/__tests__/better-auth-credential-signup.integration.test.ts index aaf46716561..f4fd9643e71 100644 --- a/server/src/__tests__/better-auth-credential-signup.integration.test.ts +++ b/server/src/__tests__/better-auth-credential-signup.integration.test.ts @@ -28,11 +28,6 @@ const ORIGIN = "http://127.0.0.1:41999"; const EMAIL = "founder@example.com"; const PASSWORD = "correct-horse-battery-staple"; -// The issuer Better Auth stamps on an email/password account: -// `createLocalAccountIssuer("credential")`. Sign-in matches on it, so a row -// written with anything else is a row nobody can sign in as. -const CREDENTIAL_ISSUER = "local:credential"; - const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; @@ -107,7 +102,7 @@ describeEmbeddedPostgres("Better Auth credential sign-up against the real schema expect(accounts).toHaveLength(1); expect(accounts[0]).toMatchObject({ providerId: "credential", - issuer: CREDENTIAL_ISSUER, + issuer: null, }); expect(accounts[0]?.password).toBeTruthy(); @@ -119,5 +114,17 @@ describeEmbeddedPostgres("Better Auth credential sign-up against the real schema expect(signIn.status).toBe(200); expect(signIn.body?.user?.email).toBe(EMAIL); expect(sessionCookies(signIn).length).toBeGreaterThan(0); + + await db.update(authAccounts).set({ issuer: "local:credential" }); + const legacySignIn = await request(app) + .post("/api/auth/sign-in/email") + .set("origin", ORIGIN) + .send({ email: EMAIL, password: PASSWORD }); + expect(legacySignIn.status).toBe(200); + expect(legacySignIn.body?.user?.email).toBe(EMAIL); + + await expect(db.insert(authAccounts).values({ ...accounts[0]!, id: "duplicate-credential" })) + .rejects.toMatchObject({ cause: { code: "23505", constraint_name: "account_provider_account_id_uq" } }); + await db.insert(authAccounts).values({ ...accounts[0]!, id: "other-provider", providerId: "other" }); }); }); diff --git a/server/src/__tests__/chat-channels.integration.test.ts b/server/src/__tests__/chat-channels.integration.test.ts index 909f9bd60a8..edee564c8b5 100644 --- a/server/src/__tests__/chat-channels.integration.test.ts +++ b/server/src/__tests__/chat-channels.integration.test.ts @@ -11666,7 +11666,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { await expect(resolveSlackTaskAuthority(db, binding)).resolves.toMatchObject({ slackUserId: "UBOARD" }); }); - it.each([false, true])("applies Slack mention settings to channel replies while exempting DMs (DM=%s)", async (isDM) => { + it.each([false, true])("allows Slack thread replies without mentions while exempting DMs (DM=%s)", async (isDM) => { const fixture = await seedCompany(); const { callbacks, endpoint, service, wakeup } = await configuredSlackEndpoint(fixture); expect((await service.get(endpoint.id)).requireAtMention).toBe(false); @@ -11680,29 +11680,23 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { expect(wakeup).toHaveBeenCalledTimes(2); expect((await service.update(endpoint.id, { requireAtMention: true })).requireAtMention).toBe(true); await send("7100.3", false, "subscribed_message"); - expect(wakeup).toHaveBeenCalledTimes(isDM ? 3 : 2); + expect(wakeup).toHaveBeenCalledTimes(3); const [filtered] = await db.select().from(chatDeliveries).where(and( eq(chatDeliveries.endpointId, endpoint.id), eq(chatDeliveries.state, "filtered"))); - if (isDM) { - expect(filtered).toBeUndefined(); - } else { - expect(filtered.redactedError).toBe("Message did not @mention the bot"); - expect(filtered.normalizedEvent).toMatchObject({ filtering: { contentRetained: false } }); - expect(filtered.normalizedEvent).not.toHaveProperty("message.text"); - } + expect(filtered).toBeUndefined(); // Both Slack callback kinds can carry an explicit mention. Retries cannot // produce a second wakeup for the same provider message. await send("7100.4", true); await send("7100.4", true, "mention"); - expect(wakeup).toHaveBeenCalledTimes(isDM ? 4 : 3); + expect(wakeup).toHaveBeenCalledTimes(4); const conversations = await readChatConversationsByEndpointId(endpoint.id); expect(conversations).toHaveLength(1); const comments = await readIssueCommentsByIssueId(conversations[0].issueId); - expect(comments).toHaveLength(isDM ? 4 : 3); + expect(comments).toHaveLength(4); expect(conversations[0].externalThreadId).toBe(thread.thread.id); await service.update(endpoint.id, { requireAtMention: false }); await send("7100.5", false); - expect(wakeup).toHaveBeenCalledTimes(isDM ? 5 : 4); + expect(wakeup).toHaveBeenCalledTimes(5); if (isDM) { await service.update(endpoint.id, { requireAtMention: true, allowDirectMessages: false }); await send("7100.6", false); @@ -11710,7 +11704,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { } }); - it.each([false, true])("rechecks queued Slack channel mentions but exempts DMs before drain (DM=%s)", async (isDM) => { + it.each([ + { isDM: false, isReply: false }, + { isDM: false, isReply: true }, + { isDM: true, isReply: false }, + ])("rechecks queued Slack channel roots but exempts replies and DMs (DM=$isDM, reply=$isReply)", async ({ isDM, isReply }) => { const fixture = await seedCompany(); const { callbacks, endpoint, service, wakeup } = await configuredSlackEndpoint(fixture, { deferWebhookProcessing: true, scheduleDeferredWork: () => undefined, @@ -11718,14 +11716,28 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => { const channelId = isDM ? "D-QUEUED-MENTION" : "C-QUEUED-MENTION"; const thread = makeThread({ channelId, id: `slack:${channelId}:${isDM ? "" : "7300.1"}`, isDM }); await deliverMessage({ callbacks, endpointId: endpoint.id, thread: thread.thread, - message: makeMessage({ id: "7300.1", text: "Start work" }), trigger: isDM ? "direct_message" : "subscribed_message" }); + message: makeMessage({ id: "7300.1", text: "@maya start", mentioned: true }), trigger: isDM ? "direct_message" : "mention" }); + const [starter] = await readChatDeliveriesByEndpointId(endpoint.id); + await db.update(chatDeliveries).set({ nextAttemptAt: new Date(0) }).where(eq(chatDeliveries.id, starter.id)); + await service.processPendingDeliveries(25, starter.id); + expect(wakeup).toHaveBeenCalledTimes(1); + const nextThread = isReply || isDM ? thread.thread : makeThread({ channelId, id: `slack:${channelId}:7300.2` }).thread; + await deliverMessage({ callbacks, endpointId: endpoint.id, thread: nextThread, + message: makeMessage({ id: "7300.2", text: "Continue work" }), trigger: isDM ? "direct_message" : "subscribed_message" }); await service.update(endpoint.id, { requireAtMention: true }); - const [delivery] = await readChatDeliveriesByEndpointId(endpoint.id); + const delivery = (await readChatDeliveriesByEndpointId(endpoint.id)).find(row => row.id !== starter.id)!; await db.update(chatDeliveries).set({ nextAttemptAt: new Date(0) }).where(eq(chatDeliveries.id, delivery.id)); await service.processPendingDeliveries(25, delivery.id); - expect(wakeup).toHaveBeenCalledTimes(isDM ? 1 : 0); - expect(await readChatConversationsByEndpointId(endpoint.id)).toHaveLength(isDM ? 1 : 0); - expect((await readChatDeliveriesById(delivery.id))[0].state).toBe(isDM ? "processed" : "filtered"); + const allowed = isDM || isReply; + expect(wakeup).toHaveBeenCalledTimes(allowed ? 2 : 1); + expect(await readChatConversationsByEndpointId(endpoint.id)).toHaveLength(1); + const [result] = await readChatDeliveriesById(delivery.id); + expect(result.state).toBe(allowed ? "processed" : "filtered"); + if (!allowed) { + expect(result.redactedError).toBe("Message did not @mention the bot"); + expect(result.normalizedEvent).toMatchObject({ filtering: { contentRetained: false } }); + expect(result.normalizedEvent).not.toHaveProperty("message.text"); + } }); it("snapshots channel workspace defaults on new tasks and preserves them on replies", async () => { diff --git a/server/src/__tests__/durable-chat-wakeup.test.ts b/server/src/__tests__/durable-chat-wakeup.test.ts index ceb16cec24d..6168d8eee74 100644 --- a/server/src/__tests__/durable-chat-wakeup.test.ts +++ b/server/src/__tests__/durable-chat-wakeup.test.ts @@ -197,11 +197,11 @@ describe("durable inbound chat scheduler receipts", () => { } it.each([ - ...["unmentioned", "mentioned", "coalesced", "recovery", "deferred"].map((scenario) => ({ scenario, isDM: false })), + ...["unmentioned", "mentioned", "reply", "coalesced_reply", "coalesced", "recovery", "deferred"].map((scenario) => ({ scenario, isDM: false })), ...["unmentioned", "coalesced"].map((scenario) => ({ scenario, isDM: true })), ])("checks Slack channel mentions after a real heartbeat queue wait ($scenario, DM=$isDM)", async ({ scenario, isDM }) => { const mentioned = scenario === "mentioned" || scenario === "coalesced"; - const allowed = isDM || scenario === "mentioned" || scenario === "recovery"; + const allowed = isDM || scenario === "mentioned" || scenario === "recovery" || scenario === "reply" || scenario === "coalesced_reply"; const f = await fixture(); await db.update(agents).set({ adapterType: "durable_chat_retry_test" }).where(eq(agents.id, f.agentId)); const applicationId = randomUUID(), connectionId = randomUUID(), endpointId = randomUUID(); @@ -215,7 +215,7 @@ describe("durable inbound chat scheduler receipts", () => { authorUserId: "board-user", body: mentioned ? "@bot work" : "Work" }); const [delivery] = await db.insert(chatDeliveries).values({ companyId: f.companyId, endpointId, providerEventId: randomUUID(), deduplicationKey: randomUUID(), eventKind: isDM ? "direct_message" : "message", state: "processed", - normalizedEvent: { trigger: isDM ? "direct_message" : "subscribed_message", conversation: { isDirectMessage: isDM }, message: { mentionedBot: mentioned } } }).returning(); + normalizedEvent: { trigger: isDM ? "direct_message" : "subscribed_message", conversation: { isDirectMessage: isDM, externalThreadId: "slack:C-MENTION:7300.1" }, message: { providerMessageId: scenario === "reply" || scenario === "coalesced_reply" ? "7300.2" : "7300.1", mentionedBot: mentioned } } }).returning(); await db.insert(chatActions).values({ id: request.id, companyId: f.companyId, endpointId, deliveryId: delivery.id, kind: "inbound_wakeup", providerActionId: randomUUID(), status: "processed", payload: { issueId: f.issueId, commentId: request.commentId } }); @@ -231,13 +231,13 @@ describe("durable inbound chat scheduler receipts", () => { .where(eq(heartbeatRuns.id, receipt.runId!)); } let deferredRequestId: string | undefined; - if (scenario === "coalesced" || scenario === "deferred") { + if (scenario === "coalesced" || scenario === "coalesced_reply" || scenario === "deferred") { const second = f.request(); await db.insert(issueComments).values({ id: second.commentId, companyId: f.companyId, issueId: f.issueId, authorUserId: "board-user", body: scenario === "deferred" ? "@bot follow-up" : "Unmentioned follow-up" }); const [secondDelivery] = await db.insert(chatDeliveries).values({ companyId: f.companyId, endpointId, providerEventId: randomUUID(), deduplicationKey: randomUUID(), eventKind: isDM ? "direct_message" : "message", state: "processed", - normalizedEvent: { trigger: isDM ? "direct_message" : "subscribed_message", conversation: { isDirectMessage: isDM }, message: { mentionedBot: scenario === "deferred" } } }).returning(); + normalizedEvent: { trigger: isDM ? "direct_message" : "subscribed_message", conversation: { isDirectMessage: isDM, externalThreadId: "slack:C-MENTION:7300.1" }, message: { providerMessageId: scenario === "coalesced_reply" ? "7300.3" : "7300.1", mentionedBot: scenario === "deferred" } } }).returning(); await db.insert(chatActions).values({ id: second.id, companyId: f.companyId, endpointId, deliveryId: secondDelivery.id, kind: "inbound_wakeup", providerActionId: randomUUID(), status: "processed", payload: { issueId: f.issueId, commentId: second.commentId } }); diff --git a/server/src/services/chat-channels/inbound/admission.ts b/server/src/services/chat-channels/inbound/admission.ts index 34329d81557..e2638a83771 100644 --- a/server/src/services/chat-channels/inbound/admission.ts +++ b/server/src/services/chat-channels/inbound/admission.ts @@ -1,3 +1,4 @@ +import { slackMessageMeetsMentionRequirement } from "../slack/messages.js"; import { createTeamsDestinationAdmission } from "../microsoft-teams/destination-admission.js"; import { createTelegramDestinationAdmission } from "../telegram/destination-admission.js"; import { createPhotonDestinationAdmission } from "../imessage-photon/destination-admission.js"; @@ -167,10 +168,7 @@ export function createInboundAdmission(db: Db, options: InboundAdmissionOptions) } const mentionRequired = endpoint.provider === "slack" && - currentEndpoint.requireAtMention && - !thread.isDM && - trigger !== "mention" && - message.isMention !== true; + !slackMessageMeetsMentionRequirement(currentEndpoint, thread, trigger, message); // GitHub sends the same comment to every installed App. Subscribed // conversations are a fallback for unaddressed replies, not permission // to wake every bot when the person addresses a different recipient. diff --git a/server/src/services/chat-channels/slack/messages.ts b/server/src/services/chat-channels/slack/messages.ts index 36196eab18f..6981d2650cc 100644 --- a/server/src/services/chat-channels/slack/messages.ts +++ b/server/src/services/chat-channels/slack/messages.ts @@ -106,9 +106,15 @@ export function stripSlackTitleMentions(text: string): string { export function slackMessageMeetsMentionRequirement( endpoint: Pick, - thread: Pick, + thread: Pick, trigger: ChatSdkMessageCallbackEvent["trigger"], - message: Pick, + message: Pick, ): boolean { - return !endpoint.requireAtMention || thread.isDM || trigger === "mention" || message.isMention === true; + return ( + !endpoint.requireAtMention || + thread.isDM || + (Boolean(message.id) && slackOrphanFollowUp(thread.id, message.id)) || + trigger === "mention" || + message.isMention === true + ); } diff --git a/server/src/services/chat-channels/slack/wake-authority.test.ts b/server/src/services/chat-channels/slack/wake-authority.test.ts new file mode 100644 index 00000000000..1fb2f11467f --- /dev/null +++ b/server/src/services/chat-channels/slack/wake-authority.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { slackDeliveryMeetsMentionRequirement } from "./wake-authority.js"; + +describe("Slack channel mention policy", () => { + const root = { + trigger: "subscribed_message", + conversation: { externalThreadId: "slack:C123:7300.000001", isDirectMessage: false }, + message: { providerMessageId: "7300.000001", mentionedBot: false }, + }; + + it("requires a mention on a channel root, even for a subscribed callback", () => { + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, root)).toBe(false); + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: false }, root)).toBe(true); + }); + + it("allows replies with a known message ID in a channel thread", () => { + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, { + ...root, message: { ...root.message, providerMessageId: "7300.000002" }, + })).toBe(true); + }); + + it.each([ + {}, + { ...root, message: {} }, + { ...root, message: { providerMessageId: "" } }, + { ...root, message: { providerMessageId: 7300 } }, + { ...root, conversation: {} }, + { ...root, conversation: { externalThreadId: "slack:C123:" } }, + { ...root, conversation: { externalThreadId: "slack:C123" } }, + { ...root, conversation: { externalThreadId: "discord:C123:7300.000001" } }, + ])("does not infer a thread reply from incomplete or invalid identity (%j)", (event) => { + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, event)).toBe(false); + }); + + it("accepts either explicit mention callback", () => { + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, { ...root, trigger: "mention" })).toBe(true); + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, { + ...root, message: { ...root.message, mentionedBot: true }, + })).toBe(true); + }); + + it("exempts DMs and uses the current conversation type when it is supplied", () => { + const directMessage = { ...root, conversation: { ...root.conversation, isDirectMessage: true } }; + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, directMessage)).toBe(true); + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, root, true)).toBe(true); + expect(slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, directMessage, false)).toBe(false); + }); +}); diff --git a/server/src/services/chat-channels/slack/wake-authority.ts b/server/src/services/chat-channels/slack/wake-authority.ts index e09a7ff93a5..79662476879 100644 --- a/server/src/services/chat-channels/slack/wake-authority.ts +++ b/server/src/services/chat-channels/slack/wake-authority.ts @@ -1,14 +1,44 @@ import { forbidden } from "../../../errors.js"; +import { slackMessageMeetsMentionRequirement } from "./messages.js"; import type { EndpointRow } from "../runtime-types.js"; import type { chatConversations, chatDeliveries } from "@paperclipai/db"; -export function assertSlackWakeMention(endpoint: EndpointRow, conversation: Pick, delivery: Pick): void { - if ( - endpoint.requireAtMention && - !conversation.isDirectMessage && - delivery.normalizedEvent.trigger !== "mention" && - (delivery.normalizedEvent.message as { mentionedBot?: boolean } | undefined)?.mentionedBot !== true - ) { + +type SlackDeliveryEvent = typeof chatDeliveries.$inferSelect["normalizedEvent"]; + +export function slackDeliveryMeetsMentionRequirement( + endpoint: Pick, + event: SlackDeliveryEvent, + isDirectMessage?: boolean, +): boolean { + const conversation = event.conversation as { + externalThreadId?: unknown; + isDirectMessage?: boolean; + } | undefined; + const message = event.message as { + providerMessageId?: unknown; + mentionedBot?: boolean; + } | undefined; + return slackMessageMeetsMentionRequirement( + endpoint, + { + id: typeof conversation?.externalThreadId === "string" ? conversation.externalThreadId : "", + isDM: isDirectMessage ?? conversation?.isDirectMessage === true, + }, + event.trigger === "mention" ? "mention" : "subscribed_message", + { + id: typeof message?.providerMessageId === "string" ? message.providerMessageId : "", + isMention: message?.mentionedBot === true, + }, + ); +} + +export function assertSlackWakeMention( + endpoint: EndpointRow, + conversation: Pick, + delivery: Pick, +): void { + if (!slackDeliveryMeetsMentionRequirement(endpoint, delivery.normalizedEvent, conversation.isDirectMessage)) { throw forbidden("Message did not @mention the bot", { code: "chat_action_authorization_changed", }); diff --git a/server/src/services/durable-chat-wakeup.ts b/server/src/services/durable-chat-wakeup.ts index 367386ffaca..acfb4b4ca63 100644 --- a/server/src/services/durable-chat-wakeup.ts +++ b/server/src/services/durable-chat-wakeup.ts @@ -1,3 +1,4 @@ +import { slackDeliveryMeetsMentionRequirement } from "./chat-channels/slack/wake-authority.js"; import { createHash } from "node:crypto"; import { agentWakeupRequests, chatActions, chatConversations, chatDeliveries, chatEndpoints, type Db } from "@paperclipai/db"; import { and, eq, exists, inArray, or, sql, type SQLWrapper } from "drizzle-orm"; @@ -410,7 +411,5 @@ export async function slackMentionAllowsRunStart( ), )); return inputs.every(({ event }) => - (event.conversation as { isDirectMessage?: boolean } | undefined)?.isDirectMessage === true || - event.trigger === "mention" || - (event.message as { mentionedBot?: boolean } | undefined)?.mentionedBot === true); + slackDeliveryMeetsMentionRequirement({ requireAtMention: true }, event)); } diff --git a/server/src/services/native-runtime/native-governed-wait-lost-state-continuation.integration.test.ts b/server/src/services/native-runtime/native-governed-wait-lost-state-continuation.integration.test.ts new file mode 100644 index 00000000000..932ba718e59 --- /dev/null +++ b/server/src/services/native-runtime/native-governed-wait-lost-state-continuation.integration.test.ts @@ -0,0 +1,175 @@ +import { randomUUID } from "node:crypto"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { eq, sql } from "drizzle-orm"; +import { afterEach, expect, it, vi } from "vitest"; +import { completionContracts, heartbeatRunEvents, heartbeatRuns, issues, nativeRunFinalizations, nativeRunResults, toolInvocations } from "@paperclipai/db"; +import { describeEmbeddedPostgres, nativeRestartFixture } from "../../__tests__/helpers/native-restart.js"; +import { DurablePrpControlPlane, parseNativeExecutionInput, type NativeSessionBackend, type PersistedNativeSession } from "../../vendor/paperclip-runner/index.js"; +import { createNativeControllerRecovery } from "./native-controller-recovery.js"; +import { nativeRunnerCheckpointWriter } from "./native-runner-checkpoint.js"; +import { executePaperclipNativeSession, nativeControllerMigrationDescriptor } from "./native-session-executor.js"; +import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js"; +import { nativeToolContractFingerprintForTarget, rebindNativeSessionCheckpoint } from "./native-session-resume.js"; +import { prepareNativeHeartbeatRun } from "./prepare-native-run.js"; +import { nativeRuntimeContextFixture } from "./runtime-context.test-fixture.js"; + +vi.mock("./paperclip-runner-tool-authority.js", () => ({ PaperclipRunnerToolAuthority: class { + readonly binding = {}; + async definitions() { return []; } + close() {} +} })); + +// Staging class P after #20757: run A yielded for an approval, its harness +// backup was never saved, and its sandbox was destroyed. The follow-up run B +// rebinds A's session in a new sandbox whose Runner has no saved state for it. +// B has not started a turn, so a fresh provider session cannot repeat provider +// work or a tool side effect. B continues in that fresh session on its own, +// with no Retry. Any durable sign that B already did provider work keeps the +// permanent named refusal. +describeEmbeddedPostgres("governed-wait follow-up whose saved provider state is gone", () => { + const environment = nativeRestartFixture(); + const homes: string[] = []; + afterEach(async () => { vi.unstubAllEnvs(); for (const home of homes.splice(0)) await rm(home, { recursive: true, force: true }); }); + + async function followUpRun() { + vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", "61".repeat(32)); + const f = await environment.seedRun(randomUUID()); + const { companyId, agentId, runtimeRoot } = environment.current; + const home = await mkdtemp(join(tmpdir(), "native-governed-lost-state-")); homes.push(home); + vi.stubEnv("PAPERCLIP_HOME", home); + const [sourceRun] = await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)); + const [contract] = await f.db.select().from(completionContracts).where(eq(completionContracts.id, sourceRun!.completionContractId!)); + const execution = parseNativeExecutionInput({ + schema: "paperclip.native-execution-input.v5", executionMode: "default", planningContext: null, + provider: { kind: "codex", model: null, approvalPolicy: "never" }, + binding: { companyId, agentId, issueId: f.issueId, runId: f.runId, executionWorkspaceId: randomUUID() }, + task: { identifier: "GWL-1", title: "Approved work", description: null, prompt: "Continue after the approval.", workMode: "standard" }, + workspace: { cwd: runtimeRoot, repoUrl: null, repoRef: null, branchName: null }, + session: { normalizedSessionId: f.native.normalizedSessionId, driverKind: "codex_app_server", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }, + completionContract: { id: contract!.id, sha256: contract!.canonicalSha256, schemaVersion: contract!.schemaVersion, contract: contract!.contractJson }, + runtimeContext: nativeRuntimeContextFixture(), credentialBindings: [], interactionResponses: [], + }); + const yielded = { schema: "paperclip.run_result.v1", reportedWorkDisposition: "yielded", summary: "Waiting for approval", + completionClaim: { contractRevision: "1", objectiveSatisfied: false, criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [] }], remainingWork: [] }, + evidence: [], verification: [], attentionRequests: [], artifacts: [], + continuation: { kind: "response_wake", summary: "Continue after the answer", idempotencyKey: "fixture-approval" } }; + const snapshot: PersistedNativeSession = { backendKind: "runner", driverKind: "codex_app_server", sessionId: "lost-driver-session", + providerSessionId: "lost-provider-session", identity: { companyId, agentId, issueId: f.issueId, runId: f.runId, sessionId: f.native.normalizedSessionId }, + cursor: "0", activeTurnId: null, pendingRuntimeRequests: [], terminalTurns: [], lineage: [], semanticResult: yielded as never, goal: null }; + const bootId = randomUUID(), leaseOwner = randomUUID(); + await f.db.update(nativeRunFinalizations).set({ controllerBootId: bootId, controllerGeneration: 1, controllerPid: 2_147_483_000, + controllerProcessStartedAt: new Date(Date.now() - 60_000), leaseOwner, leaseExpiresAt: new Date(Date.now() + 60_000) }).where(eq(nativeRunFinalizations.runId, f.runId)); + const core = new DurablePrpControlPlane({ stateDirectory: join(runtimeRoot, randomUUID()), identity: { runId: f.runId, normalizedSessionId: f.native.normalizedSessionId, + runnerInstanceId: f.native.runnerInstanceId, environmentLeaseId: f.native.environmentLeaseId, turnId: f.native.turnId, itemId: f.native.itemId }, + expectedRunnerVersion: "governed-lost-state-fixture", expectedRunnerDigest: `sha256:${"a".repeat(64)}` }); + for (const type of ["run.prepare", "session.open", "turn.start", "runner.suspend"]) Object.assign(core.queueCommand(type), { status: "completed", result: { status: "completed" } }); + core.store.state.committedEvents.push({ sourceSeq: 1, sourceEventId: "fixture-terminal", eventType: "run.terminal", priority: 0, envelope: { ...core.store.state.identity }, deliveryCount: 1, logicalEffectCount: 1 }); + core.store.state.ackedSourceSeq = 1; + core.setCheckpointPersistence(nativeRunnerCheckpointWriter(f.db, { companyId, issueId: f.issueId, runId: f.runId, authorityRunId: f.runId, bootId, generation: 1, leaseOwner })); + await core.flushCheckpoint(); await core.stop(); + const [result] = await f.db.insert(nativeRunResults).values({ companyId, issueId: f.issueId, runId: f.runId, completionContractId: sourceRun!.completionContractId!, + serverFingerprint: randomUUID(), schemaStatus: "accepted", resultJson: yielded, canonicalSha256: "a".repeat(64) }).returning(); + const fingerprint = nativeToolContractFingerprintForTarget("remote", "codex"); + const descriptor = nativeControllerMigrationDescriptor(execution, fingerprint); + const sourceProfile = { ...f.preparedProfile, nativeExecutionInput: execution, sessionCheckpoint: snapshot, nativeToolContractFingerprint: fingerprint, + nativeControllerRecovery: createNativeControllerRecovery(snapshot, { configDigest: descriptor.configDigest, processConfigDigest: null }, + { runId: f.runId, runnerInstanceId: f.native.runnerInstanceId, normalizedSessionId: f.native.normalizedSessionId }) }; + await f.db.update(heartbeatRuns).set({ status: "succeeded", finishedAt: new Date(), runnerProfileJson: sourceProfile }).where(eq(heartbeatRuns.id, f.runId)); + await f.db.update(nativeRunFinalizations).set({ phase: "committed", resultId: result!.id, leaseOwner: null, leaseExpiresAt: null }).where(eq(nativeRunFinalizations.runId, f.runId)); + + const runId = randomUUID(); + const [run] = await f.db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, nativeIssueId: f.issueId, issueId: f.issueId, status: "running", + runtimeMode: "native", nativeSessionId: f.native.normalizedSessionId, runnerInstanceId: f.native.runnerInstanceId, + completionContractId: sourceRun!.completionContractId, completionContractSha256: sourceRun!.completionContractSha256 }).returning(); + const native = await prepareNativeHeartbeatRun({ db: f.db, run: run!, issue: { id: f.issueId, title: "Approved work", description: null }, environmentLeaseId: randomUUID() }); + const current = { ...execution, binding: { ...execution.binding, runId } }; + const rebound = rebindNativeSessionCheckpoint({ previousRun: { id: f.runId, companyId, agentId, nativeSessionId: f.native.normalizedSessionId, runnerProfileJson: sourceProfile }, + currentExecution: current, executionTargetKind: "remote" }); + expect(rebound?.providerRecoveryPolicy).toBe("allow_replacement_after_governed_wait"); + const [prepared] = await f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId)); + await f.db.update(heartbeatRuns).set({ runnerProfileJson: { ...prepared!.runnerProfileJson, nativeExecutionInput: current, sessionCheckpoint: rebound } }).where(eq(heartbeatRuns.id, runId)); + await f.db.insert(nativeRunFinalizations).values({ runId, companyId, issueId: f.issueId, phase: "observed" }); + await f.db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, f.issueId)); + + const recover = vi.fn(async () => ({ recovered: false as const, failureClass: "provider_state_incompatible" as const, resumeStateUnavailable: true as const, + reason: "Error: NativeRunnerResumeStateUnavailableError: PRP provider resume state is unavailable" })); + const replacementObserved = new Error("fixture fresh provider session opened"); + const fresh = vi.fn(async () => { throw replacementObserved; }); + const backend: NativeSessionBackend = { + descriptor: async () => ({ kind: "runner", name: "controlled provider boundary", version: "fixture", + capabilities: { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }, + runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" } }), + recoverSession: recover, openSession: fresh, openReplacementSession: fresh, + }; + const execute = () => executePaperclipNativeSession({ db: f.db, execution: current, runnerInstanceId: native.runnerInstanceId, useRunnerd: true, backend, + runnerExecutionTarget: { kind: "remote", transport: "sandbox", remoteCwd: runtimeRoot, environmentId: randomUUID(), leaseId: randomUUID(), + providerKey: "fixture", reusableLeaseConfigured: true, runner: { execute: async () => { throw new Error("unexpected remote process execution"); } }, + sandboxLeaseAcquisition: { outcome: "created", providerLeaseId: "new-fixture-sandbox" } } as never }).catch((error: unknown) => error); + return { f, companyId, agentId, runId, recover, fresh, replacementObserved, execute }; + } + + const proofFor = (h: Awaited>) => new PaperclipControlPlanePort(h.f.db, { + companyId: h.companyId, issueId: h.f.issueId, runId: h.runId, agentId: h.agentId, sessionId: "proof-session", + completionContractId: "proof-contract", completionContractSha256: "proof-contract", sourceInstanceId: "proof-runner", + controlPlaneSourceInstanceId: "proof-control", + }).provesNoRunProviderWork({ signal: new AbortController().signal }); + + it("opens a fresh provider session for the follow-up without a Retry", async () => { + const h = await followUpRun(); + expect(await proofFor(h)).toBe(true); + const failure = await h.execute(); + expect(h.recover, String(failure)).toHaveBeenCalledOnce(); + // The fixture stops at the fresh session's boundary; on master the run + // ended with native_provider_state_incompatible before reaching it. + expect(failure).toBe(h.replacementObserved); + expect(h.fresh).toHaveBeenCalledOnce(); + }, 60_000); + + it.each([ + ["a provider event", "event"], + ["a tool invocation", "tool"], + ["an accepted result", "result"], + // A runner event of a non-provider type still carries a source identity: + // a runner wrote it for this run, so the run is not untouched. + ["a sourced runner event of a non-provider type", "sourced_event"], + // The foreign key normally forbids this. A row restored without its result + // must still deny the proof rather than look like an untouched run. + ["a finalized result whose result row is missing", "dangling_result"], + ] as const)("keeps the named refusal when the follow-up run already recorded %s", async (_label, evidence) => { + const h = await followUpRun(); + if (evidence === "event") await h.f.db.insert(heartbeatRunEvents).values({ companyId: h.companyId, runId: h.runId, agentId: h.agentId, + seq: 1_000, eventType: "turn.started", stream: "system", level: "info", message: "turn started", payload: {} }); + if (evidence === "tool") await h.f.db.insert(toolInvocations).values({ companyId: h.companyId, runId: h.runId, agentId: h.agentId, + toolName: "paperclip_get_issue", riskLevel: "read", status: "succeeded" } as never); + if (evidence === "result") { + const [run] = await h.f.db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, h.runId)); + await h.f.db.insert(nativeRunResults).values({ companyId: h.companyId, issueId: h.f.issueId, runId: h.runId, completionContractId: run!.completionContractId!, + serverFingerprint: randomUUID(), schemaStatus: "accepted", resultJson: {}, canonicalSha256: "b".repeat(64) }); + } + if (evidence === "sourced_event") await h.f.db.insert(heartbeatRunEvents).values({ companyId: h.companyId, runId: h.runId, agentId: h.agentId, + seq: 1_000, eventType: "native.diagnostic", stream: "system", level: "info", message: "runner diagnostic", payload: {}, + sourceInstanceId: "foreign-runner-instance", sourceEventId: "foreign-runner-instance:event:1", sourceSeq: 1 }); + if (evidence === "dangling_result") await h.f.db.transaction(async (tx) => { + await tx.execute(sql`set local session_replication_role = 'replica'`); + await tx.update(nativeRunFinalizations).set({ resultId: randomUUID() }).where(eq(nativeRunFinalizations.runId, h.runId)); + }); + if (evidence === "sourced_event" || evidence === "dangling_result") { + // The fixture is the only evidence: no tool invocation or result row. + expect(await h.f.db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, h.runId))).toEqual([]); + expect(await h.f.db.select().from(toolInvocations).where(eq(toolInvocations.runId, h.runId))).toEqual([]); + } + // The durable proof itself refuses each kind of evidence. + expect(await proofFor(h)).toBe(false); + const failure = await h.execute(); + expect(h.fresh).not.toHaveBeenCalled(); + if (evidence === "dangling_result") { + // Admission stops even earlier on a finalized result: still no fresh session. + expect(String(failure)).toContain("native_result_pending_finalization"); + return; + } + expect(h.recover, String(failure)).toHaveBeenCalledOnce(); + expect((failure as { code?: string }).code).toBe("native_provider_state_incompatible"); + }, 60_000); +}); diff --git a/server/src/services/native-runtime/paperclip-control-plane-port.ts b/server/src/services/native-runtime/paperclip-control-plane-port.ts index 39af5d919de..5b21bc744db 100644 --- a/server/src/services/native-runtime/paperclip-control-plane-port.ts +++ b/server/src/services/native-runtime/paperclip-control-plane-port.ts @@ -10,6 +10,7 @@ import { nativeRunFinalizations, nativeRunResults, nativeRunnerCheckpoints, + toolInvocations, } from "@paperclipai/db"; import type { CompleteControlPlaneRunInput, @@ -36,6 +37,7 @@ import { eligibleNativeTaskSessionPublication, publishNativeTaskSessionInTransac type NativeTaskSessionPublicationContext, type NativeTaskSessionPublicationIssue } from "./native-task-session-publication.js"; import { lockNativeRunnerCheckpointOwner, nativeRunnerCheckpointOwnerLock, validateNativeRunnerCheckpointOwner, inspectNativeRunnerUpgradeHeader, type NativeRunnerCheckpointOwner } from "./native-runner-checkpoint.js"; import { crashPoint } from "@paperclipai/shared/crash-points"; +import { NATIVE_PROVIDER_DURABLE_EVENT_TYPES } from "./native-retired-backup-source.js"; export interface PaperclipControlPlaneBinding { companyId: string; @@ -512,6 +514,34 @@ export class PaperclipControlPlanePort implements ControlPlanePort { return { events, highestContiguousSourceSeq: cursor }; } + /** + * Proves from durable rows, across every runner instance, that this run has + * no provider event, source-sequenced runner event, tool invocation, or + * accepted result. Any such row, or a read failure, denies the proof. + */ + async provesNoRunProviderWork(options: { signal: AbortSignal }): Promise { + options.signal.throwIfAborted(); + const { companyId, runId } = this.#binding; + const providerEvents = sql.join([...NATIVE_PROVIDER_DURABLE_EVENT_TYPES].map(value => sql`${value}`), sql`, `); + const result = await this.#db.execute(sql`select + exists (select 1 from ${heartbeatRunEvents} event where event.company_id = ${companyId} and event.run_id = ${runId} + and (event.source_event_id is not null or event.source_instance_id is not null + or event.event_type in (${providerEvents}) + or event.event_type ~ '^(turn|item|tool|semantic_tool|run[.]result)[.]')) as "providerEvent", + exists (select 1 from ${toolInvocations} invocation where invocation.company_id = ${companyId} + and invocation.run_id = ${runId}) as "toolInvocation", + exists (select 1 from ${nativeRunResults} result where result.company_id = ${companyId} + and result.run_id = ${runId}) as "acceptedResult", + exists (select 1 from ${nativeRunFinalizations} coordinator where coordinator.run_id = ${runId} + and coordinator.result_id is not null) as "finalizedResult"`); + options.signal.throwIfAborted(); + const rows = (Array.isArray(result) ? result : (result as { rows?: unknown[] }).rows ?? []) as Array>; + const row = rows[0]; + if (!row) return false; + return row.providerEvent === false && row.toolInvocation === false + && row.acceptedResult === false && row.finalizedResult === false; + } + async completeRun(value: NativeRunResult | CompleteControlPlaneRunInput, options?: { signal: AbortSignal }): Promise { this.#assertActive(options); if (this.#deferredEventObservers.size > 0 || this.#deferredObserverFailure) await this.#settleDeferredObservers(options); diff --git a/tests/runner-e2e/report-lock.json b/tests/runner-e2e/report-lock.json index aee216eede7..407c60b8ed8 100644 --- a/tests/runner-e2e/report-lock.json +++ b/tests/runner-e2e/report-lock.json @@ -1,6 +1,6 @@ { - "inputSha256": "2360d5e93876ce3cb6e810257d92d669ac2b109027b0e01af36f2048861648c0", - "outputSha256": "b32083066ce89ba9a570a2927b3c4c8385294a5f04efcbae462db82b2e8c4455", - "patchSha256": "8da9b1447b76f9b105d4fce42824f80d25c3f6f3b61f544331892fa9509c0f09", + "inputSha256": "b32083066ce89ba9a570a2927b3c4c8385294a5f04efcbae462db82b2e8c4455", + "outputSha256": "17625672b08d67bc1d69fd452dfd245508cafe38315a0eedb502ac27cb97e828", + "patchSha256": "ea5842b579c1b36b85de1bb30de4b6eb911a1e7faed4595fb37e9b73dddd54a5", "basis": "Generated with the trusted master lockfile refresh. Review this offline correction and its hashes with the dependency changes." } diff --git a/tests/runner-e2e/report-lock.patch b/tests/runner-e2e/report-lock.patch index 42d96996eb6..195f35bdfee 100644 --- a/tests/runner-e2e/report-lock.patch +++ b/tests/runner-e2e/report-lock.patch @@ -1,54 +1,373 @@ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml -index 851e65af..6d825f97 100644 +index 6d825f97..d24edac1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml -@@ -28,7 +28,7 @@ patchedDependencies: - hash: x25bk7vvljpm57viwan5qoaddi - path: patches/@chat-adapter__github@4.39.0.patch - '@chat-adapter/slack@4.39.0': -- hash: 226jj24akljccsfbw7bcxujz4u -+ hash: eskb5kxp7eud5v3ostluhynfoa - path: patches/@chat-adapter__slack@4.39.0.patch - '@chat-adapter/teams@4.39.0': - hash: jfm44m5wrw3jrn2jijafzk7s5a -@@ -934,7 +934,7 @@ importers: - version: 4.39.0(patch_hash=x25bk7vvljpm57viwan5qoaddi)(ai@7.0.130(zod@4.6.7))(zod@4.6.7) - '@chat-adapter/slack': - specifier: 4.39.0 -- version: 4.39.0(patch_hash=226jj24akljccsfbw7bcxujz4u)(ai@7.0.130(zod@4.6.7))(zod@4.6.7) -+ version: 4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7) - '@chat-adapter/teams': - specifier: 4.39.0 - version: 4.39.0(patch_hash=jfm44m5wrw3jrn2jijafzk7s5a)(ai@7.0.130(zod@4.6.7))(zod@4.6.7) @@ -1015,7 +1015,7 @@ importers: version: 2.2.0(@grpc/grpc-js@1.14.5)(nice-grpc-common@2.0.4)(nice-grpc@2.1.17) '@vercel/connect': specifier: 2.3.3 -- version: 2.3.3(@chat-adapter/slack@4.39.0(patch_hash=226jj24akljccsfbw7bcxujz4u)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))) -+ version: 2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))) +- version: 2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))) ++ version: 2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))) acorn: specifier: 8.18.0 version: 8.18.0 -@@ -10043,7 +10043,7 @@ snapshots: - - workflow - - zod +@@ -1032,8 +1032,8 @@ importers: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.20.0) + better-auth: +- specifier: 1.7.2 +- version: 1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) ++ specifier: 1.7.16 ++ version: 1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) + chat: + specifier: 4.39.0 + version: 4.39.0(ai@7.0.130(zod@4.6.7))(zod@4.6.7) +@@ -1804,64 +1804,61 @@ packages: + '@types/react': + optional: true -- '@chat-adapter/slack@4.39.0(patch_hash=226jj24akljccsfbw7bcxujz4u)(ai@7.0.130(zod@4.6.7))(zod@4.6.7)': -+ '@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7)': +- '@better-auth/core@1.7.2': +- resolution: {integrity: sha512-j0nM4ygsWbF/fcYRoKtDn8gn8uLXkmC+075HqSqsJEAV828cJR9bvYBCUQ1zmxNyRBk6Iz/qXsA0Zm2oksiOTg==} ++ '@better-auth/core@1.7.16': ++ resolution: {integrity: sha512-dwVAtEAEpjPsCV/TqOKRHreWyuiqvja+TnTTAuVbi3P0afM0+b1+ga+RTXNjVE86Q4HRwRg45Ek3txFrY8/1DA==} + peerDependencies: +- '@better-auth/utils': 0.4.2 +- '@better-fetch/fetch': 1.3.1 +- '@cloudflare/workers-types': '>=4' ++ '@better-auth/utils': ^0.4.2 || ^0.5.0 ++ '@better-fetch/fetch': 1.3.3 + '@opentelemetry/api': ^1.9.0 +- better-call: 1.4.0 ++ better-call: 1.4.5 + jose: ^6.1.0 + kysely: ^0.28.5 || ^0.29.0 + nanostores: ^1.0.1 + peerDependenciesMeta: +- '@cloudflare/workers-types': +- optional: true + '@opentelemetry/api': + optional: true + +- '@better-auth/drizzle-adapter@1.7.2': +- resolution: {integrity: sha512-A5wE10PIv3aS5LGePecEHntQylKy6OOF17B4dqlE0DwJeqU/IOBSd7/LZhMop9cNJ3WFjKMpazVSf91yYM/NFg==} ++ '@better-auth/drizzle-adapter@1.7.16': ++ resolution: {integrity: sha512-8cgtcWkCmJal+ecrl681Y+SKvMDy5Vg2ydCOPE7X8Mcx1Ud7Mqve3pASG0I2JWMJtrge1m8YbpNmCxhVGh+gpQ==} + peerDependencies: +- '@better-auth/core': ^1.7.2 +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': ^1.7.16 ++ '@better-auth/utils': ^0.4.2 || ^0.5.0 + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + peerDependenciesMeta: + drizzle-orm: + optional: true + +- '@better-auth/kysely-adapter@1.7.2': +- resolution: {integrity: sha512-LYdSRLOvZiF+6S0UThu+wE/Qxsq9P2jQs7ZKkY6BIBJqUjYyxVDmi8HFcantBvWWW1/BeQCSsD7YVDG4gICMIQ==} ++ '@better-auth/kysely-adapter@1.7.16': ++ resolution: {integrity: sha512-MkLZwHxhYbVr1QGGDJDZXMT1g54Q5yucDMdLGaz3Y5P8ERRfaqVEe+3MCVCb8XxJ7Rh+VNJKUix4VOBq7vXimg==} + peerDependencies: +- '@better-auth/core': ^1.7.2 +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': ^1.7.16 ++ '@better-auth/utils': ^0.4.2 || ^0.5.0 + kysely: ^0.28.17 || ^0.29.0 + peerDependenciesMeta: + kysely: + optional: true + +- '@better-auth/memory-adapter@1.7.2': +- resolution: {integrity: sha512-0q1SXMzm5esH9L0xVuM6IxCk59E4G+3HySX4My9gvEwqtmUobykn+iuc/si3Y4xwUO7JODqQ5o+/pPcLDDMIrA==} ++ '@better-auth/memory-adapter@1.7.16': ++ resolution: {integrity: sha512-b0uUE3mhQJDu2QqLDN3XVHhXUhpr6zK+UhjL7IdNteTJJaAMhCcXfpLc7+kpOcNVEEBdRiFXBwsEtxRjcB3rLQ==} + peerDependencies: +- '@better-auth/core': ^1.7.2 +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': ^1.7.16 ++ '@better-auth/utils': ^0.4.2 || ^0.5.0 + +- '@better-auth/mongo-adapter@1.7.2': +- resolution: {integrity: sha512-4879SmUWHUs0OYlvHoCFbycZ7i1bqytkcgAUdt9RLQMvZ5H3LRMTgax2YVlGZEXgwNjY/X7xAoXOecWLhlQWeA==} ++ '@better-auth/mongo-adapter@1.7.16': ++ resolution: {integrity: sha512-uZG9DhCHv7QcF8HtEIgYEL7Lawu0/1Pcp6ABe4ImYQdLV3HNRaAuQU/PxVxFQFAMSpnCVi4Rwg8+Qk8qVn2YMg==} + peerDependencies: +- '@better-auth/core': ^1.7.2 +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': ^1.7.16 ++ '@better-auth/utils': ^0.4.2 || ^0.5.0 + mongodb: ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + mongodb: + optional: true + +- '@better-auth/prisma-adapter@1.7.2': +- resolution: {integrity: sha512-mXTr/83WrNWLrvzIjtgDgdu9iXhOcSG1+qBQOAKlbGSFiOB+z4IMRneQ2wmMOiB8mKY9qGkClVUjKRFXqtHnFQ==} ++ '@better-auth/prisma-adapter@1.7.16': ++ resolution: {integrity: sha512-rSVMa4sWxvEURqVR+o4SZvq6dosCvD58nLw2LOw7woz791/CKOMxhrqrjiHLLzx+OfzGO0Ak9x7lrFTUwYO/KQ==} + peerDependencies: +- '@better-auth/core': ^1.7.2 +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': ^1.7.16 ++ '@better-auth/utils': ^0.4.2 || ^0.5.0 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: +@@ -1870,21 +1867,18 @@ packages: + prisma: + optional: true + +- '@better-auth/telemetry@1.7.2': +- resolution: {integrity: sha512-LcWu+O0zrxYDQj8E36vfkJwGPW4k9ZDA/rCo0zST6ihzL+juR7pBowoZIM9E6tK0Vit52mf6412bGT4XM4eTjQ==} ++ '@better-auth/telemetry@1.7.16': ++ resolution: {integrity: sha512-mwQC0xB8KO636CjBxMZCNgglaIwh18DOemBJyNCy7DBOngkjPdNggPWYB8BpJ8wMqcn3SaKchdWZrNNqLRZV4Q==} + peerDependencies: +- '@better-auth/core': ^1.7.2 +- '@better-auth/utils': 0.4.2 +- '@better-fetch/fetch': 1.3.1 +- +- '@better-auth/utils@0.4.2': +- resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} ++ '@better-auth/core': ^1.7.16 ++ '@better-auth/utils': ^0.4.2 || ^0.5.0 ++ '@better-fetch/fetch': 1.3.3 + +- '@better-auth/utils@0.5.0': +- resolution: {integrity: sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==} ++ '@better-auth/utils@0.5.1': ++ resolution: {integrity: sha512-WETqw5vQ52uQ/xwWR4E8rTgT6fDEr/wnS+otDNkBCa/69281FCF32OKDdQUcahMCnjsvqXBNiEvAQHFB0EVRRg==} + +- '@better-fetch/fetch@1.3.1': +- resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} ++ '@better-fetch/fetch@1.3.3': ++ resolution: {integrity: sha512-U6SlDt/GUWtK2JAxS/NhdoYTEim+EL1G5byz2TUj83KjLZBUpYzb5SmX0aXaCnbGkAjFKnV/o/0+cyXMbEcqqg==} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} +@@ -6058,15 +6052,14 @@ packages: + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + +- better-auth@1.7.2: +- resolution: {integrity: sha512-gKapKBEvYIGcMxi74RjQ7EbFLiqyQt58vdoJmL1qAlWSkY1Bc2Vqshl524/3u1NxauiOU03M/Ebh762Brmac9A==} ++ better-auth@1.7.16: ++ resolution: {integrity: sha512-ur6xj8jeUC9lcH8ni9LrwB4FiFMPDTWFJmb89yZ2SHsFSUobYgGMFsMq6aOorlVtqRr9ckTemRunCR6vzsO65Q==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 +- '@sveltejs/kit': ^2.0.0 ++ '@sveltejs/kit': ^2.0.0 || ^3.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 +- better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 + mongodb: ^6.0.0 || ^7.0.0 +@@ -6078,7 +6071,7 @@ packages: + react-dom: ^19.2.8 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 +- vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 ++ vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': +@@ -6091,8 +6084,6 @@ packages: + optional: true + '@tanstack/solid-start': + optional: true +- better-sqlite3: +- optional: true + drizzle-kit: + optional: true + drizzle-orm: +@@ -6120,8 +6111,8 @@ packages: + vue: + optional: true + +- better-call@1.4.0: +- resolution: {integrity: sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==} ++ better-call@1.4.5: ++ resolution: {integrity: sha512-GVOWfn3pjEfUXn6GrfGf2h7tPAqj1oMLFBWBEswJrSkDKcLk6TIcjV0zexibcZZ0c5N1mkLMsPFopXWzWj/gDQ==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: +@@ -7301,9 +7292,6 @@ packages: + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + +- jose@6.2.10: +- resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} +- + jose@6.2.12: + resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} + +@@ -9938,64 +9926,59 @@ snapshots: + optionalDependencies: + '@types/react': 19.2.18 + +- '@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3)': ++ '@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)': dependencies: - '@chat-adapter/shared': 4.39.0(ai@7.0.130(zod@4.6.7))(zod@4.6.7) - '@slack/socket-mode': 2.0.7 -@@ -13803,11 +13803,11 @@ snapshots: +- '@better-auth/utils': 0.4.2 +- '@better-fetch/fetch': 1.3.1 +- '@opentelemetry/semantic-conventions': 1.43.0 ++ '@better-auth/utils': 0.5.1 ++ '@better-fetch/fetch': 1.3.3 + '@standard-schema/spec': 1.1.0 +- better-call: 1.4.0(zod@4.6.7) +- jose: 6.2.10 ++ better-call: 1.4.5(zod@4.6.7) ++ jose: 6.2.12 + kysely: 0.29.5 + nanostores: 1.5.3 + zod: 4.6.7 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + +- '@better-auth/drizzle-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))': ++ '@better-auth/drizzle-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))': + dependencies: +- '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) ++ '@better-auth/utils': 0.5.1 + optionalDependencies: + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)) + +- '@better-auth/kysely-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(kysely@0.29.5)': ++ '@better-auth/kysely-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(kysely@0.29.5)': + dependencies: +- '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) ++ '@better-auth/utils': 0.5.1 + optionalDependencies: + kysely: 0.29.5 + +- '@better-auth/memory-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)': ++ '@better-auth/memory-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)': + dependencies: +- '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) ++ '@better-auth/utils': 0.5.1 + +- '@better-auth/mongo-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)': ++ '@better-auth/mongo-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)': + dependencies: +- '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) ++ '@better-auth/utils': 0.5.1 + +- '@better-auth/prisma-adapter@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)': ++ '@better-auth/prisma-adapter@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)': + dependencies: +- '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) +- '@better-auth/utils': 0.4.2 ++ '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) ++ '@better-auth/utils': 0.5.1 + +- '@better-auth/telemetry@1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': ++ '@better-auth/telemetry@1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)': + dependencies: +- '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) +- '@better-auth/utils': 0.4.2 +- '@better-fetch/fetch': 1.3.1 ++ '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) ++ '@better-auth/utils': 0.5.1 ++ '@better-fetch/fetch': 1.3.3 + +- '@better-auth/utils@0.4.2': ++ '@better-auth/utils@0.5.1': + dependencies: + '@noble/hashes': 2.4.0 + +- '@better-auth/utils@0.5.0': +- dependencies: +- '@noble/hashes': 2.4.0 +- +- '@better-fetch/fetch@1.3.1': {} ++ '@better-fetch/fetch@1.3.3': {} + + '@braintree/sanitize-url@7.1.2': {} + +@@ -13803,14 +13786,14 @@ snapshots: dependencies: execa: 5.1.1 -- '@vercel/connect@2.3.3(@chat-adapter/slack@4.39.0(patch_hash=226jj24akljccsfbw7bcxujz4u)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))))': -+ '@vercel/connect@2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))))': +- '@vercel/connect@2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))))': ++ '@vercel/connect@2.3.3(@chat-adapter/slack@4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7))(@modelcontextprotocol/sdk@1.31.0(zod@4.6.7))(ai@7.0.130(zod@4.6.7))(better-auth@1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))))': dependencies: '@vercel/oidc': 3.8.9 optionalDependencies: -- '@chat-adapter/slack': 4.39.0(patch_hash=226jj24akljccsfbw7bcxujz4u)(ai@7.0.130(zod@4.6.7))(zod@4.6.7) -+ '@chat-adapter/slack': 4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7) + '@chat-adapter/slack': 4.39.0(patch_hash=eskb5kxp7eud5v3ostluhynfoa)(ai@7.0.130(zod@4.6.7))(zod@4.6.7) '@modelcontextprotocol/sdk': 1.31.0(zod@4.6.7) ai: 7.0.130(zod@4.6.7) - better-auth: 1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) +- better-auth: 1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) ++ better-auth: 1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) + + '@vercel/oidc@3.2.0': {} + +@@ -14079,22 +14062,22 @@ snapshots: + + before-after-hook@4.0.0: {} + +- better-auth@1.7.2(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))): +- dependencies: +- '@better-auth/core': 1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3) +- '@better-auth/drizzle-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda))) +- '@better-auth/kysely-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(kysely@0.29.5) +- '@better-auth/memory-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2) +- '@better-auth/mongo-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2) +- '@better-auth/prisma-adapter': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2) +- '@better-auth/telemetry': 1.7.2(@better-auth/core@1.7.2(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.7))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) +- '@better-auth/utils': 0.4.2 +- '@better-fetch/fetch': 1.3.1 ++ better-auth@1.7.16(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.11)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda)))(pg@8.18.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))): ++ dependencies: ++ '@better-auth/core': 1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) ++ '@better-auth/drizzle-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(kysely@0.29.5)(pg@8.18.0)(postgres@3.4.9(patch_hash=6q4cic6qawywsyeo2spgnjamda))) ++ '@better-auth/kysely-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(kysely@0.29.5) ++ '@better-auth/memory-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1) ++ '@better-auth/mongo-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1) ++ '@better-auth/prisma-adapter': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1) ++ '@better-auth/telemetry': 1.7.16(@better-auth/core@1.7.16(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3)(@opentelemetry/api@1.9.1)(better-call@1.4.5(zod@4.6.7))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.5.1)(@better-fetch/fetch@1.3.3) ++ '@better-auth/utils': 0.5.1 ++ '@better-fetch/fetch': 1.3.3 + '@noble/ciphers': 2.4.0 + '@noble/hashes': 2.4.0 +- better-call: 1.4.0(zod@4.6.7) ++ better-call: 1.4.5(zod@4.6.7) + defu: 6.1.7 +- jose: 6.2.10 ++ jose: 6.2.12 + kysely: 0.29.5 + nanostores: 1.5.3 + zod: 4.6.7 +@@ -14106,13 +14089,12 @@ snapshots: + react-dom: 19.2.8(react@19.2.8) + vitest: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + transitivePeerDependencies: +- - '@cloudflare/workers-types' + - '@opentelemetry/api' + +- better-call@1.4.0(zod@4.6.7): ++ better-call@1.4.5(zod@4.6.7): + dependencies: +- '@better-auth/utils': 0.5.0 +- '@better-fetch/fetch': 1.3.1 ++ '@better-auth/utils': 0.5.1 ++ '@better-fetch/fetch': 1.3.3 + rou3: 0.9.2 + set-cookie-parser: 3.1.2 + optionalDependencies: +@@ -15327,8 +15309,6 @@ snapshots: + + jose@5.10.0: {} + +- jose@6.2.10: {} +- + jose@6.2.12: {} + + joycon@3.1.1: {} diff --git a/ui/src/pages/apps/chat/ChatEndpointDetail.tsx b/ui/src/pages/apps/chat/ChatEndpointDetail.tsx index 2560e7bee1c..476173a6cb9 100644 --- a/ui/src/pages/apps/chat/ChatEndpointDetail.tsx +++ b/ui/src/pages/apps/chat/ChatEndpointDetail.tsx @@ -621,8 +621,8 @@ function Settings({ /> {endpoint.provider === "slack" && (