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/commands/init-hooks.ts b/src/commands/init-hooks.ts index 8897e412..58a43074 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, + }, }, }; } @@ -293,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'); diff --git a/src/infrastructure/repositories/MemoryRepository.ts b/src/infrastructure/repositories/MemoryRepository.ts index 193162f4..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) { @@ -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..f2ddea60 100644 --- a/tests/unit/application/services/MemoryService.test.ts +++ b/tests/unit/application/services/MemoryService.test.ts @@ -198,6 +198,36 @@ 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: 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)); + }); }); 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..0280f36e 100644 --- a/tests/unit/infrastructure/repositories/MemoryRepository.test.ts +++ b/tests/unit/infrastructure/repositories/MemoryRepository.test.ts @@ -119,6 +119,41 @@ 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: 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)); + }); }); describe('delete', () => {