molecule-core/platform/migrations
rabbitblood ba1e45b27f feat(memory): optimistic-locking via if_match_version on workspace_memory writes
Closes the silent-overwrite hole where two agents racing a read-modify-
write on the same memory key left only one agent's update. Relevant for
orchestrators (PM, Dev Lead, Marketing Lead) keeping structured running
state (delegation-result ledgers, task queues) in memory, and for the
``research-backlog:*`` keys that multiple idle loops write in parallel.

## Semantics

### Back-compat path (no if_match_version)
Unchanged: ``INSERT ... ON CONFLICT UPDATE`` last-write-wins. Every
existing agent tool, every existing ``commit_memory`` call, every
existing cron that writes memory — all continue to work with no edit.

### Optimistic-lock path (if_match_version set)
1. Client calls ``GET /memory/:key`` → ``{value, version: V}``
2. Client modifies value locally
3. Client ``POST /memory {key, value, if_match_version: V}``
4. Server: ``UPDATE ... WHERE version = V`` + RETURNING new version
5. On match → 200 + ``{version: V+1}``
6. On mismatch → 409 + ``{expected_version: V, current_version: <actual>}``
7. Client reads the actual version and retries.

### Create-only marker
``if_match_version: 0`` means "create iff the key doesn't exist yet".
Two agents simultaneously seeding a shared key will see exactly one
success + one 409 — no silent collision, no duplicate-init work.

### Schema

Migration 023 adds ``version BIGINT NOT NULL DEFAULT 1``. Existing rows
baseline at 1. New rows start at 1. Every successful write (both paths)
increments: ``version = version + 1`` on update, ``1`` on insert.

## Why version, not updated_at

``updated_at`` has second-granularity and can collide between concurrent
writers on a fast clock. A monotonic counter is collision-free and more
readable in the 409 response body ("expected 5, current is 7 — you
missed 2 writes" tells an agent exactly what to re-read).

## Why ``if_match_version`` and not an ETag header

JSON field keeps it in the request body, visible alongside the value
payload. Agents assembling requests programmatically don't have to
remember to thread a header through their HTTP client wrapper; the
existing ``commit_memory`` tool can grow one optional kwarg and match
the existing signature shape.

## Tests

11 memory-handler cases covering every path:
- GET list / get (with version in response shape)
- Set with no version (back-compat upsert, returns new version)
- Set with if_match_version match (happy path, increment)
- Set with if_match_version mismatch (409 + expected/current fields)
- Set with if_match_version=0 on absent key (create-only success)
- Set with if_match_version=N on absent key (409 — caller's mental
  model is wrong)
- Bad inputs (missing key, malformed JSON)
- Delete happy + error path

Full ``go test ./internal/handlers/`` green.

## Follow-up (not in this PR)

- Workspace-template tool update: ``commit_memory(content, *,
  if_match_version=None)`` surfaces the new option + on 409 surfaces
  the current_version so agents can retry without manual re-read.
- Named checkpoints table (``workspace_checkpoints``) for durable
  orchestrator state snapshots. Different concern than per-key locking;
  separate PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 02:32:46 -07:00
..
001_workspaces.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
002_agents.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
003_events.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
004_secrets.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
005_canvas_layouts.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
006_workspace_config_memory.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
007_approvals.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
008_agent_memories.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
009_activity_logs.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
010_workspace_awareness.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
011_workspace_runtime.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
012_global_secrets.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
013_workspace_dir.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
014_indexes.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
015_workspace_schedules.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
016_workspace_channels.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
017_memories_fts_namespace.down.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
017_memories_fts_namespace.up.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
018_secrets_encryption_version.down.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
018_secrets_encryption_version.up.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
019_workspace_access.down.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
019_workspace_access.up.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
020_workspace_auth_tokens.down.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
020_workspace_auth_tokens.up.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
021_delegation_idempotency.down.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
021_delegation_idempotency.up.sql initial commit — Molecule AI platform 2026-04-13 11:55:37 -07:00
022_workspace_schedules_source.down.sql fix(schedules): backfill legacy rows to 'template' + extract import SQL const 2026-04-14 14:30:22 -07:00
022_workspace_schedules_source.up.sql fix(schedules): backfill legacy rows to 'template' + extract import SQL const 2026-04-14 14:30:22 -07:00
023_workspace_memory_version.down.sql feat(memory): optimistic-locking via if_match_version on workspace_memory writes 2026-04-16 02:32:46 -07:00
023_workspace_memory_version.up.sql feat(memory): optimistic-locking via if_match_version on workspace_memory writes 2026-04-16 02:32:46 -07:00