Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions dcp.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,18 @@
}
}
},
"supersedeReads": {
"type": "object",
"description": "Prune stale read outputs when the same file has been subsequently written or edited",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable supersede reads strategy"
}
}
},
"purgeErrors": {
"type": "object",
"description": "Remove tool outputs that resulted in errors",
Expand Down
27 changes: 27 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface SupersedeWrites {
enabled: boolean
}

export interface SupersedeReads {
enabled: boolean
}

export interface PurgeErrors {
enabled: boolean
turns: number
Expand Down Expand Up @@ -70,6 +74,7 @@ export interface PluginConfig {
strategies: {
deduplication: Deduplication
supersedeWrites: SupersedeWrites
supersedeReads: SupersedeReads
purgeErrors: PurgeErrors
}
}
Expand Down Expand Up @@ -130,6 +135,8 @@ export const VALID_CONFIG_KEYS = new Set([
"strategies.deduplication.protectedTools",
"strategies.supersedeWrites",
"strategies.supersedeWrites.enabled",
"strategies.supersedeReads",
"strategies.supersedeReads.enabled",
"strategies.purgeErrors",
"strategies.purgeErrors.enabled",
"strategies.purgeErrors.turns",
Expand Down Expand Up @@ -545,6 +552,19 @@ export function validateConfigTypes(config: Record<string, any>): ValidationErro
}
}

if (strategies.supersedeReads) {
if (
strategies.supersedeReads.enabled !== undefined &&
typeof strategies.supersedeReads.enabled !== "boolean"
) {
errors.push({
key: "strategies.supersedeReads.enabled",
expected: "boolean",
actual: typeof strategies.supersedeReads.enabled,
})
}
}

if (strategies.purgeErrors) {
if (
strategies.purgeErrors.enabled !== undefined &&
Expand Down Expand Up @@ -681,6 +701,9 @@ const defaultConfig: PluginConfig = {
supersedeWrites: {
enabled: true,
},
supersedeReads: {
enabled: true,
},
purgeErrors: {
enabled: true,
turns: 4,
Expand Down Expand Up @@ -808,6 +831,9 @@ function mergeStrategies(
supersedeWrites: {
enabled: override.supersedeWrites?.enabled ?? base.supersedeWrites.enabled,
},
supersedeReads: {
enabled: override.supersedeReads?.enabled ?? base.supersedeReads.enabled,
},
purgeErrors: {
enabled: override.purgeErrors?.enabled ?? base.purgeErrors.enabled,
turns: override.purgeErrors?.turns ?? base.purgeErrors.turns,
Expand Down Expand Up @@ -909,6 +935,7 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
protectedTools: [...config.strategies.deduplication.protectedTools],
},
supersedeWrites: { ...config.strategies.supersedeWrites },
supersedeReads: { ...config.strategies.supersedeReads },
purgeErrors: {
...config.strategies.purgeErrors,
protectedTools: [...config.strategies.purgeErrors.protectedTools],
Expand Down
1 change: 1 addition & 0 deletions lib/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { deduplicate } from "./deduplication"
export { supersedeWrites } from "./supersede-writes"
export { supersedeReads } from "./supersede-reads"
export { purgeErrors } from "./purge-errors"
121 changes: 121 additions & 0 deletions lib/strategies/supersede-reads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { PluginConfig } from "../config"
import { Logger } from "../logger"
import type { SessionState, WithParts } from "../state"
import { getFilePathsFromParameters, isFilePathProtected } from "../protected-patterns"
import { getTotalToolTokens } from "./utils"

/**
* Supersede Reads strategy - prunes read tool outputs for files that have
* subsequently been written or edited. When a file is read and later modified,
* the original read output becomes stale since the file contents have changed.
*
* Only prunes reads that are followed by a *successful* write/edit to the same
* file. Errored writes do not supersede reads because the file was not actually
* changed.
*
* Modifies the session state in place to add pruned tool call IDs.
*/
export const supersedeReads = (
state: SessionState,
logger: Logger,
config: PluginConfig,
messages: WithParts[],
): void => {
if (state.manualMode && !config.manualMode.automaticStrategies) {
return
}

if (!config.strategies.supersedeReads.enabled) {
return
}

const allToolIds = state.toolIdList
if (allToolIds.length === 0) {
return
}

// Filter out IDs already pruned
const unprunedIds = allToolIds.filter((id) => !state.prune.tools.has(id))
if (unprunedIds.length === 0) {
return
}

// Track read tools by file path: filePath -> [{ id, index }]
// We track index to determine chronological order
const readsByFile = new Map<string, { id: string; index: number }[]>()

// Track successful write/edit file paths with their index
const writesByFile = new Map<string, number[]>()

for (let i = 0; i < allToolIds.length; i++) {
const id = allToolIds[i]
const metadata = state.toolParameters.get(id)
if (!metadata) {
continue
}

const filePaths = getFilePathsFromParameters(metadata.tool, metadata.parameters)
if (filePaths.length === 0) {
continue
}
const filePath = filePaths[0]

if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
continue
}

if (metadata.tool === "read") {
if (!readsByFile.has(filePath)) {
readsByFile.set(filePath, [])
}
const reads = readsByFile.get(filePath)
if (reads) {
reads.push({ id, index: i })
}
} else if (
(metadata.tool === "write" || metadata.tool === "edit") &&
metadata.status === "completed"
) {
if (!writesByFile.has(filePath)) {
writesByFile.set(filePath, [])
}
const writes = writesByFile.get(filePath)
if (writes) {
writes.push(i)
}
}
}

// Find reads that are superseded by subsequent writes/edits
const newPruneIds: string[] = []

for (const [filePath, reads] of readsByFile.entries()) {
const writes = writesByFile.get(filePath)
if (!writes || writes.length === 0) {
continue
}

// For each read, check if there's a write that comes after it
for (const read of reads) {
// Skip if already pruned
if (state.prune.tools.has(read.id)) {
continue
}

// Check if any write comes after this read
const hasSubsequentWrite = writes.some((writeIndex) => writeIndex > read.index)
if (hasSubsequentWrite) {
newPruneIds.push(read.id)
}
}
}

if (newPruneIds.length > 0) {
state.stats.totalPruneTokens += getTotalToolTokens(state, newPruneIds)
for (const id of newPruneIds) {
const entry = state.toolParameters.get(id)
state.prune.tools.set(id, entry?.tokenCount ?? 0)
}
logger.debug(`Marked ${newPruneIds.length} superseded read tool calls for pruning`)
}
}
3 changes: 2 additions & 1 deletion lib/tools/compress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { isIgnoredUserMessage } from "../messages/utils"
import { assignMessageRefs } from "../message-ids"
import { getCurrentParams, getCurrentTokenUsage, countTokens } from "../strategies/utils"
import { deduplicate, supersedeWrites, purgeErrors } from "../strategies"
import { deduplicate, supersedeWrites, supersedeReads, purgeErrors } from "../strategies"
import { saveSessionState } from "../state/persistence"
import { sendCompressNotification } from "../ui/notification"
import { NESTED_FORMAT_OVERLAY, FLAT_FORMAT_OVERLAY } from "../prompts/internal-overlays"
Expand Down Expand Up @@ -117,6 +117,7 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {

deduplicate(ctx.state, ctx.logger, ctx.config, rawMessages)
// supersedeWrites(ctx.state, ctx.logger, ctx.config, rawMessages)
supersedeReads(ctx.state, ctx.logger, ctx.config, rawMessages)
purgeErrors(ctx.state, ctx.logger, ctx.config, rawMessages)

const searchContext = buildSearchContext(ctx.state, rawMessages)
Expand Down
3 changes: 3 additions & 0 deletions tests/compress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ function buildConfig(): PluginConfig {
supersedeWrites: {
enabled: true,
},
supersedeReads: {
enabled: true,
},
purgeErrors: {
enabled: true,
turns: 4,
Expand Down
Loading
Loading