Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- A Swift reference to a type now links to the type's own declaration, not to a file that extends it. An `extension View { … }` or `extension Text { … }` used to stand in for SwiftUI's type, so every view, every `Text("…")` and every `Color.red` in an app linked to whichever file happened to extend it. Those files topped the most-depended-on lists and their impact reached the whole app. A bare name like `@State`, `@Test` or `Result<…>` no longer links to some other type's nested `State` or `Result`, and a qualified name like `Build.Id` links to the `Id` it names. Methods declared in an extension of an SDK type still resolve on the types that conform to it, and a protocol's methods declared in any of its extensions now connect to each conforming type's own implementation. Re-index Swift projects after upgrading.
- A Vapor route now links to the handler it names. `use: SearchController.show` used to link to whichever controller's `show` came first, so routes with a common handler name, like `show`, `index` or `get`, pointed at another endpoint's code in callers, impact and `codegraph_explore` answers. Nested types like `API.PackageController.get` and handlers declared in an extension of the controller now resolve too, and `use: self.index` resolves to the collection's own `index`. Re-index Vapor projects after upgrading.
- A PHP call written without a receiver, such as `redirect($url)`, `view('books.show')`, `auth()` or `basename($path)`, is a function call, and no longer links to a same-named method, field or class elsewhere in the project. These wrong links showed up in callers, impact and `codegraph_explore` answers wherever a Laravel helper or PHP built-in shared its name with a project member. Re-index PHP projects after upgrading.
- The Claude Code prompt hook no longer runs on the messages Claude Code uses to hand a subagent's report back to the main session. Before, such a long report could keep the hook busy past Claude Code's 30-second hook timeout and inject context unrelated to what you asked. (#2184)

## [1.6.1] - 2026-09-29

Expand Down
25 changes: 24 additions & 1 deletion __tests__/frontload-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'node:child_process';
import { CodeGraph } from '../src';
import { planFrontload, isTaskNotification, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory';
import { planFrontload, isTaskNotification, isAgentMessage, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory';

// Make the built-in exports configurable so HOME can point at a real temp
// fixture without changing the process environment or the user's home files.
Expand Down Expand Up @@ -374,6 +374,12 @@ export class OrderStateMachine {
}
});

it('stays silent on a subagent hand-back envelope, even one that names indexed symbols (#2184)', () => {
const report = 'how does OrderStateMachine work? submitOrder() calls into the state machine.';
expect(hook(report)).toContain('Structural context from CodeGraph');
expect(hook(`<agent-message from="agent-7f3e">\n[Subagent hand-back] ${report}\n</agent-message>`)).toBe('');
});

it('uses MEDIUM for indexed prose segments without a strong keyword or verified token', () => {
for (const prompt of ['como state machine?', 'wie state machine?']) {
const output = hook(prompt);
Expand Down Expand Up @@ -471,3 +477,20 @@ describe('system task notifications (#1832)', () => {
expect(isTaskNotification('trace AuthService login')).toBe(false);
});
});

describe('subagent hand-backs (#2184)', () => {
const handBack = '<agent-message from="a1b2c3">\n[Subagent hand-back] Traced how AuthService.login calls TokenStore.save and which callers are affected.\n</agent-message>';

it('skips the complete hand-back envelope', () => {
expect(isAgentMessage(handBack)).toBe(true);
expect(isAgentMessage(` \n${handBack}\n`)).toBe(true);
expect(isAgentMessage('<agent-message>trace AuthService login flow</agent-message>')).toBe(true);
});
it('does not suppress a user question that mentions the marker', () => {
expect(isAgentMessage(`Why does ${handBack} trigger the hook?`)).toBe(false);
expect(isAgentMessage(`${handBack} Explain this.`)).toBe(false);
expect(isAgentMessage('<agent-messages>trace AuthService</agent-messages>')).toBe(false);
expect(isAgentMessage('<agent-message from="x">trace AuthService')).toBe(false);
expect(isAgentMessage('trace AuthService login')).toBe(false);
});
});
9 changes: 5 additions & 4 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory';
import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, isAgentMessage, 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';
Expand Down Expand Up @@ -1473,9 +1473,10 @@ program
let input: { prompt?: string; cwd?: string } = {};
try { input = JSON.parse(raw); } catch { return; }
const prompt = String(input.prompt || '');
// System-injected task notifications are not user prompts: exit before
// any project lookup or explore work (#1832).
if (isTaskNotification(prompt)) return;
// 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;

// Gate telemetry: how often each tier fires vs. no-ops — counter names
// only, NEVER prompt content (see TELEMETRY.md). This is the data that
Expand Down
10 changes: 10 additions & 0 deletions src/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,3 +1079,13 @@ export function validateDirectory(projectRoot: string): {
export function isTaskNotification(prompt: string): boolean {
return /^\s*<task-notification>[\s\S]*<\/task-notification>\s*$/.test(prompt);
}

/**
* Claude Code hands a subagent's report back to the parent session as a
* `<agent-message from="…">…</agent-message>` prompt, which UserPromptSubmit
* hooks also receive (#2184). Same rule as {@link isTaskNotification}: only a
* prompt that is entirely that envelope is skipped.
*/
export function isAgentMessage(prompt: string): boolean {
return /^\s*<agent-message(?:\s[^>]*)?>[\s\S]*<\/agent-message>\s*$/.test(prompt);
}