Skip to content

Commit ed7ddd0

Browse files
committed
feat: add contextWindow and autoCompactWindow settings with related functionality
1 parent 5e0fbc5 commit ed7ddd0

15 files changed

Lines changed: 344 additions & 23 deletions

File tree

docs/configuration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两
2727
| 字段 | 类型 | 说明 |
2828
| -------------------- | --------- | ------------------------------------------------------------------- |
2929
| `env` | object | 环境变量分组(见下方子字段表) |
30+
| `contextWindow` | number/string | 上下文窗口上限,可使用精确 token 数或 `128K``1M` 等格式 |
31+
| `autoCompactWindow` | number/string | 自动压缩阈值,默认取最终上下文窗口的 50% |
3032
| `model` | string | 模型名称。优先级高于 `env.MODEL` |
3133
| `thinkingEnabled` | boolean | 是否启用思考模式(DeepSeek V4 系列默认启用) |
3234
| `reasoningEffort` | string | 推理强度,可选 `"high"``"max"`(默认 `"max"`|
@@ -53,6 +55,19 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两
5355
| `TELEMETRY_ENABLED` | string | 是否启用匿名使用数据上报 |
5456
| `<其他任意KEY>` | string | 自定义环境变量 |
5557

58+
#### 上下文窗口
59+
60+
`contextWindow``autoCompactWindow``settings.json` 的顶层字段。number 必须是正整数,表示精确 token 数;string 使用大小写不敏感的 `K``M` 后缀,按 `1K = 1024``1M = 1024²` 换算:
61+
62+
```json
63+
{
64+
"contextWindow": "1M",
65+
"autoCompactWindow": "512K"
66+
}
67+
```
68+
69+
普通模型的默认上下文窗口为 `256K`,DeepSeek V4 系列为 `1M`。未设置自动压缩阈值时取最终上下文窗口的 50%;无效值会被忽略,自动压缩阈值超过上下文窗口时会限制为上下文窗口。
70+
5671
#### `thinkingEnabled` — 思考模式
5772

5873
是否启用 DeepSeek 思考模式。设置为 `true` 启用、`false` 禁用。

docs/configuration_en.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ The following are all the top-level fields supported in `settings.json`, along w
2727
| Field | Type | Description |
2828
| ------------------ | ------- | --------------------------------------------------------------------------- |
2929
| `env` | object | Group of environment variables (see sub-field table below) |
30+
| `contextWindow` | number/string | Context-window limit as an exact token count or `128K`/`1M` value |
31+
| `autoCompactWindow` | number/string | Auto-compaction threshold; defaults to 50% of the final context window |
3032
| `model` | string | Model name. Takes precedence over `env.MODEL` |
3133
| `thinkingEnabled` | boolean | Whether to enable thinking mode (enabled by default for DeepSeek V4 series)|
3234
| `reasoningEffort` | string | Reasoning intensity, either `"high"` or `"max"` (default `"max"`) |
@@ -53,6 +55,19 @@ The following are all the top-level fields supported in `settings.json`, along w
5355
| `TELEMETRY_ENABLED`| string| Enable anonymous usage reporting |
5456
| `<any other KEY>` | string | Custom environment variable |
5557

58+
#### Context Windows
59+
60+
`contextWindow` and `autoCompactWindow` are top-level `settings.json` fields. A number must be a positive integer and represents an exact token count. A string uses a case-insensitive `K` or `M` suffix, with `1K = 1024` and `1M = 1024²`:
61+
62+
```json
63+
{
64+
"contextWindow": "1M",
65+
"autoCompactWindow": "512K"
66+
}
67+
```
68+
69+
The default context window is `256K` for regular models and `1M` for DeepSeek V4 models. If the auto-compaction threshold is omitted, it is 50% of the final context window. Invalid values are ignored, and an auto-compaction threshold larger than the context window is capped at the context window.
70+
5671
#### `thinkingEnabled` — Thinking Mode
5772

5873
Whether to enable DeepSeek thinking mode. Set to `true` to enable, `false` to disable.

packages/cli/src/tests/exec-runner.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ function createSettings(
2525
env: {},
2626
baseURL: "https://example.invalid",
2727
model: "test-model",
28+
contextWindow: 256 * 1024,
29+
autoCompactWindow: 128 * 1024,
2830
thinkingEnabled: false,
2931
reasoningEffort: "high",
3032
debugLogEnabled: false,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test } from "node:test";
2+
import assert from "node:assert/strict";
3+
import type { SessionEntry } from "@vegamo/deepcode-core";
4+
import { buildStatusLine, formatContextUsage, formatTokenCount } from "../ui/utils/index";
5+
6+
test("formatTokenCount uses binary K/M units with at most one decimal", () => {
7+
assert.equal(formatTokenCount(0), "0");
8+
assert.equal(formatTokenCount(1023), "1023");
9+
assert.equal(formatTokenCount(1126), "1.1K");
10+
assert.equal(formatTokenCount(1024 * 1024), "1M");
11+
assert.equal(formatTokenCount(1.25 * 1024 * 1024), "1.3M");
12+
});
13+
14+
test("formatContextUsage rounds a ten-cell bar to whole blocks", () => {
15+
assert.equal(formatContextUsage(0, 1000), "0/1000 [░░░░░░░░░░] 0%");
16+
assert.equal(formatContextUsage(20, 1000), "20/1000 [░░░░░░░░░░] 2%");
17+
assert.equal(formatContextUsage(200, 1000), "200/1000 [▓▓░░░░░░░░] 20%");
18+
assert.equal(formatContextUsage(550, 1000), "550/1000 [▓▓▓▓▓▓░░░░] 55%");
19+
});
20+
21+
test("formatContextUsage caps the bar and percentage at 100 percent", () => {
22+
assert.equal(formatContextUsage(1200, 1000), "1.2K/1000 [▓▓▓▓▓▓▓▓▓▓] 100%");
23+
});
24+
25+
test("buildStatusLine replaces the tokens label while preserving status and failure", () => {
26+
const entry: SessionEntry = {
27+
id: "session-1",
28+
summary: null,
29+
assistantReply: null,
30+
assistantThinking: null,
31+
assistantRefusal: null,
32+
toolCalls: null,
33+
status: "failed",
34+
failReason: "boom",
35+
usage: null,
36+
usagePerModel: null,
37+
activeTokens: 20,
38+
createTime: "2026-01-01T00:00:00.000Z",
39+
updateTime: "2026-01-01T00:00:01.000Z",
40+
processes: null,
41+
};
42+
43+
assert.equal(buildStatusLine(entry, 1000), "status: failed · 20/1000 [░░░░░░░░░░] 2% · fail: boom");
44+
});
45+
46+
test("buildStatusLine omits context usage when no active tokens exist", () => {
47+
const entry = {
48+
status: "pending",
49+
activeTokens: 0,
50+
failReason: null,
51+
} as SessionEntry;
52+
53+
assert.equal(buildStatusLine(entry, 1024 * 1024), "status: pending");
54+
});

packages/cli/src/ui/utils/index.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,36 @@ export function isCurrentSessionEmpty(sessionManager: SessionManager): boolean {
7777
return !activeSessionId || !sessionManager.getSession(activeSessionId);
7878
}
7979

80-
export function buildStatusLine(entry: SessionEntry): string {
80+
const CONTEXT_BAR_WIDTH = 10;
81+
82+
export function formatTokenCount(tokens: number): string {
83+
if (!Number.isFinite(tokens) || tokens <= 0) {
84+
return "0";
85+
}
86+
if (tokens < 1024) {
87+
return String(Math.round(tokens));
88+
}
89+
90+
const unit = tokens >= 1024 * 1024 ? "M" : "K";
91+
const divisor = unit === "M" ? 1024 * 1024 : 1024;
92+
return `${Number((tokens / divisor).toFixed(1))}${unit}`;
93+
}
94+
95+
export function formatContextUsage(activeTokens: number, contextWindow: number): string {
96+
const safeActiveTokens = Number.isFinite(activeTokens) ? Math.max(0, activeTokens) : 0;
97+
const ratio = Number.isFinite(contextWindow) && contextWindow > 0 ? safeActiveTokens / contextWindow : 0;
98+
const cappedRatio = Math.min(1, ratio);
99+
const filledBlocks = Math.round(cappedRatio * CONTEXT_BAR_WIDTH);
100+
const bar = `${"▓".repeat(filledBlocks)}${"░".repeat(CONTEXT_BAR_WIDTH - filledBlocks)}`;
101+
const percent = Math.min(100, Math.round(ratio * 100));
102+
return `${formatTokenCount(safeActiveTokens)}/${formatTokenCount(contextWindow)} [${bar}] ${percent}%`;
103+
}
104+
105+
export function buildStatusLine(entry: SessionEntry, contextWindow: number): string {
81106
const parts: string[] = [];
82107
parts.push(`status: ${entry.status}`);
83108
if (typeof entry.activeTokens === "number" && entry.activeTokens > 0) {
84-
parts.push(`tokens: ${entry.activeTokens}`);
109+
parts.push(formatContextUsage(entry.activeTokens, contextWindow));
85110
}
86111
if (entry.failReason) {
87112
parts.push(`fail: ${entry.failReason}`);

packages/cli/src/ui/views/App.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import type {
4848
UserPromptContent,
4949
} from "@vegamo/deepcode-core";
5050
import { SessionManager } from "@vegamo/deepcode-core";
51-
import { getCompactPromptTokenThreshold } from "@vegamo/deepcode-core";
5251
import { writeStdout, writeStdoutLine } from "../../utils/stdio-helpers";
5352

5453
type View = "chat" | "session-list" | "undo" | "mcp-status";
@@ -154,7 +153,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
154153
}
155154
},
156155
onSessionEntryUpdated: (entry) => {
157-
setStatusLine(buildStatusLine(entry));
156+
setStatusLine(buildStatusLine(entry, resolveCurrentSettings(projectRoot).contextWindow));
158157
setRunningProcesses(entry.processes);
159158
setActiveStatus(entry.status);
160159
setActiveAskPermissions(entry.askPermissions);
@@ -545,7 +544,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
545544
// Clear first so <Static> resets its index to 0.
546545
await resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true });
547546
const session = sessionManager.getSession(sessionId);
548-
setStatusLine(session ? buildStatusLine(session) : "");
547+
setStatusLine(session ? buildStatusLine(session, resolveCurrentSettings(projectRoot).contextWindow) : "");
549548
setRunningProcesses(session?.processes ?? null);
550549
setActiveStatus(session?.status ?? null);
551550
setActiveAskPermissions(session?.askPermissions);
@@ -556,7 +555,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
556555
}
557556
await refreshSkills(sessionId);
558557
},
559-
[sessionManager, resetStaticView, pendingPermissionReply, refreshSkills]
558+
[sessionManager, resetStaticView, pendingPermissionReply, projectRoot, refreshSkills]
560559
);
561560

