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
4 changes: 3 additions & 1 deletion packages/codingcode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import { getBuiltinTools } from '../tools/providers.js';
import { canonicalizeSchema } from '../tools/utils/canonicalize-schema.js';
import { normalizePath } from '../core/path.js';

const REACTIVE_COMPACT_MAX_RETRIES = 3;
import { RulesService } from '../rules/index.js';

const logger = createLogger();
Expand Down Expand Up @@ -126,12 +128,12 @@
const hooks = yield* HookService;
const mcp = yield* McpService;
const checkpoint = yield* CheckpointService;
const approval = yield* ApprovalService;

Check warning on line 131 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'approval' is assigned a value but never used. Allowed unused vars must match /^_/u
const skills = yield* SkillService;
const runtime = yield* ProjectRuntimeService;
const todo = yield* TodoService;

Check warning on line 134 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'todo' is assigned a value but never used. Allowed unused vars must match /^_/u
const rules = yield* RulesService;
const context = yield* ContextService;

Check warning on line 136 in packages/codingcode/src/agent/agent.ts

View workflow job for this annotation

GitHub Actions / lint

'context' is assigned a value but never used. Allowed unused vars must match /^_/u
const memory = yield* MemoryService;
const factory = yield* LLMFactoryService;

Expand Down Expand Up @@ -250,7 +252,7 @@
const system = [basePrompt, memorySection].filter(Boolean).join('\n\n');

const config = getContextConfig();
const maxOverflowRetries = config.reactiveCompactMaxRetries;
const maxOverflowRetries = REACTIVE_COMPACT_MAX_RETRIES;
const model = state.sessionMeta?.model ?? 'unknown';
const effectiveMaxSteps = opts.maxStepsOverride ?? maxSteps;

Expand Down
16 changes: 11 additions & 5 deletions packages/codingcode/src/context/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
'edit_file',
]);

const MICRO_COMPACT_THRESHOLD = 0.25;
const MICRO_COMPACT_MIN_CHARS = 120;
const COMPACTION_THRESHOLD = 0.9;
const KEEP_RECENT_TURNS = 1;
const REACTIVE_COMPACT_MAX_RETRIES = 3;

Check warning on line 36 in packages/codingcode/src/context/service.ts

View workflow job for this annotation

GitHub Actions / lint

'REACTIVE_COMPACT_MAX_RETRIES' is assigned a value but never used. Allowed unused vars must match /^_/u

export class ContextService extends Effect.Service<ContextService>()('Context', {
effect: Effect.gen(function* () {
const session = yield* SessionService;
Expand Down Expand Up @@ -107,7 +113,7 @@
contextWindow: number,
jsonlPath: string
): boolean {
if (promptEstimate <= contextWindow * config.microCompactThreshold) return false;
if (promptEstimate <= contextWindow * MICRO_COMPACT_THRESHOLD) return false;

const compactedTurnIds = new Set<number>();
for (const ev of events) {
Expand All @@ -124,7 +130,7 @@
if (ev.turnId >= currentTurnId - 1) continue;
if (compactedTurnIds.has(ev.turnId)) continue;
if (!COMPACTABLE_TOOLS.has(ev.toolName.toLowerCase())) continue;
if (ev.output.length <= config.microCompactMinChars) continue;
if (ev.output.length <= MICRO_COMPACT_MIN_CHARS) continue;
oldResults.push(ev);
}

Expand Down Expand Up @@ -165,7 +171,7 @@
return { didCompress: false, released: 0, promptEstimate };
}

const threshold = modelMaxTokens * config.compactionThreshold;
const threshold = modelMaxTokens * COMPACTION_THRESHOLD;
if (promptEstimate <= threshold) {
return { didCompress: false, released: 0, promptEstimate };
}
Expand Down Expand Up @@ -208,7 +214,7 @@

let released = 0;

const threshold = modelMaxTokens ? modelMaxTokens * config.compactionThreshold : Infinity;
const threshold = modelMaxTokens ? modelMaxTokens * COMPACTION_THRESHOLD : Infinity;
if (usage === undefined || usage - released > threshold) {
released += await tryCompaction(
sessionId,
Expand Down Expand Up @@ -236,7 +242,7 @@
currentTurnId: number,
compactedTurnIds: Set<number>
): Promise<number> {
const endTurn = currentTurnId - config.keepRecentTurns - 1;
const endTurn = currentTurnId - KEEP_RECENT_TURNS - 1;
if (endTurn < 1) return 0;

const inRange = compactedEvents.filter((ev) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/codingcode/src/session/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const COMPACTABLE_TOOLS = new Set([
'edit_file',
]);

const MICRO_COMPACT_MIN_CHARS = 120;

export interface VisibilityResult {
hidden: Set<string>;
compactedTurnIds: Set<number>;
Expand Down Expand Up @@ -109,7 +111,7 @@ export function buildMessagesFromEvents(
if (
compactedTurnIds.has(event.turnId) &&
COMPACTABLE_TOOLS.has(event.toolName.toLowerCase()) &&
event.output.length > getContextConfig().microCompactMinChars
event.output.length > MICRO_COMPACT_MIN_CHARS
) {
output = `[Earlier: used ${event.toolName}]`;
}
Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/agent-cache-stability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import { MemoryService } from '../../src/memory/index.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/agent-concurrent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import { MemoryService } from '../../src/memory/index.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/agent-todo-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import { MemoryService } from '../../src/memory/index.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ import { MemoryService } from '../../src/memory/index.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import { resolveConfig } from '../../src/agent/config.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/hooks-deps-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import { MemoryService } from '../../src/memory/index.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/loop-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import { MemoryService } from '../../src/memory/index.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/memory-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down Expand Up @@ -173,7 +168,7 @@
.reverse()
.find((m: any) => m.role === 'user');
expect(lastUserMsg).toBeDefined();
expect(lastUserMsg.content).toContain('<system-reminder>');

Check failure on line 171 in packages/codingcode/test/agent/memory-snapshot.test.ts

View workflow job for this annotation

GitHub Actions / test

packages/codingcode/test/agent/memory-snapshot.test.ts > Memory snapshot stability > injects <system-reminder> when memory changed since snapshot

AssertionError: expected 'hi' to contain '<system-reminder>' Expected: "<system-reminder>" Received: "hi" ❯ packages/codingcode/test/agent/memory-snapshot.test.ts:171:33
expect(lastUserMsg.content).toContain('Updated on disk');
});

Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/agent/stop-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import { MemoryService } from '../../src/memory/index.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down
11 changes: 1 addition & 10 deletions packages/codingcode/test/context/append-turn-end.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
Expand All @@ -9,12 +9,7 @@ import { getContextConfig } from '../../src/context/config.js';
vi.mock('@codingcode/infra/config', () => ({
loadConfig: () => ({
context: {
microCompactThreshold: 0.7,
microCompactMinChars: 200,
compactionThreshold: 0.8,
keepRecentTurns: 10,
compactionModel: '',
reactiveCompactMaxRetries: 1,
},
memory: {
enabled: false,
Expand Down Expand Up @@ -74,8 +69,4 @@ describe('appendTurnEnd', () => {
expect(parsed.tokenCount).toBe(tokens);
});

it('compression thresholds have sensible defaults', () => {
const config = getContextConfig();
expect(config.compactionThreshold).toBeGreaterThan(0);
});
});
5 changes: 0 additions & 5 deletions packages/codingcode/test/context/budget-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ const PROJECT_BASE = join(homedir(), '.codingcode', 'project');

function makeConfig() {
return {
microCompactThreshold: 0.5,
microCompactMinChars: 120,
compactionThreshold: 0.9,
keepRecentTurns: 1,
compactionModel: '',
reactiveCompactMaxRetries: 3,
};
}

Expand Down
13 changes: 4 additions & 9 deletions packages/codingcode/test/context/compressor/behavior.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,7 @@ function readSummaryEvents(jsonlPath: string): SummaryEvent[] {

function tinyConfig(overrides: Partial<ContextConfig> = {}): ContextConfig {
return {
microCompactThreshold: 0.5,
microCompactMinChars: 120,
compactionThreshold: 0.5,
keepRecentTurns: 2,
compactionModel: '',
reactiveCompactMaxRetries: 1,
...overrides,
};
}
Expand Down Expand Up @@ -164,7 +159,7 @@ describe('compressor behavior', () => {
it('writes summary event with five-section system summary', async () => {
const fx = makeFixture({ numTurns: 5 });
try {
const cfg = tinyConfig({ keepRecentTurns: 2 });
const cfg = tinyConfig();
const summary =
'## Compacted History\n\n### Goal\nfix bug\n\n### Instructions\nbe careful\n\n### Discoveries\nrace condition\n\n### Accomplished\npatched\n\n### Relevant Files\nsrc/x.ts';
const llm = makeMockLLM(summary);
Expand All @@ -182,7 +177,7 @@ describe('compressor behavior', () => {
it('returns no-op when no LLM available', async () => {
const fx = makeFixture({ numTurns: 5 });
try {
const cfg = tinyConfig({ keepRecentTurns: 2 });
const cfg = tinyConfig();
const ctx = await getCtxService();
const result = await ctx.compactWithLLM(fx.sessionId, fx.slug, cfg, null);
expect(result.didCompress).toBe(false);
Expand All @@ -198,7 +193,7 @@ describe('compressor behavior', () => {
it('appends summary event directly to JSONL after L5', async () => {
const fx = makeFixture({ numTurns: 5 });
try {
const cfg = tinyConfig({ keepRecentTurns: 2 });
const cfg = tinyConfig();
const llm = makeMockLLM(
'## Compacted History\n\n### Goal\na\n\n### Instructions\nb\n\n### Discoveries\nc\n\n### Accomplished\nd\n\n### Relevant Files\ne'
);
Expand All @@ -219,7 +214,7 @@ describe('compressor behavior', () => {
const fx = makeFixture({ numTurns: 5 });
try {
const before = estimateTokens(buildMessages(fx.transcriptPath));
const cfg = tinyConfig({ keepRecentTurns: 2 });
const cfg = tinyConfig();
const llm = makeMockLLM(
'## Compacted History\n\n### Goal\na\n\n### Instructions\nb\n\n### Discoveries\nc\n\n### Accomplished\nd\n\n### Relevant Files\ne'
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,7 @@ async function getCtxService(): Promise<ContextService> {

function config(threshold: number, maxTokens = 10000) {
return {
microCompactThreshold: 0.5,
microCompactMinChars: 120,
compactionThreshold: threshold,
keepRecentTurns: 1,
compactionModel: '',
reactiveCompactMaxRetries: 1,
} as any;
}

Expand Down
5 changes: 0 additions & 5 deletions packages/codingcode/test/context/organizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ import { LLMFactoryService } from '../../src/llm/factory.js';
import type { SessionEvent, ToolResultEvent } from '../../src/session/types.js';

const baseConfig = {
microCompactThreshold: 0.5,
microCompactMinChars: 120,
compactionThreshold: 0.9,
keepRecentTurns: 1,
compactionModel: '',
reactiveCompactMaxRetries: 3,
};

function makeUserEvent(content: string, turnId: number): SessionEvent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import { SessionService } from '../../src/session/store.js';

vi.mock('../../src/context/config.js', () => ({
getContextConfig: vi.fn(() => ({
microCompactThreshold: 0.5,
microCompactMinChars: 120,
compactionThreshold: 0.9,
keepRecentTurns: 1,
compactionModel: '',
reactiveCompactMaxRetries: 3,
})),
}));

Expand Down
2 changes: 1 addition & 1 deletion packages/desktop/src/agent/AgentWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ export default function AgentWorkspace({ sendMessage, abort }: AgentWorkspacePro

return (
<div className="flex-1 flex flex-col overflow-hidden bg-[var(--bg-panel)]">
<MessageStream threadId={currentThreadId} />
<MessageStream key={currentThreadId} threadId={currentThreadId} />
<ApprovalPanel threadId={currentThreadId} />
<TodoPanel threadId={currentThreadId} />
{isCompressing && (
Expand Down
11 changes: 10 additions & 1 deletion packages/desktop/src/agent/MessageStream.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { useEffect, useLayoutEffect, useRef, useState, useCallback, useMemo } from 'react';
import { Copy, Check } from 'lucide-react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useGlobalStore } from '../stores/global.store';
Expand Down Expand Up @@ -229,6 +229,7 @@ export default function MessageStream({ threadId }: MessageStreamProps) {
} = useAgentRollback();
const { copiedId, copy } = useCopyToClipboard();
const parentRef = useRef<HTMLDivElement>(null);
const didScrollToEndRef = useRef(false);
const markFileRestored = useGlobalStore((s) => s.markFileRestored);
const setPendingInput = useGlobalStore((s) => s.setPendingInput);

Expand Down Expand Up @@ -371,8 +372,16 @@ export default function MessageStream({ threadId }: MessageStreamProps) {
anchorTo: 'end',
followOnAppend: 'smooth',
scrollEndThreshold: 80,
initialOffset: () => Number.MAX_SAFE_INTEGER,
});

useLayoutEffect(() => {
if (renderEntries.length === 0) return;
if (didScrollToEndRef.current) return;
virtualizer.scrollToEnd({ behavior: 'instant' });
didScrollToEndRef.current = true;
}, [renderEntries.length, virtualizer]);

const turnStatusKey = useMemo(() => turns.map((t) => `${t.id}:${t.status}`).join(','), [turns]);

const handleLoadDiff = useCallback(
Expand Down
7 changes: 3 additions & 4 deletions packages/desktop/src/shared/MessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@ const MessageItem = memo(function MessageItem({
useLayoutEffect(() => {
if (!rollbackMenuOpen || !rollbackMenuRef.current || !rollbackBtnRef.current) return;
const menuRect = rollbackMenuRef.current.getBoundingClientRect();
const btnRect = rollbackBtnRef.current.getBoundingClientRect();
const flip: { vertical?: boolean; horizontal?: boolean } = {};
// If menu goes above viewport, flip to below button
if (menuRect.top < 0) {
// Account for title bar overlay (~36px on Windows, ~28px on macOS)
const safeTop = 40;
if (menuRect.top < safeTop) {
flip.vertical = true;
}
// If menu goes beyond right edge, flip to left-align
if (menuRect.right > window.innerWidth) {
flip.horizontal = true;
}
Expand Down
Loading
Loading