diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5cdc4130c..4fca7f40f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -21,6 +21,7 @@ ### Fixed +- Fixed mid-run threshold compaction silently skipping oversized trailing tool results ([#9740](https://github.com/earendil-works/pi/issues/9740)). - Fixed signal-terminated local shell commands being reported as successful with partial output ([#9577](https://github.com/earendil-works/pi/issues/9577) by [@BrendanJMurphy](https://github.com/BrendanJMurphy)). - Fixed local clipboard failures reporting success when the terminal ignored the fallback OSC 52 write, and added platform-specific setup guidance when no clipboard backend works ([#9618](https://github.com/earendil-works/pi/issues/9618)). - Capped agent-level retry backoff at `retry.maxAgentDelayMs` (60s by default) so long retry runs stay responsive during prolonged transient outages ([#8826](https://github.com/earendil-works/pi/issues/8826)). diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index 12eaa8aef..32a5ed643 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -442,13 +442,10 @@ export function findCutPoint( // Check if we've exceeded the budget if (accumulatedTokens >= keepRecentTokens) { - // Find the closest valid cut point at or after this entry - for (let c = 0; c < cutPoints.length; c++) { - if (cutPoints[c] >= i) { - cutIndex = cutPoints[c]; - break; - } - } + // Prefer the closest valid cut point at or after this entry. If trailing + // tool results exceed the budget by themselves, keep their preceding + // assistant tool call instead of falling back to the first message. + cutIndex = cutPoints.find((candidate) => candidate >= i) ?? cutPoints[cutPoints.length - 1]; break; } } diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index f901dd6b9..5ad130761 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -373,6 +373,42 @@ describe("findCutPoint", () => { expect(customFitsBudget.isSplitTurn).toBe(false); expect(customFitsBudget.turnStartIndex).toBe(-1); }); + + // Regression test for #9740. + it("should fall back to the latest valid cut point before oversized trailing tool results", () => { + const oldUser = createMessageEntry(createUserMessage("old history")); + const oldAssistant = createMessageEntry(createAssistantMessage("old answer")); + const currentUser = createMessageEntry(createUserMessage("read the large file")); + const toolCall = createMessageEntry({ + ...createAssistantMessage(""), + content: [{ type: "toolCall", id: "call-1", name: "read", arguments: { path: "big.txt" } }], + stopReason: "toolUse", + }); + const toolResult = createMessageEntry({ + role: "toolResult", + toolCallId: "call-1", + toolName: "read", + content: [{ type: "text", text: "x".repeat(8000) }], + isError: false, + timestamp: Date.now(), + }); + const entries = [oldUser, oldAssistant, currentUser, toolCall, toolResult]; + + const result = findCutPoint(entries, 0, entries.length, 1000); + expect(result).toEqual({ + firstKeptEntryIndex: 3, + turnStartIndex: 2, + isSplitTurn: true, + }); + + const preparation = prepareCompaction(entries, { + ...DEFAULT_COMPACTION_SETTINGS, + keepRecentTokens: 1000, + }); + expect(preparation?.firstKeptEntryId).toBe(toolCall.id); + expect(preparation?.messagesToSummarize).toEqual([oldUser.message, oldAssistant.message]); + expect(preparation?.turnPrefixMessages).toEqual([currentUser.message]); + }); }); describe("buildSessionContext", () => { diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index 65637f364..62dc2f10a 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -471,10 +471,11 @@ describe("AgentSession compaction characterization", () => { }); // Regression coverage for #8133: model overrides must also apply between assistant turns. + // Regression coverage for #9740: an oversized trailing tool result must still produce a cut point. it.each([false, true])( - "compacts after a tool result in the same run (model override: %s)", + "compacts after an oversized tool result in the same run (model override: %s)", async (modelOverride) => { - const toolResult = `large-tool-result:${"x".repeat(6800)}`; + const toolResult = `large-tool-result:${"x".repeat(8000)}`; const largeTool: AgentTool = { name: "large_result", label: "Large result", @@ -532,8 +533,8 @@ describe("AgentSession compaction characterization", () => { const agentStartsBefore = harness.eventsOfType("agent_start").length; await harness.session.prompt("run the large tool"); - expect(order).toEqual(["compaction", "provider"]); - expect(observedSettings).toEqual([{ enabled: true, reserveTokens: 400, keepRecentTokens: 1750 }]); + expect(order.slice(0, 2)).toEqual(["compaction", "provider"]); + expect(observedSettings[0]).toEqual({ enabled: true, reserveTokens: 400, keepRecentTokens: 1750 }); expect(harness.eventsOfType("agent_start")).toHaveLength(agentStartsBefore + 1); expect(harness.eventsOfType("compaction_start").at(-1)).toEqual({ type: "compaction_start", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 7926e8d2f..5b129fb52 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Reduced fuzzy search latency for long texts by using native substring search instead of scanning each character in JavaScript ([#9267](https://github.com/earendil-works/pi/issues/9267)). + ### Fixed - Fixed fullscreen clipboard failures hiding actionable backend error messages behind a generic notice, and extended failure notices to five seconds ([#9618](https://github.com/earendil-works/pi/issues/9618)). diff --git a/packages/tui/src/fuzzy.ts b/packages/tui/src/fuzzy.ts index 73c10dcf4..1b5352c1c 100644 --- a/packages/tui/src/fuzzy.ts +++ b/packages/tui/src/fuzzy.ts @@ -27,33 +27,34 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch { let lastMatchIndex = -1; let consecutiveMatches = 0; - for (let i = 0; i < textLower.length && queryIndex < normalizedQuery.length; i++) { - if (textLower[i] === normalizedQuery[queryIndex]) { - const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!); + while (queryIndex < normalizedQuery.length) { + const i = textLower.indexOf(normalizedQuery[queryIndex]!, lastMatchIndex + 1); + if (i === -1) break; - // Reward consecutive matches - if (lastMatchIndex === i - 1) { - consecutiveMatches++; - score -= consecutiveMatches * 5; - } else { - consecutiveMatches = 0; - // Penalize gaps - if (lastMatchIndex >= 0) { - score += (i - lastMatchIndex - 1) * 2; - } + const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!); + + // Reward consecutive matches + if (lastMatchIndex === i - 1) { + consecutiveMatches++; + score -= consecutiveMatches * 5; + } else { + consecutiveMatches = 0; + // Penalize gaps + if (lastMatchIndex >= 0) { + score += (i - lastMatchIndex - 1) * 2; } - - // Reward word boundary matches - if (isWordBoundary) { - score -= 10; - } - - // Slight penalty for later matches - score += i * 0.1; - - lastMatchIndex = i; - queryIndex++; } + + // Reward word boundary matches + if (isWordBoundary) { + score -= 10; + } + + // Slight penalty for later matches + score += i * 0.1; + + lastMatchIndex = i; + queryIndex++; } if (queryIndex < normalizedQuery.length) {