Compare commits

...
Author SHA1 Message Date
netcatty-bot 47147978dd fix(#2792): automated Cursor CLI fix 2026-08-07 09:46:58 +00:00
9 changed files with 120 additions and 19 deletions

No files matched your search

+2 -2
View File
@@ -58,8 +58,8 @@ export const enAiMessages: Messages = {
'ai.providers.defaultModel.placeholder': 'e.g. gpt-4o, claude-sonnet-4-20250514',
'ai.providers.contextWindow': 'Context window',
'ai.providers.contextWindow.placeholder': 'e.g. 128000',
'ai.providers.contextWindow.help': 'Leave blank to use the model list value when available, otherwise Netcatty uses a safe default.',
'ai.providers.contextWindow.error': 'Enter a positive whole number, or leave it blank.',
'ai.providers.contextWindow.help': 'Leave blank to use the model list value when available, otherwise Netcatty uses a safe default. Minimum is 1024 tokens.',
'ai.providers.contextWindow.error': 'Enter a whole number of at least 1024, or leave it blank.',
'ai.providers.refreshModels': 'Refresh models',
'ai.providers.searchModel': 'Search or type model ID...',
'ai.providers.filterModels': 'Filter models...',
+2 -2
View File
@@ -39,8 +39,8 @@ export const ruAiMessages: Messages = {
'ai.providers.defaultModel.placeholder': 'например, gpt-4o, claude-sonnet-4-20250514',
'ai.providers.contextWindow': 'Контекстное окно',
'ai.providers.contextWindow.placeholder': 'например, 128000',
'ai.providers.contextWindow.help': 'Оставьте пустым, чтобы использовать значение из списка моделей, если оно доступно; иначе Netcatty применит безопасное значение по умолчанию.',
'ai.providers.contextWindow.error': 'Введите положительное целое число или оставьте поле пустым.',
'ai.providers.contextWindow.help': 'Оставьте пустым, чтобы использовать значение из списка моделей, если оно доступно; иначе Netcatty применит безопасное значение по умолчанию. Минимум — 1024 токена.',
'ai.providers.contextWindow.error': 'Введите целое число не меньше 1024 или оставьте поле пустым.',
'ai.providers.refreshModels': 'Обновить модели',
'ai.providers.searchModel': 'Искать или ввести ID модели...',
'ai.providers.filterModels': 'Фильтровать модели...',
+2 -2
View File
@@ -58,8 +58,8 @@ export const zhCNAiMessages: Messages = {
'ai.providers.defaultModel.placeholder': '例如 gpt-4o, claude-sonnet-4-20250514',
'ai.providers.contextWindow': '上下文窗口',
'ai.providers.contextWindow.placeholder': '例如 128000',
'ai.providers.contextWindow.help': '留空时优先使用模型列表返回的值;如果没有,Netcatty 会使用安全默认值。',
'ai.providers.contextWindow.error': '请输入正整数,或留空。',
'ai.providers.contextWindow.help': '留空时优先使用模型列表返回的值;如果没有,Netcatty 会使用安全默认值。最小值是 1024 tokens。',
'ai.providers.contextWindow.error': '请输入不小于 1024 的正整数,或留空。',
'ai.providers.refreshModels': '刷新模型列表',
'ai.providers.searchModel': '搜索或输入模型 ID...',
'ai.providers.filterModels': '筛选模型...',
+2 -2
View File
@@ -58,8 +58,8 @@ export const zhTWAiMessages: Messages = {
'ai.providers.defaultModel.placeholder': '例如 gpt-4o, claude-sonnet-4-20250514',
'ai.providers.contextWindow': '上下文視窗',
'ai.providers.contextWindow.placeholder': '例如 128000',
'ai.providers.contextWindow.help': '留空時優先使用模型列表傳回的值;如果沒有,Netcatty 會使用安全預設值。',
'ai.providers.contextWindow.error': '請輸入正整數,或留空。',
'ai.providers.contextWindow.help': '留空時優先使用模型列表傳回的值;如果沒有,Netcatty 會使用安全預設值。最小值是 1024 tokens。',
'ai.providers.contextWindow.error': '請輸入不小於 1024 的正整數,或留空。',
'ai.providers.refreshModels': '重新整理模型列表',
'ai.providers.searchModel': '搜尋或輸入模型 ID...',
'ai.providers.filterModels': '篩選模型...',
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Check, ChevronDown, ChevronRight, Eye, EyeOff, Pencil, Upload, RotateCcw, X } from "lucide-react";
import type { ProviderConfig, ProviderAdvancedParams, ProviderStyle } from "../../../../infrastructure/ai/types";
import { PROVIDER_PRESETS, resolveProviderStyle } from "../../../../infrastructure/ai/types";
import { sanitizeContextWindow } from "../../../../infrastructure/ai/contextCompaction";
import { sanitizeContextWindow, MIN_CONTEXT_WINDOW_TOKENS } from "../../../../infrastructure/ai/contextCompaction";
import { encryptField, decryptField } from "../../../../infrastructure/persistence/secureFieldAdapter";
import { useI18n } from "../../../../application/i18n/I18nProvider";
import { Button } from "../../../ui/button";
@@ -193,7 +193,10 @@ export const ProviderConfigForm: React.FC<{
const defaultName = PROVIDER_PRESETS[provider.providerId]?.name ?? "";
const rawContextWindow = form.contextWindow.trim();
const rawContextWindowNumber = Number(rawContextWindow);
if (rawContextWindow && (!Number.isInteger(rawContextWindowNumber) || rawContextWindowNumber <= 0)) {
if (
rawContextWindow
&& (!Number.isInteger(rawContextWindowNumber) || rawContextWindowNumber < MIN_CONTEXT_WINDOW_TOKENS)
) {
setContextWindowError(t("ai.providers.contextWindow.error"));
return;
}
@@ -11,6 +11,7 @@ import {
formatMessagesForCompaction,
prepareContextCompaction,
resolveContextWindow,
sanitizeContextWindow,
shouldCompactContext,
} from "./contextCompaction.ts";
@@ -314,3 +315,33 @@ test("resolveContextWindow prefers manual override, then fetched model metadata,
128000,
);
});
test("sanitizeContextWindow rejects values below the minimum floor", () => {
assert.equal(sanitizeContextWindow(1), undefined);
assert.equal(sanitizeContextWindow(1023), undefined);
assert.equal(sanitizeContextWindow(1024), 1024);
assert.equal(sanitizeContextWindow("128000"), 128000);
});
test("resolveContextWindow ignores tiny manual overrides and falls back", () => {
assert.equal(
resolveContextWindow({
provider: {
contextWindow: 1,
modelContextWindows: { "qwen/test": 131072 },
},
modelId: "qwen/test",
defaultContextWindow: 128000,
}),
131072,
);
assert.equal(
resolveContextWindow({
provider: { contextWindow: 1 },
modelId: "unknown",
defaultContextWindow: 128000,
}),
128000,
);
});
+4 -2
View File
@@ -13,6 +13,8 @@ import { redactSecretsForModel } from "./harness/modelSecretRedaction";
const REDACTED_PAYLOAD_PREVIEW_CHARS = 80;
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 128_000;
/** Floor for manual/discovered windows — values below this are treated as unset. */
export const MIN_CONTEXT_WINDOW_TOKENS = 1_024;
export const DEFAULT_PROTECT_RECENT_MESSAGES = 10;
export const CONTEXT_COMPACTION_SYSTEM_PROMPT = `You are summarizing a long Netcatty agent conversation so it can continue without exceeding the model context window.
@@ -85,8 +87,8 @@ export function resolveContextWindow({
export function sanitizeContextWindow(value: unknown): number | undefined {
const num = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(num) || num <= 0) return undefined;
return Math.max(1, Math.round(num));
if (!Number.isFinite(num) || num < MIN_CONTEXT_WINDOW_TOKENS) return undefined;
return Math.round(num);
}
export function estimateModelMessagesTokens(
@@ -213,6 +213,51 @@ test('prepareStepContext emits step compaction trace when over budget', async ()
assert.ok(events.includes('step'));
});
test('prepareStepContext never returns empty messages after tiny context-window budget prune', async () => {
const messages: ModelMessage[] = [
{ role: 'user', content: 'which processes use the most resources?' },
{
role: 'assistant',
content: [{
type: 'tool-call',
toolCallId: 'call-1',
toolName: 'terminal_execute',
input: { command: "ps -eo pid=,pcpu=,pmem=,rss=,comm= --sort=-pcpu | head -n 11" },
}],
},
{
role: 'tool',
content: [{
type: 'tool-result',
toolCallId: 'call-1',
toolName: 'terminal_execute',
output: {
type: 'text',
value: 'PID CPU% MEM% RSS COMMAND\n1 0.1 0.2 1024 init\n'.repeat(40),
},
}],
},
];
const prepared = await prepareStepContext({
messages,
stepNumber: 1,
sessionId: 'chat-tiny-window',
chatSessionId: 'chat-tiny-window',
contextWindow: 1,
maxOutputTokens: 512,
runtimeContext: createInitialCattyRuntimeContext({
chatSessionId: 'chat-tiny-window',
turnId: 'turn-tiny',
permissionMode: 'confirm',
scopeType: 'terminal',
}),
});
assert.ok(prepared.messages.length > 0, 'step budget prune must not empty the prompt');
assert.match(JSON.stringify(prepared.messages), /tool-result|processes use the most/);
});
test('prepareStepContext retains handle notice after step budget guard', async () => {
const store = new ToolOutputStore();
store.store({
+27 -7
View File
@@ -362,17 +362,24 @@ function applyStepBudgetGuard(
reservedTokens: input.reservedTokens,
});
if (afterTotal >= threshold && splitAt > 0) {
next = keepRecentContextMessages(next, input.protectRecentMessages);
didAdjust = true;
afterTotal = computeTotalInputTokens({
messages: next,
providerId: input.providerId,
reservedTokens: input.reservedTokens,
});
const recent = keepRecentContextMessages(next, input.protectRecentMessages);
// Never accept an empty keep-recent slice — an empty prompt is worse than over-budget.
if (recent.length > 0) {
next = recent;
didAdjust = true;
afterTotal = computeTotalInputTokens({
messages: next,
providerId: input.providerId,
reservedTokens: input.reservedTokens,
});
}
}
while (afterTotal >= threshold && next.length > 2) {
const pruned = pruneFirstModelMessage(next);
if (pruned.length === next.length) break;
// A single head prune can drop a whole user+tool-call+result unit and empty the list
// (e.g. contextWindow=1). Reject empty prunes so the follow-up model call still has a prompt.
if (pruned.length === 0) break;
next = pruned;
didAdjust = true;
afterTotal = computeTotalInputTokens({
@@ -382,6 +389,10 @@ function applyStepBudgetGuard(
});
}
if (next.length === 0 && messages.length > 0) {
return { messages, didAdjust: false, didTypedCompression: false };
}
return {
messages: next,
didAdjust: didAdjust || next.length !== messages.length,
@@ -515,6 +526,15 @@ export async function prepareStepContext(
...(didAdjust ? { lastStepAdjusted: true } : {}),
};
// Last-resort guard: never hand an empty prompt to the model after a tool step.
if (working.length === 0 && input.messages.length > 0) {
return {
messages: integrity.messages.length > 0 ? integrity.messages : input.messages,
didAdjust: false,
runtimeContext,
};
}
return {
messages: working,
didAdjust,