{"record":{"id":"ffd7caca6534fa29","repo":"can1357/oh-my-pi","slug":"no-messages-to-continue-from","errorCode":null,"errorMessage":"No messages to continue from","messagePattern":"No messages to continue from","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/agent.ts","lineNumber":1250,"sourceCode":"\t\t\tconst messages = this.#state.messages;\n\t\t\tif (messages.length === 0) {\n\t\t\t\t// An empty transcript has nothing to resume, but a queued steer/follow-up\n\t\t\t\t// must still be delivered as the opening turn — mirroring the assistant-tail\n\t\t\t\t// branch below. Throwing here leaves the message undeliverable, and idle-drain\n\t\t\t\t// callers (AgentSession#scheduleQueuedMessageDrain) re-arm continue() on every\n\t\t\t\t// microtask because hasQueuedMessages() never clears, spinning an unbounded\n\t\t\t\t// allocation loop until OOM (issue #6344).\n\t\t\t\tconst queuedSteering = await this.#dequeueSteeringMessagesAfterHooks(dequeueSignal);\n\t\t\t\tif (queuedSteering.length > 0) {\n\t\t\t\t\tawait this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true }, signal, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst queuedFollowUp = await this.#dequeueFollowUpMessagesAfterHooks(dequeueSignal);\n\t\t\t\tif (queuedFollowUp.length > 0) {\n\t\t\t\t\tawait this.#runLoop(queuedFollowUp, undefined, signal, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthrow new Error(\"No messages to continue from\");\n\t\t\t}\n\t\t\tif (messages[messages.length - 1].role === \"assistant\") {\n\t\t\t\t// A tail with unpaired runnable tool calls resumes by re-executing\n\t\t\t\t// them (see `unpairedToolCallTail` in agent-loop). This must win over\n\t\t\t\t// queued-message delivery: injecting a message between the tool_use\n\t\t\t\t// blocks and their results would break the provider's pairing\n\t\t\t\t// invariant. Queued messages drain inside the resumed loop instead.\n\t\t\t\tif (unpairedToolCallTail(messages)) {\n\t\t\t\t\tawait this.#runLoop(undefined, undefined, signal, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst queuedSteering = await this.#dequeueSteeringMessagesAfterHooks(dequeueSignal);\n\t\t\t\tif (queuedSteering.length > 0) {\n\t\t\t\t\tawait this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true }, signal, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst queuedFollowUp = await this.#dequeueFollowUpMessagesAfterHooks(dequeueSignal);","sourceCodeStart":1232,"sourceCodeEnd":1268,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/agent/src/agent.ts#L1232-L1268","documentation":"Agent.continue() throws this when the transcript is empty (messages.length === 0) AND there are no queued steering or follow-up messages to deliver as the opening turn (packages/agent/src/agent.ts:1250). continue() resumes an existing conversation; with zero messages and an empty queue there is nothing to resume. The queued-message checks exist specifically so idle-drain callers don't spin on an undeliverable queue (issue #6344).","triggerScenarios":"Calling agent.continue() on a freshly constructed or reset() Agent (no messages) with no pending steering/follow-up entries; calling continue() after clearMessages() on an empty session.","commonSituations":"Resume/retry logic that calls continue() unconditionally at startup before any prompt was ever made; a crash-recovery path that finds an empty transcript file; calling continue() after reset() drained both history and queues.","solutions":["Check that the transcript is non-empty before calling continue(): if messages.length === 0 and nothing is queued, call prompt() with a new user message instead.","If you intended to start a conversation, use prompt() rather than continue().","If resuming from persisted state, verify the session file actually loaded messages (guard against a missing/empty session).","If you meant to inject work, queue it via steer()/followUp() first so continue() can deliver it."],"exampleFix":"// before\nawait agent.continue(); // throws when transcript is empty\n\n// after\nif (hasQueuedMessages(agent) || lastMessageRole(agent) !== undefined) {\n  await agent.continue();\n} else {\n  await agent.prompt(\"resume work on task X\");\n}","handlingStrategy":"validation","validationCode":"function canContinue(agent: Agent): boolean {\n  const msgs = getMessages(agent);\n  return msgs.length > 0 || hasQueuedMessages(agent);\n}\nif (canContinue(agent)) {\n  await agent.continue();\n} else {\n  await agent.prompt(initialUserMessage);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await agent.continue();\n} catch (err) {\n  if (err instanceof Error && err.message === \"No messages to continue from\") {\n    await agent.prompt(\"Starting new task: ...\");\n    return;\n  }\n  throw err;\n}","preventionTips":["Only call continue() to resume an existing/interrupted conversation; use prompt() to start one.","After reset() or clearMessages(), treat the agent as fresh and require a prompt().","When restoring sessions, verify the transcript loaded non-empty before resuming.","In idle-drain loops, stop when the transcript is empty and hasQueuedMessages() is false."],"tags":["state","agent","empty-transcript","api-misuse"],"backgroundTag":"no-messages-to-continue","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}