Skip to content

[think] Cold-start submitMessages drain executes before onStart initializes session — "Cannot read properties of undefined (reading 'appendMessage')" #1967

Description

@cgrdavies

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

  1. A Think agent DO (stock or custom configureSession).
  2. Ensure the DO is cold: no WebSocket/chat connection, evicted or never woken since deploy.
  3. From another Worker, call stub.submitMessages([message], { idempotencyKey, metadata }) over a DO stub — a "completion callback"-style programmatic turn.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions