diff --git a/packages/cli/src/context/memory/capture-signals.ts b/packages/cli/src/context/memory/capture-signals.ts index 86a123e0..5c84c589 100644 --- a/packages/cli/src/context/memory/capture-signals.ts +++ b/packages/cli/src/context/memory/capture-signals.ts @@ -16,8 +16,18 @@ export function detectCaptureSignals(input: MemoryAgentInput): CaptureSignals { const assistantMessage = input.assistantMessage?.trim() ?? ''; const reasons: string[] = []; + // The user-message length gate is a research-path proxy for "the turn was substantive + // enough to promote the assistant's SQL." External ingest carries a fixed boilerplate + // user message and keeps the real content in the assistant message, so the proxy never + // holds there - treat the deliberate submission itself as the substantive signal. + const deliberateSubmission = input.sourceType === 'external_ingest'; + let sl = false; - if (assistantMessage && SQL_AGGREGATE_PATTERN.test(assistantMessage) && userMessage.length >= 100) { + if ( + assistantMessage && + SQL_AGGREGATE_PATTERN.test(assistantMessage) && + (deliberateSubmission || userMessage.length >= 100) + ) { sl = true; reasons.push('sql aggregate in assistant message'); } diff --git a/packages/cli/test/context/memory/memory-agent.service.test.ts b/packages/cli/test/context/memory/memory-agent.service.test.ts index 36bae425..5fdebe9b 100644 --- a/packages/cli/test/context/memory/memory-agent.service.test.ts +++ b/packages/cli/test/context/memory/memory-agent.service.test.ts @@ -31,6 +31,31 @@ describe('MemoryAgentService.detectCaptureSignals', () => { expect(result.sl).toBe(false); }); + it('fires sl on assistant SQL for external_ingest despite a short boilerplate user message', () => { + // `ktx ingest --file` keeps the content in assistantMessage and uses a fixed boilerplate + // user message, so the substantive-turn gate must come from the deliberate submission itself. + const result = detectCaptureSignals({ + userId: 'u', + chatId: 'c', + userMessage: 'Ingest external text artifact "verified_pairs.md" into ktx memory.', + assistantMessage: 'Q: total claims completed?\nSQL: SELECT COUNT(DISTINCT Claim_No_Date) FROM dimJob_Details', + sourceType: 'external_ingest', + }); + expect(result.sl).toBe(true); + expect(result.reasons).toContain('sql aggregate in assistant message'); + }); + + it('keeps the substantive-turn gate for non-ingest source types with a short user message', () => { + const result = detectCaptureSignals({ + userId: 'u', + chatId: 'c', + userMessage: 'Ingest external text artifact "verified_pairs.md" into ktx memory.', + assistantMessage: 'SELECT COUNT(DISTINCT Claim_No_Date) FROM dimJob_Details', + sourceType: 'research', + }); + expect(result.sl).toBe(false); + }); + it('fires sl on definition keywords in user message regardless of length', () => { const result = detectCaptureSignals({ userId: 'u',