Coord Schema Migrations #

How we evolve the session / lock / message record shapes without breaking cooperating Claude sessions or losing data.

Source of truth: ~/.claude/coord/schema-v0.json (human-readable validator) and docs/coord/coord.proto (codegen target for v1+). Every field in one mirrors the other.

1. Ground rules #

  1. Never rename a field. If a name is wrong, add a new field with the right name, mark the old one // deprecated: use X, and keep writing both for a grace period of at least 1 minor version.
  2. Never change a field’s type. Add a new field with the new type; drop the old after the grace period.
  3. Never reuse a field number. Once used, add to the reserved range in the .proto file. JSON Schema mirrors this by listing reserved numbers in description comments.
  4. Bump schema only when a change is not fully backward-compatible. Additive changes (new optional field) don’t need a bump; deletions and type changes do.
  5. Preserve unknown fields. Readers stash everything they don’t recognize into blob._unknown so round-trip doesn’t lose data when a newer writer’s record is read by an older reader. This matches protobuf’s “unknown fields” behavior.

2. Version history #

schemastatuschangedgrace-period
1currentinitialn/a
2plannedadd graph_id on LockRecord for multi-lock transactions
3plannedkind becomes an enum instead of open stringv2→v3 grace

3. Migration flow (when we ship schema 2) #

Step 1: additive land #

Step 2: capability gate #

Step 3: deprecation #

Step 4: removal (major version boundary) #

4. coord migrate subcommand (scaffold for v0) #

Today’s v0 ships a no-op scaffold:

coord migrate [--from N] [--to M] [--dry-run]

Migration registry shape #

pub struct Migration {
    pub from: u32,
    pub to:   u32,
    pub fwd:  fn(&Value) -> Result<Value>,
    pub rev:  Option<fn(&Value) -> Result<Value>>,  // None = irreversible
    pub notes: &'static str,
}

static MIGRATIONS: &[Migration] = &[
    // no migrations yet — v0 only has schema 1
];

When v2 ships:

fn migrate_1_to_2(v: &Value) -> Result<Value> {
    let mut next = v.clone();
    next["schema"] = json!(2);
    // Additive: new fields default to null / empty
    Ok(next)
}

fn migrate_2_to_1(v: &Value) -> Result<Value> {
    let mut next = v.clone();
    // Stash v2-only fields under blob._dropped_v2 for reversibility
    let dropped = json!({
        "graph_id": next.get("graph_id"),
    });
    next["blob"]["_dropped_v2"] = dropped;
    next.as_object_mut().unwrap().remove("graph_id");
    next["schema"] = json!(1);
    Ok(next)
}

MIGRATIONS = &[
    Migration {
        from: 1, to: 2, fwd: migrate_1_to_2, rev: Some(migrate_2_to_1),
        notes: "v2: add LockRecord.graph_id for multi-lock transactions",
    },
];

5. Protobuf cut-over (v0 JSON → v1 protobuf) #

Trigger conditions (from protocol-v0.md §12):

  1. Two Claude sessions on different hosts need to coordinate, OR
  2. Filesystem becomes a performance bottleneck (hundreds of ops/sec), OR
  3. We need an audit log across reboots

When triggered:

  1. Run protoc --rust_out=crates/reverie-coord/src/pb/ docs/coord/coord.proto
  2. Compile crates/reverie-coord with LocalFsBackend, RedisBackend, and any other implementations. All implement a common CoordBackend trait.
  3. The coord shell binary gains a --backend=local|redis flag; default stays local for backward compat.
  4. Migration path for existing v0 data: coord migrate --from-backend=local --to-backend=redis reads every JSON file under /tmp/claude-coord/ and HSETs it into Redis. Idempotent, safe to re-run.
  5. Shell binary either (a) links against the compiled Rust CLI via a subprocess or (b) stays shell and shells out to a reverie-coord binary for the Redis path. Preference: (b) for minimum disruption.

Rollback: coord migrate --from-backend=redis --to-backend=local does the inverse. Both backends can coexist during the transition.

6. Compatibility matrix #

writerreader v1reader v2reader v3
v1read, round-tripread + default v2 fieldsrefuse (major boundary)
v2read (ignore unknowns, preserve in _unknown)fullread + default v3 fields
v3(ignore unknowns)full

Never allow silent data loss. A v2 reader reading a v1 record should surface a warning (“defaulting 3 v2 fields on older record”) but not fail. A v1 reader reading a v2 record should preserve unknown fields in the round-trip so nothing is dropped if the v1 reader re-writes the record.

7. Testing migrations #

Each migration function ships with:

8. Deprecation telemetry #

The coord CLI keeps a log of deprecation warnings at /tmp/claude-coord/deprecations.log:

2026-04-07T16:45:00Z v1 record read, v2 writer active, 3 fields defaulted

When users coord status, a summary warns if any peer is running an older schema version, so upgrades don’t ambush anyone.

9. Open questions #