fix(ingest): honor storage.git.auto_commit and memory.auto_commit

Both documented flags were read only for status display; every ingest path
squash-committed to main unconditionally, so setting either to false was a
silent no-op (the reported symptom: 'Memory ingest (external_ingest): ...'
commits despite memory.auto_commit: false).

Gate the commit at the squash-merge onto main — the one point where ingest work
becomes a permanent commit (intermediate session-worktree commits must still
happen for the squash to collapse). When auto-commit is off, apply the squash to
main's working tree and leave it staged instead of committing, so the run is
never silently discarded:

- GitService.stageSquashMergeIntoMain: shares the merge core with
  squashMergeIntoMain but stops before committing and returns the staged tree
  SHA (a valid diff/read ref).
- memory.auto_commit gates MemoryAgentService (its DB writes are eager, so the
  staged files stay consistent); the commit-message job is skipped.
- storage.git.auto_commit gates IngestBundleRunner; the wiki index is reconciled
  from the staged tree via the existing syncFromCommit (git diff/show accept a
  write-tree ref), and SL reindex already reads from files.

Config descriptions now state precisely what each flag gates and the staged
semantics when false.
This commit is contained in:
Andrey Avtomonov 2026-06-09 12:44:58 +02:00
parent 1a6da14f62
commit a02fcab487
15 changed files with 303 additions and 43 deletions

View file

@ -40,6 +40,7 @@ interface BuiltMocks {
slValidator: any;
toolsetFactory: any;
logger: any;
autoCommit: boolean;
}
const buildMocks = (overrides: Partial<BuiltMocks> = {}): BuiltMocks => {
@ -111,6 +112,9 @@ const buildMocks = (overrides: Partial<BuiltMocks> = {}): BuiltMocks => {
gitService: {
revParseHead: vi.fn().mockResolvedValue('basesha'),
squashMergeIntoMain: vi.fn().mockResolvedValue({ ok: true, squashSha: 'cafebabe', touchedPaths: ['a.yaml'] }),
stageSquashMergeIntoMain: vi
.fn()
.mockResolvedValue({ ok: true, touchedPaths: ['a.yaml'], stagedTree: 'deadbeeftree' }),
},
lockingService: {
withLock: vi.fn().mockImplementation((_key: string, fn: () => Promise<unknown>) => fn()),
@ -134,6 +138,7 @@ const buildMocks = (overrides: Partial<BuiltMocks> = {}): BuiltMocks => {
}),
},
logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
autoCommit: true,
};
return { ...defaults, ...overrides };
@ -151,6 +156,7 @@ const buildService = (mocks: BuiltMocks): MemoryAgentService =>
llm: {
memoryIngestionModel: mocks.appSettings.settings.llm.memoryIngestionModel,
},
autoCommit: mocks.autoCommit,
},
promptService: mocks.prompt,
skillsRegistry: mocks.skillsRegistry,
@ -242,6 +248,26 @@ describe('MemoryAgentService.ingest — session-branch orchestration', () => {
expect(result.commitHash).toBe('cafebabe');
});
it('with auto_commit disabled, stages the session on main without committing or enqueuing a note', async () => {
const mocks = buildMocks({ autoCommit: false });
const svc = buildService(mocks);
const result = await svc.ingest(baseInput);
// Applied to main via the staging path, never the committing path.
expect(mocks.gitService.stageSquashMergeIntoMain).toHaveBeenCalledWith('session/chat-1');
expect(mocks.gitService.squashMergeIntoMain).not.toHaveBeenCalled();
// No commit means no commit-message enhancement job.
expect(mocks.configService.enqueueCommitMessageJobForExternalCommit).not.toHaveBeenCalled();
// The session still applied successfully; there is just no commit hash.
expect(result.commitHash).toBeNull();
expect(mocks.sessionWorktreeService.cleanup).toHaveBeenCalledWith(
expect.objectContaining({ chatId: 'chat-1' }),
'success',
expect.any(Object),
);
});
it('normalizes load_skill output to markdown while preserving structured payload', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-memory-skill-'));
const skillDir = join(tempDir, 'memory_agent');

View file

@ -193,6 +193,7 @@ describe('MemoryAgentService.reconcileCrossRefs', () => {
knowledge: { userScopedKnowledgeEnabled: false },
slValidation: { probeRowCount: 1 },
llm: { memoryIngestionModel: 'test-model' },
autoCommit: true,
},
promptService: undefined as never,
skillsRegistry: undefined as never,
@ -369,6 +370,7 @@ describe('MemoryAgentService.gateRevertInvalidSources (J3)', () => {
knowledge: { userScopedKnowledgeEnabled: false },
slValidation: { probeRowCount: 1 },
llm: { memoryIngestionModel: 'test-model' },
autoCommit: true,
},
promptService: undefined as never,
skillsRegistry: undefined as never,