feat(eve): workflow hooks - adopt ownership checks#232
Conversation
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 | 3.40 MB | 3.40 MB | +361 B |
| Package | Unpacked publish size | 12.40 MB | 12.40 MB | +1.7 kB |
| Package | Installed footprint | 52.60 MB | 52.60 MB | +1.7 kB |
| Package | Published files | 2283 | 2285 | +2 |
| Package | Installed files | 5488 | 5490 | +2 |
| Runtime | Unique function payloads | 2 | 2 | 0 |
| Runtime | Total function bytes | 9.37 MB | 9.38 MB | +6.3 kB |
| Runtime | Public routes | 9 | 9 | 0 |
Changed function payloads vs main (9298c90) (2)
| Function | Status | Baseline | Current | Delta | Route changes |
|---|---|---|---|---|---|
functions/.well-known/workflow/v1/flow.func |
changed | 5.50 MB | 5.50 MB | +6.5 kB |
none |
functions/__server.func |
changed | 3.88 MB | 3.88 MB | -209 B ✅ | none |
Build Metadata
- Preset:
vercel - Nitro:
nitro@3.0.260610-beta - Output directory:
apps/fixtures/weather-agent/.vercel/output - Build metadata timestamp: 2026-06-24T02:21:30.069Z
- Route aliases: 9 public, 1 internal (10 total aliases)
- Vercel routes in config: 10
- Severity legend: 🔴 dominant/large, 🟠 notable, 🟡 watch, ⚪ small
Package Drill-Down
Package Details
- Package:
eve@0.13.3 - Package directory:
packages/eve - Tarball: 3.40 MB (
eve-0.13.3.tgz) - Unpacked payload: 12.40 MB across 2285 published files
- Installed footprint: 52.60 MB across 5490 installed files
- Installed root package: 11.13 MB
- Installed dependencies: 41.48 MB
- Runtime dependencies: 1
- Peer dependencies: 12 (11 optional)
Installed footprint is measured from an isolated temporary npm install of the packed tarball.
Heavy installed dependencies
@rolldown/binding-linux-x64-gnu: 20.61 MB (39.2%)eve: 11.13 MB (21.2%)ai: 6.20 MB (11.8%)zod: 5.04 MB (9.6%)nitro: 2.41 MB (4.6%)
Publish payload breakdown
Published file size
🟠 dist/src/compiled/experimental-ai-sdk-code-mo... [####....................] 1.51 MB 12.1%
🟡 dist/src/compiled/@workflow/core/runtime.js [##......................] 788.4 kB 6.4%
🟡 dist/src/compiled/@vercel/sandbox/index.js [##......................] 632.0 kB 5.1%
🟡 dist/src/compiled/@chat-adapter/slack/index.js [#.......................] 438.4 kB 3.5%
🟡 dist/src/compiled/_chunks/workflow/attribute-... [#.......................] 371.6 kB 3.0%
🔴 Other published files [########################] 8.66 MB 69.9%
Installed footprint breakdown
Installed package size
🔴 @rolldown/binding-linux-x64-gnu [########################] 20.61 MB 39.2%
🔴 eve [#############...........] 11.13 MB 21.2%
🔴 ai [#######.................] 6.20 MB 11.8%
🔴 zod [######..................] 5.04 MB 9.6%
🟠 nitro [###.....................] 2.41 MB 4.6%
🟡 rolldown [#.......................] 771.0 kB 1.5%
🔴 Other installed packages [########................] 6.46 MB 12.3%
Runtime dependencies (1)
| Package | Range | Notes |
|---|---|---|
nitro |
3.0.260610-beta |
Peer dependencies (12)
| Package | Range | Notes |
|---|---|---|
@opentelemetry/api |
^1.0.0 |
optional peer |
@sveltejs/kit |
^2.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 |
next |
^16.0.0 |
optional peer |
nuxt |
^4.0.0 |
optional peer |
react |
^19.0.0 |
optional peer |
svelte |
^5.0.0 |
optional peer |
vite |
^8.0.0 |
optional peer |
vue |
^3.5.0 |
optional peer |
Function Drill-Down
Payload Size Graph
Unique function payload size and share of total
🔴 functions/.well-known/workflow/v1/flow.func [########################] 5.50 MB 58.7%
🔴 functions/__server.func [#################.......] 3.88 MB 41.3%
Top Function Payloads
🟠 functions/.well-known/workflow/v1/flow.func • 1 public route • 5.50 MB
| Metric | Value |
|---|---|
| Public routes | /.well-known/workflow/v1/flow |
| Runtime | nodejs24.x |
| Handler | index.mjs |
| Payload | 5.50 MB |
| Function files | 5.50 MB across 27 files |
| Traced dependencies | 0 B |
| Signal | 🟠 Bundled file __eve_nitro_handler__.mjs is 1.52 MB (27.5%) |
🟠 🔎 Dependency Analysis
📦 Bundled files:
Bundled file size
🟠 __eve_nitro_handler__.mjs [########################] 1.52 MB 27.5%
🟠 _chunks/runtime.mjs [###############.........] 975.8 kB 17.7%
🟡 _chunks/sandbox.mjs [############............] 766.0 kB 13.9%
🟡 _chunks/attribute-changes-DUxG-Gic.mjs [#######.................] 473.2 kB 8.6%
🟡 _chunks/dist-DTchiX0N.mjs [#######.................] 460.6 kB 8.4%
🟠 Other bundled files [#####################...] 1.31 MB 23.8%
🧾 Vercel Config
{
"handler": "index.mjs",
"launcherType": "Nodejs",
"shouldAddHelpers": false,
"supportsResponseStreaming": true,
"runtime": "nodejs24.x",
"environment": {
"NODE_OPTIONS": "--experimental-require-module",
"WORKFLOW_QUEUE_NAMESPACE": "eve"
},
"maxDuration": "max",
"experimentalTriggers": [
{
"type": "queue/v2beta",
"topic": "__eve_wkf_workflow_*",
"consumer": "default",
"retryAfterSeconds": 5,
"initialDelaySeconds": 0
}
]
}🟠 functions/__server.func • 8 public routes, 1 internal alias • 3.88 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]/stream |
| Internal aliases | /__server |
| Runtime | nodejs24.x |
| Handler | index.mjs |
| Payload | 3.88 MB |
| Function files | 3.88 MB across 21 files |
| Traced dependencies | 0 B |
| Signal | 🟠 Bundled file index.mjs is 1.40 MB (36.2%) |
🟠 🔎 Dependency Analysis
📦 Bundled files:
Bundled file size
🟠 index.mjs [########################] 1.40 MB 36.2%
🟠 _chunks/runtime.mjs [###############.........] 883.8 kB 22.8%
🟠 _chunks/sandbox.mjs [#############...........] 766.0 kB 19.8%
🟡 _chunks/attribute-changes-DUxG-Gic.mjs [########................] 448.9 kB 11.6%
⚪ _libs/zod.mjs [##......................] 114.2 kB 2.9%
🟡 Other bundled files [####....................] 258.8 kB 6.7%
🧾 Vercel Config
{
"handler": "index.mjs",
"launcherType": "Nodejs",
"shouldAddHelpers": false,
"supportsResponseStreaming": true,
"runtime": "nodejs24.x"
}
pranaygp
left a comment
There was a problem hiding this comment.
Reviewed the full diff and traced the design against the actual Workflow SDK (@workflow/core@5.0.0-beta.24, @workflow/world@5.0.0-beta.13) by reading the published .d.ts. No blocking issues — this is a clean, well-tested change and the semantics line up precisely with the SDK contract.
What I verified:
getConflict()resolves with the conflictingRun(.runId: string) ornullonce registration is committed, and only rejects withHookConflictErrorin the rare old-world case where the owner id was never recorded.claimHookOwnershiphandles both, andRun.runIdis a plain string, soconflict.runIdis sound.resumeHook()returnsPromise<Hook>and the worldHookcarriesrunId: string, so collapsinggetHookByToken+resumeHookinto a single call is correct and removes the lookup-to-resume race.HookNotFoundErroris still thrown byresumeHook, so theRuntimeNoActiveSessionErrornormalization holds.claim-before-releaseordering inrekeyHook, the consolidatedtry/finallycleanup, and the movedcloseHookIterator/disposeHookhelpers all check out. The integration test exercises the real local world end-to-end (contender fails before any turn work; owner stays resumable).
A few non-blocking notes left inline (mostly test-coverage / readability). One out-of-diff cleanup: getHookByToken is no longer used by any production code, but packages/eve/src/execution/workflow-steps.test.ts:108 still mocks it — vestigial, safe to drop in a follow-up.
Note: I reviewed statically and could not execute the suites in this worktree (no installed deps); relying on the author's reported green runs + CI.
| try { | ||
| const hook = normalizeWorkflowHook(await getHookByToken(input.continuationToken)); | ||
| await resumeHook(input.continuationToken, hookPayload); | ||
| const hook = normalizeWorkflowHook(await resumeHook(input.continuationToken, hookPayload)); |
There was a problem hiding this comment.
Nice — folding getHookByToken + resumeHook into one call removes the lookup-to-resume race. ✅ Confirmed against @workflow/core@5.0.0-beta.24: resumeHook returns Promise<Hook>, and the @workflow/world Hook carries runId: string, so hook.runId is sound.
Non-blocking: this return contract is now load-bearing for deliver() but is only asserted via a mock (resumeHookMock.mockResolvedValue({ runId })) in workflow-runtime.test.ts. The integration tier drives resumeHook directly rather than through runtime.deliver, so the real round-trip of deliver() → owning runId isn't covered against the local world. An integration assertion would lock this cross-package contract down so a future SDK bump can't silently break it.
| // Claim the replacement before releasing the current token. A failed | ||
| // claim leaves the active hook intact until normal failure cleanup. | ||
| const nextHook = createHook<HookPayload>({ token: nextToken }); | ||
| await claimHookOwnership(nextHook); |
There was a problem hiding this comment.
getConflict() is a workflow suspension point — per the SDK docs, awaiting it "suspends the workflow to commit the hook registration." Since rekeyHook now runs before the first turn (for tokened sessions) and on every token change, channels that re-key per turn (e.g. Slack thread tokens) pay one extra durable commit per re-key versus the old synchronous createParkHook. That's the intended claim-before-release tradeoff — just flagging to confirm it was weighed for high-re-key channels.
| try { | ||
| await closeParkHook(); | ||
| } catch (error) { | ||
| await disposeHook(nextHook); |
There was a problem hiding this comment.
This branch — claim succeeded but releasing the old hook threw — doesn't appear to be covered. The sibling failures are (conflicting claim before first turn; conflicting rekey candidate disposed before touching the active hook), but a throwing closeParkHook() after a successful claim isn't.
Also worth a look: if closeParkHook() throws partway (e.g. closeHookIterator(iterator) throws), it leaves hook/iterator still pointing at the old hook, so the outer finally will call closeParkHook() again → a second disposeHook(hook) on the old hook. Harmless if the SDK's dispose is idempotent, but worth a test or a best-effort note here.
| throw error; | ||
| } | ||
|
|
||
| function normalizeHookClaimError(error: unknown, token: string): unknown { |
There was a problem hiding this comment.
Per the SDK, getConflict() only rejects with HookConflictError in the rare old-world case where the owning run id was never recorded — i.e. whenever this catch path runs, conflictingRunId is by definition absent, so the normalized error will always have conflictingRunId: undefined (which the test confirms). The field names are correct (the SDK's HookConflictError does carry token/conflictingRunId), so this is fine as defensive code — a one-line comment noting "this path implies an unidentifiable owner" would save the next reader a trip through the SDK source to understand why the run id is dropped.
Summary
getConflict()before the first agent turnresumeHook()Why
Continuation hooks use shared tokens, but eve previously relied on delayed hook registration and a separate owner lookup during delivery. That left ownership conflicts to surface after work could begin and introduced a lookup-to-resume race.
This adopts the Workflow SDK's ownership check as an explicit barrier. A competing run now fails before performing an agent turn, while the existing owner remains active and resumable. Tokenless sessions retain their existing behavior and claim ownership after the first turn establishes a token.
Validation
pnpm buildpnpm typecheckpnpm lintpnpm fmtpnpm guard:invariantspnpm docs:checkpnpm test:unit— 3,884 passedpnpm test:integration— 369 passedpnpm test:scenario— 261 passed, 15 skippedpnpm exec eve eval --strictfrome2e/fixtures/agent-toolswas attempted, but the environment has neitherAI_GATEWAY_API_KEYnorVERCEL_OIDC_TOKEN, so its 15 model-backed evals could not authenticate