Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions src/lib/infrastructure/di/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@
import { createLlmGuard } from '../services/LlmGuard';
import { createSummarizationService } from '../services/SummarizationService';
import { createTranscriptEnricher } from '../services/TranscriptEnricher';
import type { ISummarizationService } from '../../domain/interfaces/ISummarizationService';

Check warning on line 56 in src/lib/infrastructure/di/bootstrap.ts

View workflow job for this annotation

GitHub Actions / test

'ISummarizationService' is defined but never used. Allowed unused vars must match /^_/u
import { createLogger, createNullLogger } from '../logging';

// git-mem imports
import { MemoryService as GitMemService, NotesService, MemoryRepository } from 'git-mem/dist/index';
// git-mem factory (shared singleton)
import { getGitMemInstance } from '../git-mem';

/**
* Result of bootstrapping the container.
Expand Down Expand Up @@ -106,13 +106,11 @@
container.registerInstance(TOKENS.EventEmitter, events);

// ============================================================
// git-mem Memory Backend
// git-mem Memory Backend (shared singleton)
// ============================================================

// Wire git-mem: NotesService → MemoryRepository → MemoryService
const notesService = new NotesService();
const memoryRepo = new MemoryRepository(notesService);
const gitMemService = new GitMemService(memoryRepo);
// Use shared git-mem instance from GitMemFactory
const gitMemService = getGitMemInstance();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope git-mem instance per project bootstrap

bootstrapContainer now always takes the process-wide getGitMemInstance() path, which means all later container bootstraps reuse the first git-mem backend created in this process. Since that singleton is not keyed by projectRoot, any multi-repo usage in one process (e.g., repeated bootstrap calls in adapters/tests or long-lived hosts) can persist/read memory and tasks against the wrong repository state; this is a data-isolation regression compared to constructing a fresh instance per bootstrap.

Useful? React with 👍 / 👎.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed analysis! Let me explain why this is safe in Lisa's architecture:

git-mem is stateless regarding cwd:

  • NotesService.read/write/list() takes cwd as a parameter on each method call
  • MemoryRepository.create/query/delete() passes cwd through to NotesService
  • The singleton stores no repo state - it just holds the wired service instances

Lisa's usage pattern:

  • CLI commands are short-lived processes (one command = one process)
  • Hooks run per-session in the context of a single project
  • There's no multi-repo handling within a single process

For tests:

  • We export resetGitMemInstance() which tests can call between bootstrap invocations
  • This clears the singleton when needed

Fallback behavior:
When cwd is not passed (as in Lisa's services), git-mem defaults to process.cwd() at execution time - which is correct for our CLI/hook pattern since they're always invoked from the project root.

If we need multi-repo support in the future, we could add explicit cwd passing to the skill services, but for current usage the singleton is safe.


// Memory Service (singleton - wraps git-mem)
const memoryService = new GitMemMemoryService(gitMemService);
Expand Down Expand Up @@ -240,14 +238,13 @@
} = await import('../../skills/shared/services');
const {
createGhCliClientFromEnv,
createGitMem,
} = await import('../../skills/shared/clients');

const ghCli = createGhCliClientFromEnv();
const githubClient = createGitHubClient(ghCli);

const skillGitMem = createGitMem();
const skillTaskService = createSkillTaskService({ gitMem: skillGitMem });
// Use the shared git-mem singleton
const skillTaskService = createSkillTaskService({ gitMem: gitMemService });

const service = createGitHubSyncService({
github: githubClient,
Expand Down
56 changes: 56 additions & 0 deletions src/lib/infrastructure/git-mem/GitMemFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Shared git-mem factory with lazy singleton pattern.
*
* Provides a single git-mem instance that can be used by both:
* - Infrastructure DI (bootstrap.ts)
* - Skills entry points (memory.ts, tasks.ts, etc.)
*
* This consolidates the duplicate factory patterns from:
* - skills/shared/clients/GitMemFactory.ts
* - bootstrap.ts inline creation
*/

import { MemoryService, NotesService, MemoryRepository } from 'git-mem/dist/index';
import type { IMemoryService as IGitMemMemoryService } from 'git-mem/dist/index';

/** Lazy singleton instance */
let gitMemInstance: IGitMemMemoryService | null = null;

/**
* Get or create the shared git-mem MemoryService instance.
*
* Uses lazy initialization - the instance is created on first call
* and reused for subsequent calls within the same process.
*
* @returns The git-mem MemoryService instance
*/
export function getGitMemInstance(): IGitMemMemoryService {
if (!gitMemInstance) {
const notes = new NotesService();
const repo = new MemoryRepository(notes);
gitMemInstance = new MemoryService(repo);
}
return gitMemInstance;
}

