From eb9875c652aae0622fc1ea133fb4284ec3d6f711 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 09:39:50 +0000 Subject: [PATCH 1/4] fix: handle undefined content in memory query (GIT-96) 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 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 --- src/application/services/MemoryService.ts | 7 +++++-- src/infrastructure/repositories/MemoryRepository.ts | 4 ++-- tests/unit/application/services/MemoryService.test.ts | 8 ++++++++ .../infrastructure/repositories/MemoryRepository.test.ts | 7 +++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/application/services/MemoryService.ts b/src/application/services/MemoryService.ts index c8f899dc..e7b29e7d 100644 --- a/src/application/services/MemoryService.ts +++ b/src/application/services/MemoryService.ts @@ -209,6 +209,9 @@ export class MemoryService implements IMemoryService { const type = TRAILER_KEY_TO_MEMORY_TYPE[typeTrailer.key] as MemoryType; if (!type) continue; + // Skip trailers with empty or undefined values + if (!typeTrailer.value) continue; + // Pair with AI-Memory-Id by position, or generate synthetic ID. // Index suffix ensures uniqueness when a commit has multiple same-type trailers. const id = memoryIds[i] || `trailer:${commit.sha}:${type}:${i}`; @@ -232,8 +235,8 @@ export class MemoryService implements IMemoryService { private matchesQuery(entity: IMemoryEntity, query: string): boolean { const lower = query.toLowerCase(); - return entity.content.toLowerCase().includes(lower) || - entity.tags.some(t => t.toLowerCase().includes(lower)); + return (entity.content?.toLowerCase().includes(lower) ?? false) || + (entity.tags?.some(t => t?.toLowerCase().includes(lower)) ?? false); } get(id: string, cwd?: string): IMemoryEntity | null { diff --git a/src/infrastructure/repositories/MemoryRepository.ts b/src/infrastructure/repositories/MemoryRepository.ts index 193162f4..e0e06cf8 100644 --- a/src/infrastructure/repositories/MemoryRepository.ts +++ b/src/infrastructure/repositories/MemoryRepository.ts @@ -96,8 +96,8 @@ export class MemoryRepository implements IMemoryRepository { 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) ); } diff --git a/tests/unit/application/services/MemoryService.test.ts b/tests/unit/application/services/MemoryService.test.ts index f8fda6f3..ccfccdbe 100644 --- a/tests/unit/application/services/MemoryService.test.ts +++ b/tests/unit/application/services/MemoryService.test.ts @@ -198,6 +198,14 @@ describe('MemoryService', () => { const result = service.recall('nonexistent-xyz-query', { cwd: repoDir }); assert.equal(result.memories.length, 0); }); + + it('should handle query on memories with undefined content gracefully (GIT-96)', () => { + // 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)); + }); }); describe('unified recall (notes + trailers)', () => { diff --git a/tests/unit/infrastructure/repositories/MemoryRepository.test.ts b/tests/unit/infrastructure/repositories/MemoryRepository.test.ts index ef32cc0c..382f5a90 100644 --- a/tests/unit/infrastructure/repositories/MemoryRepository.test.ts +++ b/tests/unit/infrastructure/repositories/MemoryRepository.test.ts @@ -119,6 +119,13 @@ describe('MemoryRepository', () => { const result = repo.query({ limit: 1, cwd: repoDir }); assert.ok(result.memories.length <= 1); }); + + it('should handle query on memories with undefined content gracefully (GIT-96)', () => { + // Regression test: query should not throw on malformed data + // 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)); + }); }); describe('delete', () => { From ad93729966ca2b5d71feb86ff1ff8154a5ff41a9 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 16:55:12 +0000 Subject: [PATCH 2/4] fix: guard tag filter and strengthen regression tests 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 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 --- .../repositories/MemoryRepository.ts | 2 +- .../services/MemoryService.test.ts | 28 +++++++++++++-- .../repositories/MemoryRepository.test.ts | 36 ++++++++++++++++--- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/infrastructure/repositories/MemoryRepository.ts b/src/infrastructure/repositories/MemoryRepository.ts index e0e06cf8..0f3601ac 100644 --- a/src/infrastructure/repositories/MemoryRepository.ts +++ b/src/infrastructure/repositories/MemoryRepository.ts @@ -85,7 +85,7 @@ export class MemoryRepository implements IMemoryRepository { } if (options?.tag) { - filtered = filtered.filter(m => m.tags.includes(options.tag!)); + filtered = filtered.filter(m => m.tags?.includes(options.tag!) ?? false); } if (options?.since) { diff --git a/tests/unit/application/services/MemoryService.test.ts b/tests/unit/application/services/MemoryService.test.ts index ccfccdbe..f2ddea60 100644 --- a/tests/unit/application/services/MemoryService.test.ts +++ b/tests/unit/application/services/MemoryService.test.ts @@ -200,10 +200,32 @@ describe('MemoryService', () => { }); it('should handle query on memories with undefined content gracefully (GIT-96)', () => { - // Regression test: ensure matchesQuery doesn't throw on malformed data - // The service should handle memories where content could be undefined at runtime + // 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 }); - // Should not throw - just return results (or empty) assert.ok(Array.isArray(result.memories)); }); }); diff --git a/tests/unit/infrastructure/repositories/MemoryRepository.test.ts b/tests/unit/infrastructure/repositories/MemoryRepository.test.ts index 382f5a90..0280f36e 100644 --- a/tests/unit/infrastructure/repositories/MemoryRepository.test.ts +++ b/tests/unit/infrastructure/repositories/MemoryRepository.test.ts @@ -121,10 +121,38 @@ describe('MemoryRepository', () => { }); it('should handle query on memories with undefined content gracefully (GIT-96)', () => { - // Regression test: query should not throw on malformed data - // 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)); + // 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)); }); }); From 720533faa549aaaa70eec3a2da2c655ce1904ae2 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 17:03:00 +0000 Subject: [PATCH 3/4] feat: add complete hook config to init command 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 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 --- src/commands/init-hooks.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/commands/init-hooks.ts b/src/commands/init-hooks.ts index 8897e412..8e9f0d1e 100644 --- a/src/commands/init-hooks.ts +++ b/src/commands/init-hooks.ts @@ -202,13 +202,27 @@ export function buildGitMemConfig(): Record { threshold: 3, }, promptSubmit: { - enabled: false, + 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, + }, }, }; } From 7a5d725088ccb08c5c5c673f0bb9c34c1c3f7c82 Mon Sep 17 00:00:00 2001 From: Tony Casey Date: Sun, 15 Feb 2026 18:10:54 +0000 Subject: [PATCH 4/4] fix: update console output for new hook defaults - Remove "(disabled by default)" from UserPromptSubmit (now enabled) - Add CommitMsg hook to the hooks configured summary Co-Authored-By: Claude Opus 4.5 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 --- src/commands/init-hooks.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/init-hooks.ts b/src/commands/init-hooks.ts index 8e9f0d1e..58a43074 100644 --- a/src/commands/init-hooks.ts +++ b/src/commands/init-hooks.ts @@ -307,7 +307,8 @@ export async function initHooksCommand(options: IInitHooksOptions, logger?: ILog console.log('\nHooks configured:'); 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(' UserPromptSubmit — Surface relevant memories per prompt'); + console.log(' CommitMsg — Analyze commits and add AI trailers'); console.log('\nNext steps:'); console.log(' 1. Start Claude Code in this repo: claude');