Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3b7d5cf
feat!: session-scoped SQLite persistence on the OpenCode V2 plugin API
gitdamnit Oct 15, 2026
c688f27
test: exercise the built plugin instead of reimplementing it
gitdamnit Oct 15, 2026
45e3b80
docs: architecture, migration, compatibility and runtime verification
gitdamnit Oct 15, 2026
963d60e
ci: test on Node 22/24 and verify the packaged install
gitdamnit Oct 15, 2026
ee4115c
chore: carry the npm publish-on-release workflow from the 1.0.x tree
gitdamnit Oct 15, 2026
f9f223b
fix!: namespace persistence and gate legacy import to this plugin's o…
gitdamnit Oct 15, 2026
fa4129b
test: verify independence and co-existence with a stand-in companion …
gitdamnit Oct 15, 2026
dc80592
docs: independence contract, storage namespaces and companion behaviour
gitdamnit Oct 15, 2026
05ca3ec
docs: record why the sibling rows of the matrix cannot run here
gitdamnit Oct 15, 2026
bedc14b
test: keep the suites out of the developer's real data directory
gitdamnit Oct 15, 2026
e4c81e4
fix!: harden SQLite first-open, project identity and legacy-import races
gitdamnit Oct 16, 2026
09e508a
test: refuse a file that is not a database instead of trusting it
gitdamnit Oct 16, 2026
e58cb3d
docs: correct the source-level test count to 120
gitdamnit Oct 16, 2026
fbbe734
fix: copy legacy files aside only after the import is claimed
gitdamnit Oct 16, 2026
e2a4f42
docs: note that a losing legacy-import racer writes no backup
gitdamnit Oct 16, 2026
65bf698
chore: sync package-lock metadata and drop the stale V1 SDK tree
gitdamnit Oct 16, 2026
c643b7d
chore(release): v2.0.0
gitdamnit Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ name: CI

on:
push:
branches: [main]
branches: [main, "feat/**"]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18, 20, 22]
# node:sqlite is only unflagged from Node 22.13 / 23.4.
node-version: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand All @@ -20,5 +22,41 @@ jobs:
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run build
- run: npm test
# The historical 979 V1 assertions. They re-implement the old JSON logic
# instead of importing the build, so they are a V1 reference, not coverage
# of this code; run them so the combined count is reproducible in CI.
- run: npm run test:legacy

# Confirms the built package loads and works when installed, rather than only
# from a workspace checkout.
packaged-install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Pack and install into a clean project
run: |
npm pack --pack-destination /tmp
mkdir -p /tmp/consumer && cd /tmp/consumer
npm init -y
npm install /tmp/gitdamnit-checkpoint-orchestrator-*.tgz
node --input-type=module -e "
const plugin = (await import('@gitdamnit/checkpoint-orchestrator')).default
if (typeof plugin.setup !== 'function') throw new Error('default export is not a V2 plugin')
if (!plugin.id) throw new Error('plugin id missing')
console.log('packaged plugin loads:', plugin.id)
"
- name: Verify no development state is published
run: |
tar -tzf /tmp/gitdamnit-checkpoint-orchestrator-*.tgz > /tmp/contents.txt
! grep -E "\.db$|\.db-wal$|\.db-shm$|node_modules/|tests/|plugins/" /tmp/contents.txt
grep -q "package/dist/checkpoint-orchestrator.js" /tmp/contents.txt
grep -q "package/README.md" /tmp/contents.txt
65 changes: 65 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Publish to npm

# Carried over from the local working tree of the 1.0.x line, where it ran on
# `release: published`.
#
# It is now MANUAL ONLY, on purpose. Publishing a GitHub release must never
# publish to npm as a side effect: the 2.0.0 release is created with npm
# publication deliberately withheld pending explicit maintainer approval, and an
# automatic trigger would make that impossible to guarantee.
#
# To publish after approval, run this workflow from the Actions tab (or
# `gh workflow run publish.yml -f tag=v2.0.0`) and pass the released tag. The
# workflow checks that tag out and refuses to publish unless it matches
# package.json.
on:
workflow_dispatch:
inputs:
tag:
description: "Released git tag to publish, e.g. v2.0.0 (must match package.json version)"
required: true
type: string

