Skip to content

fix(sync): empty prompt content accepted at capture poisons cloud push for all projects; repair cannot fix prompt entities #686

Description

@thomaspufahl

📋 Pre-flight Checks

  • I have searched existing issues and this is not a duplicate
  • I understand this issue needs status:approved before a PR can be opened

📝 Bug Description

Prompt capture accepts empty content, but the cloud server rejects that same payload at push time with 500: prompt payload content is required for upsert. Because the push queue is sequential and global across projects, a single empty-prompt mutation permanently stalls cloud push for every project until repaired by hand. In our case one empty prompt captured on 2026-07-22 silently blocked ~500 pending mutations across 4 projects for 8 days (174 consecutive failures), while pull kept working — so devices that were already mirrored reported a healthy sync and the data loss was invisible from their side.

Three gaps combine into this:

  1. No capture-time validation. handleSavePrompt (internal/mcp/mcp.go:1564) and Store.AddPrompt (internal/store/store.go:2549) accept and persist content="" verbatim, and enqueue the upsert mutation. The server enforces content as required (cloudserver/chunkcodec canonicalization), so the mutation is un-pushable from the moment it is created. This is the prompt-entity mirror of Reject empty observation titles before persistence #459 (empty observation titles).
  2. No poison-mutation handling in the push queue. The failing mutation is retried forever and everything behind it — including other projects' mutations — waits indefinitely. Same failure family as the import-side stalls in bug(sync): import stalls forever on self-referential and orphan relation mutations #649 / Cloud import stalls permanently when a relation mutation precedes its referenced observation within the same chunk #671, but on the push side.
  3. Repair does not support the prompt entity. engram cloud upgrade repair reports manual-action-required and offers nothing; docs/engram-cloud/troubleshooting.md documents loop/interactive repair for entity=session|observation op=upsert only. Relates to Cloud repair should backfill and report legacy mutation required fields #340 (required-field repair), which lists session/observation blocker classes but not prompts.

🔄 Steps to Reproduce

  1. Configure a cloud target and enroll a project (engram cloud enroll <project>).
  2. Save a prompt with empty content (e.g. mem_save_prompt with content: "" — in our case it came from automated hook capture): a user_prompts row and an upsert mutation with "content":"" are created.
  3. Let the daemon push (or run engram sync --cloud --project <project>).
  4. Push fails: mutation push: status 500: insert mutations: cloudstore: canonicalize materialized mutation batch chunk: mutations[N]: prompt payload content is required for upsert.
  5. sync_state goes degraded, consecutive_failures grows unbounded; mutations from all projects behind that seq stop replicating.
  6. engram doctorsync_mutation_payload_missing_required_fields (correct detection).
  7. engram cloud upgrade doctor/repair --project <project>upgrade_blocked_legacy_mutation_manual, manual-action-required: prompt payload missing required upsert fields: content — no repair path offered.

Recovery required manual surgery: engram delete prompt <id> (to tombstone the source row) plus a hand-written DELETE FROM sync_mutations WHERE seq=<N> in SQLite.

✅ Expected Behavior

Any of these layers would have prevented or contained it (ordered by depth):

  1. Capture: reject or skip persisting/enqueueing prompts whose content is empty after normalization — same principle as Reject empty observation titles before persistence #459.
  2. Queue: quarantine a mutation the server permanently rejects (validation-class 500) into a dead-letter state visible in doctor, instead of retrying forever and blocking unrelated projects.
  3. Repair: extend cloud upgrade repair to the prompt entity (drop-or-tombstone flow for empty content), so no manual SQL is needed.

❌ Actual Behavior

Empty prompt persisted and enqueued; push retries the same mutation indefinitely (174 consecutive failures over 8 days); all projects' cloud replication stalls; repair declines with manual-action-required; recovery requires manual SQLite edits.

Operating System

Linux (Other)

Engram Version

1.20.0

Agent / Client

Claude Code

Relevant Logs

Sync diagnostic: degraded
reason_code: transport_failed
reason_message: push: transport push project "owlvision": cloud: mutation push: status 500: insert mutations: cloudstore: canonicalize materialized mutation batch chunk: mutations[1]: prompt payload content is required for upsert

$ engram doctor
[blocked] sync_mutation_required_fields — 1 finding(s) detected.
  - sync_mutation_payload_missing_required_fields: prompt payload missing required fields: content
    evidence: {"entity":"prompt","entity_key":"prompt-a1fdc6ceb6b624fb","missing_fields":["content"],"op":"upsert","project":"owlvision","seq":7783,"target_key":"cloud"}

$ engram cloud upgrade repair --project owlvision --dry-run
class: blocked
reason_code: upgrade_blocked_legacy_mutation_manual
message: manual-action-required: prompt payload missing required upsert fields: content (seq=7783 entity=prompt entity_key="prompt-a1fdc6ceb6b624fb" op=upsert)
applied: false

sync_state (before manual repair): cloud | degraded | last_enqueued_seq=8283 | last_acked_seq=7781 | consecutive_failures=174

Additional Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions