From 84d329b24bb0e211bb2d60fb11477d8cea381677 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Thu, 25 Jun 2026 17:15:15 -0700 Subject: [PATCH] fix(memory): fire SL capture signal for SQL-bearing external ingest The SQL-aggregate signal gated on userMessage.length >= 100, but `ktx ingest --file` sets a fixed boilerplate user message and carries the artifact in the assistant message. SQL-heavy files therefore never force-loaded the SL capture skill and defaulted to wiki capture. External ingest now treats the deliberate submission itself as the substantive signal, so verified SQL fires the SL signal regardless of the boilerplate user message. Co-Authored-By: Claude Opus 4.8 --- .../cli/src/context/memory/capture-signals.ts | 12 ++++++++- .../memory/memory-agent.service.test.ts | 25 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) 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',