Upgrading

Dawn is pre-1.0. The API surface is still moving, and the safest posture is to pin a version, read what changed, and upgrade deliberately rather than floating on a range. This page is the workflow, not a changelog — the actual list of what changed between any two versions lives on GitHub, not here.

Fixed-group versioning

Every publishable Dawn package — @dawn-ai/cli, @dawn-ai/core, @dawn-ai/sdk, the other @dawn-ai/* packages, and create-dawn-ai-app — is released together under one version number via Changesets' fixed-group mode. If @dawn-ai/cli is at 0.8.12, so is every other package in that release. There's no independent versioning between packages to reason about, and no compatibility matrix to check — install matching versions and you're done.

The two Helm charts (dawn-sandbox-infra, dawn-app) track the same package train through their appVersion field, but they're published separately as OCI artifacts, not npm packages — see Kubernetes for how to pull a specific chart version.

How to read what changed

Each release is built from the changeset entries merged since the previous one — short, per-change Markdown files that describe what changed and why, written by whoever made the change. Two places to read them:

  • GitHub Releasesgithub.com/cacheplane/dawnai/releases lists every published version with its changeset entries rolled up into release notes. This is the fastest way to scan what changed between two versions.
  • Per-package CHANGELOG.md — each package under packages/*/CHANGELOG.md in the repository carries its own changelog, generated from the same changeset entries but scoped to that package.

