fix(store): reject empty observation titles before persistence - #678
fix(store): reject empty observation titles before persistence#678ceeesar13 wants to merge 2 commits into
Conversation
📝 WalkthroughWalkthroughObservation title validation is centralized in the store and enforced before persistence across CLI, MCP, and HTTP write paths. Invalid titles return path-specific errors, create no observation or sync mutation, and are documented and tested. ChangesObservation title validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Could a maintainer apply the |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@DOCS.md`:
- Line 137: Update the documentation sentence describing title validation to
scope it only to the listed observation-create paths: engram save, mem_save, and
POST /observations. Remove the claim that the rule applies to every write path,
while preserving the cloud-sync rejection behavior for observation upserts
without a title.
In `@internal/server/server.go`:
- Around line 340-344: Validate the decoded observation title with
store.ValidateObservationTitle immediately after JSON decoding and before
validateSessionProject, while retaining AddObservation validation as a backstop.
In internal/server/server.go lines 340-344, preserve the existing title-specific
400 mapping. In internal/server/server_test.go lines 2068-2080, add a
whitespace-only title request using a nonexistent or mismatched session and
assert the title-validation 400 response.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f0c73d33-fa34-4bbe-a7fd-00de4de533d1
📒 Files selected for processing (11)
CHANGELOG.mdDOCS.mdcmd/engram/main.gocmd/engram/main_extra_test.gointernal/mcp/mcp.gointernal/mcp/mcp_test.gointernal/server/server.gointernal/server/server_test.gointernal/store/diagnostic.gointernal/store/store.gointernal/store/store_test.go
An observation saved with an empty or whitespace-only title was persisted as `observations.title = ''` and enqueued a cloud observation upsert with `payload.title = ''`. The cloud, doctor and chunk validators reject that payload, and because the mutation queue is an ordered log processed by seq, the rejected row blocked every later mutation for the project. The rule already existed on the pull/doctor side inside ValidateSyncMutationPayload. Rather than duplicating it, the underlying required-field check is now a single helper that both the payload validator and the new write-time guard call: - store.ValidateObservationTitle / ErrObservationTitleRequired in internal/store/diagnostic.go, next to the validator that owned the rule - store.AddObservation validates the post-strip title, so a title that survives only as whitespace is rejected before any INSERT or enqueue - engram save, mem_save and POST /observations reject the write with an actionable message (non-zero exit, MCP tool error, HTTP 400) Inbound paths are unaffected: cloud pull (applyPulledMutationTx) and `engram import` write observations with direct SQL and never call AddObservation, so a legitimate inbound record is not blocked. Closes Gentleman-Programming#459
Review follow-up on Gentleman-Programming#678. POST /observations checked `title == ""` on the raw body, so a whitespace-only title passed that check and the request fell through to validateSessionProject. A nonexistent or mismatched session then answered first, masking the documented title-validation 400 behind a session error. The handler now calls store.ValidateObservationTitle right after decoding the body and before the session lookup, so the title rule is reported on its own terms. The AddObservation error mapping stays as a backstop for any caller that reaches the store directly, and session_id/content keep their combined required-fields 400. DOCS.md previously claimed the title rule applies to "every write path", which overstates it: Store.UpdateObservation is deliberately out of scope. The sentence is now scoped to the observation-create paths.
4b4fc9e to
b10276c
Compare
|
Rebased onto
Verification after the rebase: Both A Windows SQLite file-lock artifact on Also confirmed against upstream's newer commits: |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/engram/main.go`:
- Around line 1035-1040: Apply title redaction before validation at every
observation entry point, reusing the store’s private-tag stripping behavior
before ValidateObservationTitle: update cmd/engram/main.go:1035-1040 before
storeNew/CreateSession, internal/mcp/mcp.go:1198-1202 before project resolution
and implicit-session creation, and internal/server/server.go:330-336 before
validateSessionProject. Add private-tag-only title cases in
cmd/engram/main_extra_test.go:4331-4348, internal/mcp/mcp_test.go:7459-7511, and
internal/server/server_test.go:2174-2259, asserting respectively no storeNew
call, no session/observation/sync mutation, and a title-specific 400 even with
an invalid session.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b7b376e9-4a05-42ca-8705-469b7a0c4b41
📒 Files selected for processing (11)
CHANGELOG.mdDOCS.mdcmd/engram/main.gocmd/engram/main_extra_test.gointernal/mcp/mcp.gointernal/mcp/mcp_test.gointernal/server/server.gointernal/server/server_test.gointernal/store/diagnostic.gointernal/store/store.gointernal/store/store_test.go
🔗 Linked Issue
Closes #459
Related: #629 (doctor/repair does not scan the
observationstable for the same empty fields) and #340 (umbrella for legacy required-field repair). This PR is the write-side half only — it stops new malformed rows being created. It does not repair rows that already exist; that remains #629/#340 territory.🏷️ PR Type
type:bug— Bug fix📝 Summary
ValidateSyncMutationPayloadalready owned instead of adding a second title check, per @ricardoarz-dev's review note on the issue — the invariant now has exactly one definition.📂 Changes
internal/store/diagnostic.goErrObservationTitleRequired,ValidateObservationTitle, and the private predicateobservationTitleIsPresent.ValidateSyncMutationPayload's observation-upsert branch now calls that predicate instead of its inlinerequire("title").internal/store/store.goAddObservationvalidates the post-stripPrivateTagstitle before opening the transaction, so nothing is inserted and nosync_mutationsrow is enqueued.internal/mcp/mcp.gomem_savereturns a tool error on a blank title, before project resolution or session creation. Placed after the existingcontent is requiredguard so error ordering is unchanged.cmd/engram/main.goengram savefails with a non-zero exit and the actionable message, before opening the store.internal/server/server.goPOST /observationsmapsErrObservationTitleRequiredto 400 instead of 500.DOCS.md,CHANGELOG.md🧪 Test Plan
New tests:
internal/store— empty / whitespace-only / empty-after-strip all rejected, asserting no observation row and nosync_mutationsrow; valid titles still accepted with exactly one enqueued mutation; plusTestValidateObservationTitleMatchesSyncPayloadRulepinning that the write-side and payload-side rules agree.internal/mcp—mem_saverejects missing / empty / whitespace titles, nothing persisted, no mutation queued.internal/server— 400 for""and" ",onWritenever fires.cmd/engram— non-zero exit, stderr carries the message and the cloud-sync explanation.Commands:
All new tests pass.
go test ./...is not fully green on Windows, and is equally not green atmain— I verified by stashing this change and re-running on the clean tree, where every failure reproduces identically. Pre-existing failures:cmd/engram(TestCmdExportDefaultAndCmdImportErrorspath-string mismatch,TestPrintUsagehang),internal/setup(tests touching the real home config),internal/store(TestMigrate_Idempotent— SQLite file lock onTempDircleanup),internal/sync(TestManifestReadWriteerror-string mismatch),internal/obsidian(watcher timing flake). All are Windows-environment artifacts unrelated to this change. Every package this PR touches passes apart from those.📌 Notes for reviewers
stripPrivateTagsdoes not produce an empty title. It substitutes the literal[REDACTED], so a title consisting only of a private tag is non-empty and is accepted. I pinned that existing behavior in a test rather than changing it — rejecting[REDACTED]titles is a separate product decision.Inbound paths are deliberately not guarded, and do not need to be. Cloud pull (
applyPulledMutationTx→applyObservationUpsertTx) andengram import(Store.Import) write observations with direct SQL and never reachAddObservation, so no legitimate inbound record can be blocked by this change.Store.PassiveCapturedoes route throughAddObservation.ExtractLearningsreturns trimmed non-empty strings so it is unaffected; a blank learning would now error instead of poisoning the queue, which is the intended direction.Known remaining gap, intentionally out of scope.
Store.UpdateObservationalso enqueues an observation upsert from astripPrivateTags'd title, soPATCH /observations/{id}with{"title":""}can still produce the same head-of-line block. It is the identical one-line guard, but it is a different entrypoint than #459 enumerates, so I kept this PR to one logical change. Happy to open a follow-up issue.Why this matters
Verified in production on v1.20.0: 19 of 1565 observations (1.2%) carried
title = '', and that was enough to keep cloud sync dead for 3 days across two projects. Because the mutation queue is an ordered log and the pusher stops at the first invalid row, one bad record blocks every later one — including valid records queued behind it. Full measurements in the issue thread.Summary by CodeRabbit
mem_savenow reject missing, empty, or whitespace-only titles before writing or syncing.POST /observationsresponds with400 Bad Request, with title validation taking precedence over session/project issues.