feat(eve): stamp every stream event with a stable id - #1239
Conversation
Every session stream event now carries `meta.id`, a unique `evt_`-prefixed ULID minted once at emission. The id is stable across reconnects, rewinds, tail reads, and replays, so a consumer can key durable work on it — for example `INSERT ... ON CONFLICT (id) DO NOTHING` in a hook — instead of guessing identity from payload content. Events are stamped before the channel adapter runs, so the adapter, the persisted chunk, and hooks all observe the same id. Client consumers now use it. `EveAgentStore` no longer double-applies an `initialEvents` prefix that the live stream replays, which also protects the React, Vue, and Svelte bindings and any user-authored reducer. The dev TUI drops re-delivered chunks up front, which fixes a subagent section that rendered its child transcript twice when the child stream reopened, and retires four content-comparison replay heuristics that could silently drop a new model call whose text prefixed the previous one. Bumps the message stream version to 20 and the hook, dynamicTool, dynamicSkill, and dynamicInstructions capability epochs. All four changed only additively, so prior-epoch extensions stay compatible and are retained. Signed-off-by: Andrew Barba <barba@hey.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Bundle + Package Summary:
|
| Area | Metric | Baseline | Current | Delta |
|---|---|---|---|---|
| Package | Packed tarball | 7.61 MB | 7.61 MB | +8.6 kB |
| Package | Unpacked publish size | 28.74 MB | 28.76 MB | +19.9 kB |
| Package | Installed footprint | 91.05 MB | 91.07 MB | +19.9 kB |
| Package | Published files | 2855 | 2861 | +6 |
| Package | Installed files | 6642 | 6648 | +6 |
| Runtime | Unique function payloads | 2 | 2 | 0 |
| Runtime | Total function bytes | 16.72 MB | 16.75 MB | +22.2 kB |
| Runtime | Public routes | 11 | 11 | 0 |
Changed function payloads vs main (c1bf6d4) (2)
| Function | Status | Baseline | Current | Delta | Route changes |
|---|---|---|---|---|---|
functions/__server.func |
changed | 8.36 MB | 8.37 MB | +11.1 kB |
none |
functions/.well-known/workflow/v1/flow.func |
changed | 8.36 MB | 8.37 MB | +11.1 kB |
none |
eve init install
| Metric | Baseline | Current | Delta |
|---|---|---|---|
| Installed footprint | 129.45 MB | 129.47 MB | +19.9 kB |
| Installed packages | 130 | 130 | 0 |
| dependencies | 4 | 4 | 0 |
| devDependencies | 2 | 2 | 0 |
| Dependency package bytes | 43.08 MB | 43.10 MB | +19.9 kB |
| devDependency package bytes | 5.04 MB | 5.04 MB | 0 B ➖ |
Build Metadata
- Preset:
vercel - Nitro:
nitro@3.0.260610-beta - Output directory:
apps/fixtures/weather-agent/.vercel/output - Build metadata timestamp: 2026-07-29T18:41:55.674Z
- Route aliases: 11 public, 1 internal (12 total aliases)
- Vercel routes in config: 14
- Severity legend: 🔴 dominant/large, 🟠 notable, 🟡 watch, ⚪ small
Package Drill-Down
Package Details
- Package:
eve@0.27.12 - Package directory:
packages/eve - Tarball: 7.61 MB (
eve-0.27.12.tgz) - Unpacked payload: 28.76 MB across 2861 published files
- Installed footprint: 91.07 MB across 6648 installed files
- Installed root package: 27.40 MB
- Installed dependencies: 63.66 MB
- Runtime dependencies: 2
- Peer dependencies: 5 (4 optional)
Installed footprint is measured from an isolated temporary npm install of the packed tarball.
Heavy installed dependencies
eve: 27.40 MB (30.1%)@rolldown/binding-linux-x64-gnu: 19.28 MB (21.2%)@rolldown/binding-wasm32-wasi: 10.66 MB (11.7%)ai: 6.53 MB (7.2%)@napi-rs/wasm-runtime: 6.39 MB (7.0%)
Publish payload breakdown
Published file size
🔴 dist/src/compiled/shadcn-registry/index.js [########################] 13.15 MB 45.7%
🟠 dist/src/compiled/experimental-ai-sdk-code-mo... [###.....................] 1.51 MB 5.2%
🟡 dist/src/compiled/@vercel/sandbox/index.js [#.......................] 632.5 kB 2.2%
🟡 dist/src/compiled/_chunks/workflow/undici-DWL... [#.......................] 502.4 kB 1.7%
🟡 dist/src/compiled/@chat-adapter/slack/index.js [#.......................] 440.5 kB 1.5%
🔴 Other published files [#######################.] 12.53 MB 43.6%
Installed footprint breakdown
Installed package size
🔴 eve [########################] 27.40 MB 30.1%
🔴 @rolldown/binding-linux-x64-gnu [#################.......] 19.28 MB 21.2%
🔴 @rolldown/binding-wasm32-wasi [#########...............] 10.66 MB 11.7%
🔴 ai [######..................] 6.53 MB 7.2%
🔴 @napi-rs/wasm-runtime [######..................] 6.39 MB 7.0%
🔴 zod [####....................] 5.07 MB 5.6%
🔴 Other installed packages [##############..........] 15.73 MB 17.3%
Runtime dependencies (2)
| Package | Range | Notes |
|---|---|---|
nitro |
3.0.260610-beta |
|
undici |
8.9.0 |
Peer dependencies (5)
| Package | Range | Notes |
|---|---|---|
@opentelemetry/api |
^1.0.0 |
optional peer |
ai |
catalog: |
|
braintrust |
^3.0.0 |
optional peer |
just-bash |
^3.0.0 |
optional peer |
microsandbox |
^0.5.0 |
optional peer |
eve init install drill-down
eve init install details
- Command:
eve init my-agent - Package manager:
npm - Installed footprint: 129.47 MB across 8516 installed files
- Installed packages: 130 total (124 transitive-only)
- dependencies: 4 direct packages totaling 43.10 MB
- devDependencies: 2 direct packages totaling 5.04 MB
- Other transitive package files: 81.33 MB
Installed footprint is measured from an isolated temporary eve init my-agent using the current packed eve tarball.
Heavy installed dependencies
@typescript/typescript-linux-x64: 27.95 MB (21.6%)eve: 27.40 MB (21.2%)@rolldown/binding-linux-x64-gnu: 19.28 MB (14.9%)@rolldown/binding-wasm32-wasi: 10.66 MB (8.2%)zod: 9.02 MB (7.0%)
Installed footprint breakdown
Installed package size
🔴 @typescript/typescript-linux-x64 [########################] 27.95 MB 21.6%
🔴 eve [########################] 27.40 MB 21.2%
🔴 @rolldown/binding-linux-x64-gnu [#################.......] 19.28 MB 14.9%
🔴 @rolldown/binding-wasm32-wasi [#########...............] 10.66 MB 8.2%
🔴 zod [########................] 9.02 MB 7.0%
🔴 ai [######..................] 6.53 MB 5.0%
🔴 @napi-rs/wasm-runtime [#####...................] 6.39 MB 4.9%
🔴 Other installed packages [###################.....] 22.23 MB 17.2%
dependencies (4)
| Package | Range | Installed size | Share |
|---|---|---|---|
@vercel/connect |
0.4.2 |
135.8 kB | 0.1% |
ai |
^7.0.38 |
6.53 MB | 5.0% |
eve |
file:eve-0.27.12.tgz |
27.40 MB | 21.2% |
zod |
4.4.3 |
9.02 MB | 7.0% |
devDependencies (2)
| Package | Range | Installed size | Share |
|---|---|---|---|
@types/node |
24.x |
2.54 MB | 2.0% |
typescript |
7.0.2 |
2.50 MB | 1.9% |
Function Drill-Down
Payload Size Graph
Unique function payload size and share of total
🔴 functions/.well-known/workflow/v1/flow.func [########################] 8.37 MB 50.0%
🔴 functions/__server.func [########################] 8.37 MB 50.0%
Top Function Payloads
🟠 functions/.well-known/workflow/v1/flow.func • 1 public route • 8.37 MB
| Metric | Value |
|---|---|
| Public routes | /.well-known/workflow/v1/flow |
| Runtime | nodejs24.x |
| Handler | index.mjs |
| Payload | 8.37 MB |
| Function files | 8.37 MB across 44 files |
| Traced dependencies | 0 B |
| Signal | 🟠 Bundled file index.mjs is 2.27 MB (27.2%) |
🟠 🔎 Dependency Analysis
📦 Bundled files:
Bundled file size
🟠 index.mjs [#######################.] 2.27 MB 27.2%
🟠 _chunks/runtime-artifacts.mjs [################........] 1.59 MB 19.0%
🟡 _libs/undici.mjs [##########..............] 980.5 kB 11.7%
🟡 _chunks/sandbox.mjs [########................] 768.8 kB 9.2%
🟡 _libs/@ai-sdk/gateway+[...].mjs [####....................] 432.8 kB 5.2%
🟠 Other bundled files [########################] 2.33 MB 27.8%
🧾 Vercel Config
{
"handler": "index.mjs",
"launcherType": "Nodejs",
"shouldAddHelpers": false,
"supportsResponseStreaming": true,
"runtime": "nodejs24.x",
"maxDuration": "max",
"experimentalTriggers": [
{
"type": "queue/v2beta",
"topic": "__eve776561746865722d6167656e74_wkf_workflow_*",
"consumer": "default",
"retryAfterSeconds": 5,
"initialDelaySeconds": 0
}
],
"environment": {
"WORKFLOW_PRECONDITION_GUARD": "1"
}
}🟠 functions/__server.func • 10 public routes, 1 internal alias • 8.37 MB
| Metric | Value |
|---|---|
| Public routes | //eve/v1/callback/[token]/eve/v1/connections/[name]/callback/[token]/eve/v1/health/eve/v1/info/eve/v1/session/eve/v1/session/[sessionId]/eve/v1/session/[sessionId]/cancel/eve/v1/session/[sessionId]/stream/eve/v1/session/reset |
| Internal aliases | /__server |
| Runtime | nodejs24.x |
| Handler | index.mjs |
| Payload | 8.37 MB |
| Function files | 8.37 MB across 44 files |
| Traced dependencies | 0 B |
| Signal | 🟠 Bundled file index.mjs is 2.27 MB (27.2%) |
🟠 🔎 Dependency Analysis
📦 Bundled files:
Bundled file size
🟠 index.mjs [#######################.] 2.27 MB 27.2%
🟠 _chunks/runtime-artifacts.mjs [################........] 1.59 MB 19.0%
🟡 _libs/undici.mjs [##########..............] 980.5 kB 11.7%
🟡 _chunks/sandbox.mjs [########................] 768.8 kB 9.2%
🟡 _libs/@ai-sdk/gateway+[...].mjs [####....................] 432.8 kB 5.2%
🟠 Other bundled files [########################] 2.32 MB 27.8%
🧾 Vercel Config
{
"handler": "index.mjs",
"launcherType": "Nodejs",
"shouldAddHelpers": false,
"supportsResponseStreaming": true,
"runtime": "nodejs24.x"
}Build Timing: e2e/fixtures/agent-tools-sandbox
This is an informational timing measurement inside eve build, from preflight through publication. Output-size measurement and profile writing are excluded.
Build mode: deployable Vercel build with sandbox template prewarm included.
- Build pipeline: 1.91 s -> 1.94 s (+21.0 ms) vs
main (c1bf6d4). - Timing is informational: shared GitHub runners are too variable for a hard timing budget.
Detailed phase timings vs `main (c1bf6d4)`
| Phase | Baseline | Current | Delta |
|---|---|---|---|
extension.check |
1.1 ms | 6.6 ms | +5.5 ms |
project.resolve |
0.7 ms | 3.4 ms | +2.7 ms |
workspace.create |
0.8 ms | 1.2 ms | +0.4 ms |
host.prepare |
137.6 ms | 156.0 ms | +18.4 ms |
vercel.service-prefix.resolve |
2.5 ms | 2.4 ms | -0.1 ms |
nitro.create |
230.8 ms | 194.1 ms | -36.7 ms |
sandbox.prewarm |
232.8 ms | 266.4 ms | +33.6 ms |
nitro.cache.prepare |
0.3 ms | 0.3 ms | 0.0 ms |
nitro.prepare |
0.9 ms | 0.9 ms | 0.0 ms |
nitro.public-assets |
0.8 ms | 0.8 ms | 0.0 ms |
nitro.prerender |
0.5 ms | 0.5 ms | 0.0 ms |
nitro.bundle |
1.27 s | 1.27 s | -2.2 ms |
nitro.cache.write |
0.4 ms | 0.4 ms | 0.0 ms |
vercel.workflow-function.materialize |
25.0 ms | 24.8 ms | -0.2 ms |
agent-summary.emit |
0.5 ms | 0.5 ms | 0.0 ms |
nitro.close |
0.1 ms | 0.1 ms | 0.0 ms |
output.publish |
3.6 ms | 3.4 ms | -0.2 ms |
workspace.remove |
2.4 ms | 2.1 ms | -0.3 ms |
Review pass over the stamped-event-id work. Correctness: - Remove the event deduper's bounded window. Once a stream exceeded the capacity, rewinding to the start re-admitted the evicted id, which evicted the next, cascading until the whole replay was applied again. Every caller already retains an object per event, so the cap bought nothing. - Tolerate a missing envelope. Events written before stream version 20 have no `meta`, so rewinding into an older part of a live session threw instead of admitting the event. - Stop promising that `meta.id` makes hook side effects idempotent. A durable step that is interrupted re-runs and re-emits with fresh ids, so `on conflict (id) do nothing` does not dedupe a retry. The guarantee is scoped to re-reading an already persisted stream. - Stop promising a total order. Ids are minted per process, so two steps on different machines can sort either way; drop the `where id > $cursor` pagination guidance in favour of stream position. - Narrow `RouteContext.agent.getEventStream()`, which still returned unstamped events. - Mark the changeset `minor`: `initialEvents` now requires stamped events. Structure: - Extract the ULID generator to `#shared/ulid.ts` with a `createUlidFactory()` for callers needing isolated monotonic state, leaving `event-id.ts` as the `evt_` prefix layer. Records why the generator is in-repo rather than an npm package, and pins the timestamp encoding with golden vectors cross-checked against the reference implementation. - Revert `callAdapterEventHandler` to its original signature and stamp after the adapter runs. Adapter handlers only ever receive `event.data`, so they never observed the envelope the comments claimed; the parameter change was churn across thirteen files. Also drops development notes, redundant assertions, and an unreachable `padStart`, and removes the test-only override parameter from `stampMessageStreamEvent`. Signed-off-by: Andrew Barba <barba@hey.com>
…vent-ids # Conflicts: # packages/eve/src/channel/schedule.test.ts # packages/eve/src/channel/send.test.ts # packages/eve/src/channel/session.ts # packages/eve/src/channel/types.ts # packages/eve/src/client/open-stream.ts
The ULID in the event envelope example decoded to 2025-07-28 while the `at` beside it read 2026-07-27. The section links the ULID spec and describes the id as a millisecond timestamp, so the mismatch is reachable by anyone who follows that. Signed-off-by: Andrew Barba <barba@hey.com>
meta and meta.at have existed since the first release; version 20 adds meta.id alone. The docs, the changeset, and the deduper comment all said older events arrive with no envelope, which would lead a reader to guard on the wrong thing. The dedupe test built that same impossible shape, so it covered a case that cannot occur while leaving the real one — an envelope holding only at — untested. Both are covered now. The runtime read was already correct: event.meta?.id is undefined either way. Signed-off-by: Andrew Barba <barba@hey.com>
The envelope docs framed re-delivery as purely a reconnect concern and left the retry limitation to a trailing sentence. Three gaps mattered. Stream writes are batched, and a batch that fails partway re-sends pages that already landed, so the durable log can hold the same chunk twice. That, not just reconnects, is why keying on the id is required. Replay and retry read as synonyms and behave oppositely: replaying a completed step serves the journaled result and emits nothing, while an interrupted step re-runs and re-emits under new ids. Only the second produces duplicates, and the docs never separated them. The hooks guide told authors to key idempotency on the turn and step coordinates. That is right for gating a once-per-turn side effect, since a retry restores those coordinates. It is wrong for stored content: the retry re-invokes the model, so one coordinate can carry different text per attempt, and conflict-on-coordinates keeps the abandoned attempt and drops the one that finished. Signed-off-by: Andrew Barba <barba@hey.com>
Rename EventDeduper.isDuplicate to admit: the old name read as a pure query while the call recorded the id, which let a side-effecting filter hide in the EveAgentStore constructor. Unroll that filter into an explicit loop and drop a copy-pasted test comment. Signed-off-by: Andrew Barba <barba@hey.com>
…vent-ids Signed-off-by: Andrew Barba <barba@hey.com> # Conflicts: # apps/docs/public/r/channel/slack.json # apps/docs/public/r/channel/web.json # packages/eve/src/evals/session.ts # pnpm-lock.yaml
Signed-off-by: Andrew Barba <barba@hey.com>
Pin the full ULID encoding and range, cover legacy id-less events through the live store path, and align reducer fixtures and public guidance with the stamped stream contract. Signed-off-by: Andrew Barba <barba@hey.com>
| // Yield any trailing content without a final newline. | ||
| const trailing = buffer.trim(); | ||
| if (trailing.length > 0) { | ||
| yield JSON.parse(trailing) as HandleMessageStreamEvent; |
There was a problem hiding this comment.
Should we MessageStreamEvent.safeParse(JSON.parse(trailing))?
Risk is that an old event is assumed to have a meta.id when it doesn't
Closes #24. ## Upstream change [vercel/eve#1239](vercel/eve#1239) — `feat(eve): stamp every stream event with a stable id`, merged as [`7ff4f7797a43`](vercel/eve@7ff4f77). Verified directly against upstream rather than the radar summary: - `packages/eve/src/protocol/message.ts` raises `EVE_MESSAGE_STREAM_VERSION` from `19` to `20` and adds a required `id` to the durable event envelope. - `packages/eve/src/protocol/event-id.ts` mints it as `evt_` + a Crockford base32 ULID. - `packages/eve/src/protocol/event-dedupe.ts` keys deduplication on `meta.id`, admits id-less events, and keeps an unbounded window on purpose. - `packages/eve/src/client/open-stream.ts` still yields every event; only `client/eve-agent-store.ts` and the CLI TUI dedupe, and both are out of scope for this package. First released in eve **0.28.0** — confirmed by unpacking the npm tarballs: `0.27.13` still ships `EVE_MESSAGE_STREAM_VERSION=19` with no `evt_` prefix, `0.28.0` ships `20` with it. ## Changes - `EveStreamEventMetadata.Id` projects `meta.id`, and is `null` for events persisted before protocol 20. - `EveStreamEventDeduplicator` mirrors the upstream framework-neutral deduper: it keys on the durable identifier, admits identifier-less events because there is nothing to deduplicate on, and never bounds its window. - `EveStreamFollower` deliberately still yields every event the server sends, matching `followStreamIterable`. Deduplication stays a caller decision. - `docs/streaming.md` and `docs/compatibility.md` document the identity contract and the legacy gap. ## Validation | Gate | Result | |---|---| | `dotnet format --verify-no-changes` | clean | | `dotnet build -c Release` | 0 warnings, 0 errors | | `dotnet test -c Release` | **91 passed**, 0 failed, 0 skipped (79 pre-existing + 12 new) | | `npm run test:client` (real eve `0.27.6` server) | passed | | `mkdocs build --strict` | built | | `dotnet pack` + `validate-packages.ps1` | validated `0.1.0-alpha-0002-g54eea4a9d9` | New coverage: stamped metadata projection; legacy id-less metadata; blank / non-string / null identifier values; nested `subagent.event` without an envelope; deduper replay-drop vs. retry-admit; id-less admit; `Clear()`; null argument; and an end-to-end turn where a reconnect replays a handled event and the deduper drops exactly one of three delivered events. The compatibility probe now asserts against the **real** pinned server that every event carries a non-empty `meta.at`, that no event carries `meta.id` on protocol 19, and that identifier-less events are never dropped. ## Disclosed gaps **HIGH — none.** **MEDIUM** - The compatibility baseline is intentionally **not** advanced. `EveProtocol.ReferenceEveVersion` stays `0.27.6` and `MessageStreamVersion` stays `19`, because the issue requires all changes through the upstream release to land first, and eve 0.28.0 also carries the input-request-kind change tracked by #25. Advancing the pinned fixture should be a separate PR once both are merged. - Because the pinned fixture is `0.27.6`, `meta.id` **presence** is verified only against synthetic payloads. Only the legacy-tolerance half is verified against a real server. - `EveStreamEventMetadata` is still surfaced only when `meta.at` is present, so a hypothetical id-only envelope would be dropped. No released eve version emits one. **LOW** - Event identifiers are compared with `StringComparer.Ordinal`; `NLF0016` is suppressed at that single field with a justification, since an opaque server-minted token that differs by case is a different token. - `EveStreamEventDeduplicator` is not thread-safe, matching the upstream deduper, and is documented as single-reader. Copilot-Session: 6f397bc8-d45e-4945-a1f7-eb8d8d91a4fc
Refs #1193.
Summary
meta.idto every session stream event. Each id is anevt_-prefixed ULID stamped once before persistence and shared with authored hooks.MessageStreamEventthe canonical public stream type.HandleMessageStreamEventremains as a deprecated alias.EveAgentStoreand the dev TUI while continuing to admit legacy id-less events.Breaking change
MessageStreamEventnow requires bothmeta.idandmeta.at. Code that constructs unstamped events usingHandleMessageStreamEventmust add the envelope or use the appropriate event-specific input type.Events persisted before stream version 20 may have
meta.atwithoutmeta.id. eve passes these events through, but they cannot be deduplicated. Retried steps produce new emissions with new ids; replaying an existing event preserves its id.Verification
pnpm fmtpnpm lintpnpm typecheckpnpm guard:invariantspnpm docs:checkPR Checklist
CONTRIBUTING.mdevepackagegit commit --signoff)