mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 04:19:33 +08:00
fix(prompt-hook): desktop reminders and other sessions' messages no longer drive the hook (#2798)
Claude Code's desktop app puts <system-reminder> blocks (which local sessions are linked, and similar status) in front of the prompt it submits, and UserPromptSubmit hooks receive them inside `prompt`. The hook gated, picked the project and explored on the whole string, so the reminder's words drove it: a linked session titled after its work, like "trace-order-flow", carries structural keywords, so every turn, a bare "yes" included, got a full explore of the reminder. A reminder in front of a task notification or a subagent hand-back also defeated the start-anchored envelope checks, and a message from another session (a <cross-session-message from=...> envelope) was never skipped at all. The hook now removes every complete <system-reminder> block before the envelope checks, the gate, project selection and explore, returns early when nothing is left, and skips a prompt that is entirely a <cross-session-message> envelope, as it already did for task notifications and subagent hand-backs. The scan stops at the last closing tag and the attribute run stops at `<`, so a pasted prompt full of unclosed tags stays linear (520 KB in under 1 ms; the `[^>]*` form took 83 ms on a tenth of that and grows quadratically). Validation: - Prompt shapes come from the Claude Code desktop transcripts on this machine (314 transcripts): reminders always lead, one to four of them, separated by one or two newlines, and cross-session messages follow one to four reminders. - frontload-hook.test.ts: 15 new tests (8 spawn the built hook with Claude Code-shaped payloads) fail on main and pass with the fix; the file's 48 other tests pass on both. - Built CLI, private HOME, 13 payloads: on main every prompt behind a reminder got a full explore of the reminder (2.1 to 5.0 KB); with the fix the 8 that are not user input inject nothing and exit in about 200 ms instead of 680 ms, and a question behind reminders injects byte-for-byte what the same question alone does. Issue #2635. Contributor PR #2642 (yang0228) was read for guidance. Fixes #2635 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
407efb1c65
commit
c1744e7e76
@@ -53,7 +53,7 @@ try {
|
||||
import { Command } from 'commander';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, isAgentMessage, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory';
|
||||
import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, isAgentMessage, isCrossSessionMessage, stripSystemReminders, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory';
|
||||
import { extractProseCandidates } from '../search/identifier-segments';
|
||||
import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree';
|
||||
import { createShimmerProgress } from '../ui/shimmer-progress';
|
||||
@@ -1512,11 +1512,14 @@ program
|
||||
|
||||
let input: { prompt?: string; cwd?: string } = {};
|
||||
try { input = JSON.parse(raw); } catch { return; }
|
||||
const prompt = String(input.prompt || '');
|
||||
// System-injected task notifications and subagent hand-backs are not
|
||||
// user prompts: exit before any project lookup or explore work (#1832,
|
||||
// #2184).
|
||||
if (isTaskNotification(prompt) || isAgentMessage(prompt)) return;
|
||||
// The desktop app's <system-reminder> blocks are host status, not what
|
||||
// the user typed: gate, pick the project and explore on the rest
|
||||
// (#2635).
|
||||
const prompt = stripSystemReminders(String(input.prompt || ''));
|
||||
// System-injected task notifications, subagent hand-backs and messages
|
||||
// from other sessions are not user prompts, and neither is nothing:
|
||||
// exit before any project lookup or explore work (#1832, #2184, #2635).
|
||||
if (!prompt || isTaskNotification(prompt) || isAgentMessage(prompt) || isCrossSessionMessage(prompt)) return;
|
||||
|
||||
// Gate telemetry: how often each tier fires vs. no-ops — counter names
|
||||
// only, NEVER prompt content (see TELEMETRY.md). This is the data that
|
||||
|
||||
@@ -1125,3 +1125,41 @@ export function isTaskNotification(prompt: string): boolean {
|
||||
export function isAgentMessage(prompt: string): boolean {
|
||||
return /^\s*<agent-message(?:\s[^>]*)?>[\s\S]*<\/agent-message>\s*$/.test(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* A message another local Claude Code session sends to this one arrives as a
|
||||
* `<cross-session-message from="…">…</cross-session-message>` prompt, which
|
||||
* UserPromptSubmit hooks also receive (#2635). Same rule as
|
||||
* {@link isTaskNotification}: only a prompt that is entirely that envelope is
|
||||
* skipped.
|
||||
*/
|
||||
export function isCrossSessionMessage(prompt: string): boolean {
|
||||
return /^\s*<cross-session-message(?:\s[^>]*)?>[\s\S]*<\/cross-session-message>\s*$/.test(prompt);
|
||||
}
|
||||
|
||||
const SYSTEM_REMINDER_CLOSE = '</system-reminder>';
|
||||
/** One complete reminder block. The attribute run stops at `<`, so a tag left
|
||||
* open in pasted text can't make the scan quadratic. */
|
||||
const SYSTEM_REMINDER_BLOCK = /<system-reminder(?:\s[^<>]*)?>[\s\S]*?<\/system-reminder>/g;
|
||||
|
||||
/**
|
||||
* The prompt the user typed, without the `<system-reminder>…</system-reminder>`
|
||||
* blocks Claude Code's desktop app puts in front of it, such as which other
|
||||
* local sessions are linked. UserPromptSubmit hooks receive them as
|
||||
* part of the prompt (#2635), but they are the host's status, not the user's
|
||||
* words: gated and explored on, they made every turn, a bare "yes" included,
|
||||
* inject context about the reminder, and they hid a task notification, a
|
||||
* subagent hand-back or a cross-session message behind them from
|
||||
* {@link isTaskNotification}, {@link isAgentMessage} and
|
||||
* {@link isCrossSessionMessage}. Every complete block is removed and the rest
|
||||
* trimmed; a `<system-reminder>` the user only mentions, with no closing tag,
|
||||
* stays.
|
||||
*/
|
||||
export function stripSystemReminders(prompt: string): string {
|
||||
// Only the text up to the last closing tag can hold a complete block, and
|
||||
// bounding the scan there keeps an unclosed tag from rescanning the tail.
|
||||
const end = prompt.lastIndexOf(SYSTEM_REMINDER_CLOSE);
|
||||
if (end < 0) return prompt.trim();
|
||||
const head = prompt.slice(0, end + SYSTEM_REMINDER_CLOSE.length);
|
||||
return (head.replace(SYSTEM_REMINDER_BLOCK, '\n') + prompt.slice(head.length)).trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user