feat(agent-memory): ADR-003 Phase 2a — POST /memory/sync with mode + dedup#191
Merged
lilyshen0722 merged 1 commit intomainfrom Apr 14, 2026
Merged
Conversation
…dedup
Adds the kernel promotion endpoint every runtime driver will target. Phase 2b
(new OpenClaw tools that call it) is a separate change in the submodule.
## What ships
**New endpoint: `POST /api/agents/runtime/memory/sync`**
- Required body: `{ sections, sourceRuntime?, mode }`
- `mode: "full"` — replaces the entire sections envelope. Sections omitted
from the payload are cleared. Use for driver snapshots.
- `mode: "patch"` — merges with existing state. Single-object sections $set
per-key (siblings preserved). Array sections (daily, relationships) merge
element-wise keyed by `date` / `otherInstanceId`. Use for incremental
promotion.
- Idempotent within the same UTC day: repeated identical payloads return
`{ deduped: true }` without writing. Dedup key is
`(dayBucket, sourceRuntime, sha256-trunc32 of canonical-stringified body)`.
Canonical stringify sorts object keys recursively so semantically
identical payloads with different key order collapse to one write.
**Supporting work**
- `AgentMemory.lastSyncKey` + `lastSyncAt` track the dedup cache state.
- Strict `'YYYY-MM-DD'` validation on `daily[].date` — regex + Date
round-trip so calendar-invalid dates (2026-02-30) are rejected.
- `mergePatchSections` in agentMemoryService pure-fns the element-level
array merge; unit-tested separately from route wiring.
- Full-mode handler always updates v1 `content` mirror from final sections,
including blanking it when the new snapshot omits long_term. Avoids
phantom data on GET when v1 readers still exist.
## Critical fix caught by review
- `PUT /memory` and `nativeRuntimeService.commonly_write_memory` now clear
`lastSyncKey`/`lastSyncAt` on every write. Without this, a non-sync
writer mutating state between two identical syncs caused the second sync
to be silently deduped, leaving the kernel stuck on the intervening
write while the driver believed its promotion succeeded. Regression
test locks the invariant.
## Other review-driven changes
- Canonical stringify (json-stable-order) replaces JSON.stringify in the
dedup key so webhook/Python drivers with different emit order don't
silently miss dedup.
- Full-mode v1 mirror rule made symmetric with sections (full replace →
full mirror refresh, including empty when long_term omitted).
- Structured logs now fire on both dedup hits and validation rejects
(REVIEW.md §Maintainability, kernel-surface observability).
- Concurrency caveat noted inline on `mergePatchSections` — read-merge-
write assumes per-instance serialization; revisit when webhook drivers
arrive in Phase 2b/later.
## Tests (17 new; 95/95 agent-memory pass; 705/705 backend total)
Unit (on top of existing):
- `isValidYMD` (4 tests — accept valid, reject malformed, reject
calendar-invalid incl. leap years, reject non-strings)
- `mergePatchSections` (6 tests — single-object per-key merge, replacement,
daily by date, relationships by otherInstanceId, missing-existing,
preserve-omitted)
- `computeSyncDedupKey` (8 tests — same-day-same-payload collapse,
mode/runtime/day/content sensitivity, key prefix, canonical-stringify
order invariance across object keys AND across top-level sections)
Integration (on top of existing):
- Rejects without sections, without valid mode, with malformed date,
with calendar-invalid date
- full mode wipes envelope, patch mode preserves siblings + merges daily
by date and relationships by otherInstanceId
- full mode WITHOUT long_term blanks v1 content mirror (Important fix)
- Patch mode with long_term mirrors v1 content
- Same-day dedup returns deduped:true; cross-content re-sync does not
- PUT /memory invalidates sync dedup cache (Critical fix)
- Server-stamps byteSize; rejects unauthenticated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New kernel promotion endpoint every runtime driver targets. Phase 2b (OpenClaw tools that call it) is a follow-up in the submodule.
Endpoint: `POST /api/agents/runtime/memory/sync` with required `{ sections, mode: 'full' | 'patch', sourceRuntime? }`
17 new tests (8 unit-merge + 4 unit-YMD + 8 unit-dedup + 13 integration); 95/95 agent-memory pass, 705/705 backend total.
Critical fix (reviewer caught pre-commit)
`PUT /memory` and `nativeRuntimeService.commonly_write_memory` now clear `lastSyncKey`/`lastSyncAt` on every write. Without this, a non-sync writer mutating state between two identical syncs caused the second sync to be silently deduped, leaving the kernel stuck on the intervening write while the driver believed its promotion succeeded. Regression test locks the invariant.
Other review-driven changes
Deferred to Phase 2b / later
Changes
Test plan
🤖 Generated with Claude Code