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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New Features

- `codegraph_sessions` and `codegraph sessions` take `full` / `--full` to return each hit's whole stored passage instead of a 24-token snippet, up to 16,000 bytes in all; hits past the budget keep their snippet and the answer says how many. Snippets stay the default.

- Installers can verify daemon readiness and safely hand writer ownership across build promotion and rollback through a supported runtime-control API.

- `codegraph_explore` now finds quoted prose in script strings and template text through a capped source scan, ignoring case and punctuation without requiring a re-index.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ codegraph unlock [path] # Remove a stale lock file that's blocking ind
codegraph query <search> # Search symbols (--kind, --limit, --json)
codegraph explore <query> # Relevant symbols' source + call paths in one shot (same output as the codegraph_explore MCP tool)
codegraph context <task...> # Context for a task: relevant symbols, relationships and code (--format markdown|json, --max-nodes, --no-code)
codegraph sessions <words...> # Search Claude Code, Codex, Cursor/T3, OpenCode, AGY, Devin, and Grok transcripts and commit messages for this project (--role, --since <days>, --session, --any, --json; same output as codegraph_sessions)
codegraph sessions <words...> # Search Claude Code, Codex, Cursor/T3, OpenCode, AGY, Devin, and Grok transcripts and commit messages for this project (--role, --since <days>, --session, --any, --full, --json; same output as codegraph_sessions)
codegraph node <symbol|file> # One symbol's source + callers, or read a file with line numbers (same output as codegraph_node)
codegraph files [path] # Show file structure (--format, --filter, --max-depth, --json)
codegraph callers <symbol> # Find what calls a function/method (--limit, --json)
Expand Down Expand Up @@ -816,7 +816,7 @@ When running as an MCP server, CodeGraph exposes **one tool for code** — `code
| Tool | Purpose |
|------|---------|
| `codegraph_explore` | Answer almost any question in one call — "how does X work", a flow ("how does X reach Y"), or surveying an area — returning the relevant symbols' verbatim source grouped by file, plus the call paths between them and a blast-radius summary. Surfaces dynamic-dispatch hops (callbacks, React re-render, interface→impl) grep can't follow. Name a file or symbol in the query to read its current line-numbered source, the same shape the Read tool gives you. |
| `codegraph_sessions` | Search this project's Claude Code, Codex, Cursor/T3, OpenCode, AGY, Devin, and Grok transcripts and its last 2000 git commit messages, including active sessions: prompts, replies, and compaction summaries, excluding tool traffic. Uses stemmed, BM25-ranked full-text search, stored locally in `.codegraph/sessions-v2.db` and refreshed for changed files on each call. Each hit includes its session (`claude:`, `codex:`, `cursor:`, `opencode:`, `agy:`, `devin:`, `grok:`, or `git:`), role, time, transcript path, and matching passage. Common words are dropped, and when too few passages hold every remaining word, passages holding some of them follow, marked. Harness-injected text (skill bodies, system reminders) is not indexed. Set `"sessions": false` in `codegraph.json` to opt out; `CODEGRAPH_SESSIONS_DIR` selects another Claude-format transcript directory exclusively. |
| `codegraph_sessions` | Search this project's Claude Code, Codex, Cursor/T3, OpenCode, AGY, Devin, and Grok transcripts and its last 2000 git commit messages, including active sessions: prompts, replies, and compaction summaries, excluding tool traffic. Uses stemmed, BM25-ranked full-text search, stored locally in `.codegraph/sessions-v2.db` and refreshed for changed files on each call. Each hit includes its session (`claude:`, `codex:`, `cursor:`, `opencode:`, `agy:`, `devin:`, `grok:`, or `git:`), role, time, transcript path, and matching passage; `full` returns each hit's whole stored passage instead of the snippet, up to 16,000 bytes in all, and later hits keep their snippet. Common words are dropped, and when too few passages hold every remaining word, passages holding some of them follow, marked. Harness-injected text (skill bodies, system reminders) is not indexed. Set `"sessions": false` in `codegraph.json` to opt out; `CODEGRAPH_SESSIONS_DIR` selects another Claude-format transcript directory exclusively. |

The other tools (`codegraph_node`, `codegraph_search`, `codegraph_callers`, `codegraph_callees`, `codegraph_impact`, `codegraph_files`, `codegraph_status`) stay fully functional but **unlisted by default** — everything they return already arrives inline on `codegraph_explore` (its blast-radius section, the relationship map, a symbol's body as its callee list). Re-enable any of them for the MCP surface with the `CODEGRAPH_MCP_TOOLS` environment variable (e.g. `CODEGRAPH_MCP_TOOLS=explore,node,search,callers`), or use their CLI equivalents (`codegraph node` / `query` / `callers` / `callees` / `impact` / `files` / `status`).

Expand Down
8 changes: 8 additions & 0 deletions __tests__/cli-sessions-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ describe('codegraph sessions — CLI command', () => {
expect(any.fallback).toBeUndefined();
});

it('--full prints the whole passage and --json carries it as text', () => {
const out = sessions(tempDir, transcripts, ['trailing', 'newlines', '--full']);
expect(out).toContain('> Trimming first keeps a trailing newline from failing the signature check.');
const parsed = JSON.parse(sessions(tempDir, transcripts, ['trailing', 'newlines', '--full', '--json']));
expect(parsed.hits[0].text).toBe('Trimming first keeps a trailing newline from failing the signature check.');
expect(parsed.fullCut).toBe(0);
});

it('a project without transcripts gets guidance, not an error', () => {
const out = sessions(tempDir, undefined, ['anything']);
expect(out).toMatch(/No agent-session transcripts to index/);
Expand Down
52 changes: 52 additions & 0 deletions __tests__/sessions-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
querySessions,
NoSessionsError,
formatSessionHits,
FULL_PASSAGE_BUDGET,
} from '../src/sessions';
import { clearProjectConfigCache } from '../src/project-config';
import { isInjectedDoc, slashCommandText, splitPassages, indexableDocs } from '../src/sessions/noise';
Expand Down Expand Up @@ -1225,3 +1226,54 @@ describe('repeat scans', () => {
expect(unavailable).toEqual([path.join(root, 'brain') + path.sep]);
});
});

describe('whole passages (full)', () => {
const store = (texts: string[]): string => {
const project = fixtureDir();
fs.mkdirSync(path.join(project, '.codegraph'));
const transcripts = fixtureDir();
writeJsonl(path.join(transcripts, 's1.jsonl'), texts.map((t) => user(t)), 1_700_000_000);
process.env.CODEGRAPH_SESSIONS_DIR = transcripts;
return project;
};

it('returns the stored passage beside the snippet only when asked, in the printed answer too', () => {
const tail = ' the second paragraph line explains the retry budget in detail and names the limit of three attempts.';
const project = store([`Decision: keep write-time dedupe.\n\nThe table lists:\n| a | b |\n|---|---|${tail}`]);
const plain = querySessions(project, 'write-time dedupe');
expect(plain.hits[0]!.text).toBeUndefined();
expect(plain.fullCut).toBeUndefined();
const full = querySessions(project, 'write-time dedupe', { full: true });
expect(full.hits[0]!.text).toContain('| a | b |\n|---|---|');
expect(full.hits[0]!.snippet).toBe(plain.hits[0]!.snippet);
expect(full.fullCut).toBe(0);
const printed = formatSessionHits('write-time dedupe', full);
expect(printed).toContain('> | a | b |\n> |---|---|');
expect(printed).toContain('> Decision: keep write-time dedupe.');
expect(formatSessionHits('write-time dedupe', plain)).not.toContain('> | a | b |');
});

it('spends one byte budget in rank order and says how many hits fell back to the snippet', () => {
const body = (i: number) => `Note ${i} on budgetword: ${`alpha${i} beta${i} gamma${i} `.repeat(160)}`.slice(0, 3000);
const project = store(Array.from({ length: 7 }, (_, i) => body(i)));
const result = querySessions(project, 'budgetword', { full: true });
expect(result.hits).toHaveLength(7);
const whole = result.hits.filter((h) => h.text !== undefined);
const bytes = whole.reduce((n, h) => n + Buffer.byteLength(h.text!), 0);
expect(bytes).toBeLessThanOrEqual(FULL_PASSAGE_BUDGET);
expect(whole.length).toBeGreaterThan(0);
expect(whole.length).toBeLessThan(7);
expect(result.fullCut).toBe(7 - whole.length);
// Whole passages are a prefix of the ranking: a smaller later hit never jumps the queue.
expect(result.hits.slice(0, whole.length).every((h) => h.text !== undefined)).toBe(true);
const printed = formatSessionHits('budgetword', result);
expect(printed).toContain(`Whole passages are limited to 16,000 bytes in all: the last ${result.fullCut} hits show only the matching snippet.`);
});

it('keeps the role filter and the one-hit wording', () => {
const project = store([`Decision: keep write-time dedupe. ${'x'.repeat(2000)}`.replace(/x/g, 'padding ')]);
const hit = querySessions(project, 'write-time dedupe', { full: true, role: 'user' });
expect(hit.hits).toHaveLength(1);
expect(querySessions(project, 'write-time dedupe', { full: true, role: 'assistant' }).hits).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ codegraph unlock [path] # Remove a stale lock file that's blocking ind
codegraph query <search> # Search symbols (--kind, --limit, --json)
codegraph explore <query> # Relevant symbols' source + call paths in one shot (same output as the codegraph_explore MCP tool)
codegraph context <task...> # Context for a task: relevant symbols, relationships and code (--format markdown|json, --max-nodes, --no-code)
codegraph sessions <words...> # Search this project's earlier agent sessions (--role, --since <days>, --session, --any, --json; same output as codegraph_sessions)
codegraph sessions <words...> # Search this project's earlier agent sessions (--role, --since <days>, --session, --any, --full, --json; same output as codegraph_sessions)
codegraph node <symbol|file> # One symbol's source + callers, or read a file with line numbers (same output as codegraph_node)
codegraph files [path] # Show file structure (--format, --filter, --pattern, --max-depth, --json)
codegraph callers <symbol> # Find what calls a function/method (--limit, --json)
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/reference/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ By default the server exposes `codegraph_explore` for code and `codegraph_sessio

