fix: prevent orphaned tool_use blocks from compression and harden autocomplete

Bug 1 (400 API error): applyCompressionBlocks could remove toolResult
messages while leaving their paired assistant(tool_use) message intact.
This produced invalid API sequences that Claude rejected with:
  messages.N: tool_use ids found without tool_result blocks immediately after

The fix adds two boundary expansions before the splice:
- Expand lo backward: if messages[lo-1] is an assistant whose toolCall
  ids appear as toolResult.toolCallId values inside [lo..hi], pull the
  assistant into the range
- Expand hi forward: for assistants inside [lo..hi], extend hi to include
  any consecutive toolResult messages that immediately follow hi

This ensures tool_use/tool_result pairs are always removed together.

Bug 2 (autocomplete crash): harden getArgumentCompletions to return an
explicitly typed AutocompleteItem[] | null, filter on value (not label,
matching pi-tui's internal contract), and add a typeof guard ensuring no
item with a non-string value can reach getBestAutocompleteMatchIndex.
This commit is contained in:
Greg Harvell
2026-03-27 18:50:56 -04:00
parent ebfde87e57
commit e4b4d13ac7
2 changed files with 64 additions and 12 deletions
+13 -10
View File
@@ -1,4 +1,5 @@
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent"
import type { AutocompleteItem } from "@mariozechner/pi-tui"
import type { DcpState } from "./state.js"
import type { DcpConfig } from "./config.js"
@@ -289,17 +290,19 @@ export function registerCommands(
): void {
pi.registerCommand("dcp", {
description: "Dynamic Context Pruning — manage context window usage",
getArgumentCompletions(prefix: string) {
const subcommands = [
{ label: "context", description: "Show context window usage breakdown" },
{ label: "stats", description: "Show pruning statistics" },
{ label: "sweep", description: "Prune tool outputs" },
{ label: "manual", description: "Toggle manual mode" },
{ label: "decompress", description: "List or restore compression blocks" },
{ label: "compress", description: "Trigger LLM compression" },
{ label: "help", description: "Show help" },
getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
const subcommands: AutocompleteItem[] = [
{ value: "context", label: "context", description: "Show context window usage breakdown" },
{ value: "stats", label: "stats", description: "Show pruning statistics" },
{ value: "sweep", label: "sweep", description: "Prune tool outputs" },
{ value: "manual", label: "manual", description: "Toggle manual mode" },
{ value: "decompress", label: "decompress", description: "List or restore compression blocks" },
{ value: "compress", label: "compress", description: "Trigger LLM compression" },
{ value: "help", label: "help", description: "Show help" },
]
const matched = subcommands.filter((s) => s.label.startsWith(prefix))
const matched = subcommands
.filter((s) => typeof s.value === "string")
.filter((s) => s.value.startsWith(prefix))
return matched.length > 0 ? matched : null
},