📋 Pre-flight Checks
📝 Bug Description
Engram v1.16.1 allows first-party save paths to persist an observation with an empty or whitespace-only title.
This creates a local observation row with observations.title = '' and enqueues a cloud observation upsert mutation with sync_mutations.payload.title = ''.
Later, engram doctor / cloud sync correctly treats the same mutation as invalid because observation upserts require a non-empty title. The project then becomes blocked until the local database and frozen mutation payload are manually repaired.
This is a write-side validation / invariant gap:
- CLI
engram save accepts title="".
- MCP
mem_save accepts title="".
store.AddObservation accepts Title="".
- cloud/doctor/chunk validators reject empty title for observation upserts.
Related but not duplicate:
This issue is about current v1.16.1 allowing new malformed empty-title observation upserts.
🔄 Steps to Reproduce
Use an isolated data directory so the real database is not touched:
tmp="$(mktemp -d)"
export ENGRAM_DATA_DIR="$tmp/.engram"
engram save "" "Content for empty-title reproduction" \
--type discovery \
--project empty-title-test
engram doctor --json --project empty-title-test
Observed CLI output:
Memory saved: #1 "" (discovery)
Inspecting the isolated SQLite database shows:
SELECT id, quote(title), length(title), project
FROM observations;
-- id=1, title='', length(title)=0, project='empty-title-test'
And the queued cloud mutation contains:
{
"sync_id": "obs-...",
"session_id": "manual-save-empty-title-test",
"type": "discovery",
"title": "",
"content": "Content for empty-title reproduction",
"project": "empty-title-test",
"scope": "project"
}
Doctor then reports a blocking malformed mutation:
sync_mutation_required_fields
sync_mutation_payload_missing_required_fields
observation payload missing required fields: title
The same behavior is reproducible through MCP mem_save:
{
"name": "mem_save",
"arguments": {
"title": "",
"content": "Content via MCP empty-title reproduction",
"type": "discovery",
"capture_prompt": false
}
}
The tool returns success:
Memory saved: "" (discovery)
and persists observations.title = '' plus sync_mutations.payload.title = ''.
✅ Expected Behavior
Engram should reject empty or whitespace-only titles before persistence.
Expected behavior:
- CLI
engram save "" "content" fails loudly.
- CLI
engram save " " "content" fails loudly.
- MCP
mem_save(title="", content="...") returns a tool error.
- MCP
mem_save(title=" ", content="...") returns a tool error.
store.AddObservation rejects empty/whitespace-only Title, so no caller can create an invalid observation or cloud mutation.
No observation row and no cloud mutation should be created for invalid titles.
❌ Actual Behavior
Engram accepts the write, creates a local observation, and queues a cloud mutation that later blocks doctor/cloud sync.
🧪 Verified On
engram 1.16.1
repo: Gentleman-Programming/engram
commit: 07445ba99eac95083d7c6d56c5d795bb68e90834
tag: v1.16.1
This was also reproduced by running from a clean upstream checkout with:
go run ./cmd/engram save "" "Content from clean source repro" \
--type discovery \
--project clean-source-empty-title
using an isolated ENGRAM_DATA_DIR=/tmp/....
The source checkout was clean and matched origin/main / v1.16.1.
🔍 Suspected Root Cause
internal/mcp/mcp.go validates content, but not title:
title, _ := req.GetArguments()["title"].(string)
content, _ := req.GetArguments()["content"].(string)
if strings.TrimSpace(content) == "" {
...
}
savedID, err := s.AddObservation(store.AddObservationParams{
Title: title,
...
})
cmd/engram/main.go also forwards the CLI title unchanged:
title := os.Args[2]
content := os.Args[3]
...
Title: title,
internal/store/store.go strips private tags but does not enforce a non-empty title:
title := stripPrivateTags(p.Title)
content := stripPrivateTags(p.Content)
But cloud/chunk validation requires non-empty title:
body.Title = strings.TrimSpace(body.Title)
if body.Title == "" {
return "", "", fmt.Errorf("observation payload title is required for upsert")
}
and cloud server validation also rejects it:
if strings.TrimSpace(observation.Title) == "" {
return fmt.Errorf("observations[%d].title is required", i)
}
🛠 Suggested Fix
Enforce the same invariant on the write side:
-
MCP mem_save:
- trim title
- reject empty/whitespace-only title
-
CLI engram save:
- trim title
- reject empty/whitespace-only title
-
store.AddObservation:
- enforce non-empty title after
stripPrivateTags and trim
- return an error before any row or sync mutation is created
Suggested tests:
mem_save(title="") fails and creates no observation/mutation.
mem_save(title=" ") fails and creates no observation/mutation.
engram save "" "content" fails and creates no observation/mutation.
engram save " " "content" fails and creates no observation/mutation.
store.AddObservation(Title:"") fails.
store.AddObservation(Title:" ") fails.
- valid non-empty titles still work.
📌 Impact
This can block project cloud sync with sync_mutation_payload_missing_required_fields and requires manual SQLite repair of both observations.title and frozen sync_mutations.payload.title.
A required JSON schema field is not sufficient here because required:["title"] only requires the key to exist; it does not reject an empty string.
📋 Pre-flight Checks
status:approvedbefore a PR can be opened.📝 Bug Description
Engram v1.16.1 allows first-party save paths to persist an observation with an empty or whitespace-only title.
This creates a local observation row with
observations.title = ''and enqueues a cloud observation upsert mutation withsync_mutations.payload.title = ''.Later,
engram doctor/ cloud sync correctly treats the same mutation as invalid because observation upserts require a non-empty title. The project then becomes blocked until the local database and frozen mutation payload are manually repaired.This is a write-side validation / invariant gap:
engram saveacceptstitle="".mem_saveacceptstitle="".store.AddObservationacceptsTitle="".Related but not duplicate:
contentinmem_session_summary.This issue is about current v1.16.1 allowing new malformed empty-title observation upserts.
🔄 Steps to Reproduce
Use an isolated data directory so the real database is not touched:
Observed CLI output:
Inspecting the isolated SQLite database shows:
And the queued cloud mutation contains:
{ "sync_id": "obs-...", "session_id": "manual-save-empty-title-test", "type": "discovery", "title": "", "content": "Content for empty-title reproduction", "project": "empty-title-test", "scope": "project" }Doctor then reports a blocking malformed mutation:
The same behavior is reproducible through MCP
mem_save:{ "name": "mem_save", "arguments": { "title": "", "content": "Content via MCP empty-title reproduction", "type": "discovery", "capture_prompt": false } }The tool returns success:
and persists
observations.title = ''plussync_mutations.payload.title = ''.✅ Expected Behavior
Engram should reject empty or whitespace-only titles before persistence.
Expected behavior:
engram save "" "content"fails loudly.engram save " " "content"fails loudly.mem_save(title="", content="...")returns a tool error.mem_save(title=" ", content="...")returns a tool error.store.AddObservationrejects empty/whitespace-onlyTitle, so no caller can create an invalid observation or cloud mutation.No observation row and no cloud mutation should be created for invalid titles.
❌ Actual Behavior
Engram accepts the write, creates a local observation, and queues a cloud mutation that later blocks doctor/cloud sync.
🧪 Verified On
This was also reproduced by running from a clean upstream checkout with:
using an isolated
ENGRAM_DATA_DIR=/tmp/....The source checkout was clean and matched
origin/main/v1.16.1.🔍 Suspected Root Cause
internal/mcp/mcp.govalidatescontent, but nottitle:cmd/engram/main.goalso forwards the CLI title unchanged:internal/store/store.gostrips private tags but does not enforce a non-empty title:But cloud/chunk validation requires non-empty title:
and cloud server validation also rejects it:
🛠 Suggested Fix
Enforce the same invariant on the write side:
MCP
mem_save:CLI
engram save:store.AddObservation:stripPrivateTagsand trimSuggested tests:
mem_save(title="")fails and creates no observation/mutation.mem_save(title=" ")fails and creates no observation/mutation.engram save "" "content"fails and creates no observation/mutation.engram save " " "content"fails and creates no observation/mutation.store.AddObservation(Title:"")fails.store.AddObservation(Title:" ")fails.📌 Impact
This can block project cloud sync with
sync_mutation_payload_missing_required_fieldsand requires manual SQLite repair of bothobservations.titleand frozensync_mutations.payload.title.A required JSON schema field is not sufficient here because
required:["title"]only requires the key to exist; it does not reject an empty string.