Skip to content

pact-memory CLI: update on entities with same-name + new-notes creates duplicate row (content-hash dedup) instead of appending/replacing — silent affordance gap #811

@michael-wojcik

Description

@michael-wojcik

Symptom

The pact-memory update <id> --stdin CLI uses content-hash dedup on list-valued fields (entities, decisions, lessons_learned, etc.). For entities specifically, this means: passing an entity with an existing name but new notes creates a second entity row with the same name, rather than appending the new notes to the existing entity or replacing it.

Every consumer of the pact-memory CLI hits this trap until burned. The skill documentation says "list fields merge additively with content-hash dedup" but doesn't surface the same-name-different-content behavior, which is the load-bearing detail.

Concrete repro

# Initial state: memory has one entity {name: "auditor_handoff_capture_gap", notes: "...recurring failure mode..."}
$ pact-memory update 18ca924b --stdin << 'JSON'
{"entities": [{"name": "auditor_handoff_capture_gap", "notes": "PROMOTED to enforcement-candidate..."}]}
JSON

# Expected: existing entity's notes appended OR replaced with the new content
# Actual: memory now has TWO entities both named "auditor_handoff_capture_gap" with different notes
$ pact-memory get 18ca924b | jq '.entities | map(select(.name == "auditor_handoff_capture_gap")) | length'
2

Workaround (which works but is awkward)

Read full memory, dedup-by-name client-side merging notes with a separator, then update --replace on the entities field with the full deduped list:

pact-memory get <id> > /tmp/cur.json 2>/dev/null
python3 << 'PY' > /tmp/payload.json
import json
d = json.load(open('/tmp/cur.json')); ents = d['result'].get('entities', [])
by_name, order = {}, []
for e in ents:
    n = e.get('name')
    if n in by_name:
        ex = (by_name[n].get('notes') or '').strip()
        nw = (e.get('notes') or '').strip()
        if nw and nw not in ex:
            by_name[n]['notes'] = (ex + ' || ' + nw) if ex else nw
    else:
        by_name[n] = dict(e); order.append(n)
print(json.dumps({'entities': [by_name[n] for n in order]}))
PY
pact-memory update <id> --stdin --replace < /tmp/payload.json

This works (verified in reflectica/webapp-frontend audit cycle) but it's ~15 lines of client-side surgery for what feels like it should be a single CLI affordance.

Suggested fix

Two paths, either resolves the failure mode (lead-confirmed at issue-filing time):

  1. Warn-on-duplicate-name + require --force to create a duplicate: default behavior errors out (or warns) when an entities update produces same-name-different-content; --force opts into the current content-hash-dedup-allows-dupes behavior. Loud-fail by default.

  2. Auto-route same-name update to update-existing-entity semantics with implicit --replace shape on that entity's fields: default behavior treats same-name as "update this entity"; new fields replace existing fields; explicit content-hash-dedup behavior requires opt-in flag. Quiet-merge by default.

Either fix is structural — turns a silent affordance gap into a loud signal at CLI level, removing the client-side surgery workaround.

Cross-references

  • pact-memory 18ca924b (webapp-frontend feat: implement Agent Teams executor backend with hook integration #158 organizational state snapshot — eos_upstream_filings_2026-05-20 entity lists this as item 4 of the 4-item upstream-filing pass)
  • Hit twice in the reflectica audit cycle (once on auditor_handoff_capture_gap, once on 23d8673f cross-link entity) — both recovered via the same client-side dedup-and-replace surgery

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions