You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
session.completed from expiry, reset or close reaches no authored handler: hooks aren't dispatched and channel handlers throw on the missing session context #3401
Read at eve@0.53.1 and eve@0.56.0 (packages/eve/src); the relevant files are unchanged between the two tags. The channel-handler failure in (2) was observed on a Vercel deployment at 0.53.1.
What happens
A session can end outside a turn: the expiry deadline, POST /eve/v1/session/:id/reset, a closed session, or the workflow entry's safety net catching a throw out of the driver loop. All of these go through emitTerminalSessionEvent (execution/terminal-session-event.ts), which calls the channel adapter and writes the durable stream — and nothing else:
Authored hooks are never dispatched.dispatchStreamEventHooks is called only from the turn step (execution/workflow-steps.ts) and execution/settle-cancelled-turn-step.ts. A defineHook handler for session.completed or session.failed does not run for these endings. In a conversation session, session.completed is emitted only on this path (harness/emission.ts: conversation mode ends a turn with session.waiting), so a session.completed hook never fires for a conversation at all.
A channel session.completed handler throws before it runs. The defineChannel event wrapper exempts only session.failed from buildCallbackContext() (public/definitions/channel.ts). buildCallbackContext does ctx.require(SessionKey); SessionKey is produced by sessionProvider, which is only installed by the turn step's provider set (context/run-step.ts). On the terminal path it is absent, so the wrapper throws Context key "eve.session" is not set. and callAdapterEventHandler logs adapter event handler threw — event swallowed.
Net effect: no authored surface can observe that a session expired, was reset or was closed. The session.completed event lands on the durable stream and nowhere else. (session.failed is reachable, but only through a channel handler — not a hook.)
Send one message to a conversation session, let the turn settle (session.waiting), then POST /eve/v1/session/<id>/reset (or wait past the session timeout). The stream ends with session.completed; neither log line prints, and the runtime log shows adapter event handler threw — event swallowed for session.completed.
Expected
The hooks guide says hooks can subscribe to "any event in the runtime stream vocabulary", that each event goes adapter → durable write → hooks, and that hook and channel handlers "both fire when both are registered". The sessions doc says that at expiration eve "emits session.completed". The channel docs single out only session.failed as running "outside session context". So an authored session.completed handler — hook or channel — should run when a session expires, is reset or is closed.
Suggested change
In emitTerminalSessionEvent, when the serialized context restores, install the framework providers the turn step installs (at least sessionProvider) and dispatch hooks after the durable write, in the same order as the in-turn path.
If the context cannot be restored, at minimum give a channel session.completed handler the same no-ctx shape as session.failed on this path instead of throwing.
If hooks are meant to stay turn-scoped, say so in the hooks guide and the event table.
Related: #2223 (another event in the hook event map that never reaches hooks).
Triage is underway for #3401. I’ve started an evidence-based reproduction on origin/main, duplicate search, assessment, issue record, and label updates.
Reproduced on origin/main at bca1b4ab5b3285ba71b8929c8fd5a09a37d66942 with a focused unit-tier reproduction of the actual out-of-turn terminal emitter.
The emitter wrote a durable stream record containing "type":"session.completed".
A registered authored session.completed channel handler was called 0 times, and the hook-dispatch spy was called 0 times (both expected one call).
The captured adapter log was adapter event handler threw — event swallowed for eventType: "session.completed"; its error was Error: Context key "eve.session" is not set. The stack reaches buildCallbackContext from the session.completed wrapper in public/definitions/channel.ts.
The direct path is confirmed: expiry/reset/closed finalization invokes emitTerminalSessionCompletionStep, which delegates to emitTerminalSessionEvent. That helper restores the durable context, calls the adapter, and writes the stream, but does not call dispatchStreamEventHooks; it also runs without the step provider scope that supplies SessionKey, so the channel wrapper fails before the authored handler can run.
No duplicate was found. #2223 is related (a different subagent.called emitter bypasses hooks) but has a distinct event and code path. Recorded as a p2core bug: it breaks documented terminal lifecycle observability but has no data-loss or security impact and stream consumers can recover the event directly. The smallest useful follow-up is a regression test covering terminal session.completed delivery after reset/expiry with both a hook and a channel handler.
Current head: 6926f23f92dcd08bc0aa750783cc603bbbaeb54c
Review owner: ruiconti
Monitoring restarted against the current green head
The fix:
Reconstructs the virtual session context for terminal events.
Dispatches channel and hook handlers after the durable stream write.
Adds regression coverage for session.completed delivery and the missing-session error.
Includes an eve patch changeset.
Validation:
Focused tests: 71 passed
Typecheck: passed
Lint: passed
Formatting: passed
Invariant checks: passed
Full unit suite after building compiled output: 8,849 passed, 1 skipped
The full suite still has two failures in the unrelated src/instrumentation/config.test.ts module-registration tests, including one timeout. The changed terminal event tests pass.
PR CI is still running. Completed checks are green, with analyze, typecheck, unit/integration jobs, E2E jobs, and Vercel Agent Review still pending.
The original watcher failed because it was tied to the stale red head and no longer had an active eve context. The replacement watcher for current head 6926f23f... is still pending registration.
PR #3402 itself is unaffected. Its code and CI state remain as reported: green focused/local validation, with GitHub checks and review still pending.
Both PR watchers failed to register with the same No active eve context error. There is currently no active automated watcher.
PR #3402 remains open at 6926f23f..., ready for review, with CI still pending. The watcher failure is infrastructure-related and does not affect the branch or PR.
Still reproduces on eve@0.69.0 and on main today, for the channel half of this issue (point 2). We haven't rechecked the hooks half.
0.69 restores the serialized context for terminal events and publishes inside contextStorage.run, but eve.session is never in that context. sessionProvider sets it as a per-step virtual value (withContextScope in context/run-step.ts), and serializeContext writes only durable values. So the defineChannel wrapper still calls buildCallbackContext() for session.completed and throws Context key "eve.session" is not set. before the handler runs (public/definitions/channel.ts, right after the if (eventType === "session.failed") branch).
Minimal check against the published 0.69.0 package, run from inside node_modules/eve:
import{defineChannel}from"#public/definitions/channel.js";import{ContextContainer,contextStorage}from"#context/container.js";import{SessionIdKey}from"#context/keys.js";letcalls=0;constchannel=defineChannel({events: {"session.completed": ()=>{calls+=1;}}});constctx=newContextContainer();ctx.set(SessionIdKey,"s1");// what the terminal path has: durable keys onlytry{awaitcontextStorage.run(ctx,()=>channel.adapter["session.completed"]({type: "session.completed"},{}));console.log("handler ran, calls =",calls);}catch(e){console.log("threw before handler ran:",e.message,"| calls =",calls);}// threw before handler ran: Context key "eve.session" is not set. | calls = 0
Impact for us: we close our own task record from a channel session.completed handler. When a session ends by expiry or reset, the handler never runs and the task stays open. We ship a postinstall patch that builds ctx lazily (getters for session and getSandbox), so a handler that doesn't read ctx.session runs. Could #3402, or that smaller change, land this week?
Confirmed the same terminal-session-event gap in pinned eve 0.51.1 while diagnosing arkhai-io/webapp#382: revoked-key admission throws and terminal session.failed reaches the chat stream but never the routine outcome hook, leaving persisted runs RUNNING. Local workaround restores SessionKey from serialized auth/session identity, stamps and writes the terminal event, then dispatches authored hooks. Regression drives the real terminal emitter + real hook registry + Postgres (both failed and completed endings), without a model. Broader upgrade is out of scope for that application fix.
Activity
roboeve commentedon Sep 15, 2026
Triage is underway for #3401. I’ve started an evidence-based reproduction on
origin/main, duplicate search, assessment, issue record, and label updates.roboeve commentedon Sep 15, 2026
Reproduced on
origin/mainatbca1b4ab5b3285ba71b8929c8fd5a09a37d66942with a focused unit-tier reproduction of the actual out-of-turn terminal emitter."type":"session.completed".session.completedchannel handler was called 0 times, and the hook-dispatch spy was called 0 times (both expected one call).adapter event handler threw — event swallowedforeventType: "session.completed"; its error wasError: Context key "eve.session" is not set.The stack reachesbuildCallbackContextfrom thesession.completedwrapper inpublic/definitions/channel.ts.The direct path is confirmed: expiry/reset/closed finalization invokes
emitTerminalSessionCompletionStep, which delegates toemitTerminalSessionEvent. That helper restores the durable context, calls the adapter, and writes the stream, but does not calldispatchStreamEventHooks; it also runs without the step provider scope that suppliesSessionKey, so the channel wrapper fails before the authored handler can run.No duplicate was found. #2223 is related (a different
subagent.calledemitter bypasses hooks) but has a distinct event and code path. Recorded as ap2corebug: it breaks documented terminal lifecycle observability but has no data-loss or security impact and stream consumers can recover the event directly. The smallest useful follow-up is a regression test covering terminalsession.completeddelivery after reset/expiry with both a hook and a channel handler.roboeve commentedon Sep 15, 2026
Implemented and published the fix for issue #3401.
6926f23f92dcd08bc0aa750783cc603bbbaeb54cruicontiThe fix:
session.completeddelivery and the missing-session error.evepatch changeset.Validation:
src/instrumentation/config.test.tsmodule-registration tests, including one timeout. The changed terminal event tests pass.PR CI is still running. Completed checks are green, with
analyze,typecheck, unit/integration jobs, E2E jobs, and Vercel Agent Review still pending.roboeve commentedon Sep 15, 2026
The original watcher failed because it was tied to the stale red head and no longer had an active eve context. The replacement watcher for current head
6926f23f...is still pending registration.PR #3402 itself is unaffected. Its code and CI state remain as reported: green focused/local validation, with GitHub checks and review still pending.
roboeve commentedon Sep 15, 2026
Both PR watchers failed to register with the same
No active eve contexterror. There is currently no active automated watcher.PR #3402 remains open at
6926f23f..., ready for review, with CI still pending. The watcher failure is infrastructure-related and does not affect the branch or PR.docs(eve): scope hook execution order to a running session
modisulak commentedon Oct 2, 2026
Still reproduces on eve@0.69.0 and on
maintoday, for the channel half of this issue (point 2). We haven't rechecked the hooks half.0.69 restores the serialized context for terminal events and publishes inside
contextStorage.run, buteve.sessionis never in that context.sessionProvidersets it as a per-step virtual value (withContextScopeincontext/run-step.ts), andserializeContextwrites only durable values. So thedefineChannelwrapper still callsbuildCallbackContext()forsession.completedand throwsContext key "eve.session" is not set.before the handler runs (public/definitions/channel.ts, right after theif (eventType === "session.failed")branch).Minimal check against the published 0.69.0 package, run from inside
node_modules/eve:Impact for us: we close our own task record from a channel
session.completedhandler. When a session ends by expiry or reset, the handler never runs and the task stays open. We ship a postinstall patch that buildsctxlazily (getters forsessionandgetSandbox), so a handler that doesn't readctx.sessionruns. Could #3402, or that smaller change, land this week?mlegls commentedon Oct 19, 2026
Confirmed the same terminal-session-event gap in pinned eve 0.51.1 while diagnosing arkhai-io/webapp#382: revoked-key admission throws and terminal session.failed reaches the chat stream but never the routine outcome hook, leaving persisted runs RUNNING. Local workaround restores SessionKey from serialized auth/session identity, stamps and writes the terminal event, then dispatches authored hooks. Regression drives the real terminal emitter + real hook registry + Postgres (both failed and completed endings), without a model. Broader upgrade is out of scope for that application fix.