`codegraph_explore` It's Read-equivalent: give it a natural-language question or a bag of symbol and file names, and it returns the **verbatim, line-numbered source** of the relevant symbols grouped by file — the same shape the `Read` tool gives you — plus the call paths between them (including dynamic-dispatch hops like callbacks, React re-render, and JSX children that grep can't follow) and a blast-radius summary of what depends on them. One call usually answers the whole question. When a complete scan of indexed source finishes without skipped files, the summary lists requested names it could not find, helping catch a guessed name.

`codegraph_sessions` searches this project's earlier agent sessions (Claude Code, Codex, Cursor/T3, OpenCode, AGY, Devin and Grok transcripts, plus git commit messages) for what a previous session asked, decided or tried. It answers "why is X like this" questions, which the code graph cannot. Set `"sessions": false` in `codegraph.json` to turn it off.
`codegraph_sessions` searches this project's earlier agent sessions (Claude Code, Codex, Cursor/T3, OpenCode, AGY, Devin and Grok transcripts, plus git commit messages) for what a previous session asked, decided or tried. It answers "why is X like this" questions, which the code graph cannot. A hit shows a short snippet; pass `full` (CLI: `--full`) to get each hit's whole stored passage instead, up to 16,000 bytes in all, with later hits keeping their snippet. Set `"sessions": false` in `codegraph.json` to turn it off.

T3-hosted provider transcripts can appear in session search; CodeGraph does not read the T3 database. The source includes a disabled-by-default offline Codex metadata prototype that associates caller-supplied T3 titles and thread links with existing hits. It has no CLI or MCP caller, adds no searchable prose and leaves provider titles, keys and ranking unchanged.

Expand Down
4 changes: 3 additions & 1 deletion src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1502,8 +1502,9 @@ program
.option('--since <days>', 'Only docs from the last N days')
.option('--session <id-prefix>', 'Only one session (id prefix)')
.option('--any', 'OR the words instead of requiring all of them')
.option('--full', 'Show each hit\'s whole passage (16,000 bytes in all) instead of a snippet')
.option('-j, --json', 'Output as JSON')
.action(async (words: string[], options: { path?: string; limit?: string; role?: string; since?: string; session?: string; any?: boolean; json?: boolean }) => {
.action(async (words: string[], options: { path?: string; limit?: string; role?: string; since?: string; session?: string; any?: boolean; full?: boolean; json?: boolean }) => {
const projectPath = resolveProjectPath(options.path);
try {
if (!isInitialized(projectPath)) {
Expand All @@ -1521,6 +1522,7 @@ program
sinceIso: sinceDays > 0 ? new Date(Date.now() - sinceDays * 86_400_000).toISOString() : undefined,
session: options.session,
any: options.any,
full: options.full,
});
} catch (err) {
if (err instanceof NoSessionsError) {
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/server-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ against dozens of greps and reads.
- **"How does X reach/become Y? / the flow / the path from X to Y"** → \`codegraph_explore\`, naming the symbols that span the flow (e.g. \`mutateElement renderScene\`) — it surfaces the call path among them, riding dynamic-dispatch hops, and returns their source.
- **Reading or editing a file/symbol you can name** → put its name or file path in the \`codegraph_explore\` query — it returns that current line-numbered source (safe to \`Edit\` from) with the call path and blast radius attached, so you don't Read it separately. For an overloaded name it returns every matching definition's body in one call.
- Name what you need as precisely as you can — a receiver type with its method, a constant or variable, a callable, or a file path. Named items are funded first within the output cap; an oversized body comes back as a bounded excerpt whose gap markers name what was left out. Treat returned ranges as already Read; for trimmed parts, query the names in the gap marker.
- **"Why is this like this? What did the last session decide / try / get told about X?"** → \`codegraph_sessions\` with a few words. It searches the prose of this project's earlier Claude Code, Codex, Cursor/T3, OpenCode, AGY, and Devin sessions (prompts, replies, compaction summaries — stemmed, ranked) and names the session each hit came from (\`claude:\`, \`codex:\`, \`cursor:\`, \`opencode:\`, \`agy:\`, \`devin:\`). History and rationale live there, not in the code.
- **"Why is this like this? What did the last session decide / try / get told about X?"** → \`codegraph_sessions\` with a few words. It searches the prose of this project's earlier Claude Code, Codex, Cursor/T3, OpenCode, AGY, and Devin sessions (prompts, replies, compaction summaries — stemmed, ranked) and names the session each hit came from (\`claude:\`, \`codex:\`, \`cursor:\`, \`opencode:\`, \`agy:\`, \`devin:\`). History and rationale live there, not in the code. A hit is a short snippet; pass \`full: true\` to read each hit's whole passage in the same call (up to 16,000 bytes in all).
- **Need more?** Call \`codegraph_explore\` again with more specific names — treat the source it returns as already Read. Suggested call counts are advisory only, NOT a quota; extra calls are never rejected or rate-limited.
- Qualified symbol names accept dots, \`::\`, or slashes, including containers whose names contain dots (for example, \`AppWeb.Format.group\`).
- Named-symbol call paths require exact matches; partial or mistyped names are never silently substituted as flow endpoints. If a graph query reports a missing symbol with did-you-mean suggestions, query the suggested name explicitly.
Expand Down
6 changes: 6 additions & 0 deletions src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,11 @@ export const tools: ToolDefinition[] = [
description: 'OR the words instead of requiring all of them (default: false).',
default: false,
},
full: {
type: 'boolean',
description: 'Return each hit\'s whole passage instead of a short snippet, up to 16,000 bytes in all; later hits keep their snippet (default: false).',
default: false,
},
projectPath: projectPathProperty,
},
required: ['query'],
Expand Down Expand Up @@ -4580,6 +4585,7 @@ export class ToolHandler {
sinceIso: sinceDays > 0 ? new Date(Date.now() - sinceDays * 86_400_000).toISOString() : undefined,
session,
any: args.any === true,
full: args.full === true,
});
return this.textResult(formatSessionHits(query, result));
} catch (err) {
Expand Down
Loading
Loading