(null);
- const markScopeRestored = useGlobalStore((s) => s.markScopeRestored);
const markFileRestored = useGlobalStore((s) => s.markFileRestored);
const setPendingInput = useGlobalStore((s) => s.setPendingInput);
@@ -420,20 +406,19 @@ export default function MessageStream({ threadId }: MessageStreamProps) {
[threadId, revertFile, undoCodeRollback, markFileRestored]
);
- const handleRevertScope = useCallback(
- async (uiTurnId: string, scope: 'agent' | 'all', isReverted: boolean) => {
+ const handleRevertTurn = useCallback(
+ async (uiTurnId: string, files: string[], isReverted: boolean) => {
if (isReverted) {
const result = await undoCodeRollback(threadId, uiTurnId, false);
if (result.restored) {
- markScopeRestored(threadId, uiTurnId, scope);
+ const key = `${threadId}:${uiTurnId}`;
+ delete useGlobalStore.getState().rollback.revertedFilesByTurnId[key];
}
- } else if (scope === 'agent') {
- await revertAgentFiles(threadId);
} else {
- await revertAllFiles(threadId);
+ await revertFiles(threadId, files);
}
},
- [threadId, revertAgentFiles, revertAllFiles, undoCodeRollback, markScopeRestored]
+ [threadId, revertFiles, undoCodeRollback]
);
const rollbackModal = showRollbackPanel && (
@@ -573,7 +558,7 @@ export default function MessageStream({ threadId }: MessageStreamProps) {
isInterrupted={isInterrupted}
threadId={threadId}
onRevertFile={handleRevertFile}
- onRevertScope={handleRevertScope}
+ onRevertTurn={handleRevertTurn}
/>
)}
diff --git a/packages/desktop/src/hooks/useAgent.ts b/packages/desktop/src/hooks/useAgent.ts
index bc2f6b3..64060d5 100644
--- a/packages/desktop/src/hooks/useAgent.ts
+++ b/packages/desktop/src/hooks/useAgent.ts
@@ -13,8 +13,6 @@ import {
getCheckpointDiff,
revertCheckpointFile,
revertCheckpointFiles,
- revertCheckpointAgentFiles,
- revertCheckpointAllFiles,
previewRollbackDiff,
rollbackCodeToTurn,
rollbackContext,
@@ -362,7 +360,6 @@ export function useAgentRollback() {
const setRollbackPreview = useGlobalStore((s) => s.setRollbackPreview);
const markFileReverted = useGlobalStore((s) => s.markFileReverted);
const markFileRestored = useGlobalStore((s) => s.markFileRestored);
- const markScopeReverted = useGlobalStore((s) => s.markScopeReverted);
const setTurnCheckpointMapping = useGlobalStore((s) => s.setTurnCheckpointMapping);
const initRevertedFilesFromState = useGlobalStore((s) => s.initRevertedFilesFromState);
@@ -422,30 +419,6 @@ export function useAgentRollback() {
[workspace.rootPath, markFileReverted, resolveUITurnId]
);
- const revertAgentFiles = useCallback(
- async (threadId: string) => {
- const cwd = useGlobalStore.getState().agent.threads[threadId]?.cwd ?? workspace.rootPath;
- const { result } = await revertCheckpointAgentFiles(threadId, cwd);
- if (result.reverted) {
- markScopeReverted(threadId, resolveUITurnId(threadId, result.throughTurnId), 'agent');
- }
- return result;
- },
- [workspace.rootPath, markScopeReverted, resolveUITurnId]
- );
-
- const revertAllFiles = useCallback(
- async (threadId: string) => {
- const cwd = useGlobalStore.getState().agent.threads[threadId]?.cwd ?? workspace.rootPath;
- const { result } = await revertCheckpointAllFiles(threadId, cwd);
- if (result.reverted) {
- markScopeReverted(threadId, resolveUITurnId(threadId, result.throughTurnId), 'all');
- }
- return result;
- },
- [workspace.rootPath, markScopeReverted, resolveUITurnId]
- );
-
const previewRollback = useCallback(
async (threadId: string, throughTurnId: number) => {
const cwd = useGlobalStore.getState().agent.threads[threadId]?.cwd ?? workspace.rootPath;
@@ -585,8 +558,6 @@ export function useAgentRollback() {
loadCheckpointDiff,
revertFile,
revertFiles,
- revertAgentFiles,
- revertAllFiles,
previewRollback,
rollbackCode,
rollbackCtx,
diff --git a/packages/desktop/src/lib/core-api.ts b/packages/desktop/src/lib/core-api.ts
index 031f7eb..9c2ed8c 100644
--- a/packages/desktop/src/lib/core-api.ts
+++ b/packages/desktop/src/lib/core-api.ts
@@ -223,7 +223,6 @@ export interface CheckpointDiff {
turnId: number;
files: Array<{
path: string;
- source: 'agent' | 'unknown';
status: string;
diff: string;
insertions: number;
@@ -298,20 +297,6 @@ export function revertCheckpointFiles(
return clients.sessions.revertCheckpointFiles({ sessionId, cwd, files }) as any;
}
-export function revertCheckpointAgentFiles(
- sessionId: string,
- cwd: string
-): Promise<{ ok: boolean; result: CodeRollbackResult }> {
- return clients.sessions.revertCheckpointAgentFiles({ sessionId, cwd }) as any;
-}
-
-export function revertCheckpointAllFiles(
- sessionId: string,
- cwd: string
-): Promise<{ ok: boolean; result: CodeRollbackResult }> {
- return clients.sessions.revertCheckpointAllFiles({ sessionId, cwd }) as any;
-}
-
export function previewRollbackDiff(
sessionId: string,
cwd: string,
diff --git a/packages/desktop/src/stores/global.store.ts b/packages/desktop/src/stores/global.store.ts
index f847b99..cb6a708 100644
--- a/packages/desktop/src/stores/global.store.ts
+++ b/packages/desktop/src/stores/global.store.ts
@@ -169,8 +169,6 @@ interface GlobalActions {
clearRollbackPreview: (threadId: string) => void;
markFileReverted: (threadId: string, turnId: string, file: string) => void;
markFileRestored: (threadId: string, turnId: string, file: string) => void;
- markScopeReverted: (threadId: string, turnId: string, scope: 'agent' | 'all') => void;
- markScopeRestored: (threadId: string, turnId: string, scope: 'agent' | 'all') => void;
initRevertedFilesFromState: (threadId: string) => void;
setTurnCheckpointMapping: (threadId: string, checkpointId: number, uiTurnId: string) => void;
startCompressing: () => void;
@@ -677,31 +675,6 @@ export const useGlobalStore = create
()(
s.rollback.revertedFilesByTurnId[key] = arr.filter((f) => f !== file);
}
}),
- markScopeReverted: (threadId, turnId, scope) =>
- set((s) => {
- const key = `${threadId}:${turnId}`;
- const sentinel =
- scope === 'agent' ? '__scope_agent_reverted__' : '__scope_all_reverted__';
- if (!s.rollback.revertedFilesByTurnId[key]) {
- s.rollback.revertedFilesByTurnId[key] = [];
- }
- if (!s.rollback.revertedFilesByTurnId[key].includes(sentinel)) {
- s.rollback.revertedFilesByTurnId[key].push(sentinel);
- }
- }),
- markScopeRestored: (threadId, turnId, scope) =>
- set((s) => {
- const key = `${threadId}:${turnId}`;
- const sentinel =
- scope === 'agent' ? '__scope_agent_reverted__' : '__scope_all_reverted__';
- const arr = s.rollback.revertedFilesByTurnId[key];
- if (arr) {
- s.rollback.revertedFilesByTurnId[key] = arr.filter((f) => f !== sentinel);
- if (s.rollback.revertedFilesByTurnId[key].length === 0) {
- delete s.rollback.revertedFilesByTurnId[key];
- }
- }
- }),
initRevertedFilesFromState: (threadId) =>
set((s) => {
const state = s.rollback.rollbackStateByThreadId[threadId];
diff --git a/packages/desktop/test/global-store-rollback-state.test.ts b/packages/desktop/test/global-store-rollback-state.test.ts
index e5a1ccf..d6022c6 100644
--- a/packages/desktop/test/global-store-rollback-state.test.ts
+++ b/packages/desktop/test/global-store-rollback-state.test.ts
@@ -38,7 +38,7 @@ describe('Rollback state in global store', () => {
files: [
{
path: '/test/a.ts',
- source: 'agent' as const,
+
status: 'M',
diff: '---\n+++\n',
insertions: 2,
@@ -104,35 +104,6 @@ describe('Rollback state in global store', () => {
expect(reverted).toEqual(['/test/b.ts']);
});
- it('markScopeReverted sets sentinel', () => {
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'agent');
- const reverted = useGlobalStore.getState().rollback.revertedFilesByTurnId['thread1:3'];
- expect(reverted).toContain('__scope_agent_reverted__');
- });
-
- it('markScopeReverted differentiates agent and all scopes', () => {
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'agent');
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'all');
- const reverted = useGlobalStore.getState().rollback.revertedFilesByTurnId['thread1:3'];
- expect(reverted).toContain('__scope_agent_reverted__');
- expect(reverted).toContain('__scope_all_reverted__');
- });
-
- it('markScopeRestored removes correct sentinel', () => {
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'agent');
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'all');
- useGlobalStore.getState().markScopeRestored('thread1', '3', 'agent');
- const reverted = useGlobalStore.getState().rollback.revertedFilesByTurnId['thread1:3'];
- expect(reverted).not.toContain('__scope_agent_reverted__');
- expect(reverted).toContain('__scope_all_reverted__');
- });
-
- it('markScopeRestored removes entry when empty', () => {
- useGlobalStore.getState().markScopeReverted('thread1', '3', 'agent');
- useGlobalStore.getState().markScopeRestored('thread1', '3', 'agent');
- expect(useGlobalStore.getState().rollback.revertedFilesByTurnId['thread1:3']).toBeUndefined();
- });
-
it('initRevertedFilesFromState populates from server state', () => {
useGlobalStore.getState().setTurnCheckpointMapping('thread1', 5, 'ui-turn-5');
const state = {
@@ -165,7 +136,7 @@ describe('Rollback state in global store', () => {
files: [
{
path: '/a.ts',
- source: 'agent' as const,
+
status: 'M',
diff: '---\n+++\n',
insertions: 1,
@@ -186,7 +157,7 @@ describe('Rollback state in global store', () => {
files: [
{
path: '/b.ts',
- source: 'agent' as const,
+
status: 'M',
diff: '---\n+++\n',
insertions: 1,
diff --git a/packages/tui/src/commands/registry.ts b/packages/tui/src/commands/registry.ts
index ea60215..bece142 100644
--- a/packages/tui/src/commands/registry.ts
+++ b/packages/tui/src/commands/registry.ts
@@ -14,12 +14,6 @@ export const COMMAND_REGISTRY = {
usage: '/sessions',
title: '恢复会话',
},
- checkpoint: {
- name: 'checkpoint',
- description: '管理文件快照,回退/前进',
- usage: '/checkpoint',
- title: '检查点',
- },
help: { name: 'help', description: '显示帮助', usage: '/help', title: '帮助' },
clear: {
name: 'clear',
diff --git a/packages/tui/src/components/App.tsx b/packages/tui/src/components/App.tsx
index acacf9e..470c328 100644
--- a/packages/tui/src/components/App.tsx
+++ b/packages/tui/src/components/App.tsx
@@ -254,24 +254,6 @@ export function App({ client }: AppProps) {
}
return;
}
- if (parsed.name === 'checkpoint') {
- try {
- const checkpoints = await client.getCheckpoints();
- setPanel({ type: 'checkpoint-list', checkpoints });
- } catch (e: any) {
- setStaticMessages((prev) => [
- ...prev,
- {
- id: generateId(),
- timestamp: Date.now(),
- role: 'system' as const,
- content: `[Checkpoint Error] ${e.message || e}`,
- },
- ]);
- return;
- }
- return;
- }
if (parsed.name === 'help') {
setPanel({ type: 'help' });
return;
@@ -426,57 +408,6 @@ export function App({ client }: AppProps) {
width={sessionW}
/>
)}
- {panel.type === 'checkpoint-list' && (
- ({
- label: `${cp.title || '(无标题)'} ${cp.agentModified.length + cp.unknownSource.length} 个文件`,
- value: cp.turnId.toString(),
- }))
- }
- onSelect={async (value) => {
- const cp = panel.checkpoints.find((c) => c.turnId.toString() === value);
- if (!cp) return;
- const hasForward = await client.hasForwardStack();
- setPanel({ type: 'checkpoint-action', cp, hasForward });
- }}
- onCancel={() => setPanel({ type: 'none' })}
- width={Math.min(60, width - 4)}
- />
- )}
- {panel.type === 'checkpoint-action' && (
- 0
- ? [
- {
- label: `仅回退 Agent 修改的文件 (${panel.cp.agentModified.length} 个)`,
- value: 'agent' as const,
- },
- {
- label: `回退全部文件 (${panel.cp.agentModified.length + panel.cp.unknownSource.length} 个)`,
- value: 'all' as const,
- },
- ]
- : [{ label: '无变更文件', value: '' as const }]),
- ...(panel.hasForward ? [{ label: '前进到最新状态', value: 'forward' as const }] : []),
- ]}
- onSelect={async (value) => {
- if (value === 'forward') {
- await client.forwardLastRevert();
- } else if (value === 'agent' || value === 'all') {
- await client.revertCheckpoint(panel.cp.turnId, value);
- }
- setPanel({ type: 'none' });
- }}
- onCancel={() => setPanel({ type: 'none' })}
- width={Math.min(60, width - 4)}
- />
- )}
{approval && (
{
description?: string;
}
-export interface CheckpointInfo {
- turnId: number;
- title: string;
- agentModified: string[];
- unknownSource: string[];
-}
-
export interface McpServerStatus {
name: string;
connected: boolean;
@@ -51,8 +44,6 @@ export type PanelState =
| { type: 'model'; items: PanelItem[]; activeValue: string }
| { type: 'sessions'; items: PanelItem[] }
| { type: 'approval'; id: string; tool: string; args: Record }
- | { type: 'checkpoint-list'; checkpoints: CheckpointInfo[] }
- | { type: 'checkpoint-action'; cp: CheckpointInfo; hasForward: boolean }
| { type: 'help' }
| { type: 'mcp'; servers: McpServerStatus[] }
| { type: 'skill'; skills: SkillStatus[] }
From 9d504355ff969ca6fb7f97c493112fa546c1d300 Mon Sep 17 00:00:00 2001
From: phantom5099 <1011668688@qq.com>
Date: Thu, 11 Jun 2026 15:50:51 +0800
Subject: [PATCH 06/11] move toGitPath
---
.../src/checkpoint/checkpoint-service.ts | 51 +++++--------------
.../src/checkpoint/commit-naming.ts | 9 ----
.../src/checkpoint/restore-planning.ts | 2 +-
.../src/checkpoint/restore-store.ts | 2 +-
.../src/checkpoint/rollback-engine.ts | 2 +-
packages/codingcode/src/checkpoint/utils.ts | 32 ++++++++++++
.../test/checkpoint/checkpoint-diff.test.ts | 24 ++-------
.../test/checkpoint/checkpoint-undo.test.ts | 8 +--
8 files changed, 57 insertions(+), 73 deletions(-)
delete mode 100644 packages/codingcode/src/checkpoint/commit-naming.ts
create mode 100644 packages/codingcode/src/checkpoint/utils.ts
diff --git a/packages/codingcode/src/checkpoint/checkpoint-service.ts b/packages/codingcode/src/checkpoint/checkpoint-service.ts
index 29354a4..6d54203 100644
--- a/packages/codingcode/src/checkpoint/checkpoint-service.ts
+++ b/packages/codingcode/src/checkpoint/checkpoint-service.ts
@@ -1,11 +1,10 @@
import { Effect } from 'effect';
import { createHash } from 'crypto';
-import { readFileSync } from 'fs';
import { resolve } from 'path';
import { ShadowGit } from './shadow-git.js';
import { ProjectLock } from './project-lock.js';
import { normalizePath } from '../core/path.js';
-import { shortSid, commitMsg } from './commit-naming.js';
+import { shortSid, commitMsg, toGitPath, hashWorkspaceFile } from './utils.js';
import { readRestoreEntry, writeRestoreEntry } from './restore-store.js';
import {
getCompletedTurnsFor,
@@ -60,28 +59,6 @@ export interface CodeRestoreEntry {
timestamp: string;
}
-// ---- Path utilities ----
-
-export function toGitPath(projectPath: string, file: string): string {
- const normalized = normalizePath(file);
- const base = normalizePath(projectPath);
- if (normalized.toLowerCase().startsWith(base.toLowerCase())) {
- let rel = normalized.slice(base.length);
- if (rel.startsWith('/') || rel.startsWith('\\')) rel = rel.slice(1);
- return rel;
- }
- return normalized;
-}
-
-export function hashWorkspaceFile(projectPath: string, file: string): string | null {
- try {
- const content = readFileSync(resolve(projectPath, toGitPath(projectPath, file)));
- return createHash('sha256').update(content).digest('hex');
- } catch {
- return null;
- }
-}
-
// ---- Service ----
export class CheckpointService extends Effect.Service()('Checkpoint', {
@@ -110,6 +87,16 @@ export class CheckpointService extends Effect.Service()('Chec
return lock;
}
+ function doSnapshotFinal(sg: ShadowGit, sessionId: string, turnId: number): void {
+ const lock = lockFor(sg.projectPath);
+ lock.lock();
+ try {
+ sg.commit(commitMsg(sessionId, turnId, 'final'));
+ } finally {
+ lock.unlock();
+ }
+ }
+
function repairIncompleteTurn(sg: ShadowGit, sessionId: string): void {
const completed = getCompletedTurnsFor(sg, sessionId);
const candidate = completed.length > 0 ? completed[completed.length - 1]! + 1 : 1;
@@ -117,13 +104,7 @@ export class CheckpointService extends Effect.Service()('Chec
if (!baseline) return;
const final = sg.findCommitByMessage(commitMsg(sessionId, candidate, 'final'));
if (final) return;
- const lock = lockFor(sg.projectPath);
- lock.lock();
- try {
- sg.commit(commitMsg(sessionId, candidate, 'final'));
- } finally {
- lock.unlock();
- }
+ doSnapshotFinal(sg, sessionId, candidate);
}
return {
@@ -153,13 +134,7 @@ export class CheckpointService extends Effect.Service()('Chec
snapshotFinal: (projectPath: string, sessionId: string, turnId: number): void => {
const sg = ensure(projectPath);
if (sg.isTooLargeForSnapshot()) return;
- const lock = lockFor(projectPath);
- lock.lock();
- try {
- sg.commit(commitMsg(sessionId, turnId, 'final'));
- } finally {
- lock.unlock();
- }
+ doSnapshotFinal(sg, sessionId, turnId);
},
// ---- Query ----
diff --git a/packages/codingcode/src/checkpoint/commit-naming.ts b/packages/codingcode/src/checkpoint/commit-naming.ts
deleted file mode 100644
index 06101f1..0000000
--- a/packages/codingcode/src/checkpoint/commit-naming.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createHash } from 'crypto';
-
-export function shortSid(sessionId: string): string {
- return createHash('sha256').update(sessionId).digest('hex').slice(0, 8);
-}
-
-export function commitMsg(sessionId: string, turnId: number, suffix: string): string {
- return `turn-${shortSid(sessionId)}-${turnId}-${suffix}`;
-}
diff --git a/packages/codingcode/src/checkpoint/restore-planning.ts b/packages/codingcode/src/checkpoint/restore-planning.ts
index cfec93b..b4e86aa 100644
--- a/packages/codingcode/src/checkpoint/restore-planning.ts
+++ b/packages/codingcode/src/checkpoint/restore-planning.ts
@@ -1,5 +1,5 @@
import type { ShadowGit } from './shadow-git.js';
-import { shortSid, commitMsg } from './commit-naming.js';
+import { shortSid, commitMsg } from './utils.js';
export interface RestorePlan {
throughTurnId: number;
diff --git a/packages/codingcode/src/checkpoint/restore-store.ts b/packages/codingcode/src/checkpoint/restore-store.ts
index ec14674..0ca7f5f 100644
--- a/packages/codingcode/src/checkpoint/restore-store.ts
+++ b/packages/codingcode/src/checkpoint/restore-store.ts
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import type { CodeRestoreEntry } from './checkpoint-service.js';
-import { shortSid } from './commit-naming.js';
+import { shortSid } from './utils.js';
function restorePath(gitDir: string, sessionId: string): string {
return join(gitDir, '..', `last-restore-${shortSid(sessionId)}.json`);
diff --git a/packages/codingcode/src/checkpoint/rollback-engine.ts b/packages/codingcode/src/checkpoint/rollback-engine.ts
index 4f82837..949edad 100644
--- a/packages/codingcode/src/checkpoint/rollback-engine.ts
+++ b/packages/codingcode/src/checkpoint/rollback-engine.ts
@@ -3,7 +3,7 @@ import { normalizePath } from '../core/path.js';
import type { ShadowGit } from './shadow-git.js';
import type { ProjectLock } from './project-lock.js';
import type { CodeRollbackResult, CodeRestoreEntry } from './checkpoint-service.js';
-import { commitMsg } from './commit-naming.js';
+import { commitMsg } from './utils.js';
import { readRestoreEntry, writeRestoreEntry } from './restore-store.js';
export function emptyRollbackResult(
diff --git a/packages/codingcode/src/checkpoint/utils.ts b/packages/codingcode/src/checkpoint/utils.ts
new file mode 100644
index 0000000..75d0164
--- /dev/null
+++ b/packages/codingcode/src/checkpoint/utils.ts
@@ -0,0 +1,32 @@
+import { createHash } from 'crypto';
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+import { normalizePath } from '../core/path.js';
+
+export function shortSid(sessionId: string): string {
+ return createHash('sha256').update(sessionId).digest('hex').slice(0, 8);
+}
+
+export function commitMsg(sessionId: string, turnId: number, suffix: string): string {
+ return `turn-${shortSid(sessionId)}-${turnId}-${suffix}`;
+}
+
+export function toGitPath(projectPath: string, file: string): string {
+ const normalized = normalizePath(file);
+ const base = normalizePath(projectPath);
+ if (normalized.toLowerCase().startsWith(base.toLowerCase())) {
+ let rel = normalized.slice(base.length);
+ if (rel.startsWith('/') || rel.startsWith('\\')) rel = rel.slice(1);
+ return rel;
+ }
+ return normalized;
+}
+
+export function hashWorkspaceFile(projectPath: string, file: string): string | null {
+ try {
+ const content = readFileSync(resolve(projectPath, toGitPath(projectPath, file)));
+ return createHash('sha256').update(content).digest('hex');
+ } catch {
+ return null;
+ }
+}
diff --git a/packages/codingcode/test/checkpoint/checkpoint-diff.test.ts b/packages/codingcode/test/checkpoint/checkpoint-diff.test.ts
index 84e354e..a6ae912 100644
--- a/packages/codingcode/test/checkpoint/checkpoint-diff.test.ts
+++ b/packages/codingcode/test/checkpoint/checkpoint-diff.test.ts
@@ -34,39 +34,25 @@ function writeFile(projectPath: string, filename: string, content: string) {
writeFileSync(filePath, content, 'utf8');
}
-describe('toGitPath and hashWorkspaceFile', () => {
- it('toGitPath converts absolute to relative', async () => {
- const { toGitPath } = await import('../../src/checkpoint/checkpoint-service.js');
+describe('toGitPath', () => {
+ it('converts absolute to relative', async () => {
+ const { toGitPath } = await import('../../src/checkpoint/utils.js');
const result = toGitPath('/tmp/project', '/tmp/project/src/file.ts');
expect(result).toBe('src/file.ts');
});
- it('toGitPath returns normalized path when not under project', async () => {
- const { toGitPath } = await import('../../src/checkpoint/checkpoint-service.js');
+ it('returns normalized path when not under project', async () => {
+ const { toGitPath } = await import('../../src/checkpoint/utils.js');
const result = toGitPath('/tmp/project', '/other/file.ts');
expect(result).toContain('file.ts');
});
-
- it('hashWorkspaceFile returns null for non-existent file', async () => {
- const { hashWorkspaceFile } = await import('../../src/checkpoint/checkpoint-service.js');
- const result = hashWorkspaceFile('/tmp/nonexistent', 'nonexistent.ts');
- expect(result).toBeNull();
- });
});
describe('CodeRestoreEntry types', () => {
it('CodeRestoreEntry type is exported', async () => {
const mod = await import('../../src/checkpoint/checkpoint-service.js');
- // Verify the service class is exported
expect(typeof mod.CheckpointService).toBe('function');
});
-
- it('toGitPath and hashWorkspaceFile are exported as functions', async () => {
- const { toGitPath, hashWorkspaceFile } =
- await import('../../src/checkpoint/checkpoint-service.js');
- expect(typeof toGitPath).toBe('function');
- expect(typeof hashWorkspaceFile).toBe('function');
- });
});
describe('CheckpointDiff type with insertions/deletions', () => {
diff --git a/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts b/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
index 6b21b59..eb61e8a 100644
--- a/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
+++ b/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
@@ -43,7 +43,7 @@ function writeFile(projectPath: string, filename: string, content: string) {
describe('toGitPath case-insensitive matching', () => {
it('handles Windows case-mismatched projectPath and file path', async () => {
- const { toGitPath } = await import('../../src/checkpoint/checkpoint-service.js');
+ const { toGitPath } = await import('../../src/checkpoint/utils.js');
// projectPath has mixed case (Users, Desktop), file path is all lowercase
const projectPath = 'c:/Users/Alice/Desktop/MyProject';
@@ -54,7 +54,7 @@ describe('toGitPath case-insensitive matching', () => {
});
it('handles lowercase projectPath with uppercase file path', async () => {
- const { toGitPath } = await import('../../src/checkpoint/checkpoint-service.js');
+ const { toGitPath } = await import('../../src/checkpoint/utils.js');
const projectPath = 'c:/users/alice/desktop/myproject';
const filePath = 'c:/Users/Alice/Desktop/MyProject/src/file.ts';
@@ -64,7 +64,7 @@ describe('toGitPath case-insensitive matching', () => {
});
it('still returns normalized absolute path when file is outside project', async () => {
- const { toGitPath } = await import('../../src/checkpoint/checkpoint-service.js');
+ const { toGitPath } = await import('../../src/checkpoint/utils.js');
const result = toGitPath('c:/Users/Alice/Desktop/MyProject', 'c:/other/file.ts');
@@ -310,7 +310,7 @@ describe('rollbackCodeToTurn uses inclusive target turn', () => {
describe('toGitPath preserves original casing for git paths', () => {
it('returns relative path with original casing from git diff', async () => {
- const { toGitPath } = await import('../../src/checkpoint/checkpoint-service.js');
+ const { toGitPath } = await import('../../src/checkpoint/utils.js');
// Simulate a path that git returns with original casing
const projectPath = 'c:/Users/Alice/Desktop/MyProject';
From 37a981628bebcfaab4a6d5d2f9f8a2e1971b43cd Mon Sep 17 00:00:00 2001
From: phantom5099 <1011668688@qq.com>
Date: Thu, 11 Jun 2026 16:18:58 +0800
Subject: [PATCH 07/11] Optimize context module structure
---
.../src/checkpoint/checkpoint-service.ts | 23 +++++++------------
packages/codingcode/src/checkpoint/utils.ts | 17 ++++++++++++++
.../llm-resolver.ts => compaction-llm.ts} | 6 ++---
.../prompt.ts => compaction-prompt.ts} | 0
.../{compressor/index.ts => compressor.ts} | 20 ++++++++--------
packages/codingcode/src/context/context.ts | 2 +-
packages/codingcode/src/context/organizer.ts | 2 +-
.../src/context/{utils/tokens.ts => util.ts} | 2 +-
packages/codingcode/src/session/store.ts | 2 +-
.../test/context/append-turn-end.test.ts | 2 +-
.../test/context/compressor/behavior.test.ts | 4 ++--
.../compressor/compact-if-needed.test.ts | 8 +++----
.../context/compressor/llm-resolver.test.ts | 2 +-
.../codingcode/test/context/tokens.test.ts | 2 +-
.../test/session/prompt-estimate.test.ts | 2 +-
15 files changed, 52 insertions(+), 42 deletions(-)
rename packages/codingcode/src/context/{compressor/llm-resolver.ts => compaction-llm.ts} (85%)
rename packages/codingcode/src/context/{compressor/prompt.ts => compaction-prompt.ts} (100%)
rename packages/codingcode/src/context/{compressor/index.ts => compressor.ts} (93%)
rename packages/codingcode/src/context/{utils/tokens.ts => util.ts} (94%)
diff --git a/packages/codingcode/src/checkpoint/checkpoint-service.ts b/packages/codingcode/src/checkpoint/checkpoint-service.ts
index 6d54203..a42f01d 100644
--- a/packages/codingcode/src/checkpoint/checkpoint-service.ts
+++ b/packages/codingcode/src/checkpoint/checkpoint-service.ts
@@ -4,7 +4,7 @@ import { resolve } from 'path';
import { ShadowGit } from './shadow-git.js';
import { ProjectLock } from './project-lock.js';
import { normalizePath } from '../core/path.js';
-import { shortSid, commitMsg, toGitPath, hashWorkspaceFile } from './utils.js';
+import { shortSid, commitMsg, toGitPath, hashWorkspaceFile, ProjectCache } from './utils.js';
import { readRestoreEntry, writeRestoreEntry } from './restore-store.js';
import {
getCompletedTurnsFor,
@@ -63,28 +63,21 @@ export interface CodeRestoreEntry {
export class CheckpointService extends Effect.Service()('Checkpoint', {
effect: Effect.gen(function* () {
- const shadowGitByProject = new Map();
- const lockByProject = new Map();
+ const shadowGitByProject = new ProjectCache(10);
+ const lockByProject = new ProjectCache(10);
function ensure(projectPath: string): ShadowGit {
const normalized = normalizePath(projectPath);
- let sg = shadowGitByProject.get(normalized);
- if (!sg || sg.projectPath !== normalized) {
- sg = new ShadowGit(normalized);
+ return shadowGitByProject.get(normalized, () => {
+ const sg = new ShadowGit(normalized);
sg.init();
- shadowGitByProject.set(normalized, sg);
- }
- return sg;
+ return sg;
+ });
}
function lockFor(projectPath: string): ProjectLock {
const normalized = normalizePath(projectPath);
- let lock = lockByProject.get(normalized);
- if (!lock) {
- lock = new ProjectLock(normalized);
- lockByProject.set(normalized, lock);
- }
- return lock;
+ return lockByProject.get(normalized, () => new ProjectLock(normalized));
}
function doSnapshotFinal(sg: ShadowGit, sessionId: string, turnId: number): void {
diff --git a/packages/codingcode/src/checkpoint/utils.ts b/packages/codingcode/src/checkpoint/utils.ts
index 75d0164..a222948 100644
--- a/packages/codingcode/src/checkpoint/utils.ts
+++ b/packages/codingcode/src/checkpoint/utils.ts
@@ -30,3 +30,20 @@ export function hashWorkspaceFile(projectPath: string, file: string): string | n
return null;
}
}
+
+export class ProjectCache {
+ private map = new Map();
+ private order: string[] = [];
+ constructor(private max: number) {}
+ get(key: string, factory: () => T): T {
+ const hit = this.map.get(key);
+ if (hit) return hit;
+ const value = factory();
+ this.map.set(key, value);
+ this.order.push(key);
+ if (this.order.length > this.max) {
+ this.map.delete(this.order.shift()!);
+ }
+ return value;
+ }
+}
diff --git a/packages/codingcode/src/context/compressor/llm-resolver.ts b/packages/codingcode/src/context/compaction-llm.ts
similarity index 85%
rename from packages/codingcode/src/context/compressor/llm-resolver.ts
rename to packages/codingcode/src/context/compaction-llm.ts
index f762416..9b510ad 100644
--- a/packages/codingcode/src/context/compressor/llm-resolver.ts
+++ b/packages/codingcode/src/context/compaction-llm.ts
@@ -1,6 +1,6 @@
-import { findModel, createClient } from '../../llm/factory.js';
-import type { LLMClient } from '../../llm/client.js';
-import type { ContextConfig } from '../config.js';
+import { findModel, createClient } from '../llm/factory.js';
+import type { LLMClient } from '../llm/client.js';
+import type { ContextConfig } from './config.js';
/**
* Resolve which LLM client to use for compaction.
diff --git a/packages/codingcode/src/context/compressor/prompt.ts b/packages/codingcode/src/context/compaction-prompt.ts
similarity index 100%
rename from packages/codingcode/src/context/compressor/prompt.ts
rename to packages/codingcode/src/context/compaction-prompt.ts
diff --git a/packages/codingcode/src/context/compressor/index.ts b/packages/codingcode/src/context/compressor.ts
similarity index 93%
rename from packages/codingcode/src/context/compressor/index.ts
rename to packages/codingcode/src/context/compressor.ts
index 2e740e9..28d913a 100644
--- a/packages/codingcode/src/context/compressor/index.ts
+++ b/packages/codingcode/src/context/compressor.ts
@@ -1,18 +1,18 @@
import { randomUUID } from 'crypto';
-import { resolveSessionDir } from '../../session/io.js';
+import { resolveSessionDir } from '../session/io.js';
import {
estimateTokens,
estimateMessageTokens,
estimateTokensForContent,
-} from '../utils/tokens.js';
-import { applyVisibilityEvents } from '../../session/messages.js';
-import { resolveCompactionLLM } from './llm-resolver.js';
-import { COMPACTION_SYSTEM_PROMPT } from './prompt.js';
-import type { ContextConfig } from '../config.js';
-import type { Message } from '../../core/types.js';
-import type { SessionEvent, SummaryEvent } from '../../session/types.js';
-import type { LLMClient } from '../../llm/client.js';
-import { assemblePayload } from '../organizer.js';
+} from './util.js';
+import { applyVisibilityEvents } from '../session/messages.js';
+import { resolveCompactionLLM } from './compaction-llm.js';
+import { COMPACTION_SYSTEM_PROMPT } from './compaction-prompt.js';
+import type { ContextConfig } from './config.js';
+import type { Message } from '../core/types.js';
+import type { SessionEvent, SummaryEvent } from '../session/types.js';
+import type { LLMClient } from '../llm/client.js';
+import { assemblePayload } from './organizer.js';
import { join } from 'path';
import { appendFileSync } from 'fs';
diff --git a/packages/codingcode/src/context/context.ts b/packages/codingcode/src/context/context.ts
index 09c3733..7ce866b 100644
--- a/packages/codingcode/src/context/context.ts
+++ b/packages/codingcode/src/context/context.ts
@@ -1,6 +1,6 @@
import { Effect } from 'effect';
import { getContextConfig, type ContextConfig } from './config.js';
-import { compactWithLLM, compactIfNeeded, type CompressResult } from './compressor/index.js';
+import { compactWithLLM, compactIfNeeded, type CompressResult } from './compressor.js';
import { assemblePayload, type BuildResult } from './organizer.js';
import type { LLMClient } from '../llm/client.js';
diff --git a/packages/codingcode/src/context/organizer.ts b/packages/codingcode/src/context/organizer.ts
index 06afdcf..d598016 100644
--- a/packages/codingcode/src/context/organizer.ts
+++ b/packages/codingcode/src/context/organizer.ts
@@ -2,7 +2,7 @@ import type { ContextConfig } from './config.js';
import type { Message } from '../core/types.js';
import { findSessionIndex, resolveSessionDir, readHistory, appendLine } from '../session/io.js';
import { applyVisibilityEvents, buildMessagesFromEvents } from '../session/messages.js';
-import { estimateTokens } from './utils/tokens.js';
+import { estimateTokens } from './util.js';
import { join } from 'path';
import { randomUUID } from 'crypto';
import type { SessionEvent, ToolResultEvent, CompactEvent } from '../session/types.js';
diff --git a/packages/codingcode/src/context/utils/tokens.ts b/packages/codingcode/src/context/util.ts
similarity index 94%
rename from packages/codingcode/src/context/utils/tokens.ts
rename to packages/codingcode/src/context/util.ts
index 02fdec7..d8fa3ba 100644
--- a/packages/codingcode/src/context/utils/tokens.ts
+++ b/packages/codingcode/src/context/util.ts
@@ -1,4 +1,4 @@
-import type { Message } from '../../core/types.js';
+import type { Message } from '../core/types.js';
export function estimateMessageTokens(m: Message): number {
let tokens = estimateTokensForContent(m.content ?? '');
diff --git a/packages/codingcode/src/session/store.ts b/packages/codingcode/src/session/store.ts
index 30aa96b..7484d35 100644
--- a/packages/codingcode/src/session/store.ts
+++ b/packages/codingcode/src/session/store.ts
@@ -21,7 +21,7 @@ import {
estimateTokens,
estimateTokensForContent,
estimateMessageTokens,
-} from '../context/utils/tokens.js';
+} from '../context/util.js';
import {
projectSessionsDir,
ensureDirs,
diff --git a/packages/codingcode/test/context/append-turn-end.test.ts b/packages/codingcode/test/context/append-turn-end.test.ts
index bb73fc7..f15bde6 100644
--- a/packages/codingcode/test/context/append-turn-end.test.ts
+++ b/packages/codingcode/test/context/append-turn-end.test.ts
@@ -3,7 +3,7 @@ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { randomUUID } from 'crypto';
-import { estimateTokensForContent } from '../../src/context/utils/tokens.js';
+import { estimateTokensForContent } from '../../src/context/util.js';
import { getContextConfig } from '../../src/context/config.js';
const PROJECT_BASE = join(homedir(), '.codingcode', 'project');
diff --git a/packages/codingcode/test/context/compressor/behavior.test.ts b/packages/codingcode/test/context/compressor/behavior.test.ts
index 9426041..393d727 100644
--- a/packages/codingcode/test/context/compressor/behavior.test.ts
+++ b/packages/codingcode/test/context/compressor/behavior.test.ts
@@ -3,13 +3,13 @@ import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { randomUUID } from 'crypto';
-import { compactWithLLM } from '../../../src/context/compressor/index.js';
+import { compactWithLLM } from '../../../src/context/compressor.js';
import type { ContextConfig } from '../../../src/context/config.js';
import type { LLMClient } from '../../../src/llm/client.js';
import { Result } from '../../../src/core/result.js';
import type { SessionIndex, SessionEvent, SummaryEvent } from '../../../src/session/types.js';
import { buildMessages } from '../../../src/session/messages.js';
-import { estimateTokens } from '../../../src/context/utils/tokens.js';
+import { estimateTokens } from '../../../src/context/util.js';
const PROJECT_BASE = join(homedir(), '.codingcode', 'project');
diff --git a/packages/codingcode/test/context/compressor/compact-if-needed.test.ts b/packages/codingcode/test/context/compressor/compact-if-needed.test.ts
index a96d5b1..394e04c 100644
--- a/packages/codingcode/test/context/compressor/compact-if-needed.test.ts
+++ b/packages/codingcode/test/context/compressor/compact-if-needed.test.ts
@@ -39,7 +39,7 @@ vi.mock('../../../src/session/io.js', async (importOriginal) => {
};
});
-vi.mock('../../../src/context/compressor/llm-resolver.js', () => ({
+vi.mock('../../../src/context/compaction-llm.js', () => ({
resolveCompactionLLM: vi.fn(() => Promise.resolve(mockLLM)),
}));
@@ -51,15 +51,15 @@ vi.mock('fs', async (importOriginal) => {
};
});
-vi.mock('../../../src/context/utils/tokens.js', () => ({
+vi.mock('../../../src/context/util.js', () => ({
estimateTokens: vi.fn(),
estimateMessageTokens: vi.fn(),
estimateTokensForContent: vi.fn(),
}));
-import { compactIfNeeded } from '../../../src/context/compressor/index.js';
+import { compactIfNeeded } from '../../../src/context/compressor.js';
import { findSessionIndex } from '../../../src/session/io.js';
-import { estimateTokens, estimateMessageTokens } from '../../../src/context/utils/tokens.js';
+import { estimateTokens, estimateMessageTokens } from '../../../src/context/util.js';
function config(threshold: number, maxTokens = 10000) {
return {
diff --git a/packages/codingcode/test/context/compressor/llm-resolver.test.ts b/packages/codingcode/test/context/compressor/llm-resolver.test.ts
index 5c2ce76..ed59183 100644
--- a/packages/codingcode/test/context/compressor/llm-resolver.test.ts
+++ b/packages/codingcode/test/context/compressor/llm-resolver.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { resolveCompactionLLM } from '../../../src/context/compressor/llm-resolver.js';
+import { resolveCompactionLLM } from '../../../src/context/compaction-llm.js';
import type { LLMClient } from '../../../src/llm/client.js';
import type { ContextConfig } from '../../../src/context/config.js';
diff --git a/packages/codingcode/test/context/tokens.test.ts b/packages/codingcode/test/context/tokens.test.ts
index ab6f220..3a56af3 100644
--- a/packages/codingcode/test/context/tokens.test.ts
+++ b/packages/codingcode/test/context/tokens.test.ts
@@ -3,7 +3,7 @@ import {
estimateTokensForContent,
estimateTokens,
estimateMessageTokens,
-} from '../../src/context/utils/tokens.js';
+} from '../../src/context/util.js';
describe('token estimation', () => {
it('empty content returns 0', () => {
diff --git a/packages/codingcode/test/session/prompt-estimate.test.ts b/packages/codingcode/test/session/prompt-estimate.test.ts
index 94871bb..32778de 100644
--- a/packages/codingcode/test/session/prompt-estimate.test.ts
+++ b/packages/codingcode/test/session/prompt-estimate.test.ts
@@ -7,7 +7,7 @@ import { Effect } from 'effect';
import { forkSession, SessionService } from '../../src/session/store.js';
import { findSessionIndex } from '../../src/session/io.js';
import { findLastVisibleAssistantUsage, buildMessages } from '../../src/session/messages.js';
-import { estimateTokensForContent, estimateTokens } from '../../src/context/utils/tokens.js';
+import { estimateTokensForContent, estimateTokens } from '../../src/context/util.js';
import { encodeProjectPath } from '../../src/core/path.js';
import type { SessionIndex, SessionEvent } from '../../src/session/types.js';
From d107100c37c256593f3db67caca8154e86c01a3d Mon Sep 17 00:00:00 2001
From: phantom5099 <1011668688@qq.com>
Date: Thu, 11 Jun 2026 19:33:13 +0800
Subject: [PATCH 08/11] delete repeat revertCheckpointFile
---
.../src/checkpoint/checkpoint-service.ts | 16 +---------------
packages/codingcode/src/client/direct.ts | 16 ----------------
.../codingcode/src/client/direct/sessions.ts | 10 ----------
packages/codingcode/src/client/http.ts | 9 ---------
packages/codingcode/src/client/http/sessions.ts | 5 -----
packages/codingcode/src/client/types.ts | 1 -
.../codingcode/src/server/routes/sessions.ts | 2 +-
.../test/checkpoint/checkpoint-undo.test.ts | 4 ++--
packages/codingcode/test/context/context.test.ts | 1 -
packages/codingcode/test/orchestrate.test.ts | 1 -
packages/codingcode/test/server/handler.test.ts | 1 -
packages/desktop/src/hooks/useAgent.ts | 3 +--
packages/desktop/src/lib/core-api.ts | 8 --------
13 files changed, 5 insertions(+), 72 deletions(-)
diff --git a/packages/codingcode/src/checkpoint/checkpoint-service.ts b/packages/codingcode/src/checkpoint/checkpoint-service.ts
index a42f01d..ea1658d 100644
--- a/packages/codingcode/src/checkpoint/checkpoint-service.ts
+++ b/packages/codingcode/src/checkpoint/checkpoint-service.ts
@@ -51,7 +51,7 @@ export interface RollbackPreviewDiff {
export interface CodeRestoreEntry {
id: string;
sessionId: string;
- action: 'checkpoint-file' | 'checkpoint-files' | 'rollback-to-turn';
+ action: 'checkpoint-files' | 'rollback-to-turn';
throughTurnId: number;
affectedTurns: number[];
selectedFiles: string[];
@@ -225,20 +225,6 @@ export class CheckpointService extends Effect.Service()('Chec
// ---- Revert ----
- revertCheckpointFile: (
- projectPath: string,
- sessionId: string,
- turnId: number,
- file: string
- ): CodeRollbackResult => {
- const sg = ensure(projectPath);
- const plan = getTurnRestorePlan(sg, sessionId, turnId);
- if (!plan) {
- return emptyRollbackResult(turnId);
- }
- return executeRollback(sessionId, plan, [file], 'checkpoint-file', sg, lockFor(projectPath));
- },
-
revertCheckpointFiles: (
projectPath: string,
sessionId: string,
diff --git a/packages/codingcode/src/client/direct.ts b/packages/codingcode/src/client/direct.ts
index 872627b..36875ac 100644
--- a/packages/codingcode/src/client/direct.ts
+++ b/packages/codingcode/src/client/direct.ts
@@ -194,22 +194,6 @@ export async function createDirectClient(llm: any): Promise {
});
},
- async revertCheckpointFile(turnId: number, file: string) {
- if (!currentSessionId)
- return {
- reverted: false,
- throughTurnId: turnId,
- affectedTurns: [],
- selectedFiles: [],
- restoreEntry: null,
- };
- return clients.sessions.revertCheckpointFile({
- sessionId: currentSessionId,
- cwd: cwd(),
- file,
- });
- },
-
async revertCheckpointFiles(turnId: number, files: string[]) {
if (!currentSessionId)
return {
diff --git a/packages/codingcode/src/client/direct/sessions.ts b/packages/codingcode/src/client/direct/sessions.ts
index 7ea4acc..00788ef 100644
--- a/packages/codingcode/src/client/direct/sessions.ts
+++ b/packages/codingcode/src/client/direct/sessions.ts
@@ -17,7 +17,6 @@ export interface SessionClient {
setSessionPermissionMode(input: { sessionId: string; mode: PermissionMode }): Promise;
getCheckpointDiff(input: { sessionId: string; cwd: string; turnId?: number }): Promise;
- revertCheckpointFile(input: { sessionId: string; cwd: string; file: string }): Promise;
revertCheckpointFiles(input: { sessionId: string; cwd: string; files: string[] }): Promise;
previewRollbackDiff(input: {
sessionId: string;
@@ -119,15 +118,6 @@ export function createDirectSessionClient(
async getCheckpointDiff() {
return { turnId: 0, files: [] };
},
- async revertCheckpointFile() {
- return {
- reverted: false,
- throughTurnId: 0,
- affectedTurns: [],
- selectedFiles: [],
- restoreEntry: null,
- };
- },
async revertCheckpointFiles() {
return {
reverted: false,
diff --git a/packages/codingcode/src/client/http.ts b/packages/codingcode/src/client/http.ts
index 13d3815..d2afac2 100644
--- a/packages/codingcode/src/client/http.ts
+++ b/packages/codingcode/src/client/http.ts
@@ -133,15 +133,6 @@ export async function createHttpClient(serverUrl: string): Promise
async getCheckpointDiff() {
return { turnId: 0, files: [] };
},
- async revertCheckpointFile() {
- return {
- reverted: false,
- throughTurnId: 0,
- affectedTurns: [],
- selectedFiles: [],
- restoreEntry: null,
- };
- },
async revertCheckpointFiles() {
return {
reverted: false,
diff --git a/packages/codingcode/src/client/http/sessions.ts b/packages/codingcode/src/client/http/sessions.ts
index c83adee..a370a8d 100644
--- a/packages/codingcode/src/client/http/sessions.ts
+++ b/packages/codingcode/src/client/http/sessions.ts
@@ -18,7 +18,6 @@ export interface SessionClient {
cwd: string;
turnId?: number;
}): Promise<{ turnId: number; files: any[] }>;
- revertCheckpointFile(input: { sessionId: string; cwd: string; file: string }): Promise;
revertCheckpointFiles(input: { sessionId: string; cwd: string; files: string[] }): Promise;
previewRollbackDiff(input: {
sessionId: string;
@@ -95,10 +94,6 @@ export function createHttpSessionClient(
);
},
- async revertCheckpointFile({ sessionId, cwd, file }) {
- return apiPost(`/api/sessions/${sessionId}/checkpoints/latest/revert-file`, { cwd, file });
- },
-
async revertCheckpointFiles({ sessionId, cwd, files }) {
return apiPost(`/api/sessions/${sessionId}/checkpoints/latest/revert-files`, { cwd, files });
},
diff --git a/packages/codingcode/src/client/types.ts b/packages/codingcode/src/client/types.ts
index d89cc58..43be54b 100644
--- a/packages/codingcode/src/client/types.ts
+++ b/packages/codingcode/src/client/types.ts
@@ -36,7 +36,6 @@ export interface AgentClient {
Array<{ turnId: number; title: string; files: string[] }>
>;
getCheckpointDiff(turnId?: number): Promise;
- revertCheckpointFile(turnId: number, file: string): Promise;
revertCheckpointFiles(turnId: number, files: string[]): Promise;
previewRollbackDiff(throughTurnId: number): Promise;
rollbackCodeToTurn(throughTurnId: number): Promise;
diff --git a/packages/codingcode/src/server/routes/sessions.ts b/packages/codingcode/src/server/routes/sessions.ts
index 381f8b8..4a641ea 100644
--- a/packages/codingcode/src/server/routes/sessions.ts
+++ b/packages/codingcode/src/server/routes/sessions.ts
@@ -229,7 +229,7 @@ sessionsRouter.post('/:id/checkpoints/latest/revert-file', async (c) => {
restoreEntry: null,
};
const latestTurnId = completedTurns[completedTurns.length - 1]!;
- return checkpoint.revertCheckpointFile(cwd, sessionId, latestTurnId, body.file);
+ return checkpoint.revertCheckpointFiles(cwd, sessionId, latestTurnId, [body.file]);
})
);
if (!result.ok) {
diff --git a/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts b/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
index eb61e8a..fd1217f 100644
--- a/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
+++ b/packages/codingcode/test/checkpoint/checkpoint-undo.test.ts
@@ -163,7 +163,7 @@ describe('undoLastCodeRollback end-to-end via ShadowGit', () => {
const entry = {
id: 'test123',
sessionId,
- action: 'checkpoint-file',
+ action: 'checkpoint-files',
throughTurnId: 1,
affectedTurns: [],
selectedFiles: [join(projectPath, 'src/main.ts')],
@@ -364,7 +364,7 @@ describe('undoLastCodeRollback case-insensitive path matching', () => {
const entry = {
id: 'test123',
sessionId,
- action: 'checkpoint-file',
+ action: 'checkpoint-files',
throughTurnId: 1,
affectedTurns: [],
selectedFiles: [join(projectPath, 'src/main.ts').toLowerCase()],
diff --git a/packages/codingcode/test/context/context.test.ts b/packages/codingcode/test/context/context.test.ts
index 214777a..3314c8d 100644
--- a/packages/codingcode/test/context/context.test.ts
+++ b/packages/codingcode/test/context/context.test.ts
@@ -87,7 +87,6 @@ const MockCheckpointLayer = Layer.succeed(
getCompletedTurns: () => [],
getCheckpoints: () => [],
getCheckpointDiff: () => ({ turnId: 0, files: [] }),
- revertCheckpointFile: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
revertCheckpointFiles: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
previewRollbackDiff: () => ({ throughTurnId: 0, affectedTurns: [], diff: '' }),
rollbackCodeToTurn: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
diff --git a/packages/codingcode/test/orchestrate.test.ts b/packages/codingcode/test/orchestrate.test.ts
index 2aae804..7cc6322 100644
--- a/packages/codingcode/test/orchestrate.test.ts
+++ b/packages/codingcode/test/orchestrate.test.ts
@@ -186,7 +186,6 @@ const MockCheckpointLayer = Layer.succeed(
getCompletedTurns: () => [],
getCheckpoints: () => [],
getCheckpointDiff: () => ({ turnId: 0, files: [] }),
- revertCheckpointFile: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
revertCheckpointFiles: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
previewRollbackDiff: () => ({ throughTurnId: 0, affectedTurns: [], diff: '' }),
rollbackCodeToTurn: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
diff --git a/packages/codingcode/test/server/handler.test.ts b/packages/codingcode/test/server/handler.test.ts
index c69d35e..a238dfc 100644
--- a/packages/codingcode/test/server/handler.test.ts
+++ b/packages/codingcode/test/server/handler.test.ts
@@ -197,7 +197,6 @@ const MockCheckpointLayer = Layer.succeed(
getCompletedTurns: () => [],
getCheckpoints: () => [],
getCheckpointDiff: () => ({ turnId: 0, files: [] }),
- revertCheckpointFile: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
revertCheckpointFiles: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
previewRollbackDiff: () => ({ throughTurnId: 0, affectedTurns: [], diff: '' }),
rollbackCodeToTurn: () => ({ reverted: false, throughTurnId: 0, affectedTurns: [], selectedFiles: [], restoreEntry: null }),
diff --git a/packages/desktop/src/hooks/useAgent.ts b/packages/desktop/src/hooks/useAgent.ts
index 64060d5..5857ac5 100644
--- a/packages/desktop/src/hooks/useAgent.ts
+++ b/packages/desktop/src/hooks/useAgent.ts
@@ -11,7 +11,6 @@ import {
deleteSession,
sendApprovalResponse,
getCheckpointDiff,
- revertCheckpointFile,
revertCheckpointFiles,
previewRollbackDiff,
rollbackCodeToTurn,
@@ -395,7 +394,7 @@ export function useAgentRollback() {
const revertFile = useCallback(
async (threadId: string, file: string) => {
const cwd = useGlobalStore.getState().agent.threads[threadId]?.cwd ?? workspace.rootPath;
- const { result } = await revertCheckpointFile(threadId, cwd, file);
+ const { result } = await revertCheckpointFiles(threadId, cwd, [file]);
if (result.reverted) {
markFileReverted(threadId, resolveUITurnId(threadId, result.throughTurnId), file);
}
diff --git a/packages/desktop/src/lib/core-api.ts b/packages/desktop/src/lib/core-api.ts
index 9c2ed8c..804f04c 100644
--- a/packages/desktop/src/lib/core-api.ts
+++ b/packages/desktop/src/lib/core-api.ts
@@ -281,14 +281,6 @@ export function getCheckpointDiff(
return clients.sessions.getCheckpointDiff({ sessionId, cwd, turnId }) as any;
}
-export function revertCheckpointFile(
- sessionId: string,
- cwd: string,
- file: string
-): Promise<{ ok: boolean; result: CodeRollbackResult }> {
- return clients.sessions.revertCheckpointFile({ sessionId, cwd, file }) as any;
-}
-
export function revertCheckpointFiles(
sessionId: string,
cwd: string,
From 9d5024068ebe67f42ae0b64c1810967f58ab095e Mon Sep 17 00:00:00 2001
From: phantom5099 <1011668688@qq.com>
Date: Thu, 11 Jun 2026 20:11:23 +0800
Subject: [PATCH 09/11] delete distributed llmclient sources
---
.../codingcode/src/context/compaction-llm.ts | 33 -----
packages/codingcode/src/context/compressor.ts | 6 +-
packages/codingcode/src/llm/llm-resolver.ts | 18 +++
packages/codingcode/src/memory/index.ts | 4 +-
.../codingcode/src/memory/llm-resolver.ts | 26 ----
.../compressor/compact-if-needed.test.ts | 10 +-
.../context/compressor/llm-resolver.test.ts | 84 ++++++++---
.../test/memory/llm-resolver.test.ts | 137 ++++++------------
8 files changed, 140 insertions(+), 178 deletions(-)
delete mode 100644 packages/codingcode/src/context/compaction-llm.ts
create mode 100644 packages/codingcode/src/llm/llm-resolver.ts
delete mode 100644 packages/codingcode/src/memory/llm-resolver.ts
diff --git a/packages/codingcode/src/context/compaction-llm.ts b/packages/codingcode/src/context/compaction-llm.ts
deleted file mode 100644
index 9b510ad..0000000
--- a/packages/codingcode/src/context/compaction-llm.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { findModel, createClient } from '../llm/factory.js';
-import type { LLMClient } from '../llm/client.js';
-import type { ContextConfig } from './config.js';
-
-/**
- * Resolve which LLM client to use for compaction.
- *
- * Selection order:
- * 1. If `config.compactionModel` is empty → fallback (main session LLM).
- * 2. Match in `config/models.json` with priority:
- * 1) Full id format (e.g. "deepseek-chat@DEEPSEEK_API_KEY") - exact match only
- * 2) Bare model id (e.g. "deepseek-chat") - first match
- * 3) Display name (e.g. "DeepSeek Chat") - first match
- * To avoid ambiguity when multiple providers have same model name, use full id.
- * 3. If no match or build fails → fallback.
- */
-export async function resolveCompactionLLM(
- config: ContextConfig,
- fallback: LLMClient | null
-): Promise {
- const target = config.compactionModel?.trim();
- if (!target) return fallback;
-
- const found = findModel(target);
- if (!found) return fallback;
-
- try {
- const created = await createClient(found);
- return created.ok ? created.value : fallback;
- } catch {
- return fallback;
- }
-}
diff --git a/packages/codingcode/src/context/compressor.ts b/packages/codingcode/src/context/compressor.ts
index 28d913a..929f1e0 100644
--- a/packages/codingcode/src/context/compressor.ts
+++ b/packages/codingcode/src/context/compressor.ts
@@ -6,7 +6,7 @@ import {
estimateTokensForContent,
} from './util.js';
import { applyVisibilityEvents } from '../session/messages.js';
-import { resolveCompactionLLM } from './compaction-llm.js';
+import { resolveLLM } from '../llm/llm-resolver.js';
import { COMPACTION_SYSTEM_PROMPT } from './compaction-prompt.js';
import type { ContextConfig } from './config.js';
import type { Message } from '../core/types.js';
@@ -146,7 +146,7 @@ async function tryCompaction(
const totalTokens = targetEvents.reduce((sum, e) => sum + estimateEventTokens(e), 0);
- let compactionLlm = await resolveCompactionLLM(config, llm);
+ let compactionLlm = await resolveLLM(config.compactionModel, llm);
if (compactionLlm && compactionLlm.modelInfo.maxTokens < totalTokens + 25000) {
compactionLlm = llm;
}
@@ -254,7 +254,7 @@ async function callLLMForCompaction(
fallbackLlm: LLMClient | null,
config: ContextConfig
): Promise {
- const llm = await resolveCompactionLLM(config, fallbackLlm);
+ const llm = await resolveLLM(config.compactionModel, fallbackLlm);
if (!llm) return null;
const transcriptText = transcript
diff --git a/packages/codingcode/src/llm/llm-resolver.ts b/packages/codingcode/src/llm/llm-resolver.ts
new file mode 100644
index 0000000..9657430
--- /dev/null
+++ b/packages/codingcode/src/llm/llm-resolver.ts
@@ -0,0 +1,18 @@
+import { findModel, createClient } from './factory.js';
+import type { LLMClient } from './client.js';
+
+export async function resolveLLM(
+ target: string | null | undefined,
+ fallback: LLMClient | null,
+): Promise {
+ const trimmed = target?.trim();
+ if (!trimmed) return fallback;
+ const found = findModel(trimmed);
+ if (!found) return fallback;
+ try {
+ const created = await createClient(found);
+ return created.ok ? created.value : fallback;
+ } catch {
+ return fallback;
+ }
+}
diff --git a/packages/codingcode/src/memory/index.ts b/packages/codingcode/src/memory/index.ts
index 67ee952..5b1a4d5 100644
--- a/packages/codingcode/src/memory/index.ts
+++ b/packages/codingcode/src/memory/index.ts
@@ -12,7 +12,7 @@ import {
writeMemoryFileAtomic,
stripMarkersForPrompt,
} from './storage.js';
-import { resolveMemoryLLM } from './llm-resolver.js';
+import { resolveLLM } from '../llm/llm-resolver.js';
import { getMemoryConfig, getEffectiveTypes } from './config.js';
import { updateMemoryEnabled } from '@codingcode/infra/config';
import { extractMemory, type StructuredTranscript } from './extractor.js';
@@ -154,7 +154,7 @@ export async function flushSessionToMemory(
const transcript = buildStructuredTranscript(events);
const types = getEffectiveTypes(cfg);
- const resolvedLlm = await resolveMemoryLLM(cfg, llm);
+ const resolvedLlm = await resolveLLM(cfg.model, llm);
if (!resolvedLlm) {
return { written: false, bytes: 0 };
}
diff --git a/packages/codingcode/src/memory/llm-resolver.ts b/packages/codingcode/src/memory/llm-resolver.ts
deleted file mode 100644
index 8d2abd1..0000000
--- a/packages/codingcode/src/memory/llm-resolver.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { listModels, createClient } from '../llm/factory.js';
-import type { LLMClient } from '../llm/client.js';
-import type { MemoryConfig } from '@codingcode/infra/config';
-
-export async function resolveMemoryLLM(
- config: MemoryConfig,
- fallback: LLMClient | null
-): Promise {
- const target = config.model?.trim();
- if (!target) return fallback;
-
- const listResult = listModels();
- if (!listResult.ok) return fallback;
-
- const found = listResult.value.find(
- (m) => m.id === target || m.model === target || m.name === target
- );
- if (!found) return fallback;
-
- try {
- const created = await createClient(found);
- return created.ok ? created.value : fallback;
- } catch {
- return fallback;
- }
-}
diff --git a/packages/codingcode/test/context/compressor/compact-if-needed.test.ts b/packages/codingcode/test/context/compressor/compact-if-needed.test.ts
index 394e04c..0e59945 100644
--- a/packages/codingcode/test/context/compressor/compact-if-needed.test.ts
+++ b/packages/codingcode/test/context/compressor/compact-if-needed.test.ts
@@ -39,9 +39,13 @@ vi.mock('../../../src/session/io.js', async (importOriginal) => {
};
});
-vi.mock('../../../src/context/compaction-llm.js', () => ({
- resolveCompactionLLM: vi.fn(() => Promise.resolve(mockLLM)),
-}));
+vi.mock('../../../src/llm/llm-resolver.js', async (importOriginal) => {
+ const actual: any = await importOriginal();
+ return {
+ ...actual,
+ resolveLLM: vi.fn(() => Promise.resolve(mockLLM)),
+ };
+});
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal();
diff --git a/packages/codingcode/test/context/compressor/llm-resolver.test.ts b/packages/codingcode/test/context/compressor/llm-resolver.test.ts
index ed59183..49a77cc 100644
--- a/packages/codingcode/test/context/compressor/llm-resolver.test.ts
+++ b/packages/codingcode/test/context/compressor/llm-resolver.test.ts
@@ -1,7 +1,21 @@
-import { describe, it, expect } from 'vitest';
-import { resolveCompactionLLM } from '../../../src/context/compaction-llm.js';
+import { describe, it, expect, vi, afterEach } from 'vitest';
import type { LLMClient } from '../../../src/llm/client.js';
-import type { ContextConfig } from '../../../src/context/config.js';
+
+const { mockFindModel, mockCreateClient } = vi.hoisted(() => ({
+ mockFindModel: vi.fn(() => null),
+ mockCreateClient: vi.fn(),
+}));
+
+vi.mock('../../../src/llm/factory.js', async (importOriginal) => {
+ const actual: any = await importOriginal();
+ return {
+ ...actual,
+ findModel: mockFindModel,
+ createClient: mockCreateClient,
+ };
+});
+
+import { resolveLLM } from '../../../src/llm/llm-resolver.js';
const fakeFallback: LLMClient = {
complete: async () => ({ ok: true as const, value: { content: '', finishReason: 'stop' } }),
@@ -18,35 +32,61 @@ const fakeFallback: LLMClient = {
},
};
-function cfg(compactionModel: string): ContextConfig {
- return {
- microCompactThreshold: 0.5,
- microCompactMinChars: 120,
- compactionThreshold: 0.9,
- keepRecentTurns: 1,
- compactionModel,
- reactiveCompactMaxRetries: 1,
- };
-}
+describe('resolveLLM (compaction)', () => {
+ afterEach(() => {
+ vi.resetAllMocks();
+ });
+
+ it('returns fallback when target is empty', async () => {
+ const result = await resolveLLM('', fakeFallback);
+ expect(result).toBe(fakeFallback);
+ });
-describe('resolveCompactionLLM', () => {
- it('returns fallback when compactionModel is empty', async () => {
- const result = await resolveCompactionLLM(cfg(''), fakeFallback);
+ it('returns fallback when target is whitespace-only', async () => {
+ const result = await resolveLLM(' ', fakeFallback);
expect(result).toBe(fakeFallback);
});
- it('returns fallback when compactionModel is whitespace-only', async () => {
- const result = await resolveCompactionLLM(cfg(' '), fakeFallback);
+ it('returns fallback when target is null', async () => {
+ const result = await resolveLLM(null, fakeFallback);
expect(result).toBe(fakeFallback);
});
- it('returns fallback when target model is not in models.json', async () => {
- const result = await resolveCompactionLLM(cfg('definitely-not-a-real-model-xyz'), fakeFallback);
+ it('returns fallback when target is undefined', async () => {
+ const result = await resolveLLM(undefined, fakeFallback);
expect(result).toBe(fakeFallback);
});
- it('returns null when compactionModel empty and no fallback given', async () => {
- const result = await resolveCompactionLLM(cfg(''), null);
+ it('returns null when target empty and fallback is null', async () => {
+ const result = await resolveLLM('', null);
expect(result).toBeNull();
});
+
+ it('returns fallback when model not found', async () => {
+ mockFindModel.mockReturnValue(null);
+ const result = await resolveLLM('definitely-not-a-real-model-xyz', fakeFallback);
+ expect(result).toBe(fakeFallback);
+ });
+
+ it('returns fallback when createClient throws', async () => {
+ mockFindModel.mockReturnValue({ id: 'test-model' } as any);
+ mockCreateClient.mockRejectedValue(new Error('creation failed'));
+ const result = await resolveLLM('test-model', fakeFallback);
+ expect(result).toBe(fakeFallback);
+ });
+
+ it('returns fallback when createClient returns error', async () => {
+ mockFindModel.mockReturnValue({ id: 'test-model' } as any);
+ mockCreateClient.mockResolvedValue({ ok: false, error: 'error' });
+ const result = await resolveLLM('test-model', fakeFallback);
+ expect(result).toBe(fakeFallback);
+ });
+
+ it('returns created client on success', async () => {
+ const client = { modelInfo: { maxTokens: 100 } } as LLMClient;
+ mockFindModel.mockReturnValue({ id: 'test-model' } as any);
+ mockCreateClient.mockResolvedValue({ ok: true, value: client });
+ const result = await resolveLLM('test-model', fakeFallback);
+ expect(result).toBe(client);
+ });
});
diff --git a/packages/codingcode/test/memory/llm-resolver.test.ts b/packages/codingcode/test/memory/llm-resolver.test.ts
index ac62395..425a61d 100644
--- a/packages/codingcode/test/memory/llm-resolver.test.ts
+++ b/packages/codingcode/test/memory/llm-resolver.test.ts
@@ -1,114 +1,73 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
-import { resolveMemoryLLM } from '../../src/memory/llm-resolver.js';
import type { LLMClient } from '../../src/llm/client.js';
-import type { MemoryConfig } from '@codingcode/infra/config';
+import type { SelectableModel } from '../../src/llm/factory.js';
-vi.mock('../../src/llm/factory.js', () => ({
- listModels: vi.fn(() => ({
- ok: true,
- value: [
- {
- id: 'claude-opus-4-7',
- model: 'claude-opus-4-7',
- name: 'Claude Opus 4.7',
- provider: 'anthropic',
- },
- {
- id: 'deepseek@deepseek',
- model: 'deepseek-chat',
- name: 'DeepSeek Chat',
- provider: 'deepseek',
- },
- ],
- })),
- createClient: vi.fn(async (_modelInfo: any) => ({
- ok: true,
- value: {
- complete: () => Promise.resolve({ ok: true, value: { content: '' } }),
- completeStream: () => ({
- stream: async function* () {},
- response: Promise.resolve({ ok: true, value: { content: '' } }),
- }),
- modelInfo: {
- provider: 'mock',
- model: 'mock',
- maxTokens: 4096,
- supportsToolCalling: true,
- supportsStreaming: true,
- },
- } as any,
- })),
+const { mockFindModel, mockCreateClient } = vi.hoisted(() => ({
+ mockFindModel: vi.fn(() => null),
+ mockCreateClient: vi.fn(),
}));
-describe('Memory LLM Resolver', () => {
+vi.mock('../../src/llm/factory.js', async (importOriginal) => {
+ const actual: any = await importOriginal();
+ return {
+ ...actual,
+ findModel: mockFindModel,
+ createClient: mockCreateClient,
+ };
+});
+
+import { resolveLLM } from '../../src/llm/llm-resolver.js';
+
+const fallbackClient = {} as LLMClient;
+
+describe('resolveLLM (memory)', () => {
afterEach(() => {
vi.resetAllMocks();
});
- const createCfg = (model: string): MemoryConfig => ({
- enabled: true,
- model,
- projectFile: '',
- userFile: '',
- maxBytes: 16384,
- promptMaxBytes: 8192,
- extraTypes: [],
- disabledTypes: [],
+ it('returns fallback when target is empty', async () => {
+ const result = await resolveLLM('', fallbackClient);
+ expect(result).toBe(fallbackClient);
});
- it('returns fallback when model is empty', async () => {
- const cfg = createCfg('');
- const fallback = {} as LLMClient;
- const result = await resolveMemoryLLM(cfg, fallback);
- expect(result).toBe(fallback);
- });
-
- it('returns fallback when listModels fails', async () => {
- const { listModels } = await import('../../src/llm/factory.js');
- vi.mocked(listModels).mockReturnValue({ ok: false, error: 'error' } as any);
-
- const cfg = createCfg('claude-opus-4-7');
- const fallback = {} as LLMClient;
- const result = await resolveMemoryLLM(cfg, fallback);
- expect(result).toBe(fallback);
+ it('returns fallback when target is whitespace-only', async () => {
+ const result = await resolveLLM(' ', fallbackClient);
+ expect(result).toBe(fallbackClient);
});
it('returns fallback when model not found', async () => {
- const cfg = createCfg('nonexistent-model');
- const fallback = {} as LLMClient;
- const result = await resolveMemoryLLM(cfg, fallback);
- expect(result).toBe(fallback);
+ mockFindModel.mockReturnValue(null);
+ const result = await resolveLLM('nonexistent-model', fallbackClient);
+ expect(result).toBe(fallbackClient);
});
- it('returns null fallback when create fails', async () => {
- const { createClient } = await import('../../src/llm/factory.js');
- vi.mocked(createClient).mockRejectedValue(new Error('creation failed'));
-
- const cfg = createCfg('claude-opus-4-7');
- const result = await resolveMemoryLLM(cfg, null);
- expect(result).toBe(null);
+ it('returns null when fallback is null and create fails', async () => {
+ mockFindModel.mockReturnValue({ id: 'claude-opus-4-7' } as SelectableModel);
+ mockCreateClient.mockRejectedValue(new Error('creation failed'));
+ const result = await resolveLLM('claude-opus-4-7', null);
+ expect(result).toBeNull();
});
- it('returns null fallback when create returns error', async () => {
- const { createClient } = await import('../../src/llm/factory.js');
- vi.mocked(createClient).mockResolvedValue({ ok: false, error: 'error' } as any);
-
- const cfg = createCfg('claude-opus-4-7');
- const result = await resolveMemoryLLM(cfg, null);
- expect(result).toBe(null);
+ it('returns null when fallback is null and create returns error', async () => {
+ mockFindModel.mockReturnValue({ id: 'claude-opus-4-7' } as SelectableModel);
+ mockCreateClient.mockResolvedValue({ ok: false, error: 'error' });
+ const result = await resolveLLM('claude-opus-4-7', null);
+ expect(result).toBeNull();
});
it('creates and returns client when model matches by id', async () => {
- const cfg = createCfg('claude-opus-4-7');
- const fallback = {} as LLMClient;
- const result = await resolveMemoryLLM(cfg, fallback);
- expect(result).not.toBe(fallback);
+ const client = { modelInfo: { maxTokens: 4096 } } as LLMClient;
+ mockFindModel.mockReturnValue({ id: 'claude-opus-4-7@ANTHROPIC_API_KEY' } as SelectableModel);
+ mockCreateClient.mockResolvedValue({ ok: true, value: client });
+ const result = await resolveLLM('claude-opus-4-7@ANTHROPIC_API_KEY', fallbackClient);
+ expect(result).toBe(client);
});
- it('creates and returns client when model matches by bare id', async () => {
- const cfg = createCfg('deepseek-chat');
- const fallback = {} as LLMClient;
- const result = await resolveMemoryLLM(cfg, fallback);
- expect(result).not.toBe(fallback);
+ it('creates and returns client when model matches by bare model id', async () => {
+ const client = { modelInfo: { maxTokens: 4096 } } as LLMClient;
+ mockFindModel.mockReturnValue({ id: 'deepseek-chat@DEEPSEEK_API_KEY', model: 'deepseek-chat' } as SelectableModel);
+ mockCreateClient.mockResolvedValue({ ok: true, value: client });
+ const result = await resolveLLM('deepseek-chat', fallbackClient);
+ expect(result).toBe(client);
});
});
From b52756c6f708addb8e539764fcb43f1a9c4c502c Mon Sep 17 00:00:00 2001
From: phantom5099 <1011668688@qq.com>
Date: Thu, 11 Jun 2026 20:28:04 +0800
Subject: [PATCH 10/11] rename restore planning and store
---
.../src/checkpoint/checkpoint-service.ts | 4 ++--
.../codingcode/src/checkpoint/rollback-engine.ts | 2 +-
.../{restore-planning.ts => turn-query.ts} | 0
.../{restore-store.ts => undo-store.ts} | 0
packages/codingcode/src/context/compressor.ts | 15 ++++-----------
5 files changed, 7 insertions(+), 14 deletions(-)
rename packages/codingcode/src/checkpoint/{restore-planning.ts => turn-query.ts} (100%)
rename packages/codingcode/src/checkpoint/{restore-store.ts => undo-store.ts} (100%)
diff --git a/packages/codingcode/src/checkpoint/checkpoint-service.ts b/packages/codingcode/src/checkpoint/checkpoint-service.ts
index ea1658d..5c18953 100644
--- a/packages/codingcode/src/checkpoint/checkpoint-service.ts
+++ b/packages/codingcode/src/checkpoint/checkpoint-service.ts
@@ -5,12 +5,12 @@ import { ShadowGit } from './shadow-git.js';
import { ProjectLock } from './project-lock.js';
import { normalizePath } from '../core/path.js';
import { shortSid, commitMsg, toGitPath, hashWorkspaceFile, ProjectCache } from './utils.js';
-import { readRestoreEntry, writeRestoreEntry } from './restore-store.js';
+import { readRestoreEntry, writeRestoreEntry } from './undo-store.js';
import {
getCompletedTurnsFor,
getTurnRestorePlan,
getRollbackToTurnPlan,
-} from './restore-planning.js';
+} from './turn-query.js';
import { emptyRollbackResult, executeRollback } from './rollback-engine.js';
// ---- Exported types ----
diff --git a/packages/codingcode/src/checkpoint/rollback-engine.ts b/packages/codingcode/src/checkpoint/rollback-engine.ts
index 949edad..3238e44 100644
--- a/packages/codingcode/src/checkpoint/rollback-engine.ts
+++ b/packages/codingcode/src/checkpoint/rollback-engine.ts
@@ -4,7 +4,7 @@ import type { ShadowGit } from './shadow-git.js';
import type { ProjectLock } from './project-lock.js';
import type { CodeRollbackResult, CodeRestoreEntry } from './checkpoint-service.js';
import { commitMsg } from './utils.js';
-import { readRestoreEntry, writeRestoreEntry } from './restore-store.js';
+import { readRestoreEntry, writeRestoreEntry } from './undo-store.js';
export function emptyRollbackResult(
turnId: number
diff --git a/packages/codingcode/src/checkpoint/restore-planning.ts b/packages/codingcode/src/checkpoint/turn-query.ts
similarity index 100%
rename from packages/codingcode/src/checkpoint/restore-planning.ts
rename to packages/codingcode/src/checkpoint/turn-query.ts
diff --git a/packages/codingcode/src/checkpoint/restore-store.ts b/packages/codingcode/src/checkpoint/undo-store.ts
similarity index 100%
rename from packages/codingcode/src/checkpoint/restore-store.ts
rename to packages/codingcode/src/checkpoint/undo-store.ts
diff --git a/packages/codingcode/src/context/compressor.ts b/packages/codingcode/src/context/compressor.ts
index 929f1e0..e2f631d 100644
--- a/packages/codingcode/src/context/compressor.ts
+++ b/packages/codingcode/src/context/compressor.ts
@@ -1,5 +1,5 @@
import { randomUUID } from 'crypto';
-import { resolveSessionDir } from '../session/io.js';
+import { resolveSessionDir, appendLine } from '../session/io.js';
import {
estimateTokens,
estimateMessageTokens,
@@ -14,7 +14,6 @@ import type { SessionEvent, SummaryEvent } from '../session/types.js';
import type { LLMClient } from '../llm/client.js';
import { assemblePayload } from './organizer.js';
import { join } from 'path';
-import { appendFileSync } from 'fs';
export interface CompressResult {
didCompress: boolean;
@@ -107,14 +106,6 @@ export async function compactWithLLM(
};
}
-// ---------- Summary persistence ----------
-
-function appendSummaryToSession(sessionId: string, event: SummaryEvent): void {
- const dir = resolveSessionDir(sessionId);
- if (!dir) throw new Error(`Session ${sessionId} not found`);
- const jsonlPath = join(dir, `${sessionId}.jsonl`);
- appendFileSync(jsonlPath, JSON.stringify(event) + '\n', 'utf8');
-}
// ---------- LLM Compaction ----------
@@ -173,7 +164,9 @@ async function tryCompaction(
lastSummarizedTurnId: lastTurnId,
timestamp: new Date().toISOString(),
};
- appendSummaryToSession(sessionId, event);
+ const dir = resolveSessionDir(sessionId);
+ if (!dir) throw new Error(`Session ${sessionId} not found`);
+ appendLine(join(dir, `${sessionId}.jsonl`), event);
for (const u of replacedUuids) hidden.add(u);
const summaryMsg: Message = { role: 'system', name: 'compacted_history', content: summary };
From 1e152008b6815ccc3b00c2b761601cdfbf37c2f6 Mon Sep 17 00:00:00 2001
From: phantom5099 <1011668688@qq.com>
Date: Thu, 11 Jun 2026 21:40:00 +0800
Subject: [PATCH 11/11] delete buildTranscript
---
packages/codingcode/src/context/compressor.ts | 94 +++----------------
packages/codingcode/src/context/organizer.ts | 13 +--
packages/codingcode/src/session/io.ts | 6 ++
packages/codingcode/src/session/messages.ts | 8 +-
.../compressor/compact-if-needed.test.ts | 8 +-
5 files changed, 39 insertions(+), 90 deletions(-)
diff --git a/packages/codingcode/src/context/compressor.ts b/packages/codingcode/src/context/compressor.ts
index e2f631d..3282f3f 100644
--- a/packages/codingcode/src/context/compressor.ts
+++ b/packages/codingcode/src/context/compressor.ts
@@ -1,11 +1,10 @@
import { randomUUID } from 'crypto';
-import { resolveSessionDir, appendLine } from '../session/io.js';
+import { resolveSessionJsonlPath, appendLine } from '../session/io.js';
import {
estimateTokens,
estimateMessageTokens,
- estimateTokensForContent,
} from './util.js';
-import { applyVisibilityEvents } from '../session/messages.js';
+import { buildMessagesFromEvents } from '../session/messages.js';
import { resolveLLM } from '../llm/llm-resolver.js';
import { COMPACTION_SYSTEM_PROMPT } from './compaction-prompt.js';
import type { ContextConfig } from './config.js';
@@ -13,7 +12,6 @@ import type { Message } from '../core/types.js';
import type { SessionEvent, SummaryEvent } from '../session/types.js';
import type { LLMClient } from '../llm/client.js';
import { assemblePayload } from './organizer.js';
-import { join } from 'path';
export interface CompressResult {
didCompress: boolean;
@@ -85,8 +83,8 @@ export async function compactWithLLM(
usage?: number,
modelMaxTokens?: number
): Promise {
+ const payload = assemblePayload(sessionId, encodedProjectPath, config, modelMaxTokens);
if (!compactedEvents || currentTurnId === undefined) {
- const payload = assemblePayload(sessionId, encodedProjectPath, config, modelMaxTokens);
compactedEvents = payload.compactedEvents;
currentTurnId = payload.currentTurnId;
}
@@ -95,38 +93,33 @@ export async function compactWithLLM(
const threshold = modelMaxTokens ? modelMaxTokens * config.compactionThreshold : Infinity;
if (usage === undefined || usage - released > threshold) {
- released += await tryCompaction(sessionId, config, llm, compactedEvents, currentTurnId);
+ released += await tryCompaction(sessionId, config, llm, compactedEvents, currentTurnId, payload.compactedTurnIds);
}
- const payload = assemblePayload(sessionId, encodedProjectPath, config, modelMaxTokens);
+ const postPayload = assemblePayload(sessionId, encodedProjectPath, config, modelMaxTokens);
return {
didCompress: released > 0,
released,
- promptEstimate: estimateTokens(payload.messages),
+ promptEstimate: estimateTokens(postPayload.messages),
};
}
// ---------- LLM Compaction ----------
-const ESTIMATED_SUMMARY_TOKENS = 5000;
-const MAX_TOOL_RESULT_TOKENS = 30000;
-
async function tryCompaction(
sessionId: string,
config: ContextConfig,
llm: LLMClient | null,
compactedEvents: SessionEvent[],
- currentTurnId: number
+ currentTurnId: number,
+ compactedTurnIds: Set,
): Promise