Repository navigation
Description
Eve version
0.27.13 (bundled workflow core 5.0.0-beta.37, deployment dpl_local@5.0.0-beta.31), Node v24.15.0, Linux x86_64, default world-local, production mode (eve build + eve start).
Problem
A client session parked at session.waiting cannot be continued after the eve server is restarted. The continuation is accepted — session.send() resolves, the saved continuationToken is echoed back — but result() never resolves, the model provider is never called, and the run produces no further work. It hangs indefinitely (observed well past the 60s timeout used in the repro below).
The failure is completely silent: no error, no warning, no rejected queue delivery, nothing on stdout/stderr beyond the normal [world-local] Re-enqueued 2 active run(s) on startup.
Sessions created after the restart work normally on the same server process, so the runtime, the queue handler, and the provider are all healthy — only the pre-restart parked session is stuck.
This looks like the same class of bug as #715 ("Local world re-enqueues parked interactive sessions into an unhandled queue on restart", fixed in 0.22.6 by "Resume active local workflow runs on the agent-scoped queue after restarting the eve server"), but with the outcome inverted. In #715 the re-enqueue was rejected as Unhandled queue and yet resume-by-continuation still worked; here the re-enqueue is accepted silently and resume-by-continuation does not work at all.
What the durable state looks like while wedged
After the resume attempt times out, .eve/.workflow-data shows the delivery was persisted and then dropped on the floor:
- Exactly one new file appears, the incoming message event for the parked run:
events/<parkedRunId>-evnt_....json,"eventType": "hook_received", carrying the correct conversation hook token and the user's text in its payload. - No new step is created, no run record is updated (
runs/<parkedRunId>.jsonmtime unchanged), no stream event is emitted. - The parked run stays
"status": "running"withstartedAt/updatedAtfrozen at its original creation time. - Its run lock stays
.locks/runs/<parkedRunId>.pending. - The mock provider records 0 requests during the resume attempt (4 for the whole run, all from the turns that worked).
So the hook-received event is durably written for the right run, and then the run is simply never woken to consume it.
Reproduction
A minimal, self-contained app (one agent file, no tools, no skills, no channels) and a local mock OpenAI-compatible provider — no network and no real model needed. All four files are inlined below.
mkdir eve-repro && cd eve-repro
npm init -y
npm pkg set type=module
npm i eve@0.27.13 ai@7.0.39 @ai-sdk/openai-compatible@3.0.18 zod@4.4.3
# add agent/agent.ts, agent/instructions.md, mock-openai.mjs, repro.mjs from below
node repro.mjsThe driver:
eve build, theneve start --host 127.0.0.1 --port <free>, pollGET /until healthy.- Turn 1 on a fresh session →
REPLICA_OK. - Turn 2 seeds a marker:
Remember this code: CEDAR-4729→REMEMBERED. - Control A (pre-restart): rehydrate with
client.session(session.state)and take another turn — works. This proves the saved state is a usable handle; only the restart breaks it. - Save
session.state, then stop the server withSIGTERMto the process group (15s grace, well past eve's 5s graceful window), andeve startagain on the same port and data dir. - Control B (post-restart): a brand-new session takes a turn — works.
- Snapshot
.eve/.workflow-data(path → mtime, recursive), thenclient.session(savedState).send("What code did I ask you to remember? ...")under a 60s timeout, then snapshot again and diff.
Observed
All output below is from a single run; the parked session is wrun_01KYV2ZGM639PTPVB366TSRWH8.
[repro] turn 1 on a fresh session
[repro] reply: "REPLICA_OK"
[repro] turn 2: seed the marker
[repro] reply: "REMEMBERED"
[repro] control: resume from saved state WITHOUT restarting (proves the saved state is usable)
[repro] reply: "REPLICA_OK"
[repro] saved session state: {"continuationToken":"eve:78d5149d-7ff7-40a7-9f5b-920f2cfe57c5","sessionId":"wrun_01KYV2ZGM639PTPVB366TSRWH8","streamIndex":25}
[repro] restart eve (SIGTERM to the process group, then start again)
Stopping server gracefully (5s)...
[world-local] Re-enqueued 2 active run(s) on startup
[repro] control: a fresh session after the restart
[repro] send accepted (sessionId=wrun_01KYV2ZPBHGF9DDX8HHD2GR151) token=eve:6996ac6a-7b76-4439-92ae-ff1f0fd619da
[repro] reply: "REPLICA_OK" (fresh sessions work)
[repro] .workflow-data before resume: 259 files
[repro] resume the parked session across the restart
[repro] send accepted (sessionId=wrun_01KYV2ZGM639PTPVB366TSRWH8) token=eve:78d5149d-7ff7-40a7-9f5b-920f2cfe57c5
[repro] --- resume result ---
[repro] REPRODUCED: turn timed out after 60s (POST accepted: true, continuationToken: eve:78d5149d-7ff7-40a7-9f5b-920f2cfe57c5)
[repro] provider calls during resume: 0
[repro] .workflow-data new files: 1 -> events/wrun_01KYV2ZGM639PTPVB366TSRWH8-evnt_01KYV2ZVX3NRHWJ4BXRXFX3V1S.json
[repro] .workflow-data touched files: 0
[repro] new: events/wrun_01KYV2ZGM639PTPVB366TSRWH8-evnt_01KYV2ZVX3NRHWJ4BXRXFX3V1S.json
[repro] total provider calls this run: 4
That single new file, events/wrun_01KYV2ZGM639PTPVB366TSRWH8-evnt_01KYV2ZVX3NRHWJ4BXRXFX3V1S.json — note the correct run id and the user's message inside the payload:
{
"eventType": "hook_received",
"specVersion": 5,
"correlationId": "hook_01KYV2ZGM6BCQXCGJ6FJGP7PFR",
"eventData": {
"token": "eve:eve:78d5149d-7ff7-40a7-9f5b-920f2cfe57c5",
"payload": { "__type": "Uint8Array", "data": "<base64>" }
},
"runId": "wrun_01KYV2ZGM639PTPVB366TSRWH8",
"eventId": "evnt_01KYV2ZVX3NRHWJ4BXRXFX3V1S",
"createdAt": "2026-07-31T03:20:22.691Z"
}Its decoded payload is exactly the message that should have woken the run:
devl[{"auth":1,"kind":4,"payloads":5,"requestId":-1},{...},{},"local-dev","deliver",[6],
{"inputResponses":-1,"message":7,"context":-1,"outputSchema":-1},
"What code did I ask you to remember? Reply with the code only."]
The full event timeline for that run makes the contrast plain. Before the restart, every conversation hook_received is followed within ~200ms by turn-control hook activity and a step_created / step_started / step_completed triplet. The post-restart hook_received at 03:20:22.691 is the last event the run ever produces:
03:20:11.172 run_created
03:20:11.216 run_started
03:20:11.277 step_created / step_started
03:20:11.320 step_completed
03:20:11.355 hook_created (conversation token eve:eve:78d5149d-…)
03:20:11.355 hook_created (…:auth)
03:20:11.425 hook_created (…:turn-control:0)
03:20:11.451 step_created / step_started
03:20:11.517 step_completed
03:20:11.955 hook_received (conversation token) <- turn 2, works
03:20:12.105 hook_received (…:turn-control:0)
03:20:12.127 hook_created (…:turn-control:1)
03:20:12.160 step_created / step_started
03:20:12.213 step_completed
03:20:12.432 hook_received (conversation token) <- control A, works
03:20:12.491 hook_received (…:turn-control:1)
03:20:12.521 hook_created (…:turn-control:2)
03:20:12.521 hook_disposed (…:turn-control:0)
03:20:12.549 step_created / step_started
03:20:12.611 step_completed
--- server restarted here ---
03:20:22.691 hook_received (conversation token) <- resume, nothing follows, ever
And the run record and lock from the same run, after the resume attempt timed out:
{ "status": "running",
"createdAt": "2026-07-31T03:20:11.172Z",
"startedAt": "2026-07-31T03:20:11.216Z",
"updatedAt": "2026-07-31T03:20:11.216Z" }.locks/runs/wrun_01KYV2ZGM639PTPVB366TSRWH8.pending
Reproduced 5 out of 5 runs, deterministically.
Expected
client.session(savedState).send(...) after a server restart resumes the parked session: the run wakes, the model is called with the replayed transcript, and result() resolves with the answer (here, CEDAR-4729). Failing that, the continuation should be rejected loudly rather than accepted and silently discarded.
Files
package.json
{
"name": "eve-repro",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "eve build",
"start": "eve start",
"repro": "node repro.mjs"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@ai-sdk/openai-compatible": "^3.0.18",
"ai": "^7.0.39",
"eve": "^0.27.13",
"zod": "^4.4.3"
}
}agent/agent.ts
import { defineAgent } from "eve";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
const model = createOpenAICompatible({
name: "mock",
baseURL: process.env.MOCK_BASE_URL!,
apiKey: "x",
})("mock-model");
// The mock model has no AI Gateway metadata, so the context window must be stated here
// or `eve build` refuses to compile the compaction trigger.
export default defineAgent({ model, modelContextWindowTokens: 131072 });agent/instructions.md
You are a terse test agent. Answer in as few words as possible.mock-openai.mjs
// Standalone mock OpenAI-compatible provider: POST /v1/chat/completions.
// Default reply "REPLICA_OK"; echoes a CEDAR-#### marker found in the transcript when asked.
// Logs every request to MOCK_LOG (one JSON line per request) so the driver can tell
// whether the agent server ever called the provider.
import { createServer } from "node:http";
import { appendFileSync } from "node:fs";
const MODEL = "mock-model";
const LOG = process.env.MOCK_LOG;
function partText(content) {
if (typeof content === "string") return content;
if (Array.isArray(content)) return content.map((p) => (typeof p === "string" ? p : (p?.text ?? ""))).join(" ");
return "";
}
const transcriptText = (messages) => messages.map((m) => partText(m?.content)).join("\n");
function lastUserText(messages) {
for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === "user") return partText(messages[i].content);
return "";
}
function chooseResponse(body) {
const messages = Array.isArray(body?.messages) ? body.messages : [];
const last = lastUserText(messages);
if (/What code did I ask you to remember/i.test(last)) {
const match = transcriptText(messages).match(/CEDAR-\d+/);
return match ? match[0] : "MISSING_MARKER";
}
if (/Remember this code/i.test(last)) return "REMEMBERED";
return "REPLICA_OK";
}
const completionJson = (text) => ({
id: "chatcmpl-mock",
object: "chat.completion",
created: 1,
model: MODEL,
choices: [{ index: 0, message: { role: "assistant", content: text }, finish_reason: "stop" }],
usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 },
});
function streamChunks(text) {
const base = { id: "chatcmpl-mock", object: "chat.completion.chunk", created: 1, model: MODEL };
return [
{ ...base, choices: [{ index: 0, delta: { role: "assistant", content: text }, finish_reason: null }] },
{ ...base, choices: [{ index: 0, delta: {}, finish_reason: "stop" }] },
{ ...base, choices: [], usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 } },
];
}
async function readBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
return Buffer.concat(chunks).toString("utf8");
}
export async function startMockOpenAiServer() {
const requests = [];
const server = createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/v1/chat/completions") {
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: { message: `no route: ${req.method} ${req.url}` } }));
return;
}
let body;
try {
body = JSON.parse(await readBody(req));
} catch {
res.writeHead(400, { "content-type": "application/json" });
res.end(JSON.stringify({ error: { message: "invalid JSON body" } }));
return;
}
requests.push(body);
if (LOG) appendFileSync(LOG, JSON.stringify({ at: new Date().toISOString(), last: lastUserText(body.messages ?? []) }) + "\n");
const text = chooseResponse(body);
if (body?.stream) {
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
for (const chunk of streamChunks(text)) res.write(`data: ${JSON.stringify(chunk)}\n\n`);
res.end("data: [DONE]\n\n");
} else {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify(completionJson(text)));
}
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const { port } = server.address();
return {
baseUrl: `http://127.0.0.1:${port}/v1`,
requests,
close: () => new Promise((resolve) => server.close(resolve)),
};
}
// Standalone mode: `node mock-openai.mjs` prints its base URL and stays up.
if (import.meta.url === `file://${process.argv[1]}`) {
const mock = await startMockOpenAiServer();
console.log(mock.baseUrl);
}repro.mjs
// Minimal reproduction for the eve 0.27.13 session-resume wedge.
//
// Scenario: park a client session, restart the eve server, then continue the parked
// session with client.session(savedState).send(...). The POST is accepted (a continuation
// token comes back) but result() never resolves and the workflow data directory stays frozen.
//
// Run: MOCK_BASE_URL is wired up automatically; just `node repro.mjs` from this directory.
import { spawn } from "node:child_process";
import { createServer } from "node:net";
import { readdirSync, statSync, existsSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { startMockOpenAiServer } from "./mock-openai.mjs";
const APP = dirname(fileURLToPath(import.meta.url));
const MARKER = "CEDAR-4729";
const HEALTH_TIMEOUT_MS = 90_000;
const TURN_TIMEOUT_MS = 60_000;
const WORKFLOW_DIR = join(APP, ".eve", ".workflow-data");
const logs = [];
const note = (line) => {
logs.push(line);
if (logs.length > 600) logs.shift();
if (process.env.VERBOSE === "1") console.error(line);
};
const say = (line) => console.log(`[repro] ${line}`);
async function freePort() {
return new Promise((resolve, reject) => {
const srv = createServer();
srv.once("error", reject);
srv.listen(0, "127.0.0.1", () => {
const { port } = srv.address();
srv.close(() => resolve(port));
});
});
}
function run(cmd, args, env) {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { cwd: APP, env, stdio: ["ignore", "pipe", "pipe"] });
const capture = (buf) => String(buf).split("\n").filter(Boolean).forEach(note);
child.stdout.on("data", capture);
child.stderr.on("data", capture);
child.on("error", reject);
child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited with ${code}`))));
});
}
function startEve(env, port) {
const child = spawn(
process.execPath,
[join(APP, "node_modules/eve/bin/eve.js"), "start", "--host", "127.0.0.1", "--port", String(port)],
{ cwd: APP, env, detached: true, stdio: ["ignore", "pipe", "pipe"] },
);
const capture = (buf) => String(buf).split("\n").filter(Boolean).forEach(note);
child.stdout.on("data", capture);
child.stderr.on("data", capture);
return child;
}
async function stopEve(child) {
if (!child || child.exitCode !== null) return;
const gone = new Promise((resolve) => child.once("exit", resolve));
try {
process.kill(-child.pid, "SIGTERM");
} catch {
return;
}
const timer = new Promise((resolve) => setTimeout(resolve, 15_000, "timeout"));
if ((await Promise.race([gone, timer])) === "timeout") {
try {
process.kill(-child.pid, "SIGKILL");
} catch {
/* already dead */
}
await gone;
}
}
async function waitForHealth(port, child) {
const deadline = Date.now() + HEALTH_TIMEOUT_MS;
while (Date.now() < deadline) {
if (child.exitCode !== null) throw new Error(`eve exited with ${child.exitCode} before becoming healthy`);
try {
const res = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(2000) });
if (res.ok) return;
} catch {
/* not up yet */
}
await new Promise((r) => setTimeout(r, 250));
}
throw new Error(`eve did not become healthy within ${HEALTH_TIMEOUT_MS / 1000}s`);
}
// Recursive snapshot of .eve/.workflow-data: relative path -> mtimeMs.
function snapshotWorkflowData(dir = WORKFLOW_DIR, base = WORKFLOW_DIR, out = new Map()) {
if (!existsSync(dir)) return out;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) snapshotWorkflowData(full, base, out);
else out.set(full.slice(base.length + 1), statSync(full).mtimeMs);
}
return out;
}
function diffSnapshots(before, after) {
const added = [...after.keys()].filter((k) => !before.has(k));
const changed = [...after.keys()].filter((k) => before.has(k) && before.get(k) !== after.get(k));
return { added, changed };
}
const accepted = { send: false, token: null };
async function turn(session, prompt, timeoutMs = TURN_TIMEOUT_MS) {
let timer;
accepted.send = false;
accepted.token = null;
try {
const result = await Promise.race([
session.send(prompt).then((r) => {
accepted.send = true;
accepted.token = session.state?.continuationToken ?? null;
note(`[repro] send accepted (sessionId=${session.state?.sessionId ?? "new"}) token=${accepted.token}`);
return r.result();
}),
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error(`turn timed out after ${timeoutMs / 1000}s (POST accepted: ${accepted.send}, continuationToken: ${accepted.token})`)),
timeoutMs,
);
}),
]);
note(`[repro] turn done: status=${JSON.stringify(result?.status)}`);
if (result.status === "failed" || !result.message) throw new Error(`turn failed: status=${result.status}`);
return result.message.trim();
} finally {
clearTimeout(timer);
}
}
async function main() {
if (process.env.KEEP_STATE !== "1") rmSync(join(APP, ".eve"), { recursive: true, force: true });
const mock = await startMockOpenAiServer();
const port = await freePort();
const env = { ...process.env, MOCK_BASE_URL: mock.baseUrl, PORT: String(port), NODE_ENV: "production" };
let eve = null;
let reproduced = false;
try {
say("build");
await run(process.execPath, [join(APP, "node_modules/eve/bin/eve.js"), "build"], env);
say(`start (port ${port})`);
eve = startEve(env, port);
await waitForHealth(port, eve);
const { Client } = await import("eve/client");
const client = new Client({ host: `http://127.0.0.1:${port}` });
say("turn 1 on a fresh session");
const session = client.session();
const first = await turn(session, "Reply with a status word.");
say(` reply: ${JSON.stringify(first)}`);
say("turn 2: seed the marker");
const seeded = await turn(session, `Remember this code: ${MARKER}`);
say(` reply: ${JSON.stringify(seeded)}`);
say("control: resume from saved state WITHOUT restarting (proves the saved state is usable)");
const rehydrated = client.session(session.state);
const preRestart = await turn(rehydrated, "Reply with a status word.");
say(` reply: ${JSON.stringify(preRestart)}`);
const savedState = rehydrated.state;
say(` saved session state: ${JSON.stringify(savedState)}`);
say("restart eve (SIGTERM to the process group, then start again)");
await stopEve(eve);
eve = startEve(env, port);
await waitForHealth(port, eve);
say("control: a fresh session after the restart");
const fresh = await turn(client.session(), "Reply with a status word.");
say(` reply: ${JSON.stringify(fresh)} (fresh sessions work)`);
// Let the control run's writes settle so the diff below only reflects the resume attempt.
await new Promise((r) => setTimeout(r, 5000));
const mockBefore = mock.requests.length;
const before = snapshotWorkflowData();
say(`.workflow-data before resume: ${before.size} files`);
say("resume the parked session across the restart");
let resumeError = null;
let resumed = null;
try {
resumed = await turn(client.session(savedState), "What code did I ask you to remember? Reply with the code only.");
} catch (err) {
resumeError = err;
}
const after = snapshotWorkflowData();
const { added, changed } = diffSnapshots(before, after);
const mockCalls = mock.requests.length - mockBefore;
say("--- resume result ---");
if (resumeError) {
reproduced = true;
say(` REPRODUCED: ${resumeError.message}`);
} else {
say(` resume returned ${JSON.stringify(resumed)} (expected ${MARKER})`);
if (resumed !== MARKER) reproduced = true;
}
say(` provider calls during resume: ${mockCalls}`);
const parked = savedState.sessionId;
const forParked = (list) => list.filter((f) => f.includes(parked));
say(` .workflow-data new files: ${added.length}${added.length ? ` -> ${added.slice(0, 20).join(", ")}` : ""}`);
say(` .workflow-data touched files: ${changed.length}${changed.length ? ` -> ${changed.slice(0, 20).join(", ")}` : ""}`);
say(` ...of those, belonging to the parked run ${parked}: ${forParked(added).length} new, ${forParked(changed).length} touched`);
for (const f of forParked(added)) say(` new: ${f}`);
for (const f of forParked(changed)) say(` touched: ${f}`);
say(` total provider calls this run: ${mock.requests.length}`);
say(reproduced ? "VERDICT: session-resume wedge REPRODUCED on bare eve" : "VERDICT: resume worked, NOT reproduced");
} catch (err) {
console.error(`[repro] FAILED: ${err?.message ?? err}`);
process.exitCode = 1;
} finally {
console.error("--- last server output ---");
for (const line of logs.slice(-80)) console.error(line);
await stopEve(eve);
await mock.close();
}
if (reproduced) process.exitCode = 2;
}
await main();Notes
- Two small things
eve buildrequired for a truly minimal agent, in case they matter for anyone re-running this:modelContextWindowTokensmust be set for a model with no AI Gateway metadata (otherwise "Cannot compile agent compaction because the primary compaction trigger model … does not have known AI Gateway context window metadata"), andagent/instructions.mdmust exist. - The repro exits
2when the wedge reproduces,1on setup failure,0if resume works.VERBOSE=1streams the eve server output;KEEP_STATE=1keeps.evefrom the previous run instead of starting clean.
Downstream context
Found while investigating smixs/iva#104, where a production assistant on eve lost every parked conversation across each deploy restart. The same scenario worked in that app before its upgrade to the 0.24.x line (reported downstream; not re-tested here), and the reproduction above shows it is not caused by anything in that app's code — a bare agent with no tools, skills, hooks, or channels wedges the same way.
Activity
iroiro147 commentedon Aug 1, 2026
Investigated as a follow-up to the high-fidelity repro in the body.
Boundary of the bug: the seam is on the
@workflow/worldside of the continuation contract, not in eve's session-delivery path.What eve does correctly (verified in source + your repro):
hook_receivedevent for the parked run, and returns the expectedcontinuationToken(events/<runId>-evnt_*.jsonwritten).What's missing is the bridge from "event durably written for run X" to "world wakes X's step runner to consume that event." The startup log
Re-enqueued N active run(s) on startupshowsworld-localdoes re-enqueue parked runs on boot, but the re-enqueue appears to put the run in a step-executor's queue that has no consumer for a new-turn continuation event — only the in-flightsession.waitingsemantics.This is between two systems (eve ↔
@workflow/world), not fixable by a single eve patch without diverging from the world's run-resumption contract. The prior #715 fix landed on the world's side (the re-enqueue itself, in 0.22.6), and any follow-up needs the same shape: either a world change that hooks newly delivered events into re-enqueued runs, or an eve-side re-dispatch helper that explicitly wakes the workflow run after persisting the event — predicated on a world API for doing so.I'd rather flag this than ship a workaround. Happy to pick it back up once the intended seam is confirmed (world-side fix vs eve-side explicit wake), or if a maintainer points at the existing re-enqueue→continuation wiring that should be carrying this event.
sloops77 commentedon Sep 15, 2026
Still present on eve 0.54.3, and reachable from
eve devas well aseve start.Environment
eve 0.54.3, bundled
@workflow/core5.0.0-beta.50,@workflow/world5.0.0-beta.34,@workflow/world-local5.0.0-beta.43. Node 24.14.0, Linux x86_64, 4 vCPU CI runner.Two differences from the original report. This is development mode, where Next's
withEveintegration forkseve dev --no-ui, rather thaneve buildpluseve start. And the restart is not graceful: an integration-test harness kills the process tree and starts a new one over the same.eve/.workflow-data.What we see
A session takes one turn and parks. The harness restarts the application. The continuation
POST /eve/v1/session/:idanswers 202, and then nothing happens. The stream delivers no event and the turn never runs.The durable state matches the description in this issue:
dispatchTurnStepstays at one, so no step is created for the continuation.startedAtandupdatedAtare frozen at the run's creation. The session's inbox hook is intact on disk when the read gives up 60 s later. The runtime prints its boot line and its health response and nothing else, and no[world-local]delivery error appears.Difference worth noting
Ours is intermittent rather than deterministic. On a loaded 4 vCPU runner it fires in roughly one run in three or four. It has never reproduced on macOS or inside a pinned Linux container, where the same case passes in about 9 s. That fits a race rather than a permanent wedge, so whatever consumes the re-enqueued run may sometimes win.
Ruled out on our side
Three harness changes left the failure rate unchanged, which supports the conclusion in the comment above that this is not an eve teardown or boot problem:
eve devprocess tree to exit before starting the replacement, so no orphaned runtime overlaps the new one on the same data directoryeve devbefore Next's exit hook can send it a second SIGTERM, so its own cleanup completes/eve/v1/health, which the worker answers two boot phases beforeworld.start()We have worked around it by not restarting the runtime in the affected tests.
roboeve commentedon Sep 28, 2026
Confirmed on tested
e574dd3(origin/mainis now8149c72; workflow code/dependencies are unchanged): after restart, the continuation POST is accepted but the saved session times out after 60s with zero provider calls; fresh sessions work. Command:node /workspace/.repro/issue-1450/repro.mjs(exit 2).arielrahmane commentedon Oct 26, 2026
Cross-reference: vercel/workflow#5083 documents a concrete local-storage crash-consistency defect that can cause a follow-up turn to stall after a restart.
In a built Eve 0.52.5 fixture with a local mock model, stopping immediately after the reply /
session.waitingleft either:.createdmarker without its canonical record orstep_createdevent; or.terminalmarker / completed record without its correspondingstep_completedevent.Recovery then reported
already created/Cannot modify step in terminal statewhile replay still needed the missing transition. One retained trace looped more than 4,000 times without progress.The linked report contains a standalone reproducer for three interruption points, with a passing no-crash control and repeated failed retries. All three storage cases also reproduce in the current published
@workflow/world-local@5.1.2and in the compiled local World bundled with currenteve@0.83.3, using spec 8. Current-version verification is storage-only; the full Eve restart fixture was run on 0.52.5.This provides one concrete mechanism to investigate here; it does not establish that every reproduction in #1450 has this cause. It is also separate from the 860-second orphaned-inline-owner delay tracked in vercel/workflow#4939 and repaired by vercel/workflow#4940. Releasing ownership does not restore missing step records/events.
[world-local] recover orphaned hooks for interrupted `hook_created` (#…