From 586f164f60c542cd1aa7a5f2403811e5cd53a4b6 Mon Sep 17 00:00:00 2001 From: "upstash-tag[bot]" <313023939+upstash-tag[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:13:33 +0000 Subject: [PATCH 1/3] fix!: rebuild eve extension against eve 0.45 and raise its peer floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `eve extension build` stamps the compatibility manifest with the building eve's current contribution-format versions, and a consumer rejects any version missing from its own supported list. Rebuilding on eve 0.45.0 re-stamps dist/extension/_manifest.json from tool 17 / dynamicTool 18 to tool 18 / dynamicTool 19, and 0.45.0 is the first eve supporting either — every 0.42 through 0.44.4 tops out at tool 17 / dynamicTool 18. So the extension's ">=0.43.0" peer floor no longer describes the built dist: on eve 0.44.3 discovery hard-fails with "requires tool contract v18, but this eve supports tool contract versions: ... v17". - bump eve to ^0.45.0 across packages/eve, packages/eve-extension and both eve demos; the rebuilt manifest stamps formatVersion 2 with tool 18 / dynamicTool 19 / hook 14 / instructions 2 (loads on eve >= 0.45.0 only — 0.45 raised both contracts it stamps, so unlike the 0.44 rebuild there is no back-compat window) - raise the extension's eve peer from ">=0.43.0" to ">=0.45.0" so an incompatible eve fails at install time with an actionable ERESOLVE instead of at eve build - leave packages/eve's eve peer at ">=0.32.0": its source still typechecks cleanly against eve 0.32.0, so no floor change is warranted No source changes were needed. eve 0.45 removed eve/tools/defaults and the defineBashTool/defineReadFileTool/defineWriteFileTool/defineGlobTool/ defineGrepTool factories, and dropped experimental.subagentPersistentSessions (subagent contracts 1 and 2 are now dropped) — AgentKit uses none of them; it imports only defineTool/defineDynamic/SessionContext/ToolContext/ ToolDefinition from eve/tools, defineHook from eve/hooks and defineExtension from eve/extension. ai stays exact-pinned at 7.0.58 (eve 0.45's peer is still ^7.0.58). Co-Authored-By: Claude Opus 4.8 --- .changeset/eve-045-upgrade.md | 31 +++++++++++++++++++++ CLAUDE.md | 32 ++++++++++++++++------ examples/eve-demo/package.json | 2 +- examples/eve-extension-demo/package.json | 2 +- packages/eve-extension/README.md | 2 +- packages/eve-extension/package.json | 4 +-- packages/eve/package.json | 2 +- pnpm-lock.yaml | 34 ++++++++++++------------ 8 files changed, 78 insertions(+), 31 deletions(-) create mode 100644 .changeset/eve-045-upgrade.md diff --git a/.changeset/eve-045-upgrade.md b/.changeset/eve-045-upgrade.md new file mode 100644 index 0000000..e388b1e --- /dev/null +++ b/.changeset/eve-045-upgrade.md @@ -0,0 +1,31 @@ +--- +"@upstash/agentkit-eve-extension": minor +--- + +fix!: rebuild against eve 0.45 and raise the `eve` peer floor to `>=0.45.0` + +`eve extension build` stamps the compatibility manifest with the *building* eve's +current contribution-format versions, and a consumer rejects any version missing +from its own supported list. Rebuilding on **eve 0.45.0** re-stamps +`dist/extension/_manifest.json` from tool 17 / dynamicTool 18 to +**tool 18 / dynamicTool 19** (hook 14 and instructions 2 are unchanged), and +eve 0.45.0 is the first release supporting either — every 0.42–0.44.4 tops out at +tool 17 / dynamicTool 18. + +- The extension is now built with **eve 0.45.0** (manifest: formatVersion 2, + tool 18 / dynamicTool 19 / hook 14 / instructions 2) and loads on **eve ≥ 0.45.0**. +- The `eve` peer dependency moves from `">=0.43.0"` to `">=0.45.0"` so an + incompatible eve fails at install time rather than at `eve build`, where it + otherwise errors with *"requires tool contract v18, but this eve supports tool + contract versions: … v17"*. + +`@upstash/agentkit-eve`'s `eve` peer stays `>=0.32.0` — its source still +typechecks cleanly against eve 0.32.0, so no floor change is warranted there. + +No runtime behavior and no source changed: all packages build, typecheck and pass +unmodified against eve 0.45.0, both demos build, and the extension's mocked-model +end-to-end eval is green. eve 0.45.0 removed `eve/tools/defaults` and the +`defineBashTool`/`defineReadFileTool`/`defineWriteFileTool`/`defineGlobTool`/ +`defineGrepTool` factories, and dropped `experimental.subagentPersistentSessions` +— AgentKit uses none of them. `ai` stays exact-pinned at `7.0.58` (eve 0.45's peer +is still `^7.0.58`). diff --git a/CLAUDE.md b/CLAUDE.md index dd23c6a..96486a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,7 +98,7 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension). has `"eve": { "extension": { "source": "./extension", "dist": "./dist/extension" } }`, `files` ships **`dist/` only** (compiled `.mjs` + `.d.ts` per contribution, plus `_manifest.json` with `builtWithEve`; eve validates compatibility from the manifest), and `eve` is a **floored peer** - (`">=0.43.0"` — was the scaffold's `"*"` until issue #22; see **Consumer eve version** below). The old 0.24 + (`">=0.45.0"` — was the scaffold's `"*"` until issue #22; see **Consumer eve version** below). The old 0.24 format (`"eve": { "extension": "./extension" }`, ships source the consumer recompiles) is rejected by eve ≥0.25 with "must declare `eve.extension.dist`" — don't regress to it. **No `prepare` script** (an install-time build broke CI: sdk isn't built yet at install; `pnpm build` handles topological order). @@ -111,9 +111,10 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension). npm/yarn hoisted layouts — was fixed upstream in eve 0.25.3; no workaround needed on ≥0.25.3.) **Consumer eve version:** `eve extension build` stamps the manifest's `requires` with the building eve's *current* contribution-format versions, and a consumer rejects any version not in its own - supported list — a dist built with eve 0.44.3 (formatVersion 2; tool 17 / dynamicTool 18 / hook 14 / - instructions 2) needs consumers on **eve ≥0.43** (tool 17 is the binding contract; hook 14 landed in - 0.40). The `eve` peer is **`">=0.43.0"`, not `"*"`** — issue #22 proved the wildcard is a trap: eve + supported list — a dist built with eve 0.45.0 (formatVersion 2; tool 18 / dynamicTool 19 / hook 14 / + instructions 2) needs consumers on **eve ≥0.45** (tool 18 *and* dynamicTool 19 are both binding — + 0.45 is the first eve supporting either; every 0.42–0.44.4 tops out at tool 17 / dynamicTool 18). + The `eve` peer is **`">=0.45.0"`, not `"*"`** — issue #22 proved the wildcard is a trap: eve 0.33 dropped hook contracts ≤9 *nine hours* after 0.32 shipped, so a wildcard install succeeds and then fails at `eve build` with a manifest error. The manifest is still the real compatibility tie; the peer floor is the install-time guard. **On every eve devDep bump: rebuild, read the new @@ -227,7 +228,7 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension). `new Ratelimit()`. ## AI SDK version strategy — IMPORTANT -- **AI SDK v7 stable everywhere.** Every package + demo pins `ai` to exactly **`7.0.58`**. `eve` (0.44) +- **AI SDK v7 stable everywhere.** Every package + demo pins `ai` to exactly **`7.0.58`**. `eve` (0.45) declares `ai` as a **peer** (`^7.0.58`), so the apps/packages provide the single copy. Providers: `@ai-sdk/openai` `^4.0.37`, `@ai-sdk/provider` `^4.0.7`, `@ai-sdk/react` `^4.0.62` (all stable ranges; bump them with `pnpm -r update "@ai-sdk/*"` when eve moves — a stale `@ai-sdk/react` range can pin a @@ -277,9 +278,10 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension). `$count`, `$histogram`, `$percentiles`, `$cardinality`. ## Eve framework facts -- The repo is on **`eve@0.44.3`** (peer `>=0.32.0` in `packages/eve` — 0.32 is where the sandbox - `stop()` contract our backend implements landed; peer `>=0.43.0` in the extension, matching the - built dist's manifest — see the eve-extension section). Subpath exports: +- The repo is on **`eve@0.45.0`** (peer `>=0.32.0` in `packages/eve` — 0.32 is where the sandbox + `stop()` contract our backend implements landed, re-verified by typechecking `packages/eve` against + eve 0.32.0; peer `>=0.45.0` in the extension, matching the built dist's manifest — see the + eve-extension section). Subpath exports: `eve/tools`, `eve/hooks`, `eve/extension`, `eve/context`, `eve/instructions`, `eve/sandbox`, `eve/sandbox/vercel`, `eve/channels/*`, `eve/next`, `eve/react`, … - **Breaking changes absorbed on the 0.25 → 0.32 jump:** (a) 0.31 replaced continuation-token session @@ -310,6 +312,20 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension). count matching seed data, cached weather tool, and the extension's dynamic search + chat-history hook/tools writing and reading real `agentkit:chat:demo-user:*` docs); `gpt-5.4-mini` does exist and responds — the "may 404" caveat in Known issues didn't materialize. +- **The 0.44.3 → 0.45.0 bump also needed no source changes** — build, typecheck, both demo builds and + the mocked-model eval all passed unmodified. It was, again, *purely* a manifest/peer-floor move: + rebuilding on 0.45.0 re-stamps **tool 17→18** and **dynamicTool 18→19**, and 0.45.0 is the first eve + supporting either, so the extension peer floor went `>=0.43.0` → **`>=0.45.0`** (a 0.45-built dist + hard-errors at discovery on 0.44.3: *"requires tool contract v18, but this eve supports … v17"*). + Note the floor is now equal to the pinned version — 0.45 raised both contracts it stamps, so there + is no back-compat window at all this time. Why 0.45's headline breaks missed us: built-in tool + definitions moved from `eve/tools/defaults` to per-tool `eve/tools/` subpaths and + `defineBashTool`/`defineReadFileTool`/`defineWriteFileTool`/`defineGlobTool`/`defineGrepTool` were + **removed** — we import only `defineTool`/`defineDynamic`/`SessionContext`/`ToolContext`/ + `ToolDefinition` from `eve/tools`, never `eve/tools/defaults` nor any of those factories; and + `experimental.subagentPersistentSessions` was removed (subagent contract 1 and 2 are now *dropped*, + only 3 is supported) — we contribute no subagents. `ai` stays exact-pinned at `7.0.58` (eve 0.45's + peer is still `^7.0.58`). - **Extension packaging changed 0.24 → 0.25**: 0.24 shipped source the consumer recompiles; 0.25 ships prebuilt `dist/extension` + `_manifest.json` (see the eve-extension section). 0.25 rejects 0.24-format packages at discovery. diff --git a/examples/eve-demo/package.json b/examples/eve-demo/package.json index 538af9f..d7cdd8b 100644 --- a/examples/eve-demo/package.json +++ b/examples/eve-demo/package.json @@ -32,7 +32,7 @@ "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", - "eve": "^0.44.3", + "eve": "^0.45.0", "lucide-react": "1.16.0", "motion": "12.40.0", "nanoid": "5.1.11", diff --git a/examples/eve-extension-demo/package.json b/examples/eve-extension-demo/package.json index a7250f2..e945a49 100644 --- a/examples/eve-extension-demo/package.json +++ b/examples/eve-extension-demo/package.json @@ -19,7 +19,7 @@ "@upstash/redis": "^1.38.0", "@vercel/connect": "0.2.2", "ai": "7.0.58", - "eve": "^0.44.3", + "eve": "^0.45.0", "zod": "4.4.3" }, "devDependencies": { diff --git a/packages/eve-extension/README.md b/packages/eve-extension/README.md index fafc880..7ed753d 100644 --- a/packages/eve-extension/README.md +++ b/packages/eve-extension/README.md @@ -15,7 +15,7 @@ no repeated schemas; upgrades come through the package manager. `` is the mount file's basename — the examples below use `agentkit`. -Start from an eve project (eve ≥ 0.43 — the prebuilt extension's compatibility manifest requires 0.43's contribution formats; the package declares this as its `eve` peer range), then: +Start from an eve project (eve ≥ 0.45 — the prebuilt extension's compatibility manifest requires 0.45's contribution formats; the package declares this as its `eve` peer range), then: ```bash pnpm add @upstash/agentkit-eve-extension diff --git a/packages/eve-extension/package.json b/packages/eve-extension/package.json index d80fb86..12c4fa6 100644 --- a/packages/eve-extension/package.json +++ b/packages/eve-extension/package.json @@ -56,10 +56,10 @@ }, "devDependencies": { "@types/node": "24.x", - "eve": "^0.44.3", + "eve": "^0.45.0", "typescript": "7.0.2" }, "peerDependencies": { - "eve": ">=0.43.0" + "eve": ">=0.45.0" } } diff --git a/packages/eve/package.json b/packages/eve/package.json index e2204f3..cc37bc0 100644 --- a/packages/eve/package.json +++ b/packages/eve/package.json @@ -58,7 +58,7 @@ "@upstash/redis": "^1.38.0", "ai": "7.0.58", "dotenv": "^16.4.5", - "eve": "^0.44.3" + "eve": "^0.45.0" }, "peerDependencies": { "@upstash/box": ">=0.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f5dd27..b0222eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,7 +134,7 @@ importers: version: 1.38.0 '@vercel/connect': specifier: 0.2.2 - version: 0.2.2(@ai-sdk/mcp@2.0.30(zod@4.4.3))(ai@7.0.58(zod@4.4.3))(eve@0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0))) + version: 0.2.2(@ai-sdk/mcp@2.0.30(zod@4.4.3))(ai@7.0.58(zod@4.4.3))(eve@0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0))) ai: specifier: 7.0.58 version: 7.0.58(zod@4.4.3) @@ -148,8 +148,8 @@ importers: specifier: 1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) eve: - specifier: ^0.44.3 - version: 0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) + specifier: ^0.45.0 + version: 0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) lucide-react: specifier: 1.16.0 version: 1.16.0(react@19.2.6) @@ -216,13 +216,13 @@ importers: version: 1.38.0 '@vercel/connect': specifier: 0.2.2 - version: 0.2.2(@ai-sdk/mcp@2.0.30(zod@4.4.3))(ai@7.0.58(zod@4.4.3))(eve@0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0))) + version: 0.2.2(@ai-sdk/mcp@2.0.30(zod@4.4.3))(ai@7.0.58(zod@4.4.3))(eve@0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0))) ai: specifier: 7.0.58 version: 7.0.58(zod@4.4.3) eve: - specifier: ^0.44.3 - version: 0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) + specifier: ^0.45.0 + version: 0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) zod: specifier: 4.4.3 version: 4.4.3 @@ -284,8 +284,8 @@ importers: specifier: ^16.4.5 version: 16.6.1 eve: - specifier: ^0.44.3 - version: 0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) + specifier: ^0.45.0 + version: 0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) packages/eve-extension: dependencies: @@ -303,8 +303,8 @@ importers: specifier: 24.x version: 24.13.2 eve: - specifier: ^0.44.3 - version: 0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.59(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) + specifier: ^0.45.0 + version: 0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.59(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) typescript: specifier: 7.0.2 version: 7.0.2 @@ -3393,15 +3393,15 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eve@0.44.3: - resolution: {integrity: sha512-YepBrHWm2Ecf7kmlPwzNeljTkI+LesZ8vC1q8SKCjgog9tKaZxgyIms7S/R3a8vRoQ8gnAtZuZdxXZgD6eKZbA==} + eve@0.45.0: + resolution: {integrity: sha512-jbx7iYc4IwUx7vzSo619uSxzF5pX6G6RHH+J9kE05VXwdR9Isl65m99PSYSXNhV5/WwJnRR4G7QfHuhvbt5Irw==} engines: {node: '>=24'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 ai: ^7.0.58 braintrust: ^3.0.0 - just-bash: ^3.0.0 + just-bash: ^3.1.0 microsandbox: ^0.5.0 peerDependenciesMeta: '@opentelemetry/api': @@ -7358,13 +7358,13 @@ snapshots: dependencies: execa: 5.1.1 - '@vercel/connect@0.2.2(@ai-sdk/mcp@2.0.30(zod@4.4.3))(ai@7.0.58(zod@4.4.3))(eve@0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)))': + '@vercel/connect@0.2.2(@ai-sdk/mcp@2.0.30(zod@4.4.3))(ai@7.0.58(zod@4.4.3))(eve@0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)))': dependencies: '@vercel/oidc': 3.6.1 optionalDependencies: '@ai-sdk/mcp': 2.0.30(zod@4.4.3) ai: 7.0.58(zod@4.4.3) - eve: 0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) + eve: 0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) '@vercel/oidc@3.2.0': {} @@ -7984,7 +7984,7 @@ snapshots: esutils@2.0.3: {} - eve@0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)): + eve@0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.58(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)): dependencies: ai: 7.0.58(zod@4.4.3) nitro: 3.0.260610-beta(@upstash/redis@1.38.0)(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) @@ -8031,7 +8031,7 @@ snapshots: - xml2js - zephyr-agent - eve@0.44.3(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.59(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)): + eve@0.45.0(@opentelemetry/api@1.9.1)(@upstash/redis@1.38.0)(ai@7.0.59(zod@4.4.3))(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)): dependencies: ai: 7.0.59(zod@4.4.3) nitro: 3.0.260610-beta(@upstash/redis@1.38.0)(chokidar@4.0.3)(dotenv@16.6.1)(jiti@2.7.0)(rollup@4.62.0)(vite@5.4.21(@types/node@20.19.43)(lightningcss@1.32.0)) From a82f1c3327092724b02b97a56aba824526211b66 Mon Sep 17 00:00:00 2001 From: "upstash-tag[bot]" <313023939+upstash-tag[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:06:07 +0000 Subject: [PATCH 2/3] docs: fix stale eve peer-floor note, document known live-Redis test flakiness --- CLAUDE.md | 18 ++++++++++++++++++ packages/eve-extension/AGENTS.md | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 96486a1..f6b39aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -261,6 +261,24 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension). reuse indexes and run sequentially. There is **no** `SEARCH.LIST` command to enumerate them. - **Indexing is async.** After writing (`redis.json.set`), call `searchIndex.waitIndexing()` before reading/asserting. Demos do the same. +- **`waitIndexing()` is not a hard barrier — two suites flake because of it.** A just-written doc can + still be missing from the next query even after it returns, so any assertion of the form + "write, then immediately expect N search hits" is racy. Two are known-flaky **on `main` as well as + on feature branches**, and they red-lit CI on both the 2026-08-26 nightly (`main`, eve 0.44.3) and + PR #27 with byte-identical messages: + - `packages/sdk/src/chat-history.test.ts` › "lists a user's chats" → `expected [ 'c1' ] to include 'c2'` + - `packages/eve/src/search-tools.test.ts` › "search runs a $smart query, creating the index + reactively" → `expected 0 to be greater than 0` / `expected 1 to be greater than or equal to 2` + + Both pass on re-run with no code change (measured ~1-in-2 to ~1-in-6 locally, on *both* branches). + **A red `Test` step on these two is not a regression — re-run the job before investigating**, and + don't "fix" it by loosening the assertion. A real fix means polling for the expected result (or + vitest `retry`), which nobody has done yet. +- **Throwaway DBs from `upstash start-redis` (the `@upstash/cli` command; `npm i -g @upstash/cli`) + cap at *one* search index**, not 10 — `ERR Exceeded max index count of 1`. A single `pnpm test` + cascades into bogus create-index failures on one. Run **one test file at a time** with a `FLUSHDB` + between (`curl "$URL" -H "Authorization: Bearer $TOKEN" -d '["FLUSHDB"]'`; FLUSHDB does drop + indexes, and `SEARCH.DROP ` is the only other lever — there is no list command). - Scores are **BM25 (unbounded)**, not `[0,1]` — `minScore` thresholds are BM25 values. - `.env` is gitignored — **never commit creds.** Needs `UPSTASH_REDIS_REST_URL`/`_TOKEN`; optionally `OPENAI_API_KEY` and `UPSTASH_BOX_API_KEY`. diff --git a/packages/eve-extension/AGENTS.md b/packages/eve-extension/AGENTS.md index 875b8d2..1d8a38e 100644 --- a/packages/eve-extension/AGENTS.md +++ b/packages/eve-extension/AGENTS.md @@ -32,7 +32,7 @@ unavailable, use https://eve.dev/docs/extensions as a fallback. agent-shaped source tree into `dist/extension/`, emits type declarations and a compatibility manifest, and fills the package `exports` map. Ship `dist/` only. `eve` is a required peer so the consumer's eve is the one that runs, but NOT a -wildcard: keep the floor (`>=0.43.0`) in sync with what the built manifest's +wildcard: keep the floor (`>=0.45.0`) in sync with what the built manifest's contracts require, so an incompatible eve fails at install instead of at `eve build` (see issue #22). eve validates the real compatibility from the generated manifest. From 128afde4d174500e05630762b3f284343578ed5e Mon Sep 17 00:00:00 2001 From: "upstash-tag[bot]" <313023939+upstash-tag[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:50:36 +0000 Subject: [PATCH 3/3] test: fix flaky live-Redis search tests (provision index before seeding, poll for async indexing) --- CLAUDE.md | 35 +++++++++++-------- packages/eve/src/search-tools.test.ts | 49 +++++++++++++++++++++++---- packages/sdk/src/chat-history.test.ts | 33 +++++++++++++++++- 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f6b39aa..a8309af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -259,21 +259,28 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension). - vitest: `fileParallelism: false`, `testTimeout: 30_000`. - **Upstash DB caps at 10 search indexes** (`ERR Exceeded max index count of 10`). Tests must `drop()` / reuse indexes and run sequentially. There is **no** `SEARCH.LIST` command to enumerate them. -- **Indexing is async.** After writing (`redis.json.set`), call `searchIndex.waitIndexing()` before - reading/asserting. Demos do the same. -- **`waitIndexing()` is not a hard barrier — two suites flake because of it.** A just-written doc can - still be missing from the next query even after it returns, so any assertion of the form - "write, then immediately expect N search hits" is racy. Two are known-flaky **on `main` as well as - on feature branches**, and they red-lit CI on both the 2026-08-26 nightly (`main`, eve 0.44.3) and - PR #27 with byte-identical messages: - - `packages/sdk/src/chat-history.test.ts` › "lists a user's chats" → `expected [ 'c1' ] to include 'c2'` - - `packages/eve/src/search-tools.test.ts` › "search runs a $smart query, creating the index - reactively" → `expected 0 to be greater than 0` / `expected 1 to be greater than or equal to 2` +- **Indexing is async, and visibility needs BOTH halves of a rule.** To assert on a search result you + must satisfy *both*, or the docs may never become visible — not "late", **never**: + 1. **The index must already exist when the doc is written.** A doc written while the index is still + missing can be dropped by the create-time backfill permanently. Anything relying on the reactive + create (`ReactiveSearchIndex` provisioning on the first read) to pick up docs seeded *just* + beforehand is a coin flip. + 2. **`searchIndex.waitIndexing()` must be called after the write**, on that already-existing index. - Both pass on re-run with no code change (measured ~1-in-2 to ~1-in-6 locally, on *both* branches). - **A red `Test` step on these two is not a regression — re-run the job before investigating**, and - don't "fix" it by loosening the assertion. A real fix means polling for the expected result (or - vitest `retry`), which nobody has done yet. + Measured against live Redis with a 3-way probe — `provision-then-write-then-wait` returned the full + result set on the *first* read (0ms); `provision-only` (no post-write wait) and `wait-only` (index + created after the writes) both sat at 0 hits for the full 10s probe and never recovered. Waiting + longer does not help: a 25s poll on the old ordering still failed. So order is + **provision → write → `waitIndexing()` → read**, and `waitIndexing()` on an index that doesn't + exist yet is a silent no-op, which is the trap. +- **This was the cause of the long-running `Test` flake, and it is fixed.** `packages/sdk/src/ + chat-history.test.ts` ("lists a user's chats" → `expected [ 'c1' ] to include 'c2'`) and + `packages/eve/src/search-tools.test.ts` ("search runs a $smart query…" → `expected 0 to be greater + than 0` / `expected 1 to be greater than or equal to 2`) red-lit CI repeatedly, including the + 2026-08-26 nightly on `main` (eve 0.44.3) and three runs of PR #27. Both now provision the index + before seeding and keep a small `pollUntil` helper as insurance for residual lag; assertions are + unchanged. **10/10 consecutive green each** after the fix (was 4/10 and 4/10). If either goes red + again, treat it as a real regression, not noise. - **Throwaway DBs from `upstash start-redis` (the `@upstash/cli` command; `npm i -g @upstash/cli`) cap at *one* search index**, not 10 — `ERR Exceeded max index count of 1`. A single `pnpm test` cascades into bogus create-index failures on one. Run **one test file at a time** with a `FLUSHDB` diff --git a/packages/eve/src/search-tools.test.ts b/packages/eve/src/search-tools.test.ts index f08ee4e..0cdf303 100644 --- a/packages/eve/src/search-tools.test.ts +++ b/packages/eve/src/search-tools.test.ts @@ -8,6 +8,21 @@ function call(execute: unknown, input: unknown): Promise { return (execute as (i: unknown, c: unknown) => Promise)(input, CTX); } +/** + * Re-run `read` until `ready` holds (or the deadline passes) and return the last value, so a caller + * asserting on search results doesn't race Upstash's asynchronous indexing. On a genuine breakage + * the predicate never holds and the caller's own `expect` still reports the original failure. + */ +async function pollUntil(read: () => Promise, ready: (value: R) => boolean): Promise { + const deadline = Date.now() + 8_000; // two polled reads still sit inside vitest's 30s testTimeout + let value = await read(); + while (!ready(value) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 250)); + value = await read(); + } + return value; +} + const schema = s.object({ name: s.string(), age: s.number(), @@ -35,18 +50,40 @@ describe.skipIf(!hasRedisCreds)("defineSearchTools (live Redis)", () => { }); it("search runs a $smart query, creating the index reactively", async () => { + // This first call is the reactive create (the index does not exist yet) — the behaviour this + // test is named for. It has to happen BEFORE the writes: a doc written while the index is still + // missing can be dropped by the create-time backfill *permanently*, not just late, so seeding + // first and letting the read provision made this assertion flaky. Writes into an index that + // already exists are indexed incrementally. + await call<{ count: number }>(tools.count.execute, { filter: { city: { $eq: "nowhere" } } }); + await redis.json.set(`${prefix}1`, "$", { name: "Ada Lovelace", age: 36, city: "London" }); await redis.json.set(`${prefix}2`, "$", { name: "Alan Turing", age: 41, city: "London" }); - const hits = await call<{ data?: { name?: string } }[]>(tools.search.execute, { - filter: { name: { $smart: "ada" } }, - }); + // ...and the seeded docs only become visible once indexing is flushed. Verified against live + // Redis: provisioning first *or* waiting alone leaves both reads at 0 indefinitely; only the + // pair makes them land. The poll below is the belt-and-braces for residual lag. + await redis.search.index({ name, schema }).waitIndexing(); + + // Both reads poll until the index has caught up with the two writes above; the assertions + // themselves are unchanged. + const hits = await pollUntil( + () => + call<{ data?: { name?: string } }[]>(tools.search.execute, { + filter: { name: { $smart: "ada" } }, + }), + (found) => found.some((h) => h.data?.name?.includes("Ada")), + ); expect(hits.length).toBeGreaterThan(0); expect(hits.some((h) => h.data?.name?.includes("Ada"))).toBe(true); - const result = await call<{ count: number }>(tools.count.execute, { - filter: { city: { $eq: "London" } }, - }); + const result = await pollUntil( + () => + call<{ count: number }>(tools.count.execute, { + filter: { city: { $eq: "London" } }, + }), + (counted) => counted.count >= 2, + ); expect(result.count).toBeGreaterThanOrEqual(2); }); }); diff --git a/packages/sdk/src/chat-history.test.ts b/packages/sdk/src/chat-history.test.ts index f047d0b..b0bd673 100644 --- a/packages/sdk/src/chat-history.test.ts +++ b/packages/sdk/src/chat-history.test.ts @@ -14,6 +14,22 @@ const msg = (id: string, role: Msg["role"], text: string): Msg => ({ parts: [{ type: "text", text }], }); +/** + * Re-run `read` until `ready` holds (or the deadline passes) and return the last value, so a caller + * asserting on search results doesn't race Upstash's asynchronous indexing. `waitIndexing()` is not + * a hard barrier for a doc written moments earlier. On a genuine breakage the predicate never holds + * and the caller's own `expect` still reports the original failure. + */ +async function pollUntil(read: () => Promise, ready: (value: T) => boolean): Promise { + const deadline = Date.now() + 8_000; // well inside vitest's 30s testTimeout + let value = await read(); + while (!ready(value) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 250)); + value = await read(); + } + return value; +} + describe.skipIf(!hasRedisCreds)("ChatHistory (live Redis Search)", () => { const redis = testRedis(); const history = new ChatHistory({ redis, prefix: uniquePrefix("chat") }); @@ -110,6 +126,13 @@ describe.skipIf(!hasRedisCreds)("ChatHistory (live Redis Search)", () => { }); it("lists a user's chats (filtered by userId in the index)", async () => { + // The index is created lazily by the first search read. A doc written while it does not exist + // yet can be missed by the create-time backfill *permanently* — verified against live Redis: + // c2 never became visible, even after 25s of polling. So force the index to exist first (this + // read is itself the reactive create); writes after that are indexed incrementally. c1, written + // several tests ago, has long settled and is picked up by the backfill. + await history.listChats({ userId: "nobody" }); + await history.saveChat({ userId: user, sessionId: "c2", @@ -118,7 +141,15 @@ describe.skipIf(!hasRedisCreds)("ChatHistory (live Redis Search)", () => { }); await history.searchIndex.waitIndexing(); - const list = await history.listChats({ userId: user }); + // ...and `waitIndexing()` still isn't a hard barrier, so read until both chats are visible + // rather than asserting on the first response. The assertions below are unchanged. + const list = await pollUntil( + () => history.listChats({ userId: user }), + (chats) => { + const seen = chats.map((c) => c.sessionId); + return seen.includes("c1") && seen.includes("c2"); + }, + ); const ids = list.map((c) => c.sessionId); expect(ids).toContain("c1"); expect(ids).toContain("c2");