permissions:
contents: read
id-token: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout the requested tag
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org

- name: Install dependencies
run: npm ci

- name: Verify the requested tag matches package.json
run: |
set -eu
tag="${{ inputs.tag }}"
pkg_version="$(node -p "require('./package.json').version")"
tag_version="${tag#v}"
if [ "$tag_version" != "$pkg_version" ]; then
echo "Requested tag ($tag) does not match package.json ($pkg_version)" >&2
exit 1
fi
echo "Publishing $pkg_version from tag $tag"

- name: Lint, typecheck, build, test
run: |
npm run lint
npm run typecheck
npm run build
npm test

- name: Publish
run: npm publish --access public
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,12 @@ out
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# Checkpoint Orchestrator runtime state (never commit user data)
.opencode/state/
.opencode/state/**
.opencode/plugins/
checkpoints.db
checkpoints.db-wal
checkpoints.db-shm
*.legacy-backup-*
219 changes: 219 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,224 @@
# Changelog

## [2.0.0] — 2026-10-16

First stable release of the 2.0 line. It consolidates the four development
prereleases (`2.0.0-dev.0` … `2.0.0-dev.2`, below) without further code change:
the release commit is the qualified release candidate, and only version metadata,
release documentation and the publish workflow changed after it.

### Breaking changes
- **Storage moved from project-level JSON to SQLite and is now session-scoped.**
Checkpoints, snapshots, counters and dedup hashes are keyed by session id, so
two sessions in one project can no longer overwrite or inherit each other's
state. There is no in-place upgrade of the old file: legacy JSON is imported
into a `legacy::<projectID>` namespace and adopted into a session only on
explicit request (`checkpoint_migrate`).
- **Plugin API moved from `@opencode-ai/plugin` (V1) to `@opencode/plugin`
(2.0.21) and requires OpenCode V2.** V1 is intentionally not supported; see
[docs/COMPATIBILITY.md](docs/COMPATIBILITY.md). The `1.0.1` release line is
unaffected and remains installable.
- **The configuration key changed from `plugin` to `plugins`.**
- **Derived handoff exports and migration backups moved into this plugin's own
namespace** (`.opencode/state/checkpoint-orchestrator/`).
- **`engines.node` is `>=22.13.0`.** `node:sqlite` is only available without
`--experimental-sqlite` from Node 22.13.0 / 23.4.0.

### Tools
Eight tools: the seven V1 tools (`checkpoint_save`, `checkpoint_load`,
`checkpoint_status`, `checkpoint_clear`, `checkpoint_list`,
`checkpoint_load_snapshot`, `checkpoint_restore_snapshot`) plus
`checkpoint_migrate`.

### Verified
- **Docker Plugin Test Lab: 145 passed, 0 failed, 0 skipped** against
`opencode` v2.0.21 / Node v22.23.3, at commit `65bf698` of this branch, with
Git That Shit `6069f91` and Stranger Danger `9cfd813`, all three plugins
installed together in all six load orders plus a duplicate entry. This plugin's
eight tools registered in every configuration.
- **120 source-level tests** (`npm test`) drive the built `dist/` output and the
real V2 entry point.
- 979 historical V1 conformance assertions (`npm run test:legacy`) are kept as a
V1 behaviour reference; they re-implement the old logic and are **not**
coverage of this code.

### Known limitations
- `node:sqlite` is still marked experimental in Node and prints a warning under
plain Node; it is unflagged from Node 22.13.0 / 23.4.0, and Bun (the runtime
OpenCode uses) does not warn.
- Checkpoints belong to their own session; they are never shared implicitly.
- Legacy migration never injects old data into an active session.
- A compaction after `checkpoint_clear` records a clearly marked synthesized
placeholder, which is never injected into the summary.
- The `prompt` hook is not an exactly-once event ledger.
- Project identity and directory mapping have documented edge cases.

The full list is in [README.md](README.md#known-limitations), and the immutable
release record is in [docs/releases/v2.0.0.md](docs/releases/v2.0.0.md).

## [2.0.0-dev.2] — 2026-10-15

Persistence-integrity hardening found by the final SQLite audit. No tool-surface
change, no storage-format change beyond a forward-only schema migration, and no
change to the session-isolation contract.

### Fixed
- **Concurrent first-open of a new database no longer crashes.** Schema creation
read `PRAGMA user_version` before taking the write lock, so several processes
opening the same brand-new database at once all saw version 0 and all replayed
`CREATE TABLE`. The loser died with `table projects already exists`, leaving that
plugin instance permanently inert (`Checkpoint storage unavailable`). The whole
upgrade now runs in one `BEGIN IMMEDIATE` transaction that re-reads the version
after acquiring the lock, which also makes a multi-step upgrade all-or-nothing.
- **Two project ids may share one directory again.** `projects.directory` was
`UNIQUE`, so a host reporting a second project id for the same canonical
directory (a checkout plus a worktree of it, or a moved repository) made every
write fail with `UNIQUE constraint failed: projects.directory`. Migration 2
replaces the unique index with a plain one; the directory is metadata, and
project identity is the id.
- **A write path can no longer overwrite the authoritative project directory.**
When only the project id was known, the store recorded a placeholder row whose
upsert rewrote the real `directory` with the project id string. The placeholder
now never touches `directory`/`canonical`, and an authoritative `ensureSession`
fills in the real value afterwards.
- **Concurrent legacy imports import exactly once.** The ledger check sat outside
the write transaction, so two processes starting together could both import and
duplicate the legacy snapshots. The check is now repeated inside the
transaction; only the first committer imports, and the backup copy is taken
only once the import is claimed, so a losing racer writes nothing.

### Changed
- `engines.node` raised from `>=22.5.0` to `>=22.13.0`. `node:sqlite` is only
available without `--experimental-sqlite` from Node 22.13.0 / 23.4.0, so the
previous floor claimed support for versions that cannot execute the storage
layer.
- `npm run test:all` added: runs the source-level suites and the historical V1
suites together, so the combined count is reproducible rather than implied.

### Added
- Regression tests for each fix: concurrent first-open with six processes,
concurrent legacy import with six processes, migration from a real v1 schema
with existing rows, duplicate project ids for one directory, write-path
directory preservation, a schema-version/migration-list consistency guard, and
a damaged or foreign file at the database path being refused rather than
silently answered as "no checkpoint".
- `tests/fixtures/importer.mjs` and `tests/fixtures/schema-open.mjs`.

### Notes
- `checkpoint-orchestrator/handoff@1` and the seven preserved tools are unchanged.
- The 979 "legacy" tests are V1-algorithm conformance tests that re-implement the
old logic in-file; they do not import the shipped plugin. They are kept as a V1
reference under `npm run test:legacy`, but they are not coverage of this code —
`npm test` (120 tests) is.

## [2.0.0-dev.1] — 2026-10-15

Independence and interoperability hardening. No change to the storage contract.

### Added
- `docs/INDEPENDENCE.md`: responsibilities, configuration and storage namespaces,
registered hooks, order-independence guarantees, behaviour when companion
plugins are absent, disabled, failing or outdated, the optional read-only
interoperability contract, and the integration test matrix.
- `tests/interop.test.js` (12 tests): works alone; declares no dependency on
another toolkit plugin; needs no host domain beyond the ones it documents
(enforced with a throwing proxy); coexists with a foreign plugin in either
registration order; is unaffected by a companion that throws or fails to load;
keeps storage ownership disjoint; keeps five sessions isolated; survives
cleanup, reload and double loading.
- `tests/helpers/fake-host.mjs`, a shared fake of the V2 host used by the wiring
and interoperability suites.

### Changed
- Derived handoffs moved into this plugin's own namespace:
`.opencode/state/checkpoint-orchestrator/handoff-<sessionId>.md`. It no longer
writes directly into the shared `.opencode/state/` directory, and the export
now carries the marker `checkpoint-orchestrator/handoff@1` so consumers can
detect format changes.
- Migration backups are written to
`.opencode/state/checkpoint-orchestrator/legacy-backup/` instead of beside the
originals in the shared directory.
- `handoffDir` default updated to match; a relative override resolving to an
empty path falls back to the namespaced default.

### Fixed
- A file at the legacy path that is not this plugin's data (for example another
tool's `checkpoint.json`) is now detected, reported and skipped instead of
being normalised into an empty checkpoint and imported. It is never overwritten
and it no longer prevents a genuine legacy file from importing later.

## [2.0.0-dev.0] — 2026-10-15

Development release. Not published, not tagged. Breaking storage and plugin-API
change; `1.0.1` on `main` is unaffected.

### Added
- **SQLite persistence.** `node:sqlite` (built into Node ≥ 22.5 and Bun ≥ 1.4) is
the authoritative store. No native module, no build step on install, no ABI to
match. Schema: `projects`, `sessions`, `checkpoints`, `snapshots`,
`session_runtime`, `migrations`, versioned in `PRAGMA user_version` and
migrated forward in transactions.
- **Session isolation.** Every checkpoint, snapshot, counter and dedup hash is
scoped by `session_id`. Sessions can no longer overwrite or inherit each
other's state; child sessions inherit nothing implicitly.
- **Session-scoped tool context.** Tools resolve the calling session and project
from the tool context instead of the plugin's working directory.
- **`checkpoint_migrate` tool** reporting legacy JSON data and adopting it into a
session on explicit request.
- **Legacy JSON migration.** Transactional, idempotent via a ledger row, originals
copied and never deleted, imported into a `legacy::<projectID>` namespace rather
than into a live session. Legacy names and timestamps are preserved.
- **Synthesized-state tracking.** A compaction with no prior checkpoint records a
clearly marked placeholder instead of inferring progress.
- **Per-session handoff files** at `.opencode/state/handoff-<sessionId>.md`,
regenerable from the database.
- **Source-level test suite.** `node:test` suites that import the built `dist/`
output and drive the real V2 plugin entry, plus a reproducible SQLite driver
conformance probe.
- **Documentation:** `docs/ARCHITECTURE.md`, `docs/MIGRATION.md`,
`docs/COMPATIBILITY.md`.

### Changed
- Plugin API migrated from V1 (`@opencode-ai/plugin`) to V2 (`@opencode/plugin`
2.0.21): `Plugin.define({ id, setup })`, tools via `ctx.tool.transform`,
`experimental.session.compacting` → `ctx.session.hook("compaction", …)`,
`chat.message` → `ctx.session.hook("prompt", …)`, `event` →
`ctx.event.subscribe()`, `ctx.directory` → `ctx.location`.
- OpenCode configuration key is now `plugins`, not `plugin`.
- A periodic save that produces no change no longer rewrites the checkpoint,
moves `updatedAt`, or creates a snapshot.
- Snapshot pruning always retains the newest `pre-restore` undo snapshot.
- `checkpoint_clear` clears only the calling session.
- Handoff is generated per session and is never read back as state.
- `historyLimit` is configurable and applies per session.
- TypeScript build switched to `module`/`moduleResolution` `NodeNext` with
explicit `.js` specifiers; `typecheck` script added.
- CI matrix moved to Node 22 and 24 (`node:sqlite` requires ≥ 22.5).
- Published artifact trimmed to `dist/` plus documentation and images; the
database and development state are never packaged.

### Fixed
- Read-merge-write in `save()` is now a single `BEGIN IMMEDIATE` transaction; two
writers on one session can no longer lose each other's fields.
- Automatic snapshot names are collision-free (`auto-<ms>-<pid><seq>`). The
previous millisecond-only name collided under rapid writes and silently
replaced history through the `(session_id, name)` constraint.
- A malformed or missing session id now throws instead of binding `NULL` and
silently matching no rows.
- The handoff directory is created before writing, so the first export no longer
fails silently.
- Legacy import rolls back completely on failure and can be retried.

### Removed
- `@opencode-ai/plugin` (V1 SDK) dependency.
- Project-level JSON as authoritative storage, and the project-wide
`checkpoint.json` write path.

## [1.0.1] — 2026-05-13

Version-metadata-only release: `package.json` and the lockfile were bumped from
`1.0.0` to `1.0.1`. No code or behaviour change.

## [1.0.0] — 2026-05-13

### Added
Expand Down
Loading
Loading