562561
/**
@@ -733,7 +732,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
733732
const model = settings.model || "";
734733
const thinkingEnabled = settings.thinkingEnabled;
735734
const reasoningEffort = settings.reasoningEffort;
736-
const maxContextTokens = getCompactPromptTokenThreshold(model);
735+
const maxContextTokens = settings.contextWindow;
737736
if (!activeSessionId) {
738737
return {
739738
activeSessionId: null,

packages/core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export {
1414
modelConfigKey,
1515
getUserSettingsPath,
1616
getProjectSettingsPath,
17+
getDefaultContextWindow,
18+
getDefaultAutoCompactWindow,
1719
DEFAULT_MODEL,
1820
DEFAULT_BASE_URL,
1921
} from "./settings";

packages/core/src/session.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import ejs from "ejs";
77
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
88
import { launchNotifyScript } from "./common/notify";
99
import { buildThinkingRequestOptions } from "./common/openai-thinking";
10-
import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities";
1110
import { readTextFileWithMetadata } from "./common/file-utils";
1211
import {
1312
buildSkillDocumentsPrompt,
@@ -29,7 +28,12 @@ import {
2928
type ToolExecutionHooks,
3029
} from "./tools/executor";
3130
import { McpManager } from "./mcp/mcp-manager";
32-
import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings";
31+
import {
32+
getDefaultAutoCompactWindow,
33+
type McpServerConfig,
34+
type PermissionScope,
35+
type PermissionSettings,
36+
} from "./settings";
3337
import { logApiError } from "./common/error-logger";
3438
import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger";
3539
import { describeLlmError, getLlmErrorDetails } from "./common/llm-error";
@@ -66,8 +70,6 @@ const MAX_SESSION_ENTRIES = 50;
6670
const MAX_PROJECT_CODE_LENGTH = 64;
6771
const PROJECT_CODE_HASH_LENGTH = 16;
6872
const BACKGROUND_FAILURE_LOG_TAIL_CHARS = 4000;
69-
const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024;
70-
const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024;
7173
const PLAN_MODE_ON_STATUS_MESSAGE = " └ Set Plan Mode on. Awaiting <proposed_plan>.";
7274
const PLAN_MODE_OFF_STATUS_MESSAGE = " └ Set Plan Mode off.";
7375
const PLAN_MODE_FORCE_ASK_SCOPES = [
@@ -86,9 +88,7 @@ type ChatCompletionDebugOptions = {
8688
};
8789

8890
export function getCompactPromptTokenThreshold(model: string): number {
89-
return DEEPSEEK_V4_MODELS.has(model)
90-
? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD
91-
: DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD;
91+
return getDefaultAutoCompactWindow(model);
9292
}
9393

9494
// Keep project storage paths short enough for Git's internal files on Windows.
@@ -309,6 +309,8 @@ export type SessionManagerOptions = {
309309
createOpenAIClient: CreateOpenAIClient;
310310
getResolvedSettings: () => {
311311
model: string;
312+
contextWindow?: number;
313+
autoCompactWindow?: number;
312314
webSearchTool?: string;
313315
mcpServers?: Record<string, McpServerConfig>;
314316
permissions?: Required<PermissionSettings>;
@@ -337,6 +339,8 @@ export class SessionManager {
337339
private readonly createOpenAIClient: CreateOpenAIClient;
338340
private readonly getResolvedSettings: () => {
339341
model: string;
342+
contextWindow?: number;
343+
autoCompactWindow?: number;
340344
webSearchTool?: string;
341345
mcpServers?: Record<string, McpServerConfig>;
342346
permissions?: Required<PermissionSettings>;
@@ -1362,7 +1366,8 @@ ${agentInstructions}
13621366
}
13631367
}
13641368

1365-
const compactPromptTokenThreshold = getCompactPromptTokenThreshold(model);
1369+
const compactPromptTokenThreshold =
1370+
this.getResolvedSettings().autoCompactWindow ?? getCompactPromptTokenThreshold(model);
13661371
if (session.activeTokens > compactPromptTokenThreshold) {
13671372
const message = this.buildAssistantMessage(
13681373
sessionId,

packages/core/src/settings.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defaultsToThinkingMode } from "./common/model-capabilities";
1+
import { DEEPSEEK_V4_MODELS, defaultsToThinkingMode } from "./common/model-capabilities";
22
import * as fs from "fs";
33
import * as os from "os";
44
import * as path from "path";
@@ -82,6 +82,8 @@ export type ResolvedStatusLineSettings = {
8282

8383
export type DeepcodingSettings = {
8484
env?: DeepcodingEnv;
85+
contextWindow?: number | string;
86+
autoCompactWindow?: number | string;
8587
model?: string;
8688
temperature?: number;
8789
thinkingEnabled?: boolean;
@@ -101,6 +103,8 @@ export type ResolvedDeepcodingSettings = {
101103
apiKey?: string;
102104
baseURL: string;
103105
model: string;
106+
contextWindow: number;
107+
autoCompactWindow: number;
104108
temperature?: number;
105109
thinkingEnabled: boolean;
106110
reasoningEffort: ReasoningEffort;
@@ -122,6 +126,45 @@ export type ModelConfigSelection = {
122126

123127
export type SettingsProcessEnv = Record<string, string | undefined>;
124128

129+
const DEFAULT_CONTEXT_WINDOW = 256 * 1024;
130+
const DEEPSEEK_V4_CONTEXT_WINDOW = 1024 * 1024;
131+
132+
export function getDefaultContextWindow(model: string): number {
133+
return DEEPSEEK_V4_MODELS.has(model) ? DEEPSEEK_V4_CONTEXT_WINDOW : DEFAULT_CONTEXT_WINDOW;
134+
}
135+
136+
export function getDefaultAutoCompactWindow(model: string): number {
137+
return getDefaultContextWindow(model) / 2;
138+
}
139+
140+
function parseTokenWindow(value: unknown): number | undefined {
141+
if (typeof value === "number") {
142+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
143+
}
144+
if (typeof value !== "string") {
145+
return undefined;
146+
}
147+
148+
const match = /^(\d+)([km])$/i.exec(value.trim());
149+
if (!match) {
150+
return undefined;
151+
}
152+
const amount = Number(match[1]);
153+
const multiplier = match[2]?.toLowerCase() === "m" ? 1024 * 1024 : 1024;
154+
const tokens = amount * multiplier;
155+
return Number.isSafeInteger(tokens) && tokens > 0 ? tokens : undefined;
156+
}
157+
158+
function firstTokenWindow(...values: unknown[]): number | undefined {
159+
for (const value of values) {
160+
const parsed = parseTokenWindow(value);
161+
if (parsed !== undefined) {
162+
return parsed;
163+
}
164+
}
165+
return undefined;
166+
}
167+
125168
function resolveReasoningEffort(value: unknown): ReasoningEffort | undefined {
126169
return value === "high" || value === "max" ? value : undefined;
127170
}
@@ -484,6 +527,17 @@ export function resolveSettingsSources(
484527
trimString(userEnv.MODEL) ||
485528
defaults.model;
486529

530+
const contextWindow =
531+
firstTokenWindow(systemEnv.CONTEXT_WINDOW, projectSettings?.contextWindow, userSettings?.contextWindow) ??
532+
getDefaultContextWindow(model);
533+
const configuredAutoCompactWindow = firstTokenWindow(
534+
systemEnv.AUTO_COMPACT_WINDOW,
535+
projectSettings?.autoCompactWindow,
536+
userSettings?.autoCompactWindow
537+
);
538+
const defaultAutoCompactWindow = Math.max(1, Math.floor(contextWindow / 2));
539+
const autoCompactWindow = Math.min(configuredAutoCompactWindow ?? defaultAutoCompactWindow, contextWindow);
540+
487541
const thinkingEnabled =
488542
parseBoolean(systemEnv.THINKING_ENABLED) ??
489543
parseBoolean(projectSettings?.thinkingEnabled) ??
@@ -536,6 +590,8 @@ export function resolveSettingsSources(
536590
apiKey: trimString(env.API_KEY) || undefined,
537591
baseURL: trimString(env.BASE_URL) || defaults.baseURL,
538592
model,
593+
contextWindow,
594+
autoCompactWindow,
539595
temperature,
540596
thinkingEnabled,
541597
reasoningEffort,

0 commit comments

Comments
 (0)