diff --git a/prompts.ts b/prompts.ts index cb69319..f0572ee 100644 --- a/prompts.ts +++ b/prompts.ts @@ -13,7 +13,7 @@ You operate in a context-constrained environment. Manage context continuously to The ONLY tool you have for context management is \`compress\`. It replaces older conversation content with technical summaries you produce. -\`\` and \`\` tags are environment-injected metadata. Do not output them. +\`\` and \`\` tags are environment-injected metadata. Do not output them. THE PHILOSOPHY OF COMPRESS \`compress\` transforms conversation content into dense, high-fidelity summaries. This is not cleanup — it is crystallization. Your summary becomes the authoritative record of what transpired. @@ -114,7 +114,7 @@ You specify boundaries by ID using the injected IDs visible in the conversation: - \`mNNN\` IDs identify raw messages (3 digits, zero-padded, e.g. \`m001\`, \`m042\`) - \`bN\` IDs identify previously compressed blocks -Each message has an ID inside an HTML comment like \`\`. +Each message has an ID tag like \`m001\`. The ID tag appears at the end of the message it belongs to — it identifies the message above it, not the one below it. Treat these tags as boundary metadata only, not as tool result content. @@ -215,7 +215,7 @@ Prefer multiple short, closed ranges over one large range when several independe export const MANUAL_MODE_SYSTEM_PROMPT = ` You are operating in DCP manual mode for context management. -\`\` and \`\` tags are environment-injected metadata. Do not output them. +\`\` and \`\` tags are environment-injected metadata. Do not output them. In manual mode you do NOT proactively compress conversation content. Compression is a deliberate, user-directed action. diff --git a/pruner.ts b/pruner.ts index 469542c..94a8244 100644 --- a/pruner.ts +++ b/pruner.ts @@ -151,9 +151,9 @@ function applyCompressionBlocks(messages: any[], state: DcpState): any[] { block.topic + "]\n\n" + block.summary + - "\n\n", + "", }, ], // anchorTimestamp is always finite (resolveAnchorTimestamp returns @@ -335,9 +335,27 @@ function applyToolOutputPruning(messages: any[], state: DcpState): void { } } +/** + * Strip any existing dcp-id tags from a string, so strip+inject is idempotent + * for clean messages (no cache bust) and removes model-echoed copies. + */ +function stripDcpIdTags(content: string): string { + return content.replace(/\n\S+<\/dcp-id>/g, ""); +} + +/** Test whether a text block contains a dcp-id tag (for array filtering). */ +function isDcpIdBlock(block: any): boolean { + return block.type === "text" && /\n\S+<\/dcp-id>/.test(block.text); +} + /** * Inject sequential message IDs into eligible messages. * Updates state.messageIdSnapshot. + * + * Strip-before-inject: always strips existing dcp-id tags before appending + * the fresh one. For messages that were never echoed this is idempotent + * (same result, no cache bust). For messages with model-echoed tags it + * removes the duplicate, breaking the accumulation loop. */ function injectMessageIds(messages: any[], state: DcpState): void { // Clear the snapshot and rebuild @@ -356,42 +374,48 @@ function injectMessageIds(messages: any[], state: DcpState): void { const id = "m" + String(counter).padStart(3, "0"); counter++; - const idTag = `\n`; + const idTag = `\n${id}`; if (role === "user") { if (typeof msg.content === "string") { - msg.content = msg.content + `\n\n`; + msg.content = stripDcpIdTags(msg.content) + `\n\n${id}`; } else if (Array.isArray(msg.content)) { - msg.content = [...msg.content, { type: "text", text: idTag }]; + msg.content = [ + ...msg.content.filter((b: any) => !isDcpIdBlock(b)), + { type: "text", text: idTag }, + ]; } } else if (role === "toolResult" || role === "bashExecution") { if (Array.isArray(msg.content)) { - msg.content = [...msg.content, { type: "text", text: idTag }]; + msg.content = [ + ...msg.content.filter((b: any) => !isDcpIdBlock(b)), + { type: "text", text: idTag }, + ]; } else if (typeof msg.content === "string") { - msg.content = msg.content + idTag; + msg.content = stripDcpIdTags(msg.content) + idTag; } } else if (role === "assistant") { if (Array.isArray(msg.content)) { - // Insert the ID tag before any tool_use (toolCall) blocks. + // Strip echoed tags first, then insert before any tool_use (toolCall) blocks. // Anthropic requires: thinking → text → tool_use. - // Appending after tool_use blocks violates that constraint. - const firstToolCallIdx = msg.content.findIndex( + const stripped = msg.content.filter( + (b: any) => !isDcpIdBlock(b) + ); + const firstToolCallIdx = stripped.findIndex( (b: any) => b.type === "toolCall", ); const idBlock = { type: "text", text: idTag }; if (firstToolCallIdx === -1) { - // No tool_use blocks — append as usual - msg.content = [...msg.content, idBlock]; + msg.content = [...stripped, idBlock]; } else { - // Insert immediately before the first tool_use block msg.content = [ - ...msg.content.slice(0, firstToolCallIdx), + ...stripped.slice(0, firstToolCallIdx), idBlock, - ...msg.content.slice(firstToolCallIdx), + ...stripped.slice(firstToolCallIdx), ]; } } else if (typeof msg.content === "string") { - msg.content = msg.content + idTag; + msg.content = stripDcpIdTags(msg.content) + idTag; } }