`ctx.facets.clone()` of a Think facet comes back empty in production (works locally) · Issue #2607 · cloudflare/agents · GitHub
Skip to content

ctx.facets.clone() of a Think facet comes back empty in production (works locally) #2607

Open

Description

@jackdnl

Drafted 2026-10-16 from Joke's production (account schematik, script joke).
Versions: agents 0.28.0, @cloudflare/think 0.20.2, ai 7.0.137, wrangler 4.147.0,
compatibility_date 2026-10-01, nodejs_compat. Object layout: JokeScope (root DO) →
JokeSession (Think facet) → JokeWorker (Think facet, run with runAgentTool, detached).


(This is a runtime API; it may belong in workerd. Filed here because the facet is an agents sub-agent and
the archive path is the documented way to snapshot one without waking it.)

What happens

To archive a session without waking or stopping it, the root clones the facet and reads the clone:

const clone = `recovery-export:${crypto.randomUUID()}`;
this.ctx.facets.clone(`${className}\0${sessionId}`, clone);
try {
  const copy = this.ctx.facets.get<RecoverySession>(clone, () => ({ class: this.ctx.exports.RecoverySession }));
  return await copy.exportRecovery(key);   // reads sqlite_master and every table of the copy
} finally { this.ctx.facets.delete(clone); }

Locally (wrangler dev / miniflare, same compatibility date) the copy has the session's tables and rows. Hosted,
every copy has no tables.

Evidence

  • Nightly archive of 2026-10-15 (nightly/2026-10-15T06-23-37-000Z/ ): 201 conversations,
    27 sessions exported, 27 of 27 with "tables": 0, "rows": 0, e.g.
    {"id":"c728855c-…","tables":0,"rows":0,"children":0} under a scope whose own export has 17 tables / 6 rows.
    The live sessions are not empty (they serve the conversations in Desk).
  • Every nightly session export since we introduced the clone path (2026-10-11) was empty; one run also reset a
    scope and its copy.
  • The source facet had been written by the same root shortly before in some cases; a facet started after the
    root's own uncommitted writes is a known hosted reset for us.

Repro notes

  1. Deploy a root DO with a Think facet; write messages into the facet.
  2. In a later request on the root, ctx.facets.clone(sourceName, newName), then ctx.facets.get(newName, …)
    with a class that lists sqlite_master.
  3. Hosted: zero tables. Local: the source's tables.
  4. await ctx.storage.sync() before the clone and again before starting the copy does not help: in the
    2026-10-16 run all 404 conversations with sessions failed with "its copy had no tables".
    An empty copy is what workerd also gives when the source name has no storage of its own, so hosted
    clone may be resolving the facet's storage differently from facets.get (e.g. by name without the
    root's facet registry). Joke now archives each session from itself instead (Ovfe #803).

Questions

  • Is facets.clone expected to copy a facet that is currently live (open SQLite connection) in production?
  • Is there an ordering requirement (sync, or the source being idle) that the local runtime does not enforce?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions