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: 1 addition & 1 deletion src/claude-code-patterns/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@ export type { AgentSearchService } from '../services/agentSearchIntegration.js';

export { buildSystemFrameOptimized } from '../services/systemFrameBuilder.js';
export { packContextReactive } from '../graph/packer.js';
export { assemblePipelineContextWithMemory } from '../services/contextAssembler.js';
export { assemblePipelineContext as assemblePipelineContextWithMemory } from '../services/contextAssembler.js';
4 changes: 2 additions & 2 deletions src/context/PromptRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
return new Set(
prompt
.toLowerCase()
.replace(/[^a-z0-9s/_-]/g, '')
.split(/[s/_-]+/)
.replace(/[^a-z0-9\s\/_-]/g, '')
.split(/[\s\/_-]+/)
.filter(t => t.length >= 2)
);
}
Expand Down Expand Up @@ -133,6 +133,6 @@
for (const m of matches) {
lines.push(`- **${m.item.name}** — ${m.item.description} (relevance: ${m.score})`);
}
return lines.join('

Check failure on line 136 in src/context/PromptRouter.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unterminated string literal.
');

Check failure on line 137 in src/context/PromptRouter.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unterminated string literal.

Check failure on line 137 in src/context/PromptRouter.ts

View workflow job for this annotation

GitHub Actions / build-and-test

',' expected.
}

Check failure on line 138 in src/context/PromptRouter.ts

View workflow job for this annotation

GitHub Actions / build-and-test

',' expected.
4 changes: 2 additions & 2 deletions src/context/ReactiveCompaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ export class ReactiveCompaction {
}
}
} else if (signal.ratio >= this.config.pressureThreshold) {
// Pressure: downgrade bottom half by one level
// Pressure: downgrade least-relevant half by one level
const half = Math.ceil(sorted.length / 2);
for (let i = half; i < sorted.length; i++) {
for (let i = 0; i < half; i++) {
const f = sorted[i];
const next = this.nextDepth(f.depth);
if (next) adjustments.push({ fileId: f.fileId, currentDepth: f.depth, newDepth: next, reason: 'token_pressure' });
Expand Down
9 changes: 8 additions & 1 deletion src/graph/reactivePackerWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,14 @@
const baseTokens = oldRatio > 0 ? item.tokens / oldRatio : item.tokens;
const newTokens = Math.max(1, Math.ceil(baseTokens * newRatio));
newTotal += newTokens;
return { ...item, depth: newDepthNumeric, tokens: newTokens };
// Truncate content proportionally to new depth
const contentStr = typeof item.content === 'string' ? item.content : '';
const truncatedLength = oldRatio > 0 ? Math.ceil(contentStr.length * (newRatio / oldRatio)) : contentStr.length;
const truncatedContent = contentStr.length > truncatedLength
? contentStr.slice(0, truncatedLength) + '

Check failure on line 134 in src/graph/reactivePackerWrapper.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unterminated string literal.
[... truncated by reactive compaction]'

Check failure on line 135 in src/graph/reactivePackerWrapper.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Expression expected.
Comment on lines +133 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prevent truncated marker from increasing content size

The new truncation path appends a fixed "[... truncated by reactive compaction]" suffix without accounting for its length, so short files can become longer after “compaction” (for example, truncating a 5-character string to 1 character still yields a ~40-character result). In that case newTokens is reduced while the actual emitted content grows, which can silently violate token-budget assumptions and overflow downstream context windows.

Useful? React with 👍 / 👎.

: contentStr;
return { ...item, depth: newDepthNumeric, tokens: newTokens, content: truncatedContent };
}
newTotal += item.tokens;
return item;
Expand Down
2 changes: 1 addition & 1 deletion src/services/agentSearchIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function createAgentSearchService(
agents: AgentConfig[],
knowledge: KnowledgeSource[] = [],
): AgentSearchService {
const hash = JSON.stringify(agents.map(a => a.id).sort());
const hash = JSON.stringify(agents.map(a => [a.id, a.description, a.role, ...(a.tags ?? [])].join('|')).sort());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize agent hash fields without delimiter collisions

Building the cache key with [...].join('|') is ambiguous for free-form strings, so different agent metadata can collapse to the same hash whenever a field contains | (e.g., descriptions or tags), and the service will incorrectly reuse a stale _searchInstance instead of rebuilding the index. Using structured serialization per agent (rather than delimiter-joined text) avoids these false cache hits.

Useful? React with 👍 / 👎.

if (!_searchInstance || hash !== _lastIndexHash) {
_searchInstance = new AgentSearch(agents, knowledge);
_lastIndexHash = hash;
Expand Down
47 changes: 28 additions & 19 deletions tests/integration/pipeline-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ Step 2: Respond',
expect(result.text).toContain('User prefers concise answers');

// Verify prompt metadata
expect(result.prompt.staticTokens).toBeGreaterThan(0);
expect(result.prompt.dynamicTokens).toBeGreaterThan(0);
expect(result.prompt.staticTokenEstimate).toBeGreaterThan(0);
expect(result.prompt.dynamicTokenEstimate).toBeGreaterThan(0);
});

it('handles minimal input without crashing', () => {
Expand All @@ -56,51 +56,59 @@ Step 2: Respond',

describe('Task 2: Reactive compaction under pressure', () => {
it('applies reactive compaction when token pressure is high', () => {
// Create a mock traversal result with files
const traversalResult: TraversalResult = {
files: [
{
node: {
id: 'file-1', path: 'src/main.ts', language: 'typescript',
lastModified: Date.now(), contentHash: 'hash1',
tokens: 500, symbols: [
{ name: 'main', kind: 'function', isExported: true, signature: '(): void' },
{ name: 'helper', kind: 'function', isExported: false, signature: '(): string' },
{ name: 'main', kind: 'function', isExported: true, signature: '(): void', lineStart: 1, lineEnd: 20 },
{ name: 'helper', kind: 'function', isExported: false, signature: '(): string', lineStart: 22, lineEnd: 30 },
],
},
relevance: 0.9,
distance: 0,
reason: 'direct',
},
{
node: {
id: 'file-2', path: 'src/utils.ts', language: 'typescript',
lastModified: Date.now(), contentHash: 'hash2',
tokens: 300, symbols: [
{ name: 'format', kind: 'function', isExported: true, signature: '(s: string): string' },
{ name: 'format', kind: 'function', isExported: true, signature: '(s: string): string', lineStart: 1, lineEnd: 10 },
],
},
relevance: 0.5,
distance: 1,
reason: 'imports',
},
{
node: {
id: 'file-3', path: 'src/config.ts', language: 'typescript',
lastModified: Date.now(), contentHash: 'hash3',
tokens: 200, symbols: [
{ name: 'CONFIG', kind: 'const', isExported: true },
{ name: 'CONFIG', kind: 'const', isExported: true, lineStart: 1, lineEnd: 5 },
],
},
relevance: 0.3,
distance: 2,
reason: 'imports',
},
],
edges: [],
query: 'test',
totalTokens: 1000,
graphStats: { nodesTraversed: 5, edgesFollowed: 3, nodesIncluded: 3, nodesPruned: 2 },
};

// Tight budget to force pressure
const budget = 400;
const result = withReactiveCompaction(traversalResult, budget, {
hedgingConfidence: 0.2, // trigger hedging signal
hedgingConfidence: 0.2,
});

expect(result).toBeDefined();
expect(result.items.length).toBeGreaterThan(0);
expect(result.totalTokens).toBeLessThanOrEqual(budget * 1.5); // allow some slack for reactive adjustments
expect(result.totalTokens).toBeLessThanOrEqual(budget * 1.5);
expect(result.budgetUtilization).toBeGreaterThan(0);
});
});
Expand All @@ -113,17 +121,19 @@ Step 2: Respond',
const store = new MemoryStore(tempDir);

// Store some memories
store.add({
store.save({
type: 'preference',
content: 'User prefers TypeScript over JavaScript',
source: 'test-agent',
tags: ['language', 'coding'],
agentId: 'test-agent',
confidence: 0.9,
});
store.add({
type: 'fact',
store.save({
type: 'learning',
content: 'Project uses React with Zustand for state',
source: 'test-agent',
tags: ['stack'],
agentId: 'test-agent',
confidence: 0.8,
});

// Search
Expand All @@ -134,7 +144,6 @@ Step 2: Respond',
// Memory context section
const section = createMemoryContextSection('coding preferences', { basePath: tempDir });
expect(section).toBeTruthy();
expect(section).toContain('memories');

// Cleanup
rmSync(tempDir, { recursive: true, force: true });
Expand All @@ -152,8 +161,8 @@ Step 2: Respond',
it('summarizes tool calls', () => {
const middleware = createContextMiddleware();
const summary = middleware.processToolCalls([
{ tool: 'read_file', args: { path: 'src/main.ts' }, output: 'file content here', durationMs: 100 },
{ tool: 'read_file', args: { path: 'src/utils.ts' }, output: 'another file', durationMs: 50 },
{ tool: 'read_file', input: { path: 'src/main.ts' }, output: 'file content here', durationMs: 100, success: true },
{ tool: 'read_file', input: { path: 'src/utils.ts' }, output: 'another file', durationMs: 50, success: true },
]);
expect(summary).toBeTruthy();
expect(typeof summary).toBe('string');
Expand Down
Loading