/**
* Create a fresh git-mem MemoryService instance.
*
* Use this when you need an isolated instance (e.g., for testing)
* rather than the shared singleton.
*
* @returns A new git-mem MemoryService instance
*/
export function createGitMem(): IGitMemMemoryService {
const notes = new NotesService();
const repo = new MemoryRepository(notes);
return new MemoryService(repo);
}

/**
* Reset the singleton instance (for testing only).
* @internal
*/
export function resetGitMemInstance(): void {
gitMemInstance = null;
}
1 change: 1 addition & 0 deletions src/lib/infrastructure/git-mem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Git-mem infrastructure barrel export.
*/
export { GitMemClient } from './GitMemClient';
export { getGitMemInstance, createGitMem, resetGitMemInstance } from './GitMemFactory';
export type {
GitMemType,
GitMemConfidence,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
/**
* Memory service implementation for skill scripts.
* Uses git-mem for all memory operations.
*
* This is the "rich" memory service with full CLI capabilities:
* load, add, expire, cleanup, conflicts, dedupe, curate, consolidate.
*/
import type { IMemoryService as IGitMemMemoryService, IMemoryEntity } from 'git-mem/dist/index';

import type {
IMemoryService,
ISkillMemoryService,
IFact,
IMemoryLoadResult,
IMemoryAddResult,
Expand All @@ -18,13 +21,13 @@ import type {
IMemoryCurateResult,
IMemoryConsolidateResult,
IConflictGroup,
} from './interfaces';
import { detectDuplicatesFromFacts } from '../../../domain/utils/deduplication';
import { isValidCurationMark, resolveCurationTag } from '../../../domain/interfaces/ICurationService';
import type { CurationMark } from '../../../domain/interfaces/ICurationService';
import { resolveConfidenceTag } from '../../../domain/interfaces/types/IMemoryQuality';
import { CONSOLIDATION_ACTION_VALUES } from '../../../domain/interfaces/IConsolidationService';
import type { ConsolidationAction } from '../../../domain/interfaces/IConsolidationService';
} from './skill-interfaces';
import { detectDuplicatesFromFacts } from '../../domain/utils/deduplication';
import { isValidCurationMark, resolveCurationTag } from '../../domain/interfaces/ICurationService';
import type { CurationMark } from '../../domain/interfaces/ICurationService';
import { resolveConfidenceTag } from '../../domain/interfaces/types/IMemoryQuality';
import { CONSOLIDATION_ACTION_VALUES } from '../../domain/interfaces/IConsolidationService';
import type { ConsolidationAction } from '../../domain/interfaces/IConsolidationService';

/**
* Type-to-tag mapping for memory types.
Expand Down Expand Up @@ -72,19 +75,19 @@ function toFact(entity: IMemoryEntity, groupId: string): IFact {
}

/**
* Dependencies for creating a memory service.
* Dependencies for creating a skill memory service.
*/
export interface IMemoryServiceDependencies {
export interface ISkillMemoryServiceDependencies {
gitMem: IGitMemMemoryService;
}

/**
* Creates a memory service instance backed by git-mem.
* Creates a skill memory service instance backed by git-mem.
*
* @param deps - Service dependencies
* @returns Memory service implementation
* @returns Skill memory service implementation
*/
export function createMemoryService(deps: IMemoryServiceDependencies): IMemoryService {
export function createSkillMemoryService(deps: ISkillMemoryServiceDependencies): ISkillMemoryService {
const { gitMem } = deps;

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,30 @@
/**
* Prompt service - captures user prompts to git-mem.
*
* Uses fingerprinting to detect and skip duplicate prompts.
*/
import crypto from 'crypto';
import type { IMemoryService as IGitMemMemoryService } from 'git-mem/dist/index';
import type {
ISkillPromptService,
IPromptArgs,
IPromptResult,
} from './skill-interfaces';

// ============================================================================
// Types
// ============================================================================

export interface IPromptArgs {
text: string;
role?: string;
source?: string;
force?: boolean;
groupId: string;
}

export interface IPromptResult {
status: 'ok' | 'skipped';
action?: 'add';
group?: string;
role?: string;
source?: string;
reason?: string;
}

export interface IPromptService {
fingerprint(text: string): string;
addPrompt(args: IPromptArgs): Promise<IPromptResult>;
}

export interface IPromptServiceDependencies {
/**
* Dependencies for creating a skill prompt service.
*/
export interface ISkillPromptServiceDependencies {
gitMem: IGitMemMemoryService;
}

/**
* Creates a prompt service instance backed by git-mem.
* Creates a skill prompt service instance backed by git-mem.
*
* @param deps - Service dependencies
* @returns Skill prompt service implementation
*/
export function createPromptService(deps: IPromptServiceDependencies): IPromptService {
export function createSkillPromptService(deps: ISkillPromptServiceDependencies): ISkillPromptService {
const { gitMem } = deps;

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
/**
* Task service implementation for skill scripts.
* Uses git-mem for all task operations.
*
* This is the "rich" task service with full CLI capabilities:
* list, add, update, link, unlink, listLinked.
*/
import type { IMemoryService as IGitMemMemoryService } from 'git-mem/dist/index';
import type {
ITaskService,
ITask,
ISkillTaskService,
ISkillTask,
ITaskListResult,
ITaskWriteResult,
ITaskWriteOptions,
ITaskLoadOptions,
ITaskLinkResult,
ITaskExternalLink,
ExternalLinkSource,
ITaskLoadOptions,
} from './interfaces';
} from './skill-interfaces';

/**
* Dependencies for creating a task service.
* Dependencies for creating a skill task service.
*/
export interface ITaskServiceDependencies {
export interface ISkillTaskServiceDependencies {
gitMem: IGitMemMemoryService;
}

Expand All @@ -36,16 +39,16 @@ function parseTaskContent(content: string): Record<string, unknown> | null {
}

/**
* Creates a task service instance backed by git-mem.
* Creates a skill task service instance backed by git-mem.
*
* Tasks are stored as git-mem memories with:
* - Tags: `task`, `group:<groupId>`, `status:<status>`, `task_id:<uuid>`
* - Content: JSON `{ type: "task", title, status, repo, assignee, ... }`
*
* @param deps - Service dependencies
* @returns Task service implementation
* @returns Skill task service implementation
*/
export function createTaskService(deps: ITaskServiceDependencies): ITaskService {
export function createSkillTaskService(deps: ISkillTaskServiceDependencies): ISkillTaskService {
const { gitMem } = deps;

return {
Expand Down Expand Up @@ -78,12 +81,12 @@ export function createTaskService(deps: ITaskServiceDependencies): ITaskService
// Apply limit
filtered = filtered.slice(0, limit);

// Map to ITask
const tasks: ITask[] = filtered.map(m => {
// Map to ISkillTask
const tasks: ISkillTask[] = filtered.map(m => {
const taskObj = parseTaskContent(m.content);

if (taskObj) {
const task: ITask = {
const task: ISkillTask = {
title: String(taskObj.title || ''),
status: String(taskObj.status || 'unknown'),
repo: String(taskObj.repo || defaultRepo),
Expand Down
19 changes: 19 additions & 0 deletions src/lib/infrastructure/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,27 @@
* Infrastructure service implementations.
*/

// Domain-level git-mem services (simpler interface for DI/hooks)
export { GitMemMemoryService } from './GitMemMemoryService';
export { GitMemTaskService } from './GitMemTaskService';

// Skill-level git-mem services (rich interface for CLI scripts)
export {
createSkillMemoryService,
type ISkillMemoryServiceDependencies,
} from './SkillMemoryService';
export {
createSkillTaskService,
type ISkillTaskServiceDependencies,
} from './SkillTaskService';
export {
createSkillPromptService,
type ISkillPromptServiceDependencies,
} from './SkillPromptService';

// Skill interfaces (re-export for convenience)
export * from './skill-interfaces';

export { EventEmitter } from './EventEmitter';
export { SessionCaptureService } from './SessionCaptureService';
export { RecursionService } from './RecursionService';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/**
* Memory service interface for skill scripts.
* Provides a clean API for memory/fact CRUD operations.
*
* This is the "rich" memory interface used by CLI scripts,
* with operations like dedupe, curate, conflicts, consolidate.
*/
import type { CurationMark } from '../../../../domain/interfaces/ICurationService';
import type { ConsolidationAction } from '../../../../domain/interfaces/IConsolidationService';
import type { CurationMark } from '../../../domain/interfaces/ICurationService';
import type { ConsolidationAction } from '../../../domain/interfaces/IConsolidationService';

/**
* A memory/fact item.
Expand Down Expand Up @@ -166,9 +169,13 @@ export interface IMemoryConsolidateResult {
}

/**
* Memory service interface.
* Memory service interface for skill scripts.
*
* This is the "rich" interface with full CLI capabilities.
* Contrast with IMemoryService in domain/interfaces which is
* the simpler infrastructure interface.
*/
export interface IMemoryService {
export interface ISkillMemoryService {
/**
* Load memories/facts from storage.
*
Expand Down
Loading