Describe the bug
Calling the submitMessages() RPC on a cold (hibernated/evicted) Think agent durably records the submission, but the post-accept drain can execute it before the agent's onStart has assigned this.session (this.session = await this.configureSession(baseSession)). The turn then crashes in _appendMessageToHistory with Cannot read properties of undefined (reading 'appendMessage'), and the submission is marked terminally failed — the durable row survives, but the turn it represents is lost (a retry under the same idempotency key is rejected as a duplicate; a retry under a fresh key hits the same cold-start window).
Warm agents — any instance where a chat connection or other code path already ran onStart in the same lifetime — process the identical submission fine, which makes this intermittent and easy to misdiagnose.
To Reproduce
- A Think agent DO (stock or custom
configureSession).
- Ensure the DO is cold: no WebSocket/chat connection, evicted or never woken since deploy.
- From another Worker, call
stub.submitMessages([message], { idempotencyKey, metadata }) over a DO stub — a "completion callback"-style programmatic turn.
- Observe
[Think] Submission failed … "Cannot read properties of undefined (reading 'appendMessage')" and a terminally-failed submission row.
Sequence (published dist, v0.12.1 line refs): submitMessages accepts → _startSubmissionDrain() → keepAliveWhile(() => this._drainSubmissions()) (think.js ~5206) → _runSubmission → turn execution → _appendMessageToHistory (~1311) dereferences this.session, which the wrapped onStart (~934) has not assigned yet.
Expected behavior
submitMessages (or the drain it triggers) should await the agent's internal readiness — session initialization — before executing a submission. Callers of a public RPC shouldn't have to know about onStart timing. Notably, the alarm path Think schedules via this.schedule(0, "_drainThinkSubmissions") re-enters through the normal agent lifecycle, but the immediate keepAliveWhile drain does not wait for it.
Environment
@cloudflare/think 0.12.1
- The same cross-worker callback pattern ran reliably for us on 0.8.8 under production traffic; after upgrading to 0.12.x we see daily failures whenever the target DO is cold.
Workaround
Subclass override that forces initialization before delegating:
override async submitMessages(...args: Parameters<Think['submitMessages']>) {
await this.ensureInitialized() // awaits __unsafe_ensureInitialized() and asserts this.session
return super.submitMessages(...args)
}
Impact
Any "durable completion hook" pattern (cf. #1752) that wakes an idle agent via submitMessages silently loses events whenever the target agent is cold — for us that was workflow-completion and delegated-sub-agent-completion notifications in production.
Describe the bug
Calling the
submitMessages()RPC on a cold (hibernated/evicted) Think agent durably records the submission, but the post-accept drain can execute it before the agent'sonStarthas assignedthis.session(this.session = await this.configureSession(baseSession)). The turn then crashes in_appendMessageToHistorywithCannot read properties of undefined (reading 'appendMessage'), and the submission is marked terminally failed — the durable row survives, but the turn it represents is lost (a retry under the same idempotency key is rejected as a duplicate; a retry under a fresh key hits the same cold-start window).Warm agents — any instance where a chat connection or other code path already ran
onStartin the same lifetime — process the identical submission fine, which makes this intermittent and easy to misdiagnose.To Reproduce
configureSession).stub.submitMessages([message], { idempotencyKey, metadata })over a DO stub — a "completion callback"-style programmatic turn.[Think] Submission failed … "Cannot read properties of undefined (reading 'appendMessage')"and a terminally-failed submission row.Sequence (published dist, v0.12.1 line refs):
submitMessagesaccepts →_startSubmissionDrain()→keepAliveWhile(() => this._drainSubmissions())(think.js~5206) →_runSubmission→ turn execution →_appendMessageToHistory(~1311) dereferencesthis.session, which the wrappedonStart(~934) has not assigned yet.Expected behavior
submitMessages(or the drain it triggers) should await the agent's internal readiness — session initialization — before executing a submission. Callers of a public RPC shouldn't have to know aboutonStarttiming. Notably, the alarm path Think schedules viathis.schedule(0, "_drainThinkSubmissions")re-enters through the normal agent lifecycle, but the immediatekeepAliveWhiledrain does not wait for it.Environment
@cloudflare/think0.12.1Workaround
Subclass override that forces initialization before delegating:
Impact
Any "durable completion hook" pattern (cf. #1752) that wakes an idle agent via
submitMessagessilently loses events whenever the target agent is cold — for us that was workflow-completion and delegated-sub-agent-completion notifications in production.