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
65 changes: 63 additions & 2 deletions src/application/handlers/PromptSubmitHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,91 @@
*
* Handles the prompt:submit event by loading relevant memories
* and formatting them as context for Claude Code.
*
* When intent extraction is enabled, extracts searchable keywords
* from the prompt and queries memories with those keywords.
* Falls back to loading recent memories if extraction is skipped
* or no keywords are found.
*/

import type { IPromptSubmitHandler } from '../interfaces/IPromptSubmitHandler';
import type { IPromptSubmitEvent } from '../../domain/events/HookEvents';
import type { IEventResult } from '../../domain/interfaces/IEventResult';
import type { IMemoryContextLoader } from '../../domain/interfaces/IMemoryContextLoader';
import type { IMemoryContextLoader, IMemoryContextResult } from '../../domain/interfaces/IMemoryContextLoader';
import type { IContextFormatter } from '../../domain/interfaces/IContextFormatter';
import type { ILogger } from '../../domain/interfaces/ILogger';
import type { IHookConfigLoader } from '../../domain/interfaces/IHookConfigLoader';
import type { IIntentExtractor } from '../../domain/interfaces/IIntentExtractor';

export class PromptSubmitHandler implements IPromptSubmitHandler {
constructor(
private readonly memoryContextLoader: IMemoryContextLoader,
private readonly contextFormatter: IContextFormatter,
private readonly logger?: ILogger,
private readonly hookConfigLoader?: IHookConfigLoader,
private readonly intentExtractor?: IIntentExtractor | null,
) {}

async handle(event: IPromptSubmitEvent): Promise<IEventResult> {
try {
this.logger?.info('Prompt submit handler invoked', {
sessionId: event.sessionId,
cwd: event.cwd,
hasPrompt: !!event.prompt,
});

const result = this.memoryContextLoader.load({ cwd: event.cwd });
// Load config
const config = this.hookConfigLoader?.loadConfig(event.cwd);
const promptConfig = config?.hooks.promptSubmit ?? {
surfaceContext: true,
extractIntent: false,
memoryLimit: 20,
minWords: 5,
intentTimeout: 3000,
Comment on lines +45 to +46
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The fallback promptConfig includes minWords and intentTimeout, but those values are never applied (they aren’t passed to IntentExtractor and aren’t used in the handler). As a result, the new config fields have no effect. Either wire these settings into the extraction flow (e.g., pass per-call options / enforce timeout & minWords in the handler) or remove them from the config shape to avoid misleading configuration.

Suggested change
minWords: 5,
intentTimeout: 3000,

Copilot uses AI. Check for mistakes.
};

// Early exit if context surfacing is disabled
if (!promptConfig.surfaceContext) {
this.logger?.debug('Context surfacing disabled');
return {
handler: 'PromptSubmitHandler',
success: true,
output: '',
};
}

// Try intent extraction if enabled
let result: IMemoryContextResult;

if (promptConfig.extractIntent && this.intentExtractor && event.prompt) {
const intentResult = await this.intentExtractor.extract({ prompt: event.prompt });

if (!intentResult.skipped && intentResult.intent) {
this.logger?.debug('Intent extracted, querying with keywords', {
intent: intentResult.intent,
});
result = this.memoryContextLoader.loadWithQuery(
intentResult.intent,
promptConfig.memoryLimit,
event.cwd,
);
} else {
this.logger?.debug('Intent extraction skipped', {
reason: intentResult.reason,
});
// Fall back to loading recent memories
result = this.memoryContextLoader.load({
cwd: event.cwd,
limit: promptConfig.memoryLimit,
});
}
} else {
// No intent extraction, load recent memories
result = this.memoryContextLoader.load({
cwd: event.cwd,
limit: promptConfig.memoryLimit,
});
}
Comment on lines +39 to +90
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Configured minWords/intentTimeout are ignored.
These values are loaded but never applied, so user config has no effect on extraction gating or timeout behavior. Either gate extraction in the handler and/or pass per-repo overrides into the extractor.

🔧 Example fix to honor minWords in the handler
-      if (promptConfig.extractIntent && this.intentExtractor && event.prompt) {
-        const intentResult = await this.intentExtractor.extract({ prompt: event.prompt });
+      if (promptConfig.extractIntent && this.intentExtractor && event.prompt) {
+        const wordCount = event.prompt.trim().split(/\s+/).filter(Boolean).length;
+        if (wordCount < promptConfig.minWords) {
+          this.logger?.debug('Intent extraction skipped: too short', { wordCount });
+          result = this.memoryContextLoader.load({
+            cwd: event.cwd,
+            limit: promptConfig.memoryLimit,
+          });
+        } else {
+          const intentResult = await this.intentExtractor.extract({ prompt: event.prompt });
 
-        if (!intentResult.skipped && intentResult.intent) {
+          if (!intentResult.skipped && intentResult.intent) {
             this.logger?.debug('Intent extracted, querying with keywords', {
               intent: intentResult.intent,
             });
             result = this.memoryContextLoader.loadWithQuery(
               intentResult.intent,
               promptConfig.memoryLimit,
               event.cwd,
             );
-        } else {
+          } else {
             this.logger?.debug('Intent extraction skipped', {
               reason: intentResult.reason,
             });
             // Fall back to loading recent memories
             result = this.memoryContextLoader.load({
               cwd: event.cwd,
               limit: promptConfig.memoryLimit,
             });
-        }
+          }
+        }
       } else {

Note: intentTimeout still needs wiring (e.g., extend IIntentExtractorInput with overrides or inject a repo-scoped extractor instance).

🤖 Prompt for AI Agents
In `@src/application/handlers/PromptSubmitHandler.ts` around lines 39 - 90, The
handler currently ignores promptConfig.minWords and promptConfig.intentTimeout;
update PromptSubmitHandler so intent extraction only runs when
promptConfig.extractIntent is true AND (event.prompt?.split(/\s+/).length ?? 0)
>= promptConfig.minWords, and enforce intentTimeout by passing the timeout into
the extractor call (extend IIntentExtractorInput to accept intentTimeout or wrap
intentExtractor.extract(...) in a Promise.race with a timeout) so extract
respects per-repo overrides; ensure branches that call
intentExtractor.extract(...) and the fallback
memoryContextLoader.load/loadWithQuery remain consistent and still set result:
IMemoryContextResult, and pass promptConfig.memoryLimit and event.cwd unchanged
to memoryContextLoader.loadWithQuery/load.


if (result.memories.length === 0) {
this.logger?.debug('No memories found for prompt context');
Expand Down
2 changes: 2 additions & 0 deletions src/infrastructure/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export function createContainer(options?: IContainerOptions): AwilixContainer<IC
container.cradle.memoryContextLoader,
container.cradle.contextFormatter,
container.cradle.logger,
container.cradle.hookConfigLoader,
container.cradle.intentExtractor,
));
bus.on('git:commit', new PostCommitHandler(
container.cradle.notesService,
Expand Down
19 changes: 3 additions & 16 deletions src/infrastructure/llm/IntentExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
IIntentExtractorResult,
} from '../../domain/interfaces/IIntentExtractor';
import type { ILogger } from '../../domain/interfaces/ILogger';
import { LLMError } from '../../domain/errors/LLMError';

export interface IIntentExtractorOptions {
/** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var. */
Expand Down Expand Up @@ -46,7 +45,7 @@ export class IntentExtractor implements IIntentExtractor {
constructor(options: IIntentExtractorOptions) {
const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
throw new LLMError('Anthropic API key required for IntentExtractor');
throw new Error('Anthropic API key required for IntentExtractor');
}

this.client = new Anthropic({ apiKey });
Expand Down Expand Up @@ -98,27 +97,15 @@ export class IntentExtractor implements IIntentExtractor {

/**
* Call LLM with timeout enforcement.
* Clears timeout after Promise.race to avoid lingering timers.
*/
private async extractWithTimeout(prompt: string): Promise<string | null> {
let timeoutId: ReturnType<typeof setTimeout> | undefined;

const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(
() => reject(new LLMError('Intent extraction timed out')),
this.timeout,
);
setTimeout(() => reject(new Error('Intent extraction timed out')), this.timeout);
});

const extractPromise = this.callLLM(prompt);

try {
return await Promise.race([extractPromise, timeoutPromise]);
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
}
return Promise.race([extractPromise, timeoutPromise]);
}

/**
Expand Down