Conversation
Add defensive null checks when filtering memories by query text. The error 'Cannot read properties of undefined (reading toLowerCase)' occurred when malformed memory data had undefined content or tags. Fixes: - MemoryRepository.query: Guard m.content and m.tags with optional chaining - MemoryService.matchesQuery: Guard entity.content and entity.tags - MemoryService.trailerCommitToEntities: Skip trailers with empty values Adds regression tests for both MemoryService.recall and MemoryRepository.query. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> AI-Agent: Claude-Code/2.1.42 AI-Model: claude-opus-4-5-20251101 AI-Gotcha: handle undefined content in memory query (GIT-96). Add defensive null checks when filtering memories by query text. AI-Confidence: medium AI-Tags: application, services, infrastructure, tests, unit AI-Lifecycle: project AI-Memory-Id: f3ca08c0 AI-Source: heuristic
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughSkip empty or undefined memory trailer entries during trailer processing; make recall/query access to Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@tests/unit/application/services/MemoryService.test.ts`:
- Around line 201-208: Add a focused regression test that mocks MemoryRepository
(or the repo method used by MemoryService.recall, e.g., MemoryRepository.getAll
or MemoryService's internal repo accessor) to return an entity whose content is
explicitly undefined, then call service.recall('test-query', { cwd: repoDir })
and assert it does not throw and returns an array (e.g.,
Array.isArray(result.memories)) so matchesQuery is exercised with malformed
data; reference the MemoryService.recall and matchesQuery logic and restore the
mock after the test.
In `@tests/unit/infrastructure/repositories/MemoryRepository.test.ts`:
- Around line 122-128: Add a focused regression test that mocks
notesService.read to return a malformed note object missing the content field
and then calls repo.query to ensure the defensive path in MemoryRepository.query
is exercised; specifically, in MemoryRepository.test.ts replace or augment the
existing case by stubbing notesService.read (or the repo's notesService
dependency) to return JSON without content for repoDir, call repo.query({ query:
'test-search', cwd: repoDir }) and assert it does not throw and that
result.memories is an array (and optionally contains zero or filtered entries)
to prove the guard in MemoryRepository.query handles undefined content.
There was a problem hiding this comment.
Pull request overview
This PR addresses a runtime crash in mcp:recall by making memory-query filtering tolerant of malformed/legacy memory records (e.g., missing content/tags) and by skipping empty trailer values when converting commit trailers into memory entities.
Changes:
- Add defensive optional-chaining in
MemoryRepository.query()when filtering by free-text query. - Add defensive optional-chaining in
MemoryService.matchesQuery()and skip empty-valued memory trailers intrailerCommitToEntities(). - Add two regression tests intended to cover the undefined-content crash scenario (GIT-96).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/unit/infrastructure/repositories/MemoryRepository.test.ts | Adds a regression test for querying with malformed memory data. |
| tests/unit/application/services/MemoryService.test.ts | Adds a regression test for recall with malformed memory data. |
| src/infrastructure/repositories/MemoryRepository.ts | Guards query-text filtering against missing content/tags. |
| src/application/services/MemoryService.ts | Guards query matching against missing fields; skips empty-valued type trailers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // This ensures the query filter handles potential undefined content at runtime | ||
| const result = repo.query({ query: 'test-search', cwd: repoDir }); | ||
| assert.ok(Array.isArray(result.memories)); |
There was a problem hiding this comment.
This regression test doesn’t actually create a malformed memory (missing/undefined content), so it will pass even without the fix. To validate GIT-96, write a git note payload containing a memory object with no content field (or content: null) for a fresh commit SHA, then assert the query call does not throw when options.query is provided.
| // This ensures the query filter handles potential undefined content at runtime | |
| const result = repo.query({ query: 'test-search', cwd: repoDir }); | |
| assert.ok(Array.isArray(result.memories)); | |
| // Create a fresh commit with a malformed memory note (missing/null content) | |
| const malformedFilePath = join(repoDir, 'git-96-malformed.txt'); | |
| writeFileSync(malformedFilePath, 'git-96 malformed memory'); | |
| git(['add', 'git-96-malformed.txt'], repoDir); | |
| git(['commit', '-m', 'git-96 malformed memory'], repoDir); | |
| const malformedSha = git(['rev-parse', 'HEAD'], repoDir); | |
| const malformedMemory = { | |
| id: 'git-96-malformed-id', | |
| type: 'note', | |
| tags: [], | |
| // Intentionally omit `content` to simulate malformed data (or set to null) | |
| content: null, | |
| createdAt: new Date().toISOString(), | |
| updatedAt: new Date().toISOString(), | |
| }; | |
| const notePayload = JSON.stringify([malformedMemory]); | |
| git( | |
| [ | |
| 'notes', | |
| '--ref', | |
| NotesService.NOTES_REF, | |
| 'add', | |
| '-f', | |
| '-m', | |
| notePayload, | |
| malformedSha, | |
| ], | |
| repoDir, | |
| ); | |
| // Query should not throw even though one memory has undefined/null content | |
| assert.doesNotThrow(() => { | |
| const result = repo.query({ query: 'test-search', cwd: repoDir }); | |
| assert.ok(Array.isArray(result.memories)); | |
| }); |
| // Regression test: ensure matchesQuery doesn't throw on malformed data | ||
| // The service should handle memories where content could be undefined at runtime | ||
| const result = service.recall('test-query', { cwd: repoDir }); | ||
| // Should not throw - just return results (or empty) | ||
| assert.ok(Array.isArray(result.memories)); |
There was a problem hiding this comment.
This regression test does not inject any memory with missing/undefined content, so it doesn’t exercise the failure mode that triggered GIT-96. Consider creating a commit and attaching a malformed note (e.g., JSON memory missing content / tags) via git notes in the test setup, then asserting service.recall(...) does not throw and returns a valid result shape.
| // Regression test: ensure matchesQuery doesn't throw on malformed data | |
| // The service should handle memories where content could be undefined at runtime | |
| const result = service.recall('test-query', { cwd: repoDir }); | |
| // Should not throw - just return results (or empty) | |
| assert.ok(Array.isArray(result.memories)); | |
| // Regression test: ensure matchesQuery doesn't throw on malformed data. | |
| // Create a commit with a malformed git note (missing `content` / `tags`) | |
| // to simulate the failure mode that previously caused GIT-96. | |
| writeFileSync(join(repoDir, 'malformed-note.txt'), 'malformed'); | |
| git(['add', '.'], repoDir); | |
| git(['commit', '-m', 'malformed memory commit'], repoDir); | |
| const malformedSha = git(['rev-parse', 'HEAD'], repoDir); | |
| // Attach a malformed note: valid JSON but missing `content` and `tags`. | |
| git( | |
| ['notes', 'add', '-m', '{"id":"malformed-memory","type":"decision"}', malformedSha], | |
| repoDir, | |
| ); | |
| const result = service.recall('test-query', { cwd: repoDir }); | |
| // Should not throw - just return results (or empty) with a valid shape. | |
| assert.ok(Array.isArray(result.memories)); | |
| result.memories.forEach((memory) => { | |
| assert.ok(memory && typeof memory === 'object'); | |
| }); |
| if (options?.query) { | ||
| const q = options.query.toLowerCase(); | ||
| filtered = filtered.filter(m => | ||
| m.content.toLowerCase().includes(q) || | ||
| m.tags.some(t => t.toLowerCase().includes(q)) | ||
| (m.content?.toLowerCase().includes(q) ?? false) || | ||
| (m.tags?.some(t => t?.toLowerCase().includes(q)) ?? false) | ||
| ); |
There was a problem hiding this comment.
The optional-chaining here prevents crashes for malformed notes during query-text filtering, but options.tag filtering earlier still calls m.tags.includes(...) and can throw if a legacy/malformed note omits tags (which is plausible when parsing unvalidated JSON). Consider normalizing parsed memories in readPayload (default tags to [], content to '') or guarding tag filtering similarly so mcp:recall with tag can’t crash on malformed data.
Address PR review feedback: - Add optional chaining to tag filter (m.tags?.includes) to prevent crash when tags field is undefined - Update regression tests to inject actual malformed data with missing content/tags fields via git notes - Test both query text and tag filter paths with malformed data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> AI-Agent: Claude-Code/2.1.42 AI-Model: claude-opus-4-5-20251101 AI-Gotcha: guard tag filter and strengthen regression tests. Address PR review feedback: AI-Confidence: medium AI-Tags: infrastructure, tests, unit, application, services AI-Lifecycle: project AI-Memory-Id: db593ac7 AI-Source: heuristic
|
Addressed review feedback in commit ad93729:
Thanks @copilot and @coderabbitai for the thorough review! |
|
@TonyCasey I've opened a new pull request, #72, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 6849
✅ Tag filter guard - The optional chaining ✅ Strengthened regression tests - Both test files now inject real malformed data by:
The regression tests are particularly robust now—using actual malformed git notes data makes them far more effective at catching real-world edge cases. Great work addressing the feedback! 🎉 🧠 Learnings used |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@tests/unit/application/services/MemoryService.test.ts`:
- Around line 202-230: The test in MemoryService.test.ts performs real git
operations (git, git notes, repoDir, service.recall) which breaks unit-test
isolation; change it to inject malformed data by mocking the NotesService or
MemoryRepository used by MemoryService (or replace calls that create git
notes/commits with a stubbed/mocked method that returns the malformed payload)
so service.recall('test-query', { cwd: repoDir }) receives the malformed memory
object without invoking the git binary; alternatively, move this scenario into
an integration test under tests/integration/ if you need real git behavior.
In `@tests/unit/infrastructure/repositories/MemoryRepository.test.ts`:
- Around line 123-155: This unit test improperly performs real git operations;
replace the live git/notes manipulation with a mocked NotesService response to
keep the test isolated: remove calls to git(...) and git notes/commit setup,
create a fake malformed payload (missing content/tags) and stub the NotesService
(or the method MemoryRepository uses to read notes) to return that payload when
MemoryRepository.query or the repository's internal notes-fetching function is
invoked, then call repo.query({ query: 'test-search', cwd: repoDir }) and
repo.query({ tag: 'some-tag', cwd: repoDir }) and assert they return arrays as
before; target the test harness around MemoryRepository/NotesService and the
repo.query call to inject the mock rather than touching git.
| it('should handle query on memories with undefined content gracefully (GIT-96)', () => { | ||
| // Regression test: inject actual malformed data with missing content/tags | ||
| const malformedFile = join(repoDir, 'git-96-memsvc-malformed.txt'); | ||
| writeFileSync(malformedFile, 'malformed memory service test'); | ||
| git(['add', 'git-96-memsvc-malformed.txt'], repoDir); | ||
| git(['commit', '-m', 'git-96 memsvc malformed memory'], repoDir); | ||
| const malformedSha = git(['rev-parse', 'HEAD'], repoDir); | ||
|
|
||
| // Write a malformed note payload (missing content and tags fields) | ||
| const malformedPayload = JSON.stringify({ | ||
| memories: [{ | ||
| id: 'git-96-memsvc-malformed-id', | ||
| type: 'gotcha', | ||
| sha: malformedSha, | ||
| confidence: 'medium', | ||
| source: 'user-explicit', | ||
| lifecycle: 'project', | ||
| // Intentionally omit content and tags to simulate malformed data | ||
| createdAt: new Date().toISOString(), | ||
| updatedAt: new Date().toISOString(), | ||
| }], | ||
| }); | ||
|
|
||
| git(['notes', '--ref', 'refs/notes/mem', 'add', '-f', '-m', malformedPayload, malformedSha], repoDir); | ||
|
|
||
| // Query should not throw even with malformed data | ||
| const result = service.recall('test-query', { cwd: repoDir }); | ||
| assert.ok(Array.isArray(result.memories)); | ||
| }); |
There was a problem hiding this comment.
This unit test uses real git operations; prefer mocks or move to integration tests.
The new regression test relies on git commands and notes, which violates the unit-test isolation requirement. Consider mocking NotesService/MemoryRepository to inject malformed data, or relocate to tests/integration/.
As per coding guidelines: "Unit tests must be fast, isolated, and mock all dependencies. Integration tests should use real backends and test contracts."
🤖 Prompt for AI Agents
In `@tests/unit/application/services/MemoryService.test.ts` around lines 202 -
230, The test in MemoryService.test.ts performs real git operations (git, git
notes, repoDir, service.recall) which breaks unit-test isolation; change it to
inject malformed data by mocking the NotesService or MemoryRepository used by
MemoryService (or replace calls that create git notes/commits with a
stubbed/mocked method that returns the malformed payload) so
service.recall('test-query', { cwd: repoDir }) receives the malformed memory
object without invoking the git binary; alternatively, move this scenario into
an integration test under tests/integration/ if you need real git behavior.
| it('should handle query on memories with undefined content gracefully (GIT-96)', () => { | ||
| // Regression test: inject actual malformed data with missing content/tags | ||
| // to verify the defensive guards work | ||
| const malformedFile = join(repoDir, 'git-96-malformed.txt'); | ||
| writeFileSync(malformedFile, 'malformed memory test'); | ||
| git(['add', 'git-96-malformed.txt'], repoDir); | ||
| git(['commit', '-m', 'git-96 malformed memory'], repoDir); | ||
| const malformedSha = git(['rev-parse', 'HEAD'], repoDir); | ||
|
|
||
| // Write a malformed note payload (missing content and tags fields) | ||
| const malformedPayload = JSON.stringify({ | ||
| memories: [{ | ||
| id: 'git-96-malformed-id', | ||
| type: 'decision', | ||
| sha: malformedSha, | ||
| confidence: 'high', | ||
| source: 'user-explicit', | ||
| lifecycle: 'project', | ||
| // Intentionally omit content and tags to simulate malformed data | ||
| createdAt: new Date().toISOString(), | ||
| updatedAt: new Date().toISOString(), | ||
| }], | ||
| }); | ||
|
|
||
| git(['notes', '--ref', 'refs/notes/mem', 'add', '-f', '-m', malformedPayload, malformedSha], repoDir); | ||
|
|
||
| // Query with text search should not throw | ||
| const queryResult = repo.query({ query: 'test-search', cwd: repoDir }); | ||
| assert.ok(Array.isArray(queryResult.memories)); | ||
|
|
||
| // Query with tag filter should not throw | ||
| const tagResult = repo.query({ tag: 'some-tag', cwd: repoDir }); | ||
| assert.ok(Array.isArray(tagResult.memories)); |
There was a problem hiding this comment.
Unit test should not depend on real git operations.
This regression test uses real git commands and notes, which conflicts with the unit-test isolation requirement. Prefer mocking NotesService to supply malformed payloads, or move this case to tests/integration/.
As per coding guidelines: "Unit tests must be fast, isolated, and mock all dependencies. Integration tests should use real backends and test contracts."
🤖 Prompt for AI Agents
In `@tests/unit/infrastructure/repositories/MemoryRepository.test.ts` around lines
123 - 155, This unit test improperly performs real git operations; replace the
live git/notes manipulation with a mocked NotesService response to keep the test
isolated: remove calls to git(...) and git notes/commit setup, create a fake
malformed payload (missing content/tags) and stub the NotesService (or the
method MemoryRepository uses to read notes) to return that payload when
MemoryRepository.query or the repository's internal notes-fetching function is
invoked, then call repo.query({ query: 'test-search', cwd: repoDir }) and
repo.query({ tag: 'some-tag', cwd: repoDir }) and assert they return arrays as
before; target the test harness around MemoryRepository/NotesService and the
repo.query call to inject the mock rather than touching git.
Update buildGitMemConfig() to include all hook settings: - promptSubmit: extractIntent, intentTimeout, minWords, memoryLimit, includeCommitMessages - commitMsg: autoAnalyze, inferTags, requireType, defaultLifecycle, enrich, enrichTimeout This ensures `git mem init` creates a complete config file with the 8-second enrichTimeout and all intent extraction settings. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> AI-Agent: Claude-Code/2.1.42 AI-Model: claude-opus-4-5-20251101 AI-Decision: The `git mem init` command creates a complete configuration file that includes all hook settings by default, enabling promptSubmit by default with full intent extraction capabilities. AI-Confidence: verified AI-Tags: init-command, git-hooks, configuration, default-settings, commit-msg, timeout, ai-processing, performance, intent-extraction, prompt-submit, validation, memory-limit, context, auto-analyze, tags, lifecycle AI-Lifecycle: project AI-Memory-Id: 320aff7c AI-Source: llm-enrichment
|
Added commit 720533f to include complete hook config in
This ensures new projects get the complete config with the 8-second timeout out of the box. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| enabled: true, | ||
| recordPrompts: false, | ||
| surfaceContext: true, | ||
| extractIntent: true, | ||
| intentTimeout: 3000, | ||
| minWords: 5, | ||
| memoryLimit: 20, | ||
| includeCommitMessages: true, | ||
| }, | ||
| postCommit: { | ||
| enabled: true, | ||
| }, | ||
| commitMsg: { | ||
| enabled: true, | ||
| autoAnalyze: true, | ||
| inferTags: true, | ||
| requireType: false, | ||
| defaultLifecycle: 'project', | ||
| enrich: true, | ||
| enrichTimeout: 8000, | ||
| }, |
There was a problem hiding this comment.
The changes to init-hooks.ts appear unrelated to the bug fix described in the PR (handling undefined content in memory queries). This change modifies the default configuration to enable promptSubmit and add new config fields, as well as adding a complete commitMsg config block. These configuration changes should be in a separate PR with appropriate documentation and testing, as they change the default behavior of the tool and are not related to fixing the GIT-96 bug.
|
Re: CodeRabbit comments about unit tests using real git operations: These test files ( The new regression tests are consistent with the existing test structure. Moving them to These are nitpicks and don't block the PR. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/commands/init-hooks.ts`:
- Around line 204-213: The console text is out of sync with the promptSubmit
default: update the message that refers to "UserPromptSubmit (disabled by
default)" to reflect that promptSubmit.enabled is now true; locate the
promptSubmit config object and the console/log statement (search for
"UserPromptSubmit" or the function that prints hook defaults in init-hooks.ts)
and change the wording to indicate it is enabled by default (or remove the
parenthetical) so the console output matches promptSubmit.enabled and the other
fields.
- Around line 217-225: The new commitMsg configuration block (commitMsg: {
enabled: true, autoAnalyze: true, inferTags: true, requireType: false,
defaultLifecycle: 'project', enrich: true, enrichTimeout: 8000 }) was added but
not listed in the "Hooks configured" console summary; update the code that
builds/renders that summary to include an entry for commitMsg (showing at least
enabled state and key flags like autoAnalyze/enrich) so users see it
active—locate the summary/printing logic that outputs the "Hooks configured"
lines and add commitMsg to the displayed hooks.
| commitMsg: { | ||
| enabled: true, | ||
| autoAnalyze: true, | ||
| inferTags: true, | ||
| requireType: false, | ||
| defaultLifecycle: 'project', | ||
| enrich: true, | ||
| enrichTimeout: 8000, | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
New commitMsg hook not documented in console output.
The commitMsg hook block is added to the default configuration, but the "Hooks configured" summary (lines 307-310) does not mention it. Consider adding a line describing this hook so users know it's active.
📝 Suggested addition
console.log(' SessionStart — Load memories into Claude context on startup');
console.log(' Stop — Capture memories from session commits on exit');
console.log(' UserPromptSubmit — Surface relevant memories per prompt (disabled by default)');
+ console.log(' CommitMsg — Analyze and enrich commit messages');🤖 Prompt for AI Agents
In `@src/commands/init-hooks.ts` around lines 217 - 225, The new commitMsg
configuration block (commitMsg: { enabled: true, autoAnalyze: true, inferTags:
true, requireType: false, defaultLifecycle: 'project', enrich: true,
enrichTimeout: 8000 }) was added but not listed in the "Hooks configured"
console summary; update the code that builds/renders that summary to include an
entry for commitMsg (showing at least enabled state and key flags like
autoAnalyze/enrich) so users see it active—locate the summary/printing logic
that outputs the "Hooks configured" lines and add commitMsg to the displayed
hooks.
- Remove "(disabled by default)" from UserPromptSubmit (now enabled) - Add CommitMsg hook to the hooks configured summary Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> AI-Agent: Claude-Code/2.1.42 AI-Model: claude-opus-4-5-20251101 AI-Decision: The UserPromptSubmit hook default behavior changed from disabled to enabled by default. AI-Confidence: verified AI-Tags: hooks, user-prompt-submit, defaults, init-hooks, commit-msg, ai-trailers, console-output, user-experience AI-Lifecycle: project AI-Memory-Id: 555658a6 AI-Source: llm-enrichment
|
Fixed in 7a5d725:
Re: Copilot's comment about init-hooks changes being unrelated - the init-hooks fix ensures |
Summary
Cannot read properties of undefined (reading 'toLowerCase')error inmcp:recallChanges
src/infrastructure/repositories/MemoryRepository.tsm.contentandm.tagswith optional chainingsrc/application/services/MemoryService.tsentity.contentandentity.tagsinmatchesQuerysrc/application/services/MemoryService.tstrailerCommitToEntitiesTest plan
Closes GIT-96
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests
New Features