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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,35 @@ DCP uses its own config file:

When enabled, turn protection prevents tool outputs from being pruned for a configurable number of message turns. This gives the AI time to reference recent tool outputs before they become prunable. Applies to both `discard` and `extract` tools, as well as automatic strategies.

### Protected File Patterns

`protectedFilePatterns` is a list of glob patterns matched against `tool parameters.filePath` (e.g. `read`/`write`/`edit` calls).

When a tool call matches a protected pattern:

- It will not appear in the `<prunable-tools>` list.
- `discard` / `extract` will reject attempts to prune it.
- Automatic strategies (deduplication, supersede-writes, purge-errors) will not prune it.

Supported glob tokens:

- `*` matches any characters except `/`
- `?` matches a single character except `/`
- `**` matches across `/` (e.g. `**/*.ts`)

Examples:

- `**/*.env` (protect env files)
- `**/.opencode/**` (protect project opencode configs)
- `**/credentials.*` (protect credential files)

### Nudge Reminders

When `tools.settings.nudgeEnabled` is true, DCP injects a short “use prune tools” reminder into the `<prunable-tools>` message:

- Every `tools.settings.nudgeFrequency` tool results.
- Also after a `todowrite` call transitions an existing todo to `status: "completed"`.

### Protected Tools

By default, these tools are always protected from pruning across all strategies:
Expand Down
12 changes: 9 additions & 3 deletions lib/messages/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,18 @@ export const insertPruneToolContext = (
logger.debug("prunable-tools: \n" + prunableToolsList)

let nudgeString = ""
if (
const shouldNudge =
config.tools.settings.nudgeEnabled &&
state.nudgeCounter >= config.tools.settings.nudgeFrequency
) {
(state.nudgeCounter >= config.tools.settings.nudgeFrequency ||
state.todoCompletionNudgePending)

if (shouldNudge) {
logger.info("Inserting prune nudge message")
nudgeString = "\n" + getNudgeString(config)

if (state.todoCompletionNudgePending) {
state.todoCompletionNudgePending = false
}
}

prunableToolsContent = prunableToolsList + nudgeString
Expand Down
7 changes: 5 additions & 2 deletions lib/prompts/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { readFileSync } from "fs"
import { join } from "path"
import { dirname, join } from "path"
import { fileURLToPath } from "url"

const PROMPTS_DIR = dirname(fileURLToPath(import.meta.url))

export function loadPrompt(name: string, vars?: Record<string, string>): string {
const filePath = join(__dirname, `${name}.txt`)
const filePath = join(PROMPTS_DIR, `${name}.txt`)
let content = readFileSync(filePath, "utf8").trim()
if (vars) {
for (const [key, value] of Object.entries(vars)) {
Expand Down
32 changes: 32 additions & 0 deletions lib/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const checkSession = async (
state.lastCompaction = lastCompactionTimestamp
state.toolParameters.clear()
state.prune.toolIds = []
state.todoStatusById.clear()
state.todoCompletionNudgePending = false
logger.info("Detected compaction from messages - cleared tool cache", {
timestamp: lastCompactionTimestamp,
})
Expand All @@ -56,6 +58,9 @@ export function createSessionState(): SessionState {
lastCompaction: 0,
currentTurn: 0,
variant: undefined,

todoStatusById: new Map<string, string>(),
todoCompletionNudgePending: false,
}
}

Expand All @@ -75,6 +80,9 @@ export function resetSessionState(state: SessionState): void {
state.lastCompaction = 0
state.currentTurn = 0
state.variant = undefined

state.todoStatusById.clear()
state.todoCompletionNudgePending = false
}

export async function ensureSessionInitialized(
Expand All @@ -101,6 +109,8 @@ export async function ensureSessionInitialized(
state.lastCompaction = findLastCompactionTimestamp(messages)
state.currentTurn = countTurns(state, messages)

seedTodoStatusFromMessages(state, messages)

const persisted = await loadSessionState(sessionId, logger)
if (persisted === null) {
return
Expand All @@ -115,6 +125,28 @@ export async function ensureSessionInitialized(
}
}

function seedTodoStatusFromMessages(state: SessionState, messages: WithParts[]): void {
for (const msg of messages) {
if (isMessageCompacted(state, msg)) {
continue
}
for (const part of msg.parts) {
if (part.type !== "tool" || part.tool !== "todowrite") {
continue
}
const todos = (part.state?.input as any)?.todos
if (!Array.isArray(todos)) {
continue
}
for (const todo of todos) {
if (todo && typeof todo.id === "string" && typeof todo.status === "string") {
state.todoStatusById.set(todo.id, todo.status)
}
}
}
}
}

function findLastCompactionTimestamp(messages: WithParts[]): number {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i]
Expand Down
24 changes: 24 additions & 0 deletions lib/state/tool-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,30 @@ export async function syncToolCache(
continue
}

if (part.tool === "todowrite") {
const todos = (part.state?.input as any)?.todos
if (Array.isArray(todos)) {
for (const todo of todos) {
if (
!todo ||
typeof todo.id !== "string" ||
typeof todo.status !== "string"
) {
continue
}
const previousStatus = state.todoStatusById.get(todo.id)
if (
previousStatus !== undefined &&
previousStatus !== "completed" &&
todo.status === "completed"
) {
state.todoCompletionNudgePending = true
}
state.todoStatusById.set(todo.id, todo.status)
}
}
}

state.toolParameters.set(part.callID, {
tool: part.tool,
parameters: part.state?.input ?? {},
Expand Down
3 changes: 3 additions & 0 deletions lib/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ export interface SessionState {
lastCompaction: number
currentTurn: number
variant: string | undefined

todoStatusById: Map<string, string>
todoCompletionNudgePending: boolean
}