Upgrade workflow

  1. 1

    Check the current version

    text
    pnpm why @dawn-ai/cli

    or check the @dawn-ai/* entries in your package.json.

  2. 2

    Read the release notes

    Open GitHub Releases and read every entry between your current version and the target version.

  3. 3

    Bump and reinstall

    Bump every @dawn-ai/* dependency in package.json to the same target version, then reinstall.

  4. 4

    Regenerate and verify

    text
    dawn typegen
    dawn verify
    dawn test

    dawn verify runs five phases: app discovery and config validation; route discovery; tool type extraction and typegen rendering; advisory checks for missing dependencies and provider environment variables; and runtime readiness for Node plus any configured sandbox provider. dawn test re-runs your scenario coverage against the new version. See CLI and Testing.

Node 24 is the minimum

Current Dawn packages declare node >=24.0.0, and create-dawn-ai-app refuses to scaffold on older releases. Before upgrading dependencies, move local version-manager files, CI runners, and custom container bases to Node 24 or later, reinstall with that runtime, and run dawn verify. Node 24 also supplies the npm and unflagged node:sqlite versions the scaffold expects.

Node-only imports moved to /node

If application tooling imports filesystem or process-backed helpers, update the import specifier while keeping the symbol unchanged:

  • From @dawn-ai/core/node: discoverRoutes, findDawnApp, assertDawnRoutesDir, extractToolSchemasForRoute, extractToolTypesForRoute, and registerTsxLoader.
  • From @dawn-ai/permissions/node: createPermissionsStore.
  • From @dawn-ai/workspace/node: localFilesystem and localExec.

For example, change import { discoverRoutes } from "@dawn-ai/core" to import { discoverRoutes } from "@dawn-ai/core/node". Runtime-agnostic contracts and helpers remain on each package's main entry; use /node only for the explicit Node surface.

Pinning

Pin exact versions (no ^ or ~ ranges) for @dawn-ai/* packages until the project reaches 1.0 — a floating range means an unreviewed minor bump can land in CI or production without the deliberate read-the-notes step above.

toolOutput is now gated off the hono target

If your app configures toolOutput and names "hono" in build.targets, dawn build and dawn check now fail with DAWN_E1005 where they used to pass. Nothing about your app changed; the gate did. Tool-output offloading spills oversized tool results to a file under workspace/ and hands the model a pointer to it, and an edge runtime has no filesystem to spill to — so the feature never worked on that target. It was also the only gated feature whose config is plain JSON, which is exactly why it slipped through: the other gated keys are live objects that get stripped at the build boundary, while toolOutput was inlined into the bundle intact and then ignored at runtime. A green build and a worker that silently never offloads is worse than a failed build.

Two ways forward: remove toolOutput from dawn.config.ts, or drop "hono" from build.targets and deploy with the node target, which serves offloading normally. An empty toolOutput: {} expresses no intent and is not gated.

Node deployments are unaffected — the gate is specific to the hono build target, and the matching runtime check (below) cannot fire on Node at all.

Gated features now fail loudly at request time, not just at build time

sandbox, route skills, and toolOutput used to be read and then quietly do nothing on a runtime with no filesystem. All three now raise DAWN_E1005 on every request instead, naming the feature and the config key that introduced it. See What the edge cannot serve.

This closes a gap the build gate could not: composing an entry by hand over @dawn-ai/cli/fetch is a supported way to deploy, and such an app never runs the hono target, so it never met the build gate at all.

No Node app can hit this, whatever it configures. The check short-circuits before it reads a single config key whenever the runtime supplied filesystem fallbacks, and every Node entry point supplies them unconditionally. An absent sandbox on Node remains the documented degrade it has always been. The deployments that can hit it are edge ones, and for them the affected config was already doing nothing.

@dawn-ai/postgres-storage now requires a pool on its main entry

connectionString has moved to a new @dawn-ai/postgres-storage/node subpath. In 0.8.19 the main entry accepted either and built its own pg pool from a connection string; it no longer does, because it now imports pg for types only so that the package links on a runtime with no TCP sockets — which is what makes the hono edge target possible at all.

connectionString is gone from the main entry's option type, so passing it there is a type error, and the factory throws at construction naming the missing pool. Nothing fails silently. Two ways to migrate:

ts
// 1. Change the import — same factories, connectionString still works, and the
//    store still builds and owns its pool.
import { postgresCheckpointer } from "@dawn-ai/postgres-storage/node"
 
// 2. Or build the pool yourself and keep the main entry. This is what you want
//    anyway when one pool serves all three stores.
import { Pool } from "pg"
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
// Do not skip this — see below.
pool.on("error", (error) => {
  console.error("postgres pool client error (connection dropped):", error)
})
postgresCheckpointer({ pool })

The /node entry re-exports everything the main entry does, so option 1 is usually one line. Pool ownership is unchanged in both shapes: a pool the store built is ended by close(), an injected pool is left alone (ownsPool, defaulting to false). This shipped as a patch despite being breaking: under fixed-group versioning a minor bump would move every @dawn-ai/* package to 1.0.0, which is not what a pre-1.0 project wants to say about one entry-point split. It is also why the advice at the top of this page — pin exact versions and read the notes — is not boilerplate.

kind: "reflection" is now accepted

Memory distillation wired the reflection kind. defineMemory({ kind: "reflection" }) and the generated remember tool now accept it — previously it was typed but threw "memory kind 'reflection' is not yet wired" at write time. Reflections are append-only (like episodic writes): a later insight never supersedes an earlier one, and ask mode never prompts for one. dawn memory reflect produces them, as candidate records by default.

procedural remains the one typed-but-unwired kind and still throws.

No action required. This change is purely additive — nothing that worked before behaves differently, and no existing app needs to change. Adopt it only if you want a reflection collection or want to run the distillation commands.

MemoryStore now requires browse and stats

The MemoryStore contract gained two required methods to power the Inspector's Memory panel. If you implement a custom store for config.memory.store, add both:

  • browse(q?) — cross-namespace/status listing, returning { records, total } with records ordered updated_at DESC, id ASC and total counting all matches (ignoring limit/offset). The optional query narrows by namespacePrefix, status, kind, and sourceType.
  • stats(opts?) — aggregate counts: { total, byStatus, byKind, byNamespace, bySourceType }, optionally scoped to a namespacePrefix.

The built-in SQLite and pgvector stores already implement both, and the runMemoryStoreConformance kit in @dawn-ai/testing covers them — run it against a custom store to verify the contract.

Two related behavior changes in the same release:

  • The config-facing store type is now the full MemoryStore contract — delete and listCandidates included — rather than the narrower capability-facing surface. A custom store that already satisfied the CLI's dawn memory commands is unaffected.
  • dawn memory approve (and the Inspector's Approve) now reconciles supersession: approving a candidate that contradicts an active record with the same identity key supersedes the old record instead of leaving two active rows; approving an identical duplicate dedupes it.

MemoryStore now requires prune

Episodic memory added a required retention method to the MemoryStore contract. If you implement a custom store for config.memory.store, add it:

ts
prune(opts: {
  now: string              // the clock — rows with expiresAt <= now are expired
  namespacePrefix?: string // scope the pass to matching namespaces
  cap?: number             // per-namespace cap for episodic records
}): Promise<{ deletedExpired: number; deletedOverCap: number }>

Semantics:

  • TTL — delete every record whose expiresAt is at or before now (records without expiresAt never expire).
  • Cap — within each namespace, keep at most cap episodic records, deleting the oldest beyond it. No cap means no cap pass.

The runtime episode recorder calls prune lazily after each write, and dawn memory prune runs it manually.

Two related behavior changes in the same release:

  • search and browse accept since/until (ISO instants; since inclusive, until exclusive) comparing against effectiveAt with a createdAt fallback.
  • When a query supplies now, search and browse must exclude expired rows (expiresAt <= now). Queries without now are unchanged.

The built-in SQLite and pgvector stores already implement all of this, and the runMemoryStoreConformance kit in @dawn-ai/testing covers it — run the kit against a custom store to verify the contract.