Skip to content

feat(items): field-level PATCH + conflict envelope + read-only version history (TASK-2022)#876

Merged
xarmian merged 3 commits into
mainfrom
feat/task-2022-field-patch-conflict-history
Jul 8, 2026
Merged

feat(items): field-level PATCH + conflict envelope + read-only version history (TASK-2022)#876
xarmian merged 3 commits into
mainfrom
feat/task-2022-field-patch-conflict-history

Conversation

@xarmian

@xarmian xarmian commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

Implements TASK-2022 (refs IDEA-1480): three related item-update primitives that are a prerequisite for multi-agent coordination.

1. Field-level PATCH (merge, not replace)

PATCH /workspaces/{ws}/items/{slug} accepts fields_patch: {key: value, key2: null}. The server shallow-merges it onto the item's current fields inside the write transaction (a null value deletes a key). Two concurrent single-field updates can no longer clobber each other the way the full-blob read-modify-write did (the lost-write race IDEA-1480 describes). pad item update and both MCP dispatchers now send only the changed keys.

2. Optimistic-concurrency conflict envelope

Optional expected_updated_at on update. On mismatch (checked under the write lock, before the open-children guard), the store returns *UpdateConflictError and the handler emits the pad-structured-error/v1 conflict envelope — HTTP 409, code=update_conflict, with {ref, expected_updated_at, actual_updated_at}. Surfaced on CLI (--expected-updated-at) and MCP (expected_updated_at param); rendered with the versioned stderr marker for MCP stdio lifting.

3. Read-only version history

pad item history <ref> (alias versions) + MCP pad_item.history, reusing the existing item_versions store and /versions endpoint. Token-light summary shape by default; --full includes resolved content.

Notes

  • No migration — reuses existing storage (item_versions, updated_at, fields).
  • MCP ToolSurfaceVersion 0.9 → 1.0 (new history action + expected_updated_at param + update behavior change). Drift test, README, instructions.md, and CLAUDE.md changelog updated. NOTE: sibling tasks are also bumping this constant; a version.go merge conflict is expected and will be resolved by the orchestrator.

Tests

  • Store: field-merge correctness, null-delete, sequential no-clobber, OCC match/conflict.
  • Handler: merge over the wire, mutual-exclusion 400, schema validation, required-field null-delete rejection, conflict envelope shape + no-op on conflict, malformed token 400, conflict-wins-over-guard ordering, date preservation vs auto-fill, open-children guard on the patch path, history read.
  • Items: ValidatePartialFields unit tests.

Gates

golangci-lint clean; go test ./... green (SQLite); full build; go test ./internal/mcp/... green; Postgres suite (store/server/items/mcp + remainder) green. Three Codex review rounds → CLEAN.

https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS

xarmian added 3 commits July 8, 2026 19:34
Adds three related item-update primitives (TASK-2022 / IDEA-1480):

- Field-level merge: PATCH `fields_patch` shallow-merges onto the item's
  current fields INSIDE the write transaction (null deletes a key), so
  concurrent single-field updates no longer clobber each other via the
  full-blob read-modify-write. `pad item update` and the MCP `pad_item.update`
  action now send only the changed keys.
- Optimistic concurrency: optional `expected_updated_at` on update; on
  mismatch the store returns *UpdateConflictError and the handler emits the
  pad-structured-error/v1 conflict envelope (HTTP 409, code=update_conflict).
  Surfaced on CLI (`--expected-updated-at`) and MCP (`expected_updated_at`).
- Read-only version history: `pad item history <ref>` (alias `versions`) and
  MCP `pad_item.history`, reusing the existing item_versions store + versions
  endpoint (no new store, no schema change).

MCP ToolSurfaceVersion bumped 0.9 -> 1.0 (new action + param; update
behavior change). No migration required.

Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
…ing, date/required guards

Round 1+2 review fixes for TASK-2022:
- HTTP MCP dispatcher (dispatch_http_advanced.go) now sends fields_patch (only
  changed keys) instead of a client-side merged full fields blob, and forwards
  expected_updated_at — remote MCP callers get the same race-free merge +
  optimistic concurrency the CLI/HTTP paths do.
- ValidatePartialFields rejects null-deleting a schema-declared REQUIRED field
  (would otherwise persist a blob the full-update validator rejects).
- Open-children guard on the fields_patch path merges the patch onto the IN-TX
  locked row inside the precheck (not a stale pre-lock preview), so a
  priority-only patch can't false-fire the guard.
- Optimistic-concurrency check now runs BEFORE the open-children precheck in the
  store, so a stale expected_updated_at yields update_conflict (not
  open_children) — single in-tx re-read shared by both.
- Date auto-population on the patch path only fills an EMPTY current date; an
  existing end_date the caller isn't touching is preserved.

Tests added for each fix.

Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
…patch-conflict-history

# Conflicts:
#	CLAUDE.md
#	README.md
#	internal/mcp/instructions.md
#	internal/mcp/version.go
@xarmian xarmian merged commit bed933d into main Jul 8, 2026
@xarmian xarmian deleted the feat/task-2022-field-patch-conflict-history branch July 8, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant