From 6238b98c370cc9e8868bf4adc89e4862d0774443 Mon Sep 17 00:00:00 2001 From: Paul H Date: Wed, 24 Jun 2026 09:17:41 +0100 Subject: [PATCH 1/6] Bump @ai-sdk/anthropic to 3.0.62 for compaction support Adds an npm run verify:compaction script that checks the compaction summary survives the storage round-trip and is re-applied on resend. --- package-lock.json | 547 ++++++++++++++++++++++++++++++++++- package.json | 4 +- scripts/verify-compaction.ts | 182 ++++++++++++ 3 files changed, 726 insertions(+), 7 deletions(-) create mode 100644 scripts/verify-compaction.ts diff --git a/package-lock.json b/package-lock.json index 43a6a92c..40965b28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.6.4", "license": "Apache-2.0", "devDependencies": { - "@ai-sdk/anthropic": "3.0.13", + "@ai-sdk/anthropic": "^3.0.62", "@ai-sdk/google": "3.0.30", "@ai-sdk/groq": "3.0.8", "@ai-sdk/openai": "3.0.10", @@ -65,6 +65,7 @@ "tailwind-merge": "3.5.0", "tailwindcss": "3.4.19", "tailwindcss-animate": "1.0.7", + "tsx": "^4.22.4", "typescript": "5.9.3", "typescript-eslint": "8.57.1", "vite": "6.4.3", @@ -127,14 +128,45 @@ "license": "MIT" }, "node_modules/@ai-sdk/anthropic": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.13.tgz", - "integrity": "sha512-62UqSpZWuR8pU2ZLc1IgPYiNdH01blAcaNEjrQtx4wCN7L2fUTXm/iG6Tq9qRCiRED+8eQ43olggbf0fbguqkA==", + "version": "3.0.62", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.62.tgz", + "integrity": "sha512-CkShXR8tmNO7QQnvpKbSMe2Vr1zUUcpqlp69iR+DYrbHm+tDJO9u6zZsjEHjcoRU9/e9z++p0W6NiuLC3aZ4Bg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "3.0.3", - "@ai-sdk/provider-utils": "4.0.6" + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.20" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": { + "version": "4.0.20", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.20.tgz", + "integrity": "sha512-gpUIj9uDhIGbuo9afKEgQ074BWmhvK4THJAAeBjRnroTy2yQYo6rbtGD7pQDMZM8ouXPYmT/SCdkWVJ0KcpX8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" }, "engines": { "node": ">=18" @@ -10646,6 +10678,509 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", diff --git a/package.json b/package.json index c93baaca..9aeeda00 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dev:build": "chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial", "predev": "convex init && npm run build:codegen", "build": "tsc --project ./tsconfig.build.json", + "verify:compaction": "tsx scripts/verify-compaction.ts", "build:codegen": "npx convex codegen --component-dir ./src/component && npm run build", "build:clean": "rm -rf dist *.tsbuildinfo && npm run build:codegen", "typecheck": "tsc --noEmit && tsc -p example && tsc -p example/convex", @@ -81,7 +82,7 @@ } }, "devDependencies": { - "@ai-sdk/anthropic": "3.0.13", + "@ai-sdk/anthropic": "^3.0.62", "@ai-sdk/google": "3.0.30", "@ai-sdk/groq": "3.0.8", "@ai-sdk/openai": "3.0.10", @@ -137,6 +138,7 @@ "tailwind-merge": "3.5.0", "tailwindcss": "3.4.19", "tailwindcss-animate": "1.0.7", + "tsx": "^4.22.4", "typescript": "5.9.3", "typescript-eslint": "8.57.1", "vite": "6.4.3", diff --git a/scripts/verify-compaction.ts b/scripts/verify-compaction.ts new file mode 100644 index 00000000..08b5bdaa --- /dev/null +++ b/scripts/verify-compaction.ts @@ -0,0 +1,182 @@ +/** + * End-to-end verification that Anthropic server-side compaction survives the + * convex-agent storage round-trip. + * + * What it proves (or disproves): + * 1. A real generateText call with `contextManagement.compact_20260112` returns + * a compaction summary block (text part w/ providerMetadata.anthropic.type==='compaction'). + * 2. That block survives convex-agent's serializeMessage -> toModelMessage + * (the exact functions used to persist/replay messages in Convex storage). + * 3. When the round-tripped block is resent, Anthropic RE-APPLIES the summary + * (replaces compacted history) instead of re-processing the full history. + * + * Run: ANTHROPIC_API_KEY=... npm run verify:compaction + * + * Throwaway diagnostic — not part of the library build. + */ +import { generateText, type ModelMessage } from "ai"; +import { createAnthropic } from "@ai-sdk/anthropic"; +import { serializeMessage, toModelMessage } from "../src/mapping.js"; + +const MODEL = "claude-sonnet-4-6"; +const TRIGGER_TOKENS = 50_000; // documented minimum for compact_20260112 + +if (!process.env.ANTHROPIC_API_KEY) { + console.error("ANTHROPIC_API_KEY is not set. `direnv allow` / export it first."); + process.exit(1); +} + +const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); + +const compactionOptions = { + anthropic: { + contextManagement: { + edits: [ + { + type: "compact_20260112", + trigger: { type: "input_tokens", value: TRIGGER_TOKENS }, + }, + ], + }, + }, +}; + +/** ~`approxTokens` of low-entropy-but-not-trivial filler so the trigger fires. */ +function filler(approxTokens: number, tag: string): string { + const sentence = (i: number) => + `In section ${tag}, observation ${i}: the quick brown fox audits ledger entry ${i * 7} ` + + `and reconciles balance ${i * 13} against invoice ${i * 17} before archiving record ${i * 19}.`; + const lines: string[] = []; + // ~25 tokens/sentence -> need approxTokens/25 sentences + const n = Math.ceil(approxTokens / 25); + for (let i = 1; i <= n; i++) lines.push(sentence(i)); + return lines.join("\n"); +} + +function isCompactionMarker(part: Record): boolean { + // @ai-sdk/anthropic surfaces the compaction marker under providerOptions on + // the response part; convex-agent also stores it under providerMetadata. + const po = part.providerOptions as { anthropic?: { type?: string } } | undefined; + const pm = part.providerMetadata as { anthropic?: { type?: string } } | undefined; + return po?.anthropic?.type === "compaction" || pm?.anthropic?.type === "compaction"; +} + +function findCompactionPart(msg: ModelMessage): unknown | undefined { + if (typeof msg.content === "string") return undefined; + return (msg.content as Array>).find(isCompactionMarker); +} + +/** Anthropic per-call iterations (compaction vs message), via AI-SDK providerMetadata. */ +function iterations(meta: unknown): Array<{ type: string; inputTokens?: number }> { + const m = meta as { anthropic?: { iterations?: Array<{ type: string; inputTokens?: number }> } } | undefined; + return m?.anthropic?.iterations ?? []; +} + +async function roundTrip(msg: ModelMessage): Promise { + // ctx/component are only touched by storeFile (large binary) — unused for text. + const { message } = await serializeMessage(null as never, null as never, msg); + return toModelMessage(message); +} + +async function main() { + console.log(`\n=== Compaction round-trip verification (model=${MODEL}, trigger=${TRIGGER_TOKENS} tok) ===\n`); + + // History large enough to cross the compaction trigger across a few turns. + const history: ModelMessage[] = [ + { role: "user", content: `Document part A.\n${filler(22_000, "A")}\nReply only "ack A".` }, + { role: "assistant", content: "ack A" }, + { role: "user", content: `Document part B.\n${filler(22_000, "B")}\nReply only "ack B".` }, + { role: "assistant", content: "ack B" }, + { role: "user", content: `Document part C.\n${filler(22_000, "C")}\nIn one short sentence, what kinds of records do these sections describe?` }, + ]; + + // ---- Turn 1: should trigger compaction ---- + console.log("[turn 1] sending large history with compaction enabled..."); + const r1 = await generateText({ + model: anthropic(MODEL), + messages: history, + providerOptions: compactionOptions, + }); + + console.log("[turn 1] iterations:", JSON.stringify(iterations(r1.providerMetadata))); + + const responseMsgs = r1.response.messages as ModelMessage[]; + const compactionPart = responseMsgs.map(findCompactionPart).find(Boolean); + + if (!compactionPart) { + console.error( + "\n[FAIL] No compaction block in the response. Compaction did not trigger.\n" + + " (Increase filler size, lower the trigger, or check the beta header.)", + ); + console.error("[turn 1] response provider metadata:", JSON.stringify(r1.providerMetadata, null, 2)); + process.exit(2); + } + console.log("\n[turn 1] ✓ compaction block present in response."); + console.log("[turn 1] compaction part (pre round-trip):"); + console.log(JSON.stringify(compactionPart, null, 2).slice(0, 1200)); + + // ---- Round-trip the assistant response through convex-agent storage ---- + console.log("\n[round-trip] serializeMessage -> toModelMessage on each response message..."); + const restored: ModelMessage[] = []; + for (const m of responseMsgs) restored.push(await roundTrip(m)); + + const restoredCompaction = restored.map(findCompactionPart).find(Boolean); + if (!restoredCompaction) { + console.error("\n[FAIL] Compaction block LOST during convex-agent serialize/deserialize round-trip."); + process.exit(3); + } + console.log("[round-trip] ✓ compaction metadata survived storage round-trip."); + console.log("[round-trip] compaction part (post round-trip):"); + console.log(JSON.stringify(restoredCompaction, null, 2).slice(0, 1200)); + + const before = JSON.stringify(compactionPart); + const after = JSON.stringify(restoredCompaction); + console.log(`[round-trip] byte-identical before/after: ${before === after ? "YES" : "NO (inspect above)"}`); + + // ---- Turn 2: resend full history + round-tripped compaction block ---- + // If the block is honored, Anthropic replaces the compacted span with the + // summary -> turn-2 input_tokens should be far below turn-1's raw history. + console.log("\n[turn 2] resending history + round-tripped compaction block + new question..."); + const turn2: ModelMessage[] = [ + ...history, + ...restored, + { role: "user", content: "Thanks. Reply with just the single word: OK." }, + ]; + const r2 = await generateText({ + model: anthropic(MODEL), + messages: turn2, + providerOptions: compactionOptions, + }); + const t2iters = iterations(r2.providerMetadata); + console.log("[turn 2] iterations:", JSON.stringify(t2iters)); + + // If the round-tripped compaction block is honored, the API replaces the + // already-compacted span with the stored summary instead of re-compacting it. + // Tell: turn-2 has NO fresh compaction iteration over the big (~115k) history. + const t1msgIn = iterations(r1.providerMetadata).find((i) => i.type === "message")?.inputTokens ?? Number.NaN; + const bigRecompaction = t2iters.find( + (i) => i.type === "compaction" && (i.inputTokens ?? 0) > 50_000, + ); + const t2msgIn = t2iters.find((i) => i.type === "message")?.inputTokens ?? (r2.usage.inputTokens ?? Number.NaN); + console.log(`\n[verdict] turn-1 message input=${t1msgIn}, turn-2 message input=${t2msgIn}`); + console.log(`[verdict] turn-2 re-compacted the full history? ${bigRecompaction ? "YES (block NOT honored)" : "no"}`); + const reApplied = !bigRecompaction && Number.isFinite(t2msgIn) && t2msgIn < 50_000; + + console.log("\n=== RESULT ==="); + console.log("compaction triggered: ✓"); + console.log(`block survived storage round-trip: ${restoredCompaction ? "✓" : "✗"}`); + console.log( + `summary re-applied on resend: ${reApplied ? "✓ (no re-compaction; small message input)" : "✗ (history was re-compacted — block not honored)"}`, + ); + if (restoredCompaction && reApplied) { + console.log("\n✅ PASS: proper Anthropic compaction works end-to-end through convex-agent storage."); + process.exit(0); + } + console.log("\n⚠️ Partial: storage round-trip preserves the block, but re-application on resend was not observed — inspect iterations above."); + process.exit(restoredCompaction ? 0 : 4); +} + +main().catch((err) => { + console.error("\n[ERROR]", err); + process.exit(1); +}); From 9db782a5eb7884211aedb9dea575ad394010730b Mon Sep 17 00:00:00 2001 From: Paul H Date: Wed, 24 Jun 2026 09:17:42 +0100 Subject: [PATCH 2/6] Add opt-in compaction to context options Set contextOptions.compaction to summarize earlier context via Anthropic's compact_20260112, and widen the recent-message window so history reaches the trigger. --- src/client/compaction.test.ts | 82 +++++++++++++++++++++++++++++++++++ src/client/search.ts | 7 ++- src/client/start.ts | 64 +++++++++++++++++++++++++-- src/client/types.ts | 23 +++++++++- src/shared.ts | 9 ++++ 5 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 src/client/compaction.test.ts diff --git a/src/client/compaction.test.ts b/src/client/compaction.test.ts new file mode 100644 index 00000000..330922c4 --- /dev/null +++ b/src/client/compaction.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, test } from "vitest"; +import { withCompaction } from "./start.js"; +import { DEFAULT_COMPACTION_TRIGGER_TOKENS } from "../shared.js"; + +describe("withCompaction", () => { + test("adds a compact edit with the default trigger when none exist", () => { + const result = withCompaction(undefined, {}); + expect(result.anthropic?.contextManagement).toEqual({ + edits: [ + { + type: "compact_20260112", + trigger: { + type: "input_tokens", + value: DEFAULT_COMPACTION_TRIGGER_TOKENS, + }, + }, + ], + }); + }); + + test("honors a custom trigger and instructions", () => { + const result = withCompaction(undefined, { + triggerTokens: 80_000, + instructions: "Keep the decisions.", + }); + expect(result.anthropic?.contextManagement).toEqual({ + edits: [ + { + type: "compact_20260112", + trigger: { type: "input_tokens", value: 80_000 }, + instructions: "Keep the decisions.", + }, + ], + }); + }); + + test("preserves other providers and other anthropic keys", () => { + const result = withCompaction( + { openai: { foo: "bar" }, anthropic: { thinking: { type: "enabled" } } }, + {}, + ); + expect(result.openai).toEqual({ foo: "bar" }); + expect(result.anthropic?.thinking).toEqual({ type: "enabled" }); + expect( + (result.anthropic?.contextManagement as { edits: unknown[] }).edits, + ).toHaveLength(1); + }); + + test("does not double-add when a compact edit is already present", () => { + const existing = { + anthropic: { + contextManagement: { + edits: [{ type: "compact_20260112", trigger: { type: "input_tokens", value: 10 } }], + }, + }, + }; + const result = withCompaction(existing, { triggerTokens: 99_999 }); + const edits = (result.anthropic?.contextManagement as { edits: unknown[] }) + .edits; + expect(edits).toHaveLength(1); + // keeps the existing edit untouched (does not overwrite with 99_999) + expect(edits[0]).toEqual({ + type: "compact_20260112", + trigger: { type: "input_tokens", value: 10 }, + }); + }); + + test("appends alongside a different existing edit (e.g. clear_tool_uses)", () => { + const existing = { + anthropic: { + contextManagement: { + edits: [{ type: "clear_tool_uses_20250919" }], + }, + }, + }; + const result = withCompaction(existing, {}); + const edits = (result.anthropic?.contextManagement as { edits: unknown[] }) + .edits; + expect(edits).toHaveLength(2); + expect((edits[1] as { type: string }).type).toBe("compact_20260112"); + }); +}); diff --git a/src/client/search.ts b/src/client/search.ts index 5fda77ee..4eb4428c 100644 --- a/src/client/search.ts +++ b/src/client/search.ts @@ -10,6 +10,7 @@ import { type VectorDimension, } from "../component/vector/tables.js"; import { + DEFAULT_COMPACTION_RECENT_MESSAGES, DEFAULT_MESSAGE_RANGE, DEFAULT_RECENT_MESSAGES, extractText, @@ -142,7 +143,11 @@ export async function fetchRecentAndSearchMessages( threadId: args.threadId, excludeToolMessages: opts.excludeToolMessages, paginationOpts: { - numItems: opts.recentMessages ?? DEFAULT_RECENT_MESSAGES, + numItems: + opts.recentMessages ?? + (opts.compaction + ? DEFAULT_COMPACTION_RECENT_MESSAGES + : DEFAULT_RECENT_MESSAGES), cursor: null, }, upToAndIncludingMessageId: targetMessageId, diff --git a/src/client/start.ts b/src/client/start.ts index b09ee766..7c5f72f7 100644 --- a/src/client/start.ts +++ b/src/client/start.ts @@ -14,9 +14,20 @@ import { serializeObjectResult, } from "../mapping.js"; import { embedMessages, fetchContextWithPrompt } from "./search.js"; -import type { ActionCtx, AgentComponent, Config, Options } from "./types.js"; -import type { Message, MessageDoc } from "../validators.js"; +import type { + ActionCtx, + AgentComponent, + CompactionOptions, + Config, + Options, +} from "./types.js"; +import type { + Message, + MessageDoc, + ProviderOptions, +} from "../validators.js"; import { + DEFAULT_COMPACTION_TRIGGER_TOKENS, getModelName, getProviderName, type ModelOrMetadata, @@ -27,6 +38,46 @@ import { assert, omit } from "convex-helpers"; import { saveInputMessages } from "./saveInputMessages.js"; import type { GenericActionCtx, GenericDataModel } from "convex/server"; +/** + * Merge an Anthropic `compact_20260112` context-management edit into the + * existing provider options without clobbering other providers, other anthropic + * keys, or an already-present compact edit. + */ +export function withCompaction( + providerOptions: ProviderOptions | undefined, + compaction: CompactionOptions, +): NonNullable { + const anthropic = { ...((providerOptions?.anthropic ?? {}) as Record) }; + const contextManagement = { + ...((anthropic.contextManagement ?? {}) as { edits?: unknown[] }), + }; + const existingEdits = Array.isArray(contextManagement.edits) + ? contextManagement.edits + : []; + const hasCompact = existingEdits.some( + (e) => (e as { type?: string } | null)?.type === "compact_20260112", + ); + const edits = hasCompact + ? existingEdits + : [ + ...existingEdits, + { + type: "compact_20260112", + trigger: { + type: "input_tokens", + value: compaction.triggerTokens ?? DEFAULT_COMPACTION_TRIGGER_TOKENS, + }, + ...(compaction.instructions + ? { instructions: compaction.instructions } + : {}), + }, + ]; + return { + ...providerOptions, + anthropic: { ...anthropic, contextManagement: { ...contextManagement, edits } }, + } as NonNullable; +} + export async function startGeneration< T, Tools extends ToolSet = ToolSet, @@ -185,10 +236,17 @@ export async function startGeneration< agent: opts.agentForToolCtx, } satisfies ToolCtx; const tools = wrapTools(toolCtx, args.tools) as Tools; + const baseProviderOptions = + (args as { providerOptions?: ProviderOptions }).providerOptions ?? + opts.providerOptions; + const compaction = opts.contextOptions?.compaction; + const providerOptions = compaction + ? withCompaction(baseProviderOptions, compaction) + : baseProviderOptions; const aiArgs = { ...opts.callSettings, - providerOptions: opts.providerOptions, ...omit(args, ["promptMessageId", "messages", "prompt"]), + providerOptions, model, messages: context.messages, stopWhen: diff --git a/src/client/types.ts b/src/client/types.ts index 02487a0b..1209af4e 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -184,6 +184,18 @@ export type Config = { * Options to configure what messages are fetched as context, * automatically with thread.generateText, or directly via search. */ +export type CompactionOptions = { + /** + * Input-token threshold at which the model compacts earlier context. + * Default: 50_000 (the documented minimum for `compact_20260112`). + */ + triggerTokens?: number; + /** + * Optional instructions steering what the compaction summary should preserve. + */ + instructions?: string; +}; + export type ContextOptions = { /** * Whether to include tool messages in the context. @@ -193,9 +205,18 @@ export type ContextOptions = { /** * How many recent messages to include. These are added after the search * messages, and do not count against the search limit. - * Default: 100 + * Default: 100 (or, when `compaction` is enabled and this is left unset, a + * larger window so the full history can reach the compaction trigger). */ recentMessages?: number; + /** + * Opt in to Anthropic server-side compaction. When set, the model summarizes + * earlier context once the thread crosses the token trigger, and the summary + * is stored and replayed on later turns instead of re-summarizing. Requires + * an Anthropic model (passed through as `providerOptions.anthropic`; ignored + * by other providers). Leave unset to keep the existing truncation behavior. + */ + compaction?: CompactionOptions; /** * Options for searching messages. */ diff --git a/src/shared.ts b/src/shared.ts index 79452e53..491f0d4d 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -17,6 +17,15 @@ import type { Message, MessageContentParts } from "./validators.js"; export const DEFAULT_RECENT_MESSAGES = 100; +/** Default input-token trigger for Anthropic compaction (documented minimum). */ +export const DEFAULT_COMPACTION_TRIGGER_TOKENS = 50_000; +/** + * Recent-message window used when compaction is enabled but `recentMessages` is + * left unset: large enough for the full history to reach the compaction trigger + * and to retain the stored compaction summary on later turns. + */ +export const DEFAULT_COMPACTION_RECENT_MESSAGES = 1000; + export function isTool(message: Message | ModelMessage) { return ( message.role === "tool" || From 84dacf3c7f4582853202e6f25582b38747aeaee4 Mon Sep 17 00:00:00 2001 From: Paul H Date: Wed, 24 Jun 2026 09:50:12 +0100 Subject: [PATCH 3/6] Address review feedback on compaction - Only widen the recent-message window for Anthropic models (decided in startGeneration where the provider is known), keeping search.ts provider-agnostic. - Validate compaction.triggerTokens is an integer >= 50000 before building the edit. - Make verify:compaction fail on a round-trip mismatch and use the trigger constant for recompaction detection. --- scripts/verify-compaction.ts | 9 +++++++-- src/client/compaction.test.ts | 12 ++++++++++++ src/client/search.ts | 7 +------ src/client/start.ts | 34 ++++++++++++++++++++++++++++++---- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/scripts/verify-compaction.ts b/scripts/verify-compaction.ts index 08b5bdaa..a5819060 100644 --- a/scripts/verify-compaction.ts +++ b/scripts/verify-compaction.ts @@ -131,7 +131,12 @@ async function main() { const before = JSON.stringify(compactionPart); const after = JSON.stringify(restoredCompaction); - console.log(`[round-trip] byte-identical before/after: ${before === after ? "YES" : "NO (inspect above)"}`); + const byteIdentical = before === after; + console.log(`[round-trip] byte-identical before/after: ${byteIdentical ? "YES" : "NO"}`); + if (!byteIdentical) { + console.error("\n[FAIL] Compaction block changed during the storage round-trip (expected byte-identical)."); + process.exit(4); + } // ---- Turn 2: resend full history + round-tripped compaction block ---- // If the block is honored, Anthropic replaces the compacted span with the @@ -155,7 +160,7 @@ async function main() { // Tell: turn-2 has NO fresh compaction iteration over the big (~115k) history. const t1msgIn = iterations(r1.providerMetadata).find((i) => i.type === "message")?.inputTokens ?? Number.NaN; const bigRecompaction = t2iters.find( - (i) => i.type === "compaction" && (i.inputTokens ?? 0) > 50_000, + (i) => i.type === "compaction" && (i.inputTokens ?? 0) >= TRIGGER_TOKENS, ); const t2msgIn = t2iters.find((i) => i.type === "message")?.inputTokens ?? (r2.usage.inputTokens ?? Number.NaN); console.log(`\n[verdict] turn-1 message input=${t1msgIn}, turn-2 message input=${t2msgIn}`); diff --git a/src/client/compaction.test.ts b/src/client/compaction.test.ts index 330922c4..8fda40dd 100644 --- a/src/client/compaction.test.ts +++ b/src/client/compaction.test.ts @@ -65,6 +65,18 @@ describe("withCompaction", () => { }); }); + test("rejects a triggerTokens below the 50k minimum", () => { + expect(() => withCompaction(undefined, { triggerTokens: 1000 })).toThrow( + /at least|>= 50000|integer/i, + ); + }); + + test("rejects a non-integer triggerTokens", () => { + expect(() => withCompaction(undefined, { triggerTokens: 50_000.5 })).toThrow( + /integer/i, + ); + }); + test("appends alongside a different existing edit (e.g. clear_tool_uses)", () => { const existing = { anthropic: { diff --git a/src/client/search.ts b/src/client/search.ts index 4eb4428c..5fda77ee 100644 --- a/src/client/search.ts +++ b/src/client/search.ts @@ -10,7 +10,6 @@ import { type VectorDimension, } from "../component/vector/tables.js"; import { - DEFAULT_COMPACTION_RECENT_MESSAGES, DEFAULT_MESSAGE_RANGE, DEFAULT_RECENT_MESSAGES, extractText, @@ -143,11 +142,7 @@ export async function fetchRecentAndSearchMessages( threadId: args.threadId, excludeToolMessages: opts.excludeToolMessages, paginationOpts: { - numItems: - opts.recentMessages ?? - (opts.compaction - ? DEFAULT_COMPACTION_RECENT_MESSAGES - : DEFAULT_RECENT_MESSAGES), + numItems: opts.recentMessages ?? DEFAULT_RECENT_MESSAGES, cursor: null, }, upToAndIncludingMessageId: targetMessageId, diff --git a/src/client/start.ts b/src/client/start.ts index 7c5f72f7..1e151a0f 100644 --- a/src/client/start.ts +++ b/src/client/start.ts @@ -27,6 +27,7 @@ import type { ProviderOptions, } from "../validators.js"; import { + DEFAULT_COMPACTION_RECENT_MESSAGES, DEFAULT_COMPACTION_TRIGGER_TOKENS, getModelName, getProviderName, @@ -47,6 +48,17 @@ export function withCompaction( providerOptions: ProviderOptions | undefined, compaction: CompactionOptions, ): NonNullable { + const triggerTokens = + compaction.triggerTokens ?? DEFAULT_COMPACTION_TRIGGER_TOKENS; + // compact_20260112 requires an integer trigger of at least 50_000. + if ( + !Number.isInteger(triggerTokens) || + triggerTokens < DEFAULT_COMPACTION_TRIGGER_TOKENS + ) { + throw new Error( + `compaction.triggerTokens must be an integer >= ${DEFAULT_COMPACTION_TRIGGER_TOKENS}, got ${triggerTokens}`, + ); + } const anthropic = { ...((providerOptions?.anthropic ?? {}) as Record) }; const contextManagement = { ...((anthropic.contextManagement ?? {}) as { edits?: unknown[] }), @@ -63,10 +75,7 @@ export function withCompaction( ...existingEdits, { type: "compact_20260112", - trigger: { - type: "input_tokens", - value: compaction.triggerTokens ?? DEFAULT_COMPACTION_TRIGGER_TOKENS, - }, + trigger: { type: "input_tokens", value: triggerTokens }, ...(compaction.instructions ? { instructions: compaction.instructions } : {}), @@ -175,8 +184,25 @@ export async function startGeneration< ?.userId) ?? undefined; + // Compaction widens the recent-message window so history can reach the + // trigger — but only for Anthropic models, since the feature is Anthropic-only + // and a non-Anthropic run shouldn't pay for the larger reads. + const resolvedModel = args.model ?? opts.languageModel; + const providerName = + typeof resolvedModel === "string" + ? resolvedModel.split("/").at(0) + : resolvedModel?.provider; + const useCompactionWindow = + !!opts.contextOptions?.compaction && + !!providerName?.startsWith("anthropic") && + opts.contextOptions?.recentMessages === undefined; + const contextOptions = useCompactionWindow + ? { ...opts.contextOptions, recentMessages: DEFAULT_COMPACTION_RECENT_MESSAGES } + : opts.contextOptions; + const context = await fetchContextWithPrompt(ctx, component, { ...opts, + contextOptions, userId, threadId, messages: args.messages, From 8ecb8854df60eb9936aedd118b5d6269933af5d5 Mon Sep 17 00:00:00 2001 From: Paul H Date: Wed, 24 Jun 2026 09:50:12 +0100 Subject: [PATCH 4/6] Bump ai to 6.0.209 with matching provider-utils and provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ai 6.0.209 needs @ai-sdk/provider-utils 4.0.30 and @ai-sdk/provider 3.0.10; bumping ai alone duplicates provider-utils and breaks the build with mismatched schema types. Dev deps only — peer ranges are unchanged. --- package-lock.json | 124 ++++++++++++++++++++++++++++++++++------------ package.json | 10 ++-- 2 files changed, 98 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40965b28..eee5c9b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ "@ai-sdk/google": "3.0.30", "@ai-sdk/groq": "3.0.8", "@ai-sdk/openai": "3.0.10", - "@ai-sdk/provider": "3.0.3", - "@ai-sdk/provider-utils": "4.0.6", + "@ai-sdk/provider": "^3.0.10", + "@ai-sdk/provider-utils": "^4.0.30", "@convex-dev/eslint-plugin": "^2.0.0", "@convex-dev/rag": "0.7.5", "@convex-dev/rate-limiter": "0.3.2", @@ -34,7 +34,7 @@ "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "4.7.0", - "ai": "6.0.35", + "ai": "^6.0.209", "autoprefixer": "10.4.27", "chokidar-cli": "3.0.0", "class-variance-authority": "0.7.1", @@ -176,15 +176,15 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.14.tgz", - "integrity": "sha512-udVpkDaQ00jMcBvtGGvmkEBU31XidsHB4E8HIF9l7/H7lyjOS/EtXzN2adoupDg5j1/VjjSI3Ny5P1zHUvLyMA==", + "version": "3.0.134", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.134.tgz", + "integrity": "sha512-PfUkQp01EihEyNM6e+nexXmVANiY5KcHMui/MmaHsC4KVVQYh/MwPeOsD3bTPu63e581BKO+AwaPPNmD4x943A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "3.0.3", - "@ai-sdk/provider-utils": "4.0.6", - "@vercel/oidc": "3.1.0" + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.30", + "@vercel/oidc": "3.2.0" }, "engines": { "node": ">=18" @@ -193,16 +193,6 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/gateway/node_modules/@vercel/oidc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", - "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 20" - } - }, "node_modules/@ai-sdk/google": { "version": "3.0.30", "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.30.tgz", @@ -268,6 +258,37 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/groq/node_modules/@ai-sdk/provider": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.3.tgz", + "integrity": "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/groq/node_modules/@ai-sdk/provider-utils": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.6.tgz", + "integrity": "sha512-o/SP1GQOrpXAzHjMosPHI0Pu+YkwxIMndSjSLrEXtcVixdrjqrGaA9I7xJcWf+XpRFJ9byPHrKYnprwS+36gMg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.3", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/openai": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.10.tgz", @@ -285,7 +306,7 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/provider": { + "node_modules/@ai-sdk/openai/node_modules/@ai-sdk/provider": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.3.tgz", "integrity": "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg==", @@ -298,7 +319,7 @@ "node": ">=18" } }, - "node_modules/@ai-sdk/provider-utils": { + "node_modules/@ai-sdk/openai/node_modules/@ai-sdk/provider-utils": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.6.tgz", "integrity": "sha512-o/SP1GQOrpXAzHjMosPHI0Pu+YkwxIMndSjSLrEXtcVixdrjqrGaA9I7xJcWf+XpRFJ9byPHrKYnprwS+36gMg==", @@ -316,6 +337,37 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.10.tgz", + "integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.30", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.30.tgz", + "integrity": "sha512-VO7I+vPffqI5sMnPoUq5DCSqKIgQIk/naJWRdQVpz2ma2zoprC/lqiJiUEl2s6DfvTD76TbhD3q39ROjlA6rGw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.10", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -3742,6 +3794,16 @@ "devOptional": true, "license": "ISC" }, + "node_modules/@vercel/oidc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", + "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -3900,16 +3962,16 @@ } }, "node_modules/ai": { - "version": "6.0.35", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.35.tgz", - "integrity": "sha512-MxgtU6CjnegH1rhRfomM0gptKxP6r+9sxbLvYq36C1l85+o0LacqbXLdNVYzqab+lHN4q7ZP3QS8wZq4YkZahA==", + "version": "6.0.209", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.209.tgz", + "integrity": "sha512-NE7uErmxOe+i77BPT1+AhW4wr7xzilbQLsKVmvGEyil5AjaF6C/m92hlPI4Xwtnv5n77D/29bizwcR27/S/gSQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "3.0.14", - "@ai-sdk/provider": "3.0.3", - "@ai-sdk/provider-utils": "4.0.6", - "@opentelemetry/api": "1.9.0" + "@ai-sdk/gateway": "3.0.134", + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.30", + "@opentelemetry/api": "^1.9.0" }, "engines": { "node": ">=18" @@ -6221,9 +6283,9 @@ "peer": true }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 9aeeda00..3c85ebb3 100644 --- a/package.json +++ b/package.json @@ -82,12 +82,12 @@ } }, "devDependencies": { - "@ai-sdk/anthropic": "^3.0.62", + "@ai-sdk/anthropic": "3.0.62", "@ai-sdk/google": "3.0.30", "@ai-sdk/groq": "3.0.8", "@ai-sdk/openai": "3.0.10", - "@ai-sdk/provider": "3.0.3", - "@ai-sdk/provider-utils": "4.0.6", + "@ai-sdk/provider": "3.0.10", + "@ai-sdk/provider-utils": "4.0.30", "@convex-dev/eslint-plugin": "^2.0.0", "@convex-dev/rag": "0.7.5", "@convex-dev/rate-limiter": "0.3.2", @@ -107,7 +107,7 @@ "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "4.7.0", - "ai": "6.0.35", + "ai": "6.0.209", "autoprefixer": "10.4.27", "chokidar-cli": "3.0.0", "class-variance-authority": "0.7.1", @@ -138,7 +138,7 @@ "tailwind-merge": "3.5.0", "tailwindcss": "3.4.19", "tailwindcss-animate": "1.0.7", - "tsx": "^4.22.4", + "tsx": "4.22.4", "typescript": "5.9.3", "typescript-eslint": "8.57.1", "vite": "6.4.3", From 98aa3697241ce66b697128ffb4bab871dfac8b26 Mon Sep 17 00:00:00 2001 From: Paul H Date: Wed, 24 Jun 2026 10:01:30 +0100 Subject: [PATCH 5/6] Update changelog for compaction and dependency bumps --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ff4e1b..f44e622e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +- Add opt-in Anthropic context compaction via `contextOptions.compaction` + (`triggerTokens` / `instructions`); summarizes earlier context server-side and + replays the summary on later turns. Widens the recent-message window for + Anthropic runs so history can reach the trigger. (#287) +- Bump `@ai-sdk/anthropic` to 3.0.62 (adds `compact_20260112`) and `ai` to + 6.0.209, with matching `@ai-sdk/provider-utils` 4.0.30 / `@ai-sdk/provider` + 3.0.10. + ## 0.6.4 - Fix streaming UI message dedupe (#281) From e8b0530c05ceff2b84119bdc6efc0a13d2430c6f Mon Sep 17 00:00:00 2001 From: Paul H Date: Wed, 24 Jun 2026 10:06:49 +0100 Subject: [PATCH 6/6] Split triggerTokens validation into distinct integer and bound errors Lets the unit tests assert the specific failure mode instead of matching a shared message. --- src/client/compaction.test.ts | 5 +++-- src/client/start.ts | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/client/compaction.test.ts b/src/client/compaction.test.ts index 8fda40dd..a7ae0748 100644 --- a/src/client/compaction.test.ts +++ b/src/client/compaction.test.ts @@ -66,14 +66,15 @@ describe("withCompaction", () => { }); test("rejects a triggerTokens below the 50k minimum", () => { + // 1000 is a valid integer, so this must fail on the lower-bound check only. expect(() => withCompaction(undefined, { triggerTokens: 1000 })).toThrow( - /at least|>= 50000|integer/i, + /at least 50000/, ); }); test("rejects a non-integer triggerTokens", () => { expect(() => withCompaction(undefined, { triggerTokens: 50_000.5 })).toThrow( - /integer/i, + /must be an integer/, ); }); diff --git a/src/client/start.ts b/src/client/start.ts index 1e151a0f..7613b17f 100644 --- a/src/client/start.ts +++ b/src/client/start.ts @@ -51,12 +51,14 @@ export function withCompaction( const triggerTokens = compaction.triggerTokens ?? DEFAULT_COMPACTION_TRIGGER_TOKENS; // compact_20260112 requires an integer trigger of at least 50_000. - if ( - !Number.isInteger(triggerTokens) || - triggerTokens < DEFAULT_COMPACTION_TRIGGER_TOKENS - ) { + if (!Number.isInteger(triggerTokens)) { throw new Error( - `compaction.triggerTokens must be an integer >= ${DEFAULT_COMPACTION_TRIGGER_TOKENS}, got ${triggerTokens}`, + `compaction.triggerTokens must be an integer, got ${triggerTokens}`, + ); + } + if (triggerTokens < DEFAULT_COMPACTION_TRIGGER_TOKENS) { + throw new Error( + `compaction.triggerTokens must be at least ${DEFAULT_COMPACTION_TRIGGER_TOKENS}, got ${triggerTokens}`, ); } const anthropic = { ...((providerOptions?.anthropic ?? {}) as Record) };