chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions

View File

@@ -6,7 +6,9 @@ import { getContextPruningRuntime } from "./runtime.js";
export default function contextPruningExtension(api: ExtensionAPI): void {
api.on("context", (event: ContextEvent, ctx: ExtensionContext) => {
const runtime = getContextPruningRuntime(ctx.sessionManager);
if (!runtime) return undefined;
if (!runtime) {
return undefined;
}
if (runtime.settings.mode === "cache-ttl") {
const ttlMs = runtime.settings.ttlMs;
@@ -27,7 +29,9 @@ export default function contextPruningExtension(api: ExtensionAPI): void {
contextWindowTokensOverride: runtime.contextWindowTokens ?? undefined,
});
if (next === event.messages) return undefined;
if (next === event.messages) {
return undefined;
}
if (runtime.settings.mode === "cache-ttl") {
runtime.lastCacheTouchAt = Date.now();

View File

@@ -17,29 +17,39 @@ function asText(text: string): TextContent {
function collectTextSegments(content: ReadonlyArray<TextContent | ImageContent>): string[] {
const parts: string[] = [];
for (const block of content) {
if (block.type === "text") parts.push(block.text);
if (block.type === "text") {
parts.push(block.text);
}
}
return parts;
}
function estimateJoinedTextLength(parts: string[]): number {
if (parts.length === 0) return 0;
if (parts.length === 0) {
return 0;
}
let len = 0;
for (const p of parts) len += p.length;
for (const p of parts) {
len += p.length;
}
// Joined with "\n" separators between blocks.
len += Math.max(0, parts.length - 1);
return len;
}
function takeHeadFromJoinedText(parts: string[], maxChars: number): string {
if (maxChars <= 0 || parts.length === 0) return "";
if (maxChars <= 0 || parts.length === 0) {
return "";
}
let remaining = maxChars;
let out = "";
for (let i = 0; i < parts.length && remaining > 0; i++) {
if (i > 0) {
out += "\n";
remaining -= 1;
if (remaining <= 0) break;
if (remaining <= 0) {
break;
}
}
const p = parts[i];
if (p.length <= remaining) {
@@ -54,7 +64,9 @@ function takeHeadFromJoinedText(parts: string[], maxChars: number): string {
}
function takeTailFromJoinedText(parts: string[], maxChars: number): string {
if (maxChars <= 0 || parts.length === 0) return "";
if (maxChars <= 0 || parts.length === 0) {
return "";
}
let remaining = maxChars;
const out: string[] = [];
for (let i = parts.length - 1; i >= 0 && remaining > 0; i--) {
@@ -78,7 +90,9 @@ function takeTailFromJoinedText(parts: string[], maxChars: number): string {
function hasImageBlocks(content: ReadonlyArray<TextContent | ImageContent>): boolean {
for (const block of content) {
if (block.type === "image") return true;
if (block.type === "image") {
return true;
}
}
return false;
}
@@ -86,11 +100,17 @@ function hasImageBlocks(content: ReadonlyArray<TextContent | ImageContent>): boo
function estimateMessageChars(message: AgentMessage): number {
if (message.role === "user") {
const content = message.content;
if (typeof content === "string") return content.length;
if (typeof content === "string") {
return content.length;
}
let chars = 0;
for (const b of content) {
if (b.type === "text") chars += b.text.length;
if (b.type === "image") chars += IMAGE_CHAR_ESTIMATE;
if (b.type === "text") {
chars += b.text.length;
}
if (b.type === "image") {
chars += IMAGE_CHAR_ESTIMATE;
}
}
return chars;
}
@@ -98,8 +118,12 @@ function estimateMessageChars(message: AgentMessage): number {
if (message.role === "assistant") {
let chars = 0;
for (const b of message.content) {
if (b.type === "text") chars += b.text.length;
if (b.type === "thinking") chars += b.thinking.length;
if (b.type === "text") {
chars += b.text.length;
}
if (b.type === "thinking") {
chars += b.thinking.length;
}
if (b.type === "toolCall") {
try {
chars += JSON.stringify(b.arguments ?? {}).length;
@@ -114,8 +138,12 @@ function estimateMessageChars(message: AgentMessage): number {
if (message.role === "toolResult") {
let chars = 0;
for (const b of message.content) {
if (b.type === "text") chars += b.text.length;
if (b.type === "image") chars += IMAGE_CHAR_ESTIMATE;
if (b.type === "text") {
chars += b.text.length;
}
if (b.type === "image") {
chars += IMAGE_CHAR_ESTIMATE;
}
}
return chars;
}
@@ -132,13 +160,19 @@ function findAssistantCutoffIndex(
keepLastAssistants: number,
): number | null {
// keepLastAssistants <= 0 => everything is potentially prunable.
if (keepLastAssistants <= 0) return messages.length;
if (keepLastAssistants <= 0) {
return messages.length;
}
let remaining = keepLastAssistants;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i]?.role !== "assistant") continue;
if (messages[i]?.role !== "assistant") {
continue;
}
remaining--;
if (remaining === 0) return i;
if (remaining === 0) {
return i;
}
}
// Not enough assistant messages to establish a protected tail.
@@ -147,7 +181,9 @@ function findAssistantCutoffIndex(
function findFirstUserIndex(messages: AgentMessage[]): number | null {
for (let i = 0; i < messages.length; i++) {
if (messages[i]?.role === "user") return i;
if (messages[i]?.role === "user") {
return i;
}
}
return null;
}
@@ -158,15 +194,21 @@ function softTrimToolResultMessage(params: {
}): ToolResultMessage | null {
const { msg, settings } = params;
// Ignore image tool results for now: these are often directly relevant and hard to partially prune safely.
if (hasImageBlocks(msg.content)) return null;
if (hasImageBlocks(msg.content)) {
return null;
}
const parts = collectTextSegments(msg.content);
const rawLen = estimateJoinedTextLength(parts);
if (rawLen <= settings.softTrim.maxChars) return null;
if (rawLen <= settings.softTrim.maxChars) {
return null;
}
const headChars = Math.max(0, settings.softTrim.headChars);
const tailChars = Math.max(0, settings.softTrim.tailChars);
if (headChars + tailChars >= rawLen) return null;
if (headChars + tailChars >= rawLen) {
return null;
}
const head = takeHeadFromJoinedText(parts, headChars);
const tail = takeTailFromJoinedText(parts, tailChars);
@@ -195,13 +237,19 @@ export function pruneContextMessages(params: {
params.contextWindowTokensOverride > 0
? params.contextWindowTokensOverride
: ctx.model?.contextWindow;
if (!contextWindowTokens || contextWindowTokens <= 0) return messages;
if (!contextWindowTokens || contextWindowTokens <= 0) {
return messages;
}
const charWindow = contextWindowTokens * CHARS_PER_TOKEN_ESTIMATE;
if (charWindow <= 0) return messages;
if (charWindow <= 0) {
return messages;
}
const cutoffIndex = findAssistantCutoffIndex(messages, settings.keepLastAssistants);
if (cutoffIndex === null) return messages;
if (cutoffIndex === null) {
return messages;
}
// Bootstrap safety: never prune anything before the first user message. This protects initial
// "identity" reads (SOUL.md, USER.md, etc.) which typically happen before the first inbound user
@@ -223,8 +271,12 @@ export function pruneContextMessages(params: {
for (let i = pruneStartIndex; i < cutoffIndex; i++) {
const msg = messages[i];
if (!msg || msg.role !== "toolResult") continue;
if (!isToolPrunable(msg.toolName)) continue;
if (!msg || msg.role !== "toolResult") {
continue;
}
if (!isToolPrunable(msg.toolName)) {
continue;
}
if (hasImageBlocks(msg.content)) {
continue;
}
@@ -234,12 +286,16 @@ export function pruneContextMessages(params: {
msg: msg as unknown as ToolResultMessage,
settings,
});
if (!updated) continue;
if (!updated) {
continue;
}
const beforeChars = estimateMessageChars(msg);
const afterChars = estimateMessageChars(updated as unknown as AgentMessage);
totalChars += afterChars - beforeChars;
if (!next) next = messages.slice();
if (!next) {
next = messages.slice();
}
next[i] = updated as unknown as AgentMessage;
}
@@ -255,7 +311,9 @@ export function pruneContextMessages(params: {
let prunableToolChars = 0;
for (const i of prunableToolIndexes) {
const msg = outputAfterSoftTrim[i];
if (!msg || msg.role !== "toolResult") continue;
if (!msg || msg.role !== "toolResult") {
continue;
}
prunableToolChars += estimateMessageChars(msg);
}
if (prunableToolChars < settings.minPrunableToolChars) {
@@ -263,16 +321,22 @@ export function pruneContextMessages(params: {
}
for (const i of prunableToolIndexes) {
if (ratio < settings.hardClearRatio) break;
if (ratio < settings.hardClearRatio) {
break;
}
const msg = (next ?? messages)[i];
if (!msg || msg.role !== "toolResult") continue;
if (!msg || msg.role !== "toolResult") {
continue;
}
const beforeChars = estimateMessageChars(msg);
const cleared: ToolResultMessage = {
...msg,
content: [asText(settings.hardClear.placeholder)],
};
if (!next) next = messages.slice();
if (!next) {
next = messages.slice();
}
next[i] = cleared as unknown as AgentMessage;
const afterChars = estimateMessageChars(cleared as unknown as AgentMessage);
totalChars += afterChars - beforeChars;

View File

@@ -65,9 +65,13 @@ export const DEFAULT_CONTEXT_PRUNING_SETTINGS: EffectiveContextPruningSettings =
};
export function computeEffectiveSettings(raw: unknown): EffectiveContextPruningSettings | null {
if (!raw || typeof raw !== "object") return null;
if (!raw || typeof raw !== "object") {
return null;
}
const cfg = raw as ContextPruningConfig;
if (cfg.mode !== "cache-ttl") return null;
if (cfg.mode !== "cache-ttl") {
return null;
}
const s: EffectiveContextPruningSettings = structuredClone(DEFAULT_CONTEXT_PRUNING_SETTINGS);
s.mode = cfg.mode;

View File

@@ -1,7 +1,9 @@
import type { ContextPruningToolMatch } from "./settings.js";
function normalizePatterns(patterns?: string[]): string[] {
if (!Array.isArray(patterns)) return [];
if (!Array.isArray(patterns)) {
return [];
}
return patterns
.map((p) =>
String(p ?? "")
@@ -17,8 +19,12 @@ type CompiledPattern =
| { kind: "regex"; value: RegExp };
function compilePattern(pattern: string): CompiledPattern {
if (pattern === "*") return { kind: "all" };
if (!pattern.includes("*")) return { kind: "exact", value: pattern };
if (pattern === "*") {
return { kind: "all" };
}
if (!pattern.includes("*")) {
return { kind: "exact", value: pattern };
}
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`);
@@ -31,9 +37,15 @@ function compilePatterns(patterns?: string[]): CompiledPattern[] {
function matchesAny(toolName: string, patterns: CompiledPattern[]): boolean {
for (const p of patterns) {
if (p.kind === "all") return true;
if (p.kind === "exact" && toolName === p.value) return true;
if (p.kind === "regex" && p.value.test(toolName)) return true;
if (p.kind === "all") {
return true;
}
if (p.kind === "exact" && toolName === p.value) {
return true;
}
if (p.kind === "regex" && p.value.test(toolName)) {
return true;
}
}
return false;
}
@@ -46,8 +58,12 @@ export function makeToolPrunablePredicate(
return (toolName: string) => {
const normalized = toolName.trim().toLowerCase();
if (matchesAny(normalized, deny)) return false;
if (allow.length === 0) return true;
if (matchesAny(normalized, deny)) {
return false;
}
if (allow.length === 0) {
return true;
}
return matchesAny(normalized, allow);
};
}