Two things change in 2.0.0: the plugin API (V1 → V2) and the storage contract (project-level JSON → session-scoped SQLite).
The key is plugins in V2, and a package plus options is now an object.
// V2 with options
{
"plugins": [
{ "package": "@gitdamnit/checkpoint-orchestrator", "options": { "historyLimit": 20 } }
]
}V1 plugin implementations do not run in V2. If you are porting your own plugin,
Plugin.define({ id, setup }) replaces the exported function, tools move to
ctx.tool.transform, and string-keyed hooks move onto their domain
(ctx.session.hook("prompt" | "compaction", …), ctx.event.subscribe()). See
docs/COMPATIBILITY.md for this package's V1 status.
Legacy files live at .opencode/state/checkpoint.json and
.opencode/state/checkpoint-history.json in each project.
On first load, for each project the plugin:
- Detects the legacy files and validates them. A file that parses but is not this plugin's data is reported and skipped, never imported.
- Copies them to
.opencode/state/checkpoint-orchestrator/legacy-backup/<name>.<timestamp>— this plugin's own namespace, not the shared directory. Originals are never modified or deleted. - Imports everything inside a single transaction into a clearly named legacy
namespace: a project row for the directory, and a session row whose id is
legacy::<projectID>with the title "Legacy project-level checkpoint (imported)". - Writes a ledger row (
legacy-json:<projectID>) so the import cannot run twice on repeated startup. - If any step fails, the transaction rolls back and the ledger row is not written, so the import can be retried on the next start.
Legacy snapshot names and timestamps are preserved. If two legacy snapshots share
a name, later ones are suffixed (name#2) instead of overwriting each other. The
import honours your configured historyLimit.
Set legacyImport: "off" to skip the automatic import entirely.
A project-level JSON checkpoint has no recorded session id. Copying it into "whatever session happens to be running" is exactly the collision this release removes, so the import never guesses.
Legacy state is imported, not adopted. Adoption is explicit and per session:
checkpoint_migrate # report: is there legacy data? has it been imported?
checkpoint_migrate { "adopt": true } # copy it into THIS session
Adoption creates a snapshot named adopted-legacy-<timestamp> and records the
adoption in the ledger. The legacy namespace is left intact, so nothing is
consumed and the operation is repeatable in another session.
Legacy files are untouched, so reverting is a configuration change:
plugins→plugininopencode.json(c).- Install
@gitdamnit/checkpoint-orchestrator@1.0.1. - Restart OpenCode. The 1.x plugin reads the original
.opencode/state/checkpoint.jsonagain.
Any work saved after upgrading exists only in the SQLite database. Use
checkpoint_load to read it and checkpoint_save in the 1.x line to write it
back into .opencode/state/, or keep the database and upgrade again later. The
database is never deleted by an upgrade.
~/.local/share/opencode/checkpoint-orchestrator/checkpoints.db(Linux), directory0700, file0600..opencode/state/checkpoint-orchestrator/handoff-<sessionId>.mdper session, regenerable at any time. Legacy backups land beside them in.opencode/state/checkpoint-orchestrator/legacy-backup/.
If you want the database somewhere else, set databasePath or
CHECKPOINT_ORCHESTRATOR_DB.
The SQLite database is the authoritative store, and unlike the old JSON files it
has sidecar -wal and -shm files while in use. Do not copy checkpoints.db
alone from a running OpenCode; take a consistent copy with SQLite's own backup
API, or copy all three files while OpenCode is stopped.
node -e '
const { DatabaseSync } = require("node:sqlite")
const p = process.env.CHECKPOINT_ORCHESTRATOR_DB ||
process.env.HOME + "/.local/share/opencode/checkpoint-orchestrator/checkpoints.db"
const src = new DatabaseSync(p)
src.backup(p + ".backup").then(() => { src.close(); console.log("backup written") })
'Recovery ladder:
- Per session, in place —
checkpoint_list, thencheckpoint_restore_snapshot. Restoring captures the current state as apre-restore-<timestamp>snapshot first, and that undo point is never pruned. - Whole database — stop OpenCode, replace
checkpoints.db(and remove any stale-wal/-shm) with a backup. - Damage —
PRAGMA integrity_checkandPRAGMA foreign_key_checkreport corruption. The plugin only logs the outcome; it never rewrites the file.
A user_version newer than the running build is refused rather than used, so an
older build cannot silently downgrade a newer database.
The migration itself never deletes legacy JSON, so the pre-2.0 files remain the last-resort source of truth for anything that was never imported.