opencode-acp 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2191 -1924
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/decompress.d.ts.map +1 -1
- package/dist/lib/compress/decompress-logic.d.ts +15 -0
- package/dist/lib/compress/decompress-logic.d.ts.map +1 -0
- package/dist/lib/compress/decompress.d.ts +4 -0
- package/dist/lib/compress/decompress.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +1 -0
- package/dist/lib/compress/index.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +1 -1
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/messages/query.d.ts +1 -0
- package/dist/lib/messages/query.d.ts.map +1 -1
- package/dist/lib/prompts/extensions/system.d.ts +1 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -1
- package/dist/lib/prompts/index.d.ts.map +1 -1
- package/dist/lib/prompts/store.d.ts +1 -0
- package/dist/lib/prompts/store.d.ts.map +1 -1
- package/dist/lib/prompts/system.d.ts +1 -1
- package/dist/lib/prompts/system.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1350,13 +1350,14 @@ var DEFAULT_PROTECTED_TOOLS = [
|
|
|
1350
1350
|
"todowrite",
|
|
1351
1351
|
"todoread",
|
|
1352
1352
|
"compress",
|
|
1353
|
+
"decompress",
|
|
1353
1354
|
"batch",
|
|
1354
1355
|
"plan_enter",
|
|
1355
1356
|
"plan_exit",
|
|
1356
1357
|
"write",
|
|
1357
1358
|
"edit"
|
|
1358
1359
|
];
|
|
1359
|
-
var COMPRESS_DEFAULT_PROTECTED_TOOLS = ["task", "skill", "todowrite", "todoread"];
|
|
1360
|
+
var COMPRESS_DEFAULT_PROTECTED_TOOLS = ["task", "skill", "todowrite", "todoread", "decompress"];
|
|
1360
1361
|
function showConfigWarnings(ctx, configPath, configData, isProject) {
|
|
1361
1362
|
const invalidKeys = getInvalidConfigKeys(configData);
|
|
1362
1363
|
const typeErrors = validateConfigTypes(configData);
|
|
@@ -1758,6 +1759,10 @@ function filterMessagesInPlace(messages) {
|
|
|
1758
1759
|
}
|
|
1759
1760
|
|
|
1760
1761
|
// lib/messages/query.ts
|
|
1762
|
+
function isSyntheticMessage(message) {
|
|
1763
|
+
const id = message?.info?.id;
|
|
1764
|
+
return typeof id === "string" && (id.startsWith("msg_dcp_summary_") || id.startsWith("msg_dcp_text_"));
|
|
1765
|
+
}
|
|
1761
1766
|
var getLastUserMessage = (messages, startIndex) => {
|
|
1762
1767
|
const start = startIndex ?? messages.length - 1;
|
|
1763
1768
|
for (let i = start; i >= 0; i--) {
|
|
@@ -1765,7 +1770,7 @@ var getLastUserMessage = (messages, startIndex) => {
|
|
|
1765
1770
|
if (!isMessageWithInfo(msg)) {
|
|
1766
1771
|
continue;
|
|
1767
1772
|
}
|
|
1768
|
-
if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
|
|
1773
|
+
if (msg.info.role === "user" && !isIgnoredUserMessage(msg) && !isSyntheticMessage(msg)) {
|
|
1769
1774
|
return msg;
|
|
1770
1775
|
}
|
|
1771
1776
|
}
|
|
@@ -3462,20 +3467,20 @@ function matchesGlob(inputPath, pattern) {
|
|
|
3462
3467
|
regex += "$";
|
|
3463
3468
|
return new RegExp(regex).test(input);
|
|
3464
3469
|
}
|
|
3465
|
-
function getFilePathsFromParameters(
|
|
3470
|
+
function getFilePathsFromParameters(tool4, parameters) {
|
|
3466
3471
|
if (typeof parameters !== "object" || parameters === null) {
|
|
3467
3472
|
return [];
|
|
3468
3473
|
}
|
|
3469
3474
|
const paths = [];
|
|
3470
3475
|
const params = parameters;
|
|
3471
|
-
if (
|
|
3476
|
+
if (tool4 === "apply_patch" && typeof params.patchText === "string") {
|
|
3472
3477
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3473
3478
|
let match;
|
|
3474
3479
|
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
3475
3480
|
paths.push(match[1].trim());
|
|
3476
3481
|
}
|
|
3477
3482
|
}
|
|
3478
|
-
if (
|
|
3483
|
+
if (tool4 === "multiedit") {
|
|
3479
3484
|
if (typeof params.filePath === "string") {
|
|
3480
3485
|
paths.push(params.filePath);
|
|
3481
3486
|
}
|
|
@@ -3570,13 +3575,13 @@ var deduplicate = (state, logger, config, messages) => {
|
|
|
3570
3575
|
logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
|
|
3571
3576
|
}
|
|
3572
3577
|
};
|
|
3573
|
-
function createToolSignature(
|
|
3578
|
+
function createToolSignature(tool4, parameters) {
|
|
3574
3579
|
if (!parameters) {
|
|
3575
|
-
return
|
|
3580
|
+
return tool4;
|
|
3576
3581
|
}
|
|
3577
3582
|
const normalized = normalizeParameters(parameters);
|
|
3578
3583
|
const sorted = sortObjectKeys(normalized);
|
|
3579
|
-
return `${
|
|
3584
|
+
return `${tool4}::${JSON.stringify(sorted)}`;
|
|
3580
3585
|
}
|
|
3581
3586
|
function normalizeParameters(params) {
|
|
3582
3587
|
if (typeof params !== "object" || params === null) return params;
|
|
@@ -3651,9 +3656,9 @@ var purgeErrors = (state, logger, config, messages) => {
|
|
|
3651
3656
|
};
|
|
3652
3657
|
|
|
3653
3658
|
// lib/ui/utils.ts
|
|
3654
|
-
function extractParameterKey(
|
|
3659
|
+
function extractParameterKey(tool4, parameters) {
|
|
3655
3660
|
if (!parameters) return "";
|
|
3656
|
-
if (
|
|
3661
|
+
if (tool4 === "read" && parameters.filePath) {
|
|
3657
3662
|
const offset = parameters.offset;
|
|
3658
3663
|
const limit = parameters.limit;
|
|
3659
3664
|
if (offset !== void 0 && limit !== void 0) {
|
|
@@ -3667,10 +3672,10 @@ function extractParameterKey(tool3, parameters) {
|
|
|
3667
3672
|
}
|
|
3668
3673
|
return parameters.filePath;
|
|
3669
3674
|
}
|
|
3670
|
-
if ((
|
|
3675
|
+
if ((tool4 === "write" || tool4 === "edit" || tool4 === "multiedit") && parameters.filePath) {
|
|
3671
3676
|
return parameters.filePath;
|
|
3672
3677
|
}
|
|
3673
|
-
if (
|
|
3678
|
+
if (tool4 === "apply_patch" && typeof parameters.patchText === "string") {
|
|
3674
3679
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3675
3680
|
const paths = [];
|
|
3676
3681
|
let match;
|
|
@@ -3687,51 +3692,51 @@ function extractParameterKey(tool3, parameters) {
|
|
|
3687
3692
|
}
|
|
3688
3693
|
return "patch";
|
|
3689
3694
|
}
|
|
3690
|
-
if (
|
|
3695
|
+
if (tool4 === "list") {
|
|
3691
3696
|
return parameters.path || "(current directory)";
|
|
3692
3697
|
}
|
|
3693
|
-
if (
|
|
3698
|
+
if (tool4 === "glob") {
|
|
3694
3699
|
if (parameters.pattern) {
|
|
3695
3700
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3696
3701
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3697
3702
|
}
|
|
3698
3703
|
return "(unknown pattern)";
|
|
3699
3704
|
}
|
|
3700
|
-
if (
|
|
3705
|
+
if (tool4 === "grep") {
|
|
3701
3706
|
if (parameters.pattern) {
|
|
3702
3707
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3703
3708
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3704
3709
|
}
|
|
3705
3710
|
return "(unknown pattern)";
|
|
3706
3711
|
}
|
|
3707
|
-
if (
|
|
3712
|
+
if (tool4 === "bash") {
|
|
3708
3713
|
if (parameters.description) return parameters.description;
|
|
3709
3714
|
if (parameters.command) {
|
|
3710
3715
|
return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
|
|
3711
3716
|
}
|
|
3712
3717
|
}
|
|
3713
|
-
if (
|
|
3718
|
+
if (tool4 === "webfetch" && parameters.url) {
|
|
3714
3719
|
return parameters.url;
|
|
3715
3720
|
}
|
|
3716
|
-
if (
|
|
3721
|
+
if (tool4 === "websearch" && parameters.query) {
|
|
3717
3722
|
return `"${parameters.query}"`;
|
|
3718
3723
|
}
|
|
3719
|
-
if (
|
|
3724
|
+
if (tool4 === "codesearch" && parameters.query) {
|
|
3720
3725
|
return `"${parameters.query}"`;
|
|
3721
3726
|
}
|
|
3722
|
-
if (
|
|
3727
|
+
if (tool4 === "todowrite") {
|
|
3723
3728
|
return `${parameters.todos?.length || 0} todos`;
|
|
3724
3729
|
}
|
|
3725
|
-
if (
|
|
3730
|
+
if (tool4 === "todoread") {
|
|
3726
3731
|
return "read todo list";
|
|
3727
3732
|
}
|
|
3728
|
-
if (
|
|
3733
|
+
if (tool4 === "task" && parameters.description) {
|
|
3729
3734
|
return parameters.description;
|
|
3730
3735
|
}
|
|
3731
|
-
if (
|
|
3736
|
+
if (tool4 === "skill" && parameters.name) {
|
|
3732
3737
|
return parameters.name;
|
|
3733
3738
|
}
|
|
3734
|
-
if (
|
|
3739
|
+
if (tool4 === "lsp") {
|
|
3735
3740
|
const op = parameters.operation || "lsp";
|
|
3736
3741
|
const path = parameters.filePath || "";
|
|
3737
3742
|
const line = parameters.line;
|
|
@@ -3744,7 +3749,7 @@ function extractParameterKey(tool3, parameters) {
|
|
|
3744
3749
|
}
|
|
3745
3750
|
return op;
|
|
3746
3751
|
}
|
|
3747
|
-
if (
|
|
3752
|
+
if (tool4 === "question") {
|
|
3748
3753
|
const questions = parameters.questions;
|
|
3749
3754
|
if (Array.isArray(questions) && questions.length > 0) {
|
|
3750
3755
|
const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
|
|
@@ -4706,1936 +4711,2365 @@ function extractBoundaryConsumedBlocks(startReference, endReference) {
|
|
|
4706
4711
|
return consumed;
|
|
4707
4712
|
}
|
|
4708
4713
|
|
|
4709
|
-
// lib/
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4714
|
+
// lib/compress/decompress.ts
|
|
4715
|
+
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
4716
|
+
|
|
4717
|
+
// lib/messages/utils.ts
|
|
4718
|
+
import { createHash } from "crypto";
|
|
4719
|
+
var SUMMARY_ID_HASH_LENGTH = 16;
|
|
4720
|
+
var DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/dcp-message-id>)/g;
|
|
4721
|
+
var DCP_MESSAGE_REF_TAG_REGEX = /<dcp-message-id>m\d+<\/dcp-message-id>/g;
|
|
4722
|
+
var DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi;
|
|
4723
|
+
var DCP_UNPAIRED_TAG_REGEX = /<\/?dcp[^>]*>/gi;
|
|
4724
|
+
var generateStableId = (prefix, seed) => {
|
|
4725
|
+
const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
|
|
4726
|
+
return `${prefix}_${hash}`;
|
|
4727
|
+
};
|
|
4728
|
+
var createSyntheticUserMessage = (baseMessage, content, stableSeed) => {
|
|
4729
|
+
const userInfo = baseMessage.info;
|
|
4730
|
+
const now = Date.now();
|
|
4731
|
+
const deterministicSeed = stableSeed?.trim() || userInfo.id;
|
|
4732
|
+
const messageId = generateStableId("msg_dcp_summary", deterministicSeed);
|
|
4733
|
+
const partId = generateStableId("prt_dcp_summary", deterministicSeed);
|
|
4734
|
+
return {
|
|
4735
|
+
info: {
|
|
4736
|
+
id: messageId,
|
|
4737
|
+
sessionID: userInfo.sessionID,
|
|
4738
|
+
role: "user",
|
|
4739
|
+
agent: userInfo.agent,
|
|
4740
|
+
model: userInfo.model,
|
|
4741
|
+
time: { created: now }
|
|
4742
|
+
},
|
|
4743
|
+
parts: [
|
|
4744
|
+
{
|
|
4745
|
+
id: partId,
|
|
4746
|
+
sessionID: userInfo.sessionID,
|
|
4747
|
+
messageID: messageId,
|
|
4748
|
+
type: "text",
|
|
4749
|
+
text: content
|
|
4750
|
+
}
|
|
4751
|
+
]
|
|
4752
|
+
};
|
|
4753
|
+
};
|
|
4754
|
+
var createSyntheticTextPart = (baseMessage, content, stableSeed) => {
|
|
4755
|
+
const userInfo = baseMessage.info;
|
|
4756
|
+
const deterministicSeed = stableSeed?.trim() || userInfo.id;
|
|
4757
|
+
const partId = generateStableId("prt_dcp_text", deterministicSeed);
|
|
4758
|
+
return {
|
|
4759
|
+
id: partId,
|
|
4760
|
+
sessionID: userInfo.sessionID,
|
|
4761
|
+
messageID: userInfo.id,
|
|
4762
|
+
type: "text",
|
|
4763
|
+
text: content
|
|
4764
|
+
};
|
|
4765
|
+
};
|
|
4766
|
+
var appendToLastTextPart = (message, injection) => {
|
|
4767
|
+
const textPart = findLastTextPart(message);
|
|
4768
|
+
if (!textPart) {
|
|
4769
|
+
return false;
|
|
4770
|
+
}
|
|
4771
|
+
return appendToTextPart(textPart, injection);
|
|
4772
|
+
};
|
|
4773
|
+
var findLastTextPart = (message) => {
|
|
4774
|
+
for (let i = message.parts.length - 1; i >= 0; i--) {
|
|
4775
|
+
const part = message.parts[i];
|
|
4776
|
+
if (part.type === "text") {
|
|
4777
|
+
return part;
|
|
4715
4778
|
}
|
|
4716
4779
|
}
|
|
4717
|
-
return
|
|
4780
|
+
return null;
|
|
4718
4781
|
};
|
|
4719
|
-
var
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
if (escaped.endsWith(" .*")) {
|
|
4723
|
-
escaped = escaped.slice(0, -3) + "( .*)?";
|
|
4782
|
+
var appendToTextPart = (part, injection) => {
|
|
4783
|
+
if (typeof part.text !== "string") {
|
|
4784
|
+
return false;
|
|
4724
4785
|
}
|
|
4725
|
-
const
|
|
4726
|
-
|
|
4786
|
+
const normalizedInjection = injection.replace(/^\n+/, "");
|
|
4787
|
+
if (!normalizedInjection.trim()) {
|
|
4788
|
+
return false;
|
|
4789
|
+
}
|
|
4790
|
+
if (part.text.includes(normalizedInjection)) {
|
|
4791
|
+
return true;
|
|
4792
|
+
}
|
|
4793
|
+
const baseText = part.text.replace(/\n*$/, "");
|
|
4794
|
+
part.text = baseText.length > 0 ? `${baseText}
|
|
4795
|
+
|
|
4796
|
+
${normalizedInjection}` : normalizedInjection;
|
|
4797
|
+
return true;
|
|
4727
4798
|
};
|
|
4728
|
-
var
|
|
4729
|
-
|
|
4730
|
-
for (const
|
|
4731
|
-
if (
|
|
4799
|
+
var appendToAllToolParts = (message, tag) => {
|
|
4800
|
+
let injected = false;
|
|
4801
|
+
for (const part of message.parts) {
|
|
4802
|
+
if (part.type === "tool") {
|
|
4803
|
+
injected = appendToToolPart(part, tag) || injected;
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
return injected;
|
|
4807
|
+
};
|
|
4808
|
+
var appendToToolPart = (part, tag) => {
|
|
4809
|
+
if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
|
|
4810
|
+
return false;
|
|
4811
|
+
}
|
|
4812
|
+
if (part.state.output.includes(tag)) {
|
|
4813
|
+
return true;
|
|
4814
|
+
}
|
|
4815
|
+
part.state.output = `${part.state.output}${tag}`;
|
|
4816
|
+
return true;
|
|
4817
|
+
};
|
|
4818
|
+
var hasContent = (message) => {
|
|
4819
|
+
return message.parts.some(
|
|
4820
|
+
(part) => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0 || part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string"
|
|
4821
|
+
);
|
|
4822
|
+
};
|
|
4823
|
+
function buildToolIdList(state, messages) {
|
|
4824
|
+
const toolIds = [];
|
|
4825
|
+
for (const msg of messages) {
|
|
4826
|
+
if (isMessageCompacted(state, msg)) {
|
|
4732
4827
|
continue;
|
|
4733
4828
|
}
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
for (const [pattern, action] of Object.entries(value)) {
|
|
4740
|
-
if (action === "ask" || action === "allow" || action === "deny") {
|
|
4741
|
-
rules.push({ permission, pattern, action });
|
|
4829
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
4830
|
+
if (parts.length > 0) {
|
|
4831
|
+
for (const part of parts) {
|
|
4832
|
+
if (part.type === "tool" && part.callID && part.tool) {
|
|
4833
|
+
toolIds.push(part.callID);
|
|
4742
4834
|
}
|
|
4743
4835
|
}
|
|
4744
4836
|
}
|
|
4745
4837
|
}
|
|
4746
|
-
|
|
4838
|
+
state.toolIdList = toolIds;
|
|
4839
|
+
return toolIds;
|
|
4840
|
+
}
|
|
4841
|
+
var replaceBlockIdsWithBlocked = (text) => {
|
|
4842
|
+
return text.replace(DCP_BLOCK_ID_TAG_REGEX, "$1BLOCKED$2");
|
|
4747
4843
|
};
|
|
4748
|
-
var
|
|
4749
|
-
|
|
4750
|
-
getPermissionRules(permissionConfigs),
|
|
4751
|
-
(rule) => wildcardMatch("compress", rule.permission)
|
|
4752
|
-
);
|
|
4753
|
-
return match?.pattern === "*" && match.action === "deny";
|
|
4844
|
+
var stripStaleMessageRefs = (text) => {
|
|
4845
|
+
return text.replace(DCP_MESSAGE_REF_TAG_REGEX, "");
|
|
4754
4846
|
};
|
|
4755
|
-
var
|
|
4756
|
-
|
|
4757
|
-
return "deny";
|
|
4758
|
-
}
|
|
4759
|
-
return compressDisabledByOpencode(
|
|
4760
|
-
hostPermissions.global,
|
|
4761
|
-
agentName ? hostPermissions.agents[agentName] : void 0
|
|
4762
|
-
) ? "deny" : basePermission;
|
|
4847
|
+
var stripHallucinationsFromString = (text) => {
|
|
4848
|
+
return text.replace(DCP_PAIRED_TAG_REGEX, "").replace(DCP_UNPAIRED_TAG_REGEX, "");
|
|
4763
4849
|
};
|
|
4764
|
-
var
|
|
4765
|
-
|
|
4850
|
+
var stripHallucinations = (messages) => {
|
|
4851
|
+
for (const message of messages) {
|
|
4852
|
+
for (const part of message.parts) {
|
|
4853
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
4854
|
+
part.text = stripHallucinationsFromString(part.text);
|
|
4855
|
+
}
|
|
4856
|
+
if (part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string") {
|
|
4857
|
+
part.state.output = stripHallucinationsFromString(part.state.output);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4766
4861
|
};
|
|
4767
4862
|
|
|
4768
|
-
// lib/
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
if (!existsSync3(this.logDir)) {
|
|
4783
|
-
await mkdir2(this.logDir, { recursive: true });
|
|
4863
|
+
// lib/messages/prune.ts
|
|
4864
|
+
var PRUNED_TOOL_OUTPUT_REPLACEMENT = "[Output removed to save context - information superseded or no longer needed]";
|
|
4865
|
+
var PRUNED_TOOL_ERROR_INPUT_REPLACEMENT = "[input removed due to failed tool call]";
|
|
4866
|
+
var PRUNED_QUESTION_INPUT_REPLACEMENT = "[questions removed - see output for user's answers]";
|
|
4867
|
+
var prune = (state, logger, config, messages) => {
|
|
4868
|
+
filterCompressedRanges(state, logger, config, messages);
|
|
4869
|
+
pruneToolOutputs(state, logger, messages);
|
|
4870
|
+
pruneToolInputs(state, logger, messages);
|
|
4871
|
+
pruneToolErrors(state, logger, messages);
|
|
4872
|
+
};
|
|
4873
|
+
var pruneToolOutputs = (state, logger, messages) => {
|
|
4874
|
+
for (const msg of messages) {
|
|
4875
|
+
if (isMessageCompacted(state, msg)) {
|
|
4876
|
+
continue;
|
|
4784
4877
|
}
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
for (const [key, value] of Object.entries(data)) {
|
|
4790
|
-
if (value === void 0 || value === null) continue;
|
|
4791
|
-
if (Array.isArray(value)) {
|
|
4792
|
-
if (value.length === 0) continue;
|
|
4793
|
-
parts.push(
|
|
4794
|
-
`${key}=[${value.slice(0, 3).join(",")}${value.length > 3 ? `...+${value.length - 3}` : ""}]`
|
|
4795
|
-
);
|
|
4796
|
-
} else if (typeof value === "object") {
|
|
4797
|
-
const str = JSON.stringify(value);
|
|
4798
|
-
if (str.length < 50) {
|
|
4799
|
-
parts.push(`${key}=${str}`);
|
|
4800
|
-
}
|
|
4801
|
-
} else {
|
|
4802
|
-
parts.push(`${key}=${value}`);
|
|
4878
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
4879
|
+
for (const part of parts) {
|
|
4880
|
+
if (part.type !== "tool") {
|
|
4881
|
+
continue;
|
|
4803
4882
|
}
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
}
|
|
4807
|
-
getCallerFile(skipFrames = 3) {
|
|
4808
|
-
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
4809
|
-
try {
|
|
4810
|
-
const err = new Error();
|
|
4811
|
-
Error.prepareStackTrace = (_, stack2) => stack2;
|
|
4812
|
-
const stack = err.stack;
|
|
4813
|
-
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
4814
|
-
for (let i = skipFrames; i < stack.length; i++) {
|
|
4815
|
-
const filename = stack[i]?.getFileName();
|
|
4816
|
-
if (filename && !filename.includes("/logger.")) {
|
|
4817
|
-
const match = filename.match(/([^/\\]+)\.[tj]s$/);
|
|
4818
|
-
return match ? match[1] : filename;
|
|
4819
|
-
}
|
|
4883
|
+
if (!state.prune.tools.has(part.callID)) {
|
|
4884
|
+
continue;
|
|
4820
4885
|
}
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4886
|
+
if (part.state.status !== "completed") {
|
|
4887
|
+
continue;
|
|
4888
|
+
}
|
|
4889
|
+
if (part.tool === "question" || part.tool === "edit" || part.tool === "write") {
|
|
4890
|
+
continue;
|
|
4891
|
+
}
|
|
4892
|
+
part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT;
|
|
4824
4893
|
}
|
|
4825
4894
|
}
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4895
|
+
};
|
|
4896
|
+
var pruneToolInputs = (state, logger, messages) => {
|
|
4897
|
+
for (const msg of messages) {
|
|
4898
|
+
if (isMessageCompacted(state, msg)) {
|
|
4899
|
+
continue;
|
|
4900
|
+
}
|
|
4901
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
4902
|
+
for (const part of parts) {
|
|
4903
|
+
if (part.type !== "tool") {
|
|
4904
|
+
continue;
|
|
4905
|
+
}
|
|
4906
|
+
if (!state.prune.tools.has(part.callID)) {
|
|
4907
|
+
continue;
|
|
4908
|
+
}
|
|
4909
|
+
if (part.state.status !== "completed") {
|
|
4910
|
+
continue;
|
|
4911
|
+
}
|
|
4912
|
+
if (part.tool !== "question") {
|
|
4913
|
+
continue;
|
|
4914
|
+
}
|
|
4915
|
+
if (part.state.input?.questions !== void 0) {
|
|
4916
|
+
part.state.input.questions = PRUNED_QUESTION_INPUT_REPLACEMENT;
|
|
4837
4917
|
}
|
|
4838
|
-
const logFile = join3(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
|
|
4839
|
-
await writeFile2(logFile, logLine, { flag: "a" });
|
|
4840
|
-
} catch (error) {
|
|
4841
4918
|
}
|
|
4842
4919
|
}
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
const
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
warn(message, data) {
|
|
4854
|
-
if (!this.enabled) return;
|
|
4855
|
-
const component = this.getCallerFile(2);
|
|
4856
|
-
return this.write("WARN", component, message, data);
|
|
4857
|
-
}
|
|
4858
|
-
error(message, data) {
|
|
4859
|
-
if (!this.enabled) return;
|
|
4860
|
-
const component = this.getCallerFile(2);
|
|
4861
|
-
return this.write("ERROR", component, message, data);
|
|
4862
|
-
}
|
|
4863
|
-
/**
|
|
4864
|
-
* Strips unnecessary metadata from messages for cleaner debug logs.
|
|
4865
|
-
*
|
|
4866
|
-
* Removed:
|
|
4867
|
-
* - All IDs (id, sessionID, messageID, parentID)
|
|
4868
|
-
* - summary, path, cost, model, agent, mode, finish, providerID, modelID
|
|
4869
|
-
* - step-start and step-finish parts entirely
|
|
4870
|
-
* - snapshot fields
|
|
4871
|
-
* - ignored text parts
|
|
4872
|
-
*
|
|
4873
|
-
* Kept:
|
|
4874
|
-
* - role, time (created only), tokens (input, output, reasoning, cache)
|
|
4875
|
-
* - text, reasoning, tool parts with content
|
|
4876
|
-
* - tool calls with: tool, callID, input, output, metadata
|
|
4877
|
-
*/
|
|
4878
|
-
minimizeForDebug(messages) {
|
|
4879
|
-
return messages.map((msg) => {
|
|
4880
|
-
const minimized = {
|
|
4881
|
-
role: msg.info?.role
|
|
4882
|
-
};
|
|
4883
|
-
if (msg.info?.time?.created) {
|
|
4884
|
-
minimized.time = msg.info.time.created;
|
|
4920
|
+
};
|
|
4921
|
+
var pruneToolErrors = (state, logger, messages) => {
|
|
4922
|
+
for (const msg of messages) {
|
|
4923
|
+
if (isMessageCompacted(state, msg)) {
|
|
4924
|
+
continue;
|
|
4925
|
+
}
|
|
4926
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
4927
|
+
for (const part of parts) {
|
|
4928
|
+
if (part.type !== "tool") {
|
|
4929
|
+
continue;
|
|
4885
4930
|
}
|
|
4886
|
-
if (
|
|
4887
|
-
|
|
4888
|
-
input: msg.info.tokens.input,
|
|
4889
|
-
output: msg.info.tokens.output,
|
|
4890
|
-
reasoning: msg.info.tokens.reasoning,
|
|
4891
|
-
cache: msg.info.tokens.cache
|
|
4892
|
-
};
|
|
4931
|
+
if (!state.prune.tools.has(part.callID)) {
|
|
4932
|
+
continue;
|
|
4893
4933
|
}
|
|
4894
|
-
if (
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
if (part.metadata) textPart.metadata = part.metadata;
|
|
4903
|
-
return textPart;
|
|
4904
|
-
}
|
|
4905
|
-
if (part.type === "reasoning") {
|
|
4906
|
-
const reasoningPart = { type: "reasoning", text: part.text };
|
|
4907
|
-
if (part.metadata) reasoningPart.metadata = part.metadata;
|
|
4908
|
-
return reasoningPart;
|
|
4909
|
-
}
|
|
4910
|
-
if (part.type === "tool") {
|
|
4911
|
-
const toolPart = {
|
|
4912
|
-
type: "tool",
|
|
4913
|
-
tool: part.tool,
|
|
4914
|
-
callID: part.callID
|
|
4915
|
-
};
|
|
4916
|
-
if (part.state?.status) {
|
|
4917
|
-
toolPart.status = part.state.status;
|
|
4918
|
-
}
|
|
4919
|
-
if (part.state?.input) {
|
|
4920
|
-
toolPart.input = part.state.input;
|
|
4921
|
-
}
|
|
4922
|
-
if (part.state?.output) {
|
|
4923
|
-
toolPart.output = part.state.output;
|
|
4924
|
-
}
|
|
4925
|
-
if (part.state?.error) {
|
|
4926
|
-
toolPart.error = part.state.error;
|
|
4927
|
-
}
|
|
4928
|
-
if (part.metadata) {
|
|
4929
|
-
toolPart.metadata = part.metadata;
|
|
4930
|
-
}
|
|
4931
|
-
if (part.state?.metadata) {
|
|
4932
|
-
toolPart.metadata = {
|
|
4933
|
-
...toolPart.metadata || {},
|
|
4934
|
-
...part.state.metadata
|
|
4935
|
-
};
|
|
4936
|
-
}
|
|
4937
|
-
if (part.state?.title) {
|
|
4938
|
-
toolPart.title = part.state.title;
|
|
4939
|
-
}
|
|
4940
|
-
return toolPart;
|
|
4934
|
+
if (part.state.status !== "error") {
|
|
4935
|
+
continue;
|
|
4936
|
+
}
|
|
4937
|
+
const input = part.state.input;
|
|
4938
|
+
if (input && typeof input === "object") {
|
|
4939
|
+
for (const key of Object.keys(input)) {
|
|
4940
|
+
if (typeof input[key] === "string") {
|
|
4941
|
+
input[key] = PRUNED_TOOL_ERROR_INPUT_REPLACEMENT;
|
|
4941
4942
|
}
|
|
4942
|
-
|
|
4943
|
-
}).filter(Boolean);
|
|
4943
|
+
}
|
|
4944
4944
|
}
|
|
4945
|
-
|
|
4946
|
-
});
|
|
4945
|
+
}
|
|
4947
4946
|
}
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4947
|
+
};
|
|
4948
|
+
var filterCompressedRanges = (state, logger, config, messages) => {
|
|
4949
|
+
if (state.prune.messages.byMessageId.size === 0 && state.prune.messages.activeByAnchorMessageId.size === 0) {
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
4952
|
+
const result = [];
|
|
4953
|
+
for (const msg of messages) {
|
|
4954
|
+
const msgId = msg.info.id;
|
|
4955
|
+
const blockId = state.prune.messages.activeByAnchorMessageId.get(msgId);
|
|
4956
|
+
const summary = blockId !== void 0 ? state.prune.messages.blocksById.get(blockId) : void 0;
|
|
4957
|
+
if (summary) {
|
|
4958
|
+
const rawSummaryContent = summary.summary;
|
|
4959
|
+
if (summary.active !== true || typeof rawSummaryContent !== "string" || rawSummaryContent.length === 0) {
|
|
4960
|
+
logger.warn("Skipping malformed compress summary", {
|
|
4961
|
+
anchorMessageId: msgId,
|
|
4962
|
+
blockId: summary.blockId
|
|
4963
|
+
});
|
|
4964
|
+
} else {
|
|
4965
|
+
const msgIndex = messages.indexOf(msg);
|
|
4966
|
+
const userMessage = getLastUserMessage(messages, msgIndex);
|
|
4967
|
+
const _cleaned = stripStaleMessageRefs(rawSummaryContent);
|
|
4968
|
+
if (userMessage) {
|
|
4969
|
+
const userInfo = userMessage.info;
|
|
4970
|
+
const summaryContent = config.compress.mode === "message" ? replaceBlockIdsWithBlocked(_cleaned) : _cleaned;
|
|
4971
|
+
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
|
|
4972
|
+
result.push(
|
|
4973
|
+
createSyntheticUserMessage(userMessage, summaryContent, summarySeed)
|
|
4974
|
+
);
|
|
4975
|
+
logger.info("Injected compress summary", {
|
|
4976
|
+
anchorMessageId: msgId,
|
|
4977
|
+
summaryLength: summaryContent.length
|
|
4978
|
+
});
|
|
4979
|
+
} else {
|
|
4980
|
+
const anchorInfo = msg.info;
|
|
4981
|
+
const fallbackBase = {
|
|
4982
|
+
info: {
|
|
4983
|
+
id: anchorInfo.id || msgId,
|
|
4984
|
+
sessionID: anchorInfo.sessionID || "",
|
|
4985
|
+
role: "user",
|
|
4986
|
+
agent: anchorInfo.agent || "code",
|
|
4987
|
+
model: anchorInfo.model || {
|
|
4988
|
+
providerID: "",
|
|
4989
|
+
modelID: "",
|
|
4990
|
+
variant: void 0
|
|
4991
|
+
},
|
|
4992
|
+
time: { created: anchorInfo.time?.created || Date.now() }
|
|
4993
|
+
},
|
|
4994
|
+
parts: []
|
|
4995
|
+
};
|
|
4996
|
+
const summaryContent = config.compress.mode === "message" ? replaceBlockIdsWithBlocked(_cleaned) : _cleaned;
|
|
4997
|
+
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
|
|
4998
|
+
result.push(
|
|
4999
|
+
createSyntheticUserMessage(fallbackBase, summaryContent, summarySeed)
|
|
5000
|
+
);
|
|
5001
|
+
logger.info("Injected compress summary (fallback, no preceding user message)", {
|
|
5002
|
+
anchorMessageId: msgId,
|
|
5003
|
+
summaryLength: summaryContent.length
|
|
5004
|
+
});
|
|
5005
|
+
}
|
|
4954
5006
|
}
|
|
4955
|
-
const minimized = this.minimizeForDebug(messages).filter(
|
|
4956
|
-
(msg) => msg.parts && msg.parts.length > 0
|
|
4957
|
-
);
|
|
4958
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4959
|
-
const contextFile = join3(contextDir, `${timestamp}.json`);
|
|
4960
|
-
await writeFile2(contextFile, JSON.stringify(minimized, null, 2));
|
|
4961
|
-
} catch (error) {
|
|
4962
5007
|
}
|
|
5008
|
+
const pruneEntry = state.prune.messages.byMessageId.get(msgId);
|
|
5009
|
+
if (pruneEntry && pruneEntry.activeBlockIds.length > 0) {
|
|
5010
|
+
continue;
|
|
5011
|
+
}
|
|
5012
|
+
result.push(msg);
|
|
4963
5013
|
}
|
|
5014
|
+
messages.length = 0;
|
|
5015
|
+
messages.push(...result);
|
|
4964
5016
|
};
|
|
4965
5017
|
|
|
4966
|
-
// lib/
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
// lib/prompts/system.ts
|
|
4972
|
-
var SYSTEM = `
|
|
4973
|
-
You operate in a context-constrained environment. Manage context continuously to avoid buildup and preserve retrieval quality. Efficient context management is paramount for your agentic performance.
|
|
4974
|
-
|
|
4975
|
-
The ONLY tool you have for context management is \`compress\`. It replaces older conversation content with technical summaries you produce.
|
|
4976
|
-
|
|
4977
|
-
\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
4978
|
-
|
|
4979
|
-
THE PHILOSOPHY OF COMPRESS
|
|
4980
|
-
\`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.
|
|
4981
|
-
|
|
4982
|
-
Think of compression as phase transitions: raw exploration becomes refined understanding. The original context served its purpose; your summary now carries that understanding forward.
|
|
4983
|
-
|
|
4984
|
-
COMPRESS WHEN
|
|
4985
|
-
|
|
4986
|
-
A section is genuinely closed and the raw conversation has served its purpose:
|
|
4987
|
-
|
|
4988
|
-
- Research concluded and findings are clear
|
|
4989
|
-
- Implementation finished and verified
|
|
4990
|
-
- Exploration exhausted and patterns understood
|
|
4991
|
-
- Dead-end noise can be discarded without waiting for a whole chapter to close
|
|
4992
|
-
|
|
4993
|
-
DO NOT COMPRESS IF
|
|
4994
|
-
|
|
4995
|
-
- Raw context is still relevant and needed for edits or precise references
|
|
4996
|
-
- The target content is still actively in progress
|
|
4997
|
-
- You may need exact code, error messages, or file contents in the immediate next steps
|
|
4998
|
-
|
|
4999
|
-
Before compressing, ask: _"Is this section closed enough to become summary-only right now?"_
|
|
5000
|
-
|
|
5001
|
-
Evaluate conversation signal-to-noise REGULARLY. Use \`compress\` deliberately with quality-first summaries. Prioritize stale content intelligently to maintain a high-signal context window that supports your agency.
|
|
5002
|
-
|
|
5003
|
-
It is of your responsibility to keep a sharp, high-quality context window for optimal performance.
|
|
5004
|
-
`;
|
|
5005
|
-
|
|
5006
|
-
// lib/prompts/compress-range.ts
|
|
5007
|
-
var COMPRESS_RANGE = `Collapse a range in the conversation into a detailed summary.
|
|
5008
|
-
|
|
5009
|
-
THE SUMMARY
|
|
5010
|
-
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings... EVERYTHING that maintains context integrity. This is not a brief note - it is an authoritative record so faithful that the original conversation adds no value.
|
|
5011
|
-
|
|
5012
|
-
USER INTENT FIDELITY
|
|
5013
|
-
When the compressed range includes user messages, preserve the user's intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
|
|
5014
|
-
Directly quote user messages when they are short enough to include safely. Direct quotes are preferred when they best preserve exact meaning.
|
|
5015
|
-
|
|
5016
|
-
Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool outputs, back-and-forth exploration. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
|
|
5017
|
-
|
|
5018
|
-
COMPRESSED BLOCK PLACEHOLDERS
|
|
5019
|
-
When the selected range includes previously compressed blocks, use this exact placeholder format when referencing one:
|
|
5020
|
-
|
|
5021
|
-
- \`(bN)\`
|
|
5022
|
-
|
|
5023
|
-
Compressed block sections in context are clearly marked with a header:
|
|
5024
|
-
|
|
5025
|
-
- \`[Compressed conversation section]\`
|
|
5026
|
-
|
|
5027
|
-
Compressed block IDs always use the \`bN\` form (never \`mNNNNN\`) and are represented in the same XML metadata tag format.
|
|
5028
|
-
|
|
5029
|
-
Rules:
|
|
5030
|
-
|
|
5031
|
-
- Include every required block placeholder exactly once.
|
|
5032
|
-
- Do not invent placeholders for blocks outside the selected range.
|
|
5033
|
-
- Treat \`(bN)\` placeholders as RESERVED TOKENS. Do not emit \`(bN)\` text anywhere except intentional placeholders.
|
|
5034
|
-
- If you need to mention a block in prose, use plain text like \`compressed bN\` (not as a placeholder).
|
|
5035
|
-
- Preflight check before finalizing: the set of \`(bN)\` placeholders in your summary must exactly match the required set, with no duplicates.
|
|
5036
|
-
|
|
5037
|
-
These placeholders are semantic references. They will be replaced with the full stored compressed block content when the tool processes your output.
|
|
5038
|
-
|
|
5039
|
-
FLOW PRESERVATION WITH PLACEHOLDERS
|
|
5040
|
-
When you use compressed block placeholders, write the surrounding summary text so it still reads correctly AFTER placeholder expansion.
|
|
5041
|
-
|
|
5042
|
-
- Treat each placeholder as a stand-in for a full conversation segment, not as a short label.
|
|
5043
|
-
- Ensure transitions before and after each placeholder preserve chronology and causality.
|
|
5044
|
-
- Do not write text that depends on the placeholder staying literal (for example, "as noted in \`(b2)\`").
|
|
5045
|
-
- Your final meaning must be coherent once each placeholder is replaced with its full compressed block content.
|
|
5046
|
-
|
|
5047
|
-
BOUNDARY IDS
|
|
5048
|
-
You specify boundaries by ID using the injected IDs visible in the conversation:
|
|
5049
|
-
|
|
5050
|
-
- \`mNNNNN\` IDs identify raw messages
|
|
5051
|
-
- \`bN\` IDs identify previously compressed blocks
|
|
5052
|
-
|
|
5053
|
-
Each message has an ID inside XML metadata tags like \`<dcp-message-id>...</dcp-message-id>\`.
|
|
5054
|
-
The same ID tag appears in every tool output of the message it belongs to \u2014 each unique ID identifies one complete message.
|
|
5055
|
-
Treat these tags as boundary metadata only, not as tool result content.
|
|
5056
|
-
|
|
5057
|
-
Rules:
|
|
5058
|
-
|
|
5059
|
-
- Pick \`startId\` and \`endId\` directly from injected IDs in context.
|
|
5060
|
-
- IDs must exist in the current visible context. If you cannot see an ID in the messages above, it is stale and will fail.
|
|
5061
|
-
- \`startId\` must appear before \`endId\`.
|
|
5062
|
-
- Do not invent IDs. Use only IDs that are present in context.
|
|
5063
|
-
- NEVER use IDs from compressed block summaries, previous nudges, or your own memory \u2014 only IDs currently visible as XML metadata tags in the conversation.
|
|
5064
|
-
|
|
5065
|
-
BATCHING
|
|
5066
|
-
When multiple independent ranges are ready and their boundaries do not overlap, include all of them as separate entries in the \`content\` array of a single tool call. Each entry should have its own \`startId\`, \`endId\`, and \`summary\`.
|
|
5067
|
-
`;
|
|
5068
|
-
|
|
5069
|
-
// lib/prompts/compress-message.ts
|
|
5070
|
-
var COMPRESS_MESSAGE = `Collapse selected individual messages in the conversation into detailed summaries.
|
|
5071
|
-
|
|
5072
|
-
THE SUMMARY
|
|
5073
|
-
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings, tool outcomes, and user intent details that matter... EVERYTHING that preserves the value of the selected message after the raw message is removed.
|
|
5074
|
-
|
|
5075
|
-
USER INTENT FIDELITY
|
|
5076
|
-
When a selected message contains user intent, preserve that intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
|
|
5077
|
-
Directly quote short user instructions when that best preserves exact meaning.
|
|
5078
|
-
|
|
5079
|
-
Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool output, and repetition. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
|
|
5080
|
-
If a message contains no significant technical decisions, code changes, or user requirements, produce a minimal one-line summary rather than a detailed one.
|
|
5081
|
-
|
|
5082
|
-
MESSAGE IDS
|
|
5083
|
-
You specify individual raw messages by ID using the injected IDs visible in the conversation:
|
|
5084
|
-
|
|
5085
|
-
- \`mNNNNN\` IDs identify raw messages
|
|
5086
|
-
|
|
5087
|
-
Each message has an ID inside XML metadata tags like \`<dcp-message-id priority="high">m0007</dcp-message-id>\`.
|
|
5088
|
-
The same ID tag appears in every tool output of the message it belongs to \u2014 each unique ID identifies one complete message.
|
|
5089
|
-
Treat these tags as message metadata only, not as content to summarize. Use only the inner \`mNNNNN\` value as the \`messageId\`.
|
|
5090
|
-
The \`priority\` attribute indicates relative context cost. You MUST compress high-priority messages when their full text is no longer necessary for the active task.
|
|
5091
|
-
If prior compress-tool results are present, always compress and summarize them minimally only as part of a broader compression pass. Do not invoke the compress tool solely to re-compress an earlier compression result.
|
|
5092
|
-
Messages marked as \`<dcp-message-id>BLOCKED</dcp-message-id>\` cannot be compressed.
|
|
5093
|
-
|
|
5094
|
-
Rules:
|
|
5095
|
-
|
|
5096
|
-
- Pick each \`messageId\` directly from injected IDs visible in context.
|
|
5097
|
-
- Only use raw message IDs of the form \`mNNNNN\`.
|
|
5098
|
-
- Ignore XML attributes such as \`priority\` when copying the ID; use only the inner \`mNNNNN\` value.
|
|
5099
|
-
- Do not invent IDs. Use only IDs that are present in context.
|
|
5100
|
-
|
|
5101
|
-
BATCHING
|
|
5102
|
-
Select MANY messages in a single tool call when they are safe to compress.
|
|
5103
|
-
Each entry should summarize exactly one message, and the tool can receive as many entries as needed in one batch.
|
|
5104
|
-
|
|
5105
|
-
GENERAL CLEANUP
|
|
5106
|
-
Use the topic "general cleanup" for broad cleanup passes.
|
|
5107
|
-
During general cleanup, compress all medium and high-priority messages that are not relevant to the active task.
|
|
5108
|
-
Optimize for reducing context footprint, not for grouping messages by topic.
|
|
5109
|
-
Do not compress away still-active instructions, unresolved questions, or constraints that are likely to matter soon.
|
|
5110
|
-
Prioritize the earliest messages in the context as they will be the least relevant to the active task.
|
|
5111
|
-
General cleanup should be done periodically between other normal compression tool passes, not as the primary form of compression.
|
|
5112
|
-
`;
|
|
5113
|
-
|
|
5114
|
-
// lib/prompts/context-limit-nudge.ts
|
|
5115
|
-
var CONTEXT_LIMIT_NUDGE = `
|
|
5116
|
-
<system-reminder>
|
|
5117
|
-
\u26A0\uFE0F CRITICAL: Context limit reached. You MUST use the \`compress\` tool NOW.
|
|
5118
|
-
|
|
5119
|
-
If mid-atomic-operation, finish that step first, then compress immediately.
|
|
5120
|
-
|
|
5121
|
-
HOW TO CALL COMPRESS:
|
|
5122
|
-
{
|
|
5123
|
-
"topic": "Short Label",
|
|
5124
|
-
"content": [
|
|
5125
|
-
{
|
|
5126
|
-
"startId": "<ID from early in this conversation>",
|
|
5127
|
-
"endId": "<ID from later in this conversation>",
|
|
5128
|
-
"summary": "Complete technical summary of everything in the range"
|
|
5129
|
-
}
|
|
5130
|
-
]
|
|
5131
|
-
}
|
|
5132
|
-
|
|
5133
|
-
\u26A0\uFE0F ID RULES \u2014 MOST COMMON CAUSE OF ERRORS:
|
|
5134
|
-
- ONLY use IDs you can see in <dcp-message-id> tags in the messages ABOVE.
|
|
5135
|
-
- Do NOT copy IDs from this example. Do NOT invent IDs.
|
|
5136
|
-
- Do NOT use IDs from compressed block summaries \u2014 they are stale.
|
|
5137
|
-
- startId must appear BEFORE endId in the conversation.
|
|
5138
|
-
|
|
5139
|
-
SUMMARY RULES:
|
|
5140
|
-
- Capture ALL essential details: file paths, decisions, constraints, key findings.
|
|
5141
|
-
- Preserve user intent exactly. Direct-quote short user messages.
|
|
5142
|
-
- Prefer one large range over multiple small ones.
|
|
5143
|
-
- Compress OLDER resolved history first. Keep recent active work.
|
|
5144
|
-
</system-reminder>
|
|
5145
|
-
`;
|
|
5146
|
-
|
|
5147
|
-
// lib/prompts/turn-nudge.ts
|
|
5148
|
-
var TURN_NUDGE = `
|
|
5149
|
-
<system-reminder>
|
|
5150
|
-
Context is getting full. Compress closed/older conversation ranges now.
|
|
5151
|
-
|
|
5152
|
-
{
|
|
5153
|
-
"topic": "Short Label",
|
|
5154
|
-
"content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
|
|
5155
|
-
}
|
|
5156
|
-
|
|
5157
|
-
\u26A0\uFE0F ONLY use IDs from <dcp-message-id> tags visible above. Do NOT invent or copy example IDs.
|
|
5158
|
-
</system-reminder>
|
|
5159
|
-
`;
|
|
5160
|
-
|
|
5161
|
-
// lib/prompts/iteration-nudge.ts
|
|
5162
|
-
var ITERATION_NUDGE = `
|
|
5163
|
-
<system-reminder>
|
|
5164
|
-
You've been iterating for a while. If any earlier work is closed and unlikely to be referenced, compress it now.
|
|
5165
|
-
|
|
5166
|
-
{
|
|
5167
|
-
"topic": "Short Label",
|
|
5168
|
-
"content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
|
|
5169
|
-
}
|
|
5170
|
-
|
|
5171
|
-
\u26A0\uFE0F ONLY use IDs from <dcp-message-id> tags visible above. Do NOT invent or copy example IDs.
|
|
5172
|
-
</system-reminder>
|
|
5173
|
-
`;
|
|
5174
|
-
|
|
5175
|
-
// lib/prompts/extensions/system.ts
|
|
5176
|
-
var MANUAL_MODE_SYSTEM_EXTENSION = `<dcp-system-reminder>
|
|
5177
|
-
Manual mode is enabled. Do NOT use compress unless the user has explicitly triggered it through a manual marker.
|
|
5178
|
-
|
|
5179
|
-
Only use the compress tool after seeing \`<compress triggered manually>\` in the current user instruction context.
|
|
5180
|
-
|
|
5181
|
-
Issue exactly ONE compress tool per manual trigger. Do NOT launch multiple compress tools in parallel. Each trigger grants a single compression; after it completes, wait for the next trigger.
|
|
5182
|
-
|
|
5183
|
-
After completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input.
|
|
5184
|
-
</dcp-system-reminder>
|
|
5185
|
-
`;
|
|
5186
|
-
var SUBAGENT_SYSTEM_EXTENSION = `<dcp-system-reminder>
|
|
5187
|
-
You are operating in a subagent environment.
|
|
5188
|
-
|
|
5189
|
-
The initial subagent instruction is imperative and must be followed exactly.
|
|
5190
|
-
It is the only user message intentionally not assigned a message ID, and therefore is not eligible for compression.
|
|
5191
|
-
All subsequent messages in the session will have IDs.
|
|
5192
|
-
</dcp-system-reminder>
|
|
5193
|
-
`;
|
|
5194
|
-
function buildProtectedToolsExtension(protectedTools) {
|
|
5195
|
-
if (protectedTools.length === 0) {
|
|
5196
|
-
return "";
|
|
5018
|
+
// lib/messages/sync.ts
|
|
5019
|
+
function sortBlocksByCreation(a, b) {
|
|
5020
|
+
const createdAtDiff = a.createdAt - b.createdAt;
|
|
5021
|
+
if (createdAtDiff !== 0) {
|
|
5022
|
+
return createdAtDiff;
|
|
5197
5023
|
}
|
|
5198
|
-
|
|
5199
|
-
return `<dcp-system-reminder>
|
|
5200
|
-
The following tools are environment-managed: ${toolList}.
|
|
5201
|
-
Their outputs are automatically preserved during compression.
|
|
5202
|
-
Do not include their content in compress tool summaries \u2014 the environment retains it independently.
|
|
5203
|
-
</dcp-system-reminder>`;
|
|
5024
|
+
return a.blockId - b.blockId;
|
|
5204
5025
|
}
|
|
5026
|
+
var syncCompressionBlocks = (state, logger, messages) => {
|
|
5027
|
+
const messagesState = state.prune.messages;
|
|
5028
|
+
if (!messagesState?.blocksById?.size) {
|
|
5029
|
+
return;
|
|
5030
|
+
}
|
|
5031
|
+
const messageIds = new Set(messages.map((msg) => msg.info.id));
|
|
5032
|
+
const previousActiveBlockIds = new Set(
|
|
5033
|
+
Array.from(messagesState.blocksById.values()).filter((block) => block.active).map((block) => block.blockId)
|
|
5034
|
+
);
|
|
5035
|
+
messagesState.activeBlockIds.clear();
|
|
5036
|
+
messagesState.activeByAnchorMessageId.clear();
|
|
5037
|
+
const now = Date.now();
|
|
5038
|
+
const missingOriginBlockIds = [];
|
|
5039
|
+
const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
|
|
5040
|
+
for (const block of orderedBlocks) {
|
|
5041
|
+
if (block.deactivatedByUser) {
|
|
5042
|
+
block.active = false;
|
|
5043
|
+
if (block.deactivatedAt === void 0) {
|
|
5044
|
+
block.deactivatedAt = now;
|
|
5045
|
+
}
|
|
5046
|
+
block.deactivatedByBlockId = void 0;
|
|
5047
|
+
continue;
|
|
5048
|
+
}
|
|
5049
|
+
if (typeof block.anchorMessageId === "string" && block.anchorMessageId.length > 0 && !messageIds.has(block.anchorMessageId)) {
|
|
5050
|
+
if (!messagesState.byMessageId.has(block.anchorMessageId)) {
|
|
5051
|
+
block.active = false;
|
|
5052
|
+
block.deactivatedAt = now;
|
|
5053
|
+
block.deactivatedByBlockId = void 0;
|
|
5054
|
+
continue;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
for (const consumedBlockId of block.consumedBlockIds) {
|
|
5058
|
+
if (!messagesState.activeBlockIds.has(consumedBlockId)) {
|
|
5059
|
+
continue;
|
|
5060
|
+
}
|
|
5061
|
+
const consumedBlock = messagesState.blocksById.get(consumedBlockId);
|
|
5062
|
+
if (consumedBlock) {
|
|
5063
|
+
consumedBlock.active = false;
|
|
5064
|
+
consumedBlock.deactivatedAt = now;
|
|
5065
|
+
consumedBlock.deactivatedByBlockId = block.blockId;
|
|
5066
|
+
const mappedBlockId = messagesState.activeByAnchorMessageId.get(
|
|
5067
|
+
consumedBlock.anchorMessageId
|
|
5068
|
+
);
|
|
5069
|
+
if (mappedBlockId === consumedBlock.blockId) {
|
|
5070
|
+
messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId);
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
messagesState.activeBlockIds.delete(consumedBlockId);
|
|
5074
|
+
}
|
|
5075
|
+
block.active = true;
|
|
5076
|
+
block.deactivatedAt = void 0;
|
|
5077
|
+
block.deactivatedByBlockId = void 0;
|
|
5078
|
+
messagesState.activeBlockIds.add(block.blockId);
|
|
5079
|
+
if (messageIds.has(block.anchorMessageId)) {
|
|
5080
|
+
messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
|
|
5081
|
+
}
|
|
5082
|
+
}
|
|
5083
|
+
for (const entry of messagesState.byMessageId.values()) {
|
|
5084
|
+
const allBlockIds = Array.isArray(entry.allBlockIds) ? [...new Set(entry.allBlockIds.filter((id) => Number.isInteger(id) && id > 0))] : [];
|
|
5085
|
+
entry.allBlockIds = allBlockIds;
|
|
5086
|
+
entry.activeBlockIds = allBlockIds.filter((id) => messagesState.activeBlockIds.has(id));
|
|
5087
|
+
}
|
|
5088
|
+
const nextActiveBlockIds = messagesState.activeBlockIds;
|
|
5089
|
+
let deactivatedCount = 0;
|
|
5090
|
+
let reactivatedCount = 0;
|
|
5091
|
+
for (const blockId of previousActiveBlockIds) {
|
|
5092
|
+
if (!nextActiveBlockIds.has(blockId)) {
|
|
5093
|
+
deactivatedCount++;
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
for (const blockId of nextActiveBlockIds) {
|
|
5097
|
+
if (!previousActiveBlockIds.has(blockId)) {
|
|
5098
|
+
reactivatedCount++;
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
if (missingOriginBlockIds.length > 0 || deactivatedCount > 0 || reactivatedCount > 0) {
|
|
5102
|
+
logger.info("Synced compress block state", {
|
|
5103
|
+
missingOriginCount: missingOriginBlockIds.length,
|
|
5104
|
+
deactivatedCount,
|
|
5105
|
+
reactivatedCount
|
|
5106
|
+
});
|
|
5107
|
+
}
|
|
5108
|
+
};
|
|
5205
5109
|
|
|
5206
|
-
// lib/
|
|
5207
|
-
var
|
|
5208
|
-
{
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
usage: "Injected into the model system prompt on every request",
|
|
5214
|
-
runtimeField: "system"
|
|
5215
|
-
},
|
|
5216
|
-
{
|
|
5217
|
-
key: "compress-range",
|
|
5218
|
-
fileName: "compress-range.md",
|
|
5219
|
-
label: "Compress Range",
|
|
5220
|
-
description: "range-mode compress tool instructions and summary constraints",
|
|
5221
|
-
usage: "Registered as the range-mode compress tool description",
|
|
5222
|
-
runtimeField: "compressRange"
|
|
5223
|
-
},
|
|
5224
|
-
{
|
|
5225
|
-
key: "compress-message",
|
|
5226
|
-
fileName: "compress-message.md",
|
|
5227
|
-
label: "Compress Message",
|
|
5228
|
-
description: "message-mode compress tool instructions and summary constraints",
|
|
5229
|
-
usage: "Registered as the message-mode compress tool description",
|
|
5230
|
-
runtimeField: "compressMessage"
|
|
5231
|
-
},
|
|
5232
|
-
{
|
|
5233
|
-
key: "context-limit-nudge",
|
|
5234
|
-
fileName: "context-limit-nudge.md",
|
|
5235
|
-
label: "Context Limit Nudge",
|
|
5236
|
-
description: "High-priority nudge when context is over max threshold",
|
|
5237
|
-
usage: "Injected when context usage is beyond configured max limits",
|
|
5238
|
-
runtimeField: "contextLimitNudge"
|
|
5239
|
-
},
|
|
5240
|
-
{
|
|
5241
|
-
key: "turn-nudge",
|
|
5242
|
-
fileName: "turn-nudge.md",
|
|
5243
|
-
label: "Turn Nudge",
|
|
5244
|
-
description: "Nudge to compress closed ranges at turn boundaries",
|
|
5245
|
-
usage: "Injected when context is between min and max limits at a new user turn",
|
|
5246
|
-
runtimeField: "turnNudge"
|
|
5247
|
-
},
|
|
5248
|
-
{
|
|
5249
|
-
key: "iteration-nudge",
|
|
5250
|
-
fileName: "iteration-nudge.md",
|
|
5251
|
-
label: "Iteration Nudge",
|
|
5252
|
-
description: "Nudge after many iterations without user input",
|
|
5253
|
-
usage: "Injected when iteration threshold is crossed",
|
|
5254
|
-
runtimeField: "iterationNudge"
|
|
5110
|
+
// lib/host-permissions.ts
|
|
5111
|
+
var findLastMatchingRule = (rules, predicate) => {
|
|
5112
|
+
for (let index = rules.length - 1; index >= 0; index -= 1) {
|
|
5113
|
+
const rule = rules[index];
|
|
5114
|
+
if (rule && predicate(rule)) {
|
|
5115
|
+
return rule;
|
|
5116
|
+
}
|
|
5255
5117
|
}
|
|
5256
|
-
|
|
5257
|
-
var HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
|
|
5258
|
-
var LEGACY_INLINE_COMMENT_LINE_REGEX = /^[ \t]*\/\/.*?\/\/[ \t]*$/gm;
|
|
5259
|
-
var DCP_SYSTEM_REMINDER_TAG_REGEX = /^\s*<dcp-system-reminder\b[^>]*>[\s\S]*<\/dcp-system-reminder>\s*$/i;
|
|
5260
|
-
var DEFAULTS_README_FILE = "README.md";
|
|
5261
|
-
var BUNDLED_EDITABLE_PROMPTS = {
|
|
5262
|
-
system: SYSTEM,
|
|
5263
|
-
compressRange: COMPRESS_RANGE,
|
|
5264
|
-
compressMessage: COMPRESS_MESSAGE,
|
|
5265
|
-
contextLimitNudge: CONTEXT_LIMIT_NUDGE,
|
|
5266
|
-
turnNudge: TURN_NUDGE,
|
|
5267
|
-
iterationNudge: ITERATION_NUDGE
|
|
5118
|
+
return void 0;
|
|
5268
5119
|
};
|
|
5269
|
-
var
|
|
5270
|
-
|
|
5271
|
-
|
|
5120
|
+
var wildcardMatch = (value, pattern) => {
|
|
5121
|
+
const normalizedValue = value.replaceAll("\\", "/");
|
|
5122
|
+
let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
5123
|
+
if (escaped.endsWith(" .*")) {
|
|
5124
|
+
escaped = escaped.slice(0, -3) + "( .*)?";
|
|
5125
|
+
}
|
|
5126
|
+
const flags = process.platform === "win32" ? "si" : "s";
|
|
5127
|
+
return new RegExp(`^${escaped}$`, flags).test(normalizedValue);
|
|
5272
5128
|
};
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
while (current !== "/") {
|
|
5288
|
-
const candidate = join4(current, ".opencode");
|
|
5289
|
-
if (existsSync4(candidate)) {
|
|
5290
|
-
try {
|
|
5291
|
-
if (statSync2(candidate).isDirectory()) {
|
|
5292
|
-
return candidate;
|
|
5129
|
+
var getPermissionRules = (permissionConfigs) => {
|
|
5130
|
+
const rules = [];
|
|
5131
|
+
for (const permissionConfig of permissionConfigs) {
|
|
5132
|
+
if (!permissionConfig) {
|
|
5133
|
+
continue;
|
|
5134
|
+
}
|
|
5135
|
+
for (const [permission, value] of Object.entries(permissionConfig)) {
|
|
5136
|
+
if (value === "ask" || value === "allow" || value === "deny") {
|
|
5137
|
+
rules.push({ permission, pattern: "*", action: value });
|
|
5138
|
+
continue;
|
|
5139
|
+
}
|
|
5140
|
+
for (const [pattern, action] of Object.entries(value)) {
|
|
5141
|
+
if (action === "ask" || action === "allow" || action === "deny") {
|
|
5142
|
+
rules.push({ permission, pattern, action });
|
|
5293
5143
|
}
|
|
5294
|
-
} catch {
|
|
5295
5144
|
}
|
|
5296
5145
|
}
|
|
5297
|
-
const parent = dirname2(current);
|
|
5298
|
-
if (parent === current) {
|
|
5299
|
-
break;
|
|
5300
|
-
}
|
|
5301
|
-
current = parent;
|
|
5302
5146
|
}
|
|
5303
|
-
return
|
|
5304
|
-
}
|
|
5305
|
-
|
|
5306
|
-
const
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5147
|
+
return rules;
|
|
5148
|
+
};
|
|
5149
|
+
var compressDisabledByOpencode = (...permissionConfigs) => {
|
|
5150
|
+
const match = findLastMatchingRule(
|
|
5151
|
+
getPermissionRules(permissionConfigs),
|
|
5152
|
+
(rule) => wildcardMatch("compress", rule.permission)
|
|
5153
|
+
);
|
|
5154
|
+
return match?.pattern === "*" && match.action === "deny";
|
|
5155
|
+
};
|
|
5156
|
+
var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agentName) => {
|
|
5157
|
+
if (basePermission === "deny") {
|
|
5158
|
+
return "deny";
|
|
5159
|
+
}
|
|
5160
|
+
return compressDisabledByOpencode(
|
|
5161
|
+
hostPermissions.global,
|
|
5162
|
+
agentName ? hostPermissions.agents[agentName] : void 0
|
|
5163
|
+
) ? "deny" : basePermission;
|
|
5164
|
+
};
|
|
5165
|
+
var hasExplicitToolPermission = (permissionConfig, tool4) => {
|
|
5166
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool4) : false;
|
|
5167
|
+
};
|
|
5168
|
+
|
|
5169
|
+
// lib/compress-permission.ts
|
|
5170
|
+
var compressPermission = (state, config) => {
|
|
5171
|
+
return state.compressPermission ?? config.compress.permission;
|
|
5172
|
+
};
|
|
5173
|
+
var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
|
|
5174
|
+
const activeAgent = getLastUserMessage(messages)?.info.agent;
|
|
5175
|
+
state.compressPermission = resolveEffectiveCompressPermission(
|
|
5176
|
+
config.compress.permission,
|
|
5177
|
+
hostPermissions,
|
|
5178
|
+
activeAgent
|
|
5179
|
+
);
|
|
5180
|
+
};
|
|
5181
|
+
|
|
5182
|
+
// lib/prompts/extensions/nudge.ts
|
|
5183
|
+
function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
5184
|
+
const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
|
|
5185
|
+
const refs = activeBlockIds.map((id) => `b${id}`);
|
|
5186
|
+
const blockCount = refs.length;
|
|
5187
|
+
const blockList = blockCount > 0 ? refs.join(", ") : "none";
|
|
5188
|
+
const lines = [
|
|
5189
|
+
"Compressed block context:",
|
|
5190
|
+
`- Active compressed blocks: ${blockCount} (${blockList})`,
|
|
5191
|
+
"- If your selected compression range includes any listed block, include each required placeholder exactly once in the summary using `(bN)`."
|
|
5192
|
+
];
|
|
5193
|
+
const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
|
|
5194
|
+
if (gcConfig && usageRatio > 0.5) {
|
|
5195
|
+
const promotionThreshold = gcConfig.promotionThreshold;
|
|
5196
|
+
const agingBlocks = [];
|
|
5197
|
+
for (const blockId of activeBlockIds) {
|
|
5198
|
+
const block = state.prune.messages.blocksById.get(blockId);
|
|
5199
|
+
if (!block) continue;
|
|
5200
|
+
const survived = block.survivedCount ?? 0;
|
|
5201
|
+
const gen = block.generation ?? "young";
|
|
5202
|
+
const sizeK = (block.summary.length / 1e3).toFixed(1);
|
|
5203
|
+
const preview = block.summary.slice(0, 120).replace(/\n/g, " ");
|
|
5204
|
+
if (gen === "old" || survived >= promotionThreshold - 2) {
|
|
5205
|
+
agingBlocks.push(
|
|
5206
|
+
` b${blockId}: age=${survived}/${promotionThreshold}, gen=${gen}, size=${sizeK}K chars \u2014 ${preview}...`
|
|
5207
|
+
);
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
if (agingBlocks.length > 0) {
|
|
5211
|
+
lines.push("");
|
|
5212
|
+
lines.push("\u26A0\uFE0F Block aging warning \u2014 these blocks may be truncated by GC soon:");
|
|
5213
|
+
lines.push(...agingBlocks);
|
|
5214
|
+
lines.push(
|
|
5215
|
+
"To preserve important content: use the compress tool to re-summarize these blocks into new concise ones. Unhandled blocks will be auto-truncated."
|
|
5216
|
+
);
|
|
5315
5217
|
}
|
|
5316
5218
|
}
|
|
5317
|
-
|
|
5318
|
-
const globalOverridesDir = join4(globalRoot, "overrides");
|
|
5319
|
-
const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join4(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
|
|
5320
|
-
const opencodeDir = findOpencodeDir2(workingDirectory);
|
|
5321
|
-
const projectOverridesDir = opencodeDir ? join4(opencodeDir, "acp-prompts", "overrides") : null;
|
|
5322
|
-
return {
|
|
5323
|
-
defaultsDir,
|
|
5324
|
-
globalOverridesDir,
|
|
5325
|
-
configDirOverridesDir,
|
|
5326
|
-
projectOverridesDir
|
|
5327
|
-
};
|
|
5219
|
+
return lines.join("\n");
|
|
5328
5220
|
}
|
|
5329
|
-
function
|
|
5330
|
-
const
|
|
5331
|
-
return
|
|
5221
|
+
function renderMessagePriorityGuidance(priorityLabel, refs) {
|
|
5222
|
+
const refList = refs.length > 0 ? refs.join(", ") : "none";
|
|
5223
|
+
return [
|
|
5224
|
+
"Message priority context:",
|
|
5225
|
+
"- Higher-priority older messages consume more context and should be compressed right away if it is safe to do so.",
|
|
5226
|
+
`- ${priorityLabel}-priority message IDs before this point: ${refList}`
|
|
5227
|
+
].join("\n");
|
|
5332
5228
|
}
|
|
5333
|
-
function
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
return trimmed.replace(/^\s*<dcp-system-reminder\b[^>]*>\s*/i, "").replace(/\s*<\/dcp-system-reminder>\s*$/i, "").trim();
|
|
5229
|
+
function appendGuidanceToDcpTag(nudgeText, guidance) {
|
|
5230
|
+
if (!guidance.trim()) {
|
|
5231
|
+
return nudgeText;
|
|
5337
5232
|
}
|
|
5338
|
-
|
|
5233
|
+
const closeTag = "</dcp-system-reminder>";
|
|
5234
|
+
const closeTagIndex = nudgeText.lastIndexOf(closeTag);
|
|
5235
|
+
if (closeTagIndex === -1) {
|
|
5236
|
+
return nudgeText;
|
|
5237
|
+
}
|
|
5238
|
+
const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd();
|
|
5239
|
+
const afterClose = nudgeText.slice(closeTagIndex);
|
|
5240
|
+
return `${beforeClose}
|
|
5241
|
+
|
|
5242
|
+
${guidance}
|
|
5243
|
+
${afterClose}`;
|
|
5339
5244
|
}
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5245
|
+
|
|
5246
|
+
// lib/messages/priority.ts
|
|
5247
|
+
var MEDIUM_PRIORITY_MIN_TOKENS = 500;
|
|
5248
|
+
var HIGH_PRIORITY_MIN_TOKENS = 5e3;
|
|
5249
|
+
function buildPriorityMap(config, state, messages) {
|
|
5250
|
+
if (config.compress.mode !== "message") {
|
|
5251
|
+
return /* @__PURE__ */ new Map();
|
|
5344
5252
|
}
|
|
5345
|
-
const
|
|
5346
|
-
const
|
|
5347
|
-
|
|
5348
|
-
|
|
5253
|
+
const priorities = /* @__PURE__ */ new Map();
|
|
5254
|
+
for (const message of messages) {
|
|
5255
|
+
if (isIgnoredUserMessage(message)) {
|
|
5256
|
+
continue;
|
|
5257
|
+
}
|
|
5258
|
+
if (isProtectedUserMessage(config, message)) {
|
|
5259
|
+
continue;
|
|
5260
|
+
}
|
|
5261
|
+
if (isMessageCompacted(state, message)) {
|
|
5262
|
+
continue;
|
|
5263
|
+
}
|
|
5264
|
+
const rawMessageId = message.info.id;
|
|
5265
|
+
if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
|
|
5266
|
+
continue;
|
|
5267
|
+
}
|
|
5268
|
+
const ref = state.messageIds.byRawId.get(rawMessageId);
|
|
5269
|
+
if (!ref) {
|
|
5270
|
+
continue;
|
|
5271
|
+
}
|
|
5272
|
+
const tokenCount = countAllMessageTokens(message);
|
|
5273
|
+
priorities.set(rawMessageId, {
|
|
5274
|
+
ref,
|
|
5275
|
+
tokenCount,
|
|
5276
|
+
priority: messageHasCompress(message) ? "high" : classifyMessagePriority(tokenCount)
|
|
5277
|
+
});
|
|
5349
5278
|
}
|
|
5350
|
-
return
|
|
5351
|
-
}
|
|
5352
|
-
function stripPromptComments(content) {
|
|
5353
|
-
return content.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").replace(HTML_COMMENT_REGEX, "").replace(LEGACY_INLINE_COMMENT_LINE_REGEX, "");
|
|
5279
|
+
return priorities;
|
|
5354
5280
|
}
|
|
5355
|
-
function
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
return "";
|
|
5281
|
+
function classifyMessagePriority(tokenCount) {
|
|
5282
|
+
if (tokenCount >= HIGH_PRIORITY_MIN_TOKENS) {
|
|
5283
|
+
return "high";
|
|
5359
5284
|
}
|
|
5360
|
-
if (
|
|
5361
|
-
|
|
5362
|
-
normalized = stripConditionalTag(normalized, "subagent");
|
|
5285
|
+
if (tokenCount >= MEDIUM_PRIORITY_MIN_TOKENS) {
|
|
5286
|
+
return "medium";
|
|
5363
5287
|
}
|
|
5364
|
-
|
|
5365
|
-
|
|
5288
|
+
return "low";
|
|
5289
|
+
}
|
|
5290
|
+
function listPriorityRefsBeforeIndex(messages, priorities, anchorIndex, priority) {
|
|
5291
|
+
const refs = [];
|
|
5292
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5293
|
+
const upperBound = Math.max(0, Math.min(anchorIndex, messages.length));
|
|
5294
|
+
for (let index = 0; index < upperBound; index++) {
|
|
5295
|
+
const rawMessageId = messages[index]?.info.id;
|
|
5296
|
+
if (typeof rawMessageId !== "string") {
|
|
5297
|
+
continue;
|
|
5298
|
+
}
|
|
5299
|
+
const entry = priorities.get(rawMessageId);
|
|
5300
|
+
if (!entry || entry.priority !== priority || seen.has(entry.ref)) {
|
|
5301
|
+
continue;
|
|
5302
|
+
}
|
|
5303
|
+
seen.add(entry.ref);
|
|
5304
|
+
refs.push(entry.ref);
|
|
5366
5305
|
}
|
|
5367
|
-
return
|
|
5306
|
+
return refs;
|
|
5368
5307
|
}
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5308
|
+
|
|
5309
|
+
// lib/messages/inject/utils.ts
|
|
5310
|
+
var MESSAGE_MODE_NUDGE_PRIORITY = "high";
|
|
5311
|
+
function getNudgeFrequency(config) {
|
|
5312
|
+
return Math.max(1, Math.floor(config.compress.nudgeFrequency || 1));
|
|
5313
|
+
}
|
|
5314
|
+
function getIterationNudgeThreshold(config) {
|
|
5315
|
+
return Math.max(1, Math.floor(config.compress.iterationNudgeThreshold || 1));
|
|
5316
|
+
}
|
|
5317
|
+
function findLastNonIgnoredMessage(messages) {
|
|
5318
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5319
|
+
const message = messages[i];
|
|
5320
|
+
if (isIgnoredUserMessage(message)) {
|
|
5321
|
+
continue;
|
|
5322
|
+
}
|
|
5323
|
+
if (isSyntheticMessage(message)) {
|
|
5324
|
+
continue;
|
|
5325
|
+
}
|
|
5326
|
+
return { message, index: i };
|
|
5373
5327
|
}
|
|
5374
|
-
|
|
5375
|
-
|
|
5328
|
+
return null;
|
|
5329
|
+
}
|
|
5330
|
+
function countMessagesAfterIndex(messages, index) {
|
|
5331
|
+
let count = 0;
|
|
5332
|
+
for (let i = index + 1; i < messages.length; i++) {
|
|
5333
|
+
const message = messages[i];
|
|
5334
|
+
if (isIgnoredUserMessage(message)) {
|
|
5335
|
+
continue;
|
|
5336
|
+
}
|
|
5337
|
+
count++;
|
|
5376
5338
|
}
|
|
5377
|
-
return
|
|
5378
|
-
${trimmed}
|
|
5379
|
-
</dcp-system-reminder>`;
|
|
5339
|
+
return count;
|
|
5380
5340
|
}
|
|
5381
|
-
function
|
|
5382
|
-
|
|
5383
|
-
|
|
5341
|
+
function getModelInfo(messages) {
|
|
5342
|
+
const lastUserMessage = getLastUserMessage(messages);
|
|
5343
|
+
if (!lastUserMessage) {
|
|
5344
|
+
return {
|
|
5345
|
+
providerId: void 0,
|
|
5346
|
+
modelId: void 0
|
|
5347
|
+
};
|
|
5348
|
+
}
|
|
5349
|
+
const userInfo = lastUserMessage.info;
|
|
5350
|
+
return {
|
|
5351
|
+
providerId: userInfo.model.providerID,
|
|
5352
|
+
modelId: userInfo.model.modelID
|
|
5353
|
+
};
|
|
5384
5354
|
}
|
|
5385
|
-
function
|
|
5386
|
-
const
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5355
|
+
function resolveContextTokenLimit(config, state, providerId, modelId, threshold) {
|
|
5356
|
+
const parseLimitValue = (limit) => {
|
|
5357
|
+
if (limit === void 0) {
|
|
5358
|
+
return void 0;
|
|
5359
|
+
}
|
|
5360
|
+
if (typeof limit === "number") {
|
|
5361
|
+
return limit;
|
|
5362
|
+
}
|
|
5363
|
+
if (!limit.endsWith("%") || state.modelContextLimit === void 0) {
|
|
5364
|
+
return void 0;
|
|
5365
|
+
}
|
|
5366
|
+
const parsedPercent = parseFloat(limit.slice(0, -1));
|
|
5367
|
+
if (isNaN(parsedPercent)) {
|
|
5368
|
+
return void 0;
|
|
5369
|
+
}
|
|
5370
|
+
const roundedPercent = Math.round(parsedPercent);
|
|
5371
|
+
const clampedPercent = Math.max(0, Math.min(100, roundedPercent));
|
|
5372
|
+
return Math.round(clampedPercent / 100 * state.modelContextLimit);
|
|
5373
|
+
};
|
|
5374
|
+
const modelLimits = threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits;
|
|
5375
|
+
if (modelLimits && providerId !== void 0 && modelId !== void 0) {
|
|
5376
|
+
const providerModelId = `${providerId}/${modelId}`;
|
|
5377
|
+
const modelLimit = modelLimits[providerModelId];
|
|
5378
|
+
if (modelLimit !== void 0) {
|
|
5379
|
+
return parseLimitValue(modelLimit);
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
const globalLimit = threshold === "max" ? config.compress.maxContextLimit : config.compress.minContextLimit;
|
|
5383
|
+
return parseLimitValue(globalLimit);
|
|
5384
|
+
}
|
|
5385
|
+
function isContextOverLimits(config, state, providerId, modelId, messages) {
|
|
5386
|
+
const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(state) : 0;
|
|
5387
|
+
const resolvedMaxContextLimit = resolveContextTokenLimit(
|
|
5388
|
+
config,
|
|
5389
|
+
state,
|
|
5390
|
+
providerId,
|
|
5391
|
+
modelId,
|
|
5392
|
+
"max"
|
|
5404
5393
|
);
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5394
|
+
const maxContextLimit = resolvedMaxContextLimit === void 0 ? void 0 : resolvedMaxContextLimit + summaryTokenExtension;
|
|
5395
|
+
const minContextLimit = resolveContextTokenLimit(config, state, providerId, modelId, "min");
|
|
5396
|
+
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
5397
|
+
let overMaxLimit = maxContextLimit === void 0 ? false : currentTokens > maxContextLimit;
|
|
5398
|
+
const overMinLimit = minContextLimit === void 0 ? false : currentTokens >= minContextLimit;
|
|
5399
|
+
if (overMaxLimit) {
|
|
5400
|
+
const recentCompressCount = 3;
|
|
5401
|
+
const recentMessages = messages.slice(-recentCompressCount);
|
|
5402
|
+
for (const msg of recentMessages) {
|
|
5403
|
+
if (msg.info.role === "assistant" && msg.parts) {
|
|
5404
|
+
for (const part of msg.parts) {
|
|
5405
|
+
if (part.type === "tool-invocation" && part.toolInvocation?.toolName === "compress") {
|
|
5406
|
+
overMaxLimit = false;
|
|
5407
|
+
break;
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
if (!overMaxLimit) break;
|
|
5412
|
+
}
|
|
5417
5413
|
}
|
|
5418
|
-
return
|
|
5419
|
-
|
|
5414
|
+
return {
|
|
5415
|
+
overMaxLimit,
|
|
5416
|
+
overMinLimit,
|
|
5417
|
+
currentTokens,
|
|
5418
|
+
modelContextLimit: state.modelContextLimit
|
|
5419
|
+
};
|
|
5420
5420
|
}
|
|
5421
|
-
function
|
|
5422
|
-
if (
|
|
5423
|
-
return
|
|
5421
|
+
function addAnchor(anchorMessageIds, anchorMessageId, anchorMessageIndex, messages, interval) {
|
|
5422
|
+
if (anchorMessageIndex < 0) {
|
|
5423
|
+
return false;
|
|
5424
5424
|
}
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5425
|
+
let latestAnchorMessageIndex = -1;
|
|
5426
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5427
|
+
if (anchorMessageIds.has(messages[i].info.id)) {
|
|
5428
|
+
latestAnchorMessageIndex = i;
|
|
5429
|
+
break;
|
|
5430
|
+
}
|
|
5429
5431
|
}
|
|
5432
|
+
const shouldAdd = latestAnchorMessageIndex < 0 || anchorMessageIndex - latestAnchorMessageIndex >= interval;
|
|
5433
|
+
if (!shouldAdd) {
|
|
5434
|
+
return false;
|
|
5435
|
+
}
|
|
5436
|
+
const previousSize = anchorMessageIds.size;
|
|
5437
|
+
anchorMessageIds.add(anchorMessageId);
|
|
5438
|
+
return anchorMessageIds.size !== previousSize;
|
|
5430
5439
|
}
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
customPromptsEnabled;
|
|
5435
|
-
runtimePrompts;
|
|
5436
|
-
constructor(logger, workingDirectory, customPromptsEnabled = false) {
|
|
5437
|
-
this.logger = logger;
|
|
5438
|
-
this.paths = resolvePromptPaths(workingDirectory);
|
|
5439
|
-
this.customPromptsEnabled = customPromptsEnabled;
|
|
5440
|
-
this.runtimePrompts = createBundledRuntimePrompts();
|
|
5441
|
-
if (this.customPromptsEnabled) {
|
|
5442
|
-
this.ensureDefaultFiles();
|
|
5443
|
-
}
|
|
5444
|
-
this.reload();
|
|
5440
|
+
function buildMessagePriorityGuidance(messages, compressionPriorities, anchorIndex, priority) {
|
|
5441
|
+
if (!compressionPriorities || compressionPriorities.size === 0) {
|
|
5442
|
+
return "";
|
|
5445
5443
|
}
|
|
5446
|
-
|
|
5447
|
-
|
|
5444
|
+
const refs = listPriorityRefsBeforeIndex(messages, compressionPriorities, anchorIndex, priority);
|
|
5445
|
+
const priorityLabel = `${priority[0].toUpperCase()}${priority.slice(1)}`;
|
|
5446
|
+
return renderMessagePriorityGuidance(priorityLabel, refs);
|
|
5447
|
+
}
|
|
5448
|
+
function injectAnchoredNudge(message, nudgeText) {
|
|
5449
|
+
if (!nudgeText.trim()) {
|
|
5450
|
+
return;
|
|
5448
5451
|
}
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
if (!this.customPromptsEnabled) {
|
|
5452
|
-
this.runtimePrompts = nextPrompts;
|
|
5452
|
+
if (message.info.role === "user") {
|
|
5453
|
+
if (appendToLastTextPart(message, nudgeText)) {
|
|
5453
5454
|
return;
|
|
5454
5455
|
}
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
const bundledEditable = toEditablePromptText(definition, bundledSource);
|
|
5458
|
-
const bundledRuntime = wrapRuntimePromptContent(definition, bundledEditable);
|
|
5459
|
-
const fallbackValue = bundledRuntime || bundledSource.trim();
|
|
5460
|
-
let effectiveValue = fallbackValue;
|
|
5461
|
-
for (const candidate of this.getOverrideCandidates(definition.fileName)) {
|
|
5462
|
-
const rawOverride = readFileIfExists(candidate.path);
|
|
5463
|
-
if (rawOverride === null) {
|
|
5464
|
-
continue;
|
|
5465
|
-
}
|
|
5466
|
-
const editableOverride = toEditablePromptText(definition, rawOverride);
|
|
5467
|
-
if (!editableOverride) {
|
|
5468
|
-
this.logger.warn("Prompt override is empty or invalid after normalization", {
|
|
5469
|
-
key: definition.key,
|
|
5470
|
-
path: candidate.path
|
|
5471
|
-
});
|
|
5472
|
-
continue;
|
|
5473
|
-
}
|
|
5474
|
-
const wrappedOverride = wrapRuntimePromptContent(definition, editableOverride);
|
|
5475
|
-
if (!wrappedOverride) {
|
|
5476
|
-
this.logger.warn("Prompt override could not be wrapped for runtime", {
|
|
5477
|
-
key: definition.key,
|
|
5478
|
-
path: candidate.path
|
|
5479
|
-
});
|
|
5480
|
-
continue;
|
|
5481
|
-
}
|
|
5482
|
-
effectiveValue = wrappedOverride;
|
|
5483
|
-
break;
|
|
5484
|
-
}
|
|
5485
|
-
nextPrompts[definition.runtimeField] = effectiveValue;
|
|
5486
|
-
}
|
|
5487
|
-
this.runtimePrompts = nextPrompts;
|
|
5456
|
+
message.parts.push(createSyntheticTextPart(message, nudgeText));
|
|
5457
|
+
return;
|
|
5488
5458
|
}
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
if (this.paths.projectOverridesDir) {
|
|
5492
|
-
candidates.push({
|
|
5493
|
-
path: join4(this.paths.projectOverridesDir, fileName)
|
|
5494
|
-
});
|
|
5495
|
-
}
|
|
5496
|
-
if (this.paths.configDirOverridesDir) {
|
|
5497
|
-
candidates.push({
|
|
5498
|
-
path: join4(this.paths.configDirOverridesDir, fileName)
|
|
5499
|
-
});
|
|
5500
|
-
}
|
|
5501
|
-
candidates.push({
|
|
5502
|
-
path: join4(this.paths.globalOverridesDir, fileName)
|
|
5503
|
-
});
|
|
5504
|
-
return candidates;
|
|
5459
|
+
if (message.info.role !== "assistant") {
|
|
5460
|
+
return;
|
|
5505
5461
|
}
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
globalOverridesDir: this.paths.globalOverridesDir
|
|
5514
|
-
});
|
|
5515
|
-
return;
|
|
5516
|
-
}
|
|
5517
|
-
for (const definition of PROMPT_DEFINITIONS) {
|
|
5518
|
-
const bundledEditable = toEditablePromptText(
|
|
5519
|
-
definition,
|
|
5520
|
-
BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
|
|
5521
|
-
);
|
|
5522
|
-
const managedContent = buildDefaultPromptFileContent(
|
|
5523
|
-
bundledEditable || BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
|
|
5524
|
-
);
|
|
5525
|
-
const filePath = join4(this.paths.defaultsDir, definition.fileName);
|
|
5526
|
-
try {
|
|
5527
|
-
const existing = readFileIfExists(filePath);
|
|
5528
|
-
if (existing === managedContent) {
|
|
5529
|
-
continue;
|
|
5530
|
-
}
|
|
5531
|
-
writeFileSync2(filePath, managedContent, "utf-8");
|
|
5532
|
-
} catch {
|
|
5533
|
-
this.logger.warn("Failed to write default prompt file", {
|
|
5534
|
-
key: definition.key,
|
|
5535
|
-
path: filePath
|
|
5536
|
-
});
|
|
5537
|
-
}
|
|
5538
|
-
}
|
|
5539
|
-
const readmePath = join4(this.paths.defaultsDir, DEFAULTS_README_FILE);
|
|
5540
|
-
const readmeContent = buildDefaultsReadmeContent();
|
|
5541
|
-
try {
|
|
5542
|
-
const existing = readFileIfExists(readmePath);
|
|
5543
|
-
if (existing !== readmeContent) {
|
|
5544
|
-
writeFileSync2(readmePath, readmeContent, "utf-8");
|
|
5462
|
+
if (!hasContent(message)) {
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5465
|
+
for (const part of message.parts) {
|
|
5466
|
+
if (part.type === "text") {
|
|
5467
|
+
if (appendToTextPart(part, nudgeText)) {
|
|
5468
|
+
return;
|
|
5545
5469
|
}
|
|
5546
|
-
} catch {
|
|
5547
|
-
this.logger.warn("Failed to write defaults README", {
|
|
5548
|
-
path: readmePath
|
|
5549
|
-
});
|
|
5550
5470
|
}
|
|
5551
5471
|
}
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
var DCP_MESSAGE_REF_TAG_REGEX = /<dcp-message-id>m\d+<\/dcp-message-id>/g;
|
|
5559
|
-
var DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi;
|
|
5560
|
-
var DCP_UNPAIRED_TAG_REGEX = /<\/?dcp[^>]*>/gi;
|
|
5561
|
-
var generateStableId = (prefix, seed) => {
|
|
5562
|
-
const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
|
|
5563
|
-
return `${prefix}_${hash}`;
|
|
5564
|
-
};
|
|
5565
|
-
var createSyntheticUserMessage = (baseMessage, content, stableSeed) => {
|
|
5566
|
-
const userInfo = baseMessage.info;
|
|
5567
|
-
const now = Date.now();
|
|
5568
|
-
const deterministicSeed = stableSeed?.trim() || userInfo.id;
|
|
5569
|
-
const messageId = generateStableId("msg_dcp_summary", deterministicSeed);
|
|
5570
|
-
const partId = generateStableId("prt_dcp_summary", deterministicSeed);
|
|
5571
|
-
return {
|
|
5572
|
-
info: {
|
|
5573
|
-
id: messageId,
|
|
5574
|
-
sessionID: userInfo.sessionID,
|
|
5575
|
-
role: "user",
|
|
5576
|
-
agent: userInfo.agent,
|
|
5577
|
-
model: userInfo.model,
|
|
5578
|
-
time: { created: now }
|
|
5579
|
-
},
|
|
5580
|
-
parts: [
|
|
5581
|
-
{
|
|
5582
|
-
id: partId,
|
|
5583
|
-
sessionID: userInfo.sessionID,
|
|
5584
|
-
messageID: messageId,
|
|
5585
|
-
type: "text",
|
|
5586
|
-
text: content
|
|
5587
|
-
}
|
|
5588
|
-
]
|
|
5589
|
-
};
|
|
5590
|
-
};
|
|
5591
|
-
var createSyntheticTextPart = (baseMessage, content, stableSeed) => {
|
|
5592
|
-
const userInfo = baseMessage.info;
|
|
5593
|
-
const deterministicSeed = stableSeed?.trim() || userInfo.id;
|
|
5594
|
-
const partId = generateStableId("prt_dcp_text", deterministicSeed);
|
|
5595
|
-
return {
|
|
5596
|
-
id: partId,
|
|
5597
|
-
sessionID: userInfo.sessionID,
|
|
5598
|
-
messageID: userInfo.id,
|
|
5599
|
-
type: "text",
|
|
5600
|
-
text: content
|
|
5601
|
-
};
|
|
5602
|
-
};
|
|
5603
|
-
var appendToLastTextPart = (message, injection) => {
|
|
5604
|
-
const textPart = findLastTextPart(message);
|
|
5605
|
-
if (!textPart) {
|
|
5606
|
-
return false;
|
|
5472
|
+
const syntheticPart = createSyntheticTextPart(message, nudgeText);
|
|
5473
|
+
const firstToolIndex = message.parts.findIndex((p) => p.type === "tool");
|
|
5474
|
+
if (firstToolIndex === -1) {
|
|
5475
|
+
message.parts.push(syntheticPart);
|
|
5476
|
+
} else {
|
|
5477
|
+
message.parts.splice(firstToolIndex, 0, syntheticPart);
|
|
5607
5478
|
}
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
for (
|
|
5612
|
-
const
|
|
5613
|
-
if (
|
|
5614
|
-
|
|
5479
|
+
}
|
|
5480
|
+
function collectAnchoredMessages(anchorMessageIds, messages) {
|
|
5481
|
+
const anchoredMessages = [];
|
|
5482
|
+
for (const anchorMessageId of anchorMessageIds) {
|
|
5483
|
+
const index = messages.findIndex((message) => message.info.id === anchorMessageId);
|
|
5484
|
+
if (index === -1) {
|
|
5485
|
+
continue;
|
|
5486
|
+
}
|
|
5487
|
+
anchoredMessages.push({
|
|
5488
|
+
message: messages[index],
|
|
5489
|
+
index
|
|
5490
|
+
});
|
|
5491
|
+
}
|
|
5492
|
+
return anchoredMessages;
|
|
5493
|
+
}
|
|
5494
|
+
function collectTurnNudgeAnchors2(state, config, messages) {
|
|
5495
|
+
const turnNudgeAnchors = /* @__PURE__ */ new Set();
|
|
5496
|
+
const targetRole = config.compress.nudgeForce === "strong" ? "user" : "assistant";
|
|
5497
|
+
for (const message of messages) {
|
|
5498
|
+
if (!state.nudges.turnNudgeAnchors.has(message.info.id)) continue;
|
|
5499
|
+
if (message.info.role === targetRole) {
|
|
5500
|
+
turnNudgeAnchors.add(message.info.id);
|
|
5615
5501
|
}
|
|
5616
5502
|
}
|
|
5617
|
-
return
|
|
5618
|
-
}
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5503
|
+
return turnNudgeAnchors;
|
|
5504
|
+
}
|
|
5505
|
+
function applyRangeModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText, compressedBlockGuidance) {
|
|
5506
|
+
const nudgeText = appendGuidanceToDcpTag(baseNudgeText, compressedBlockGuidance);
|
|
5507
|
+
if (!nudgeText.trim()) {
|
|
5508
|
+
return;
|
|
5622
5509
|
}
|
|
5623
|
-
const
|
|
5624
|
-
|
|
5625
|
-
return false;
|
|
5510
|
+
for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
|
|
5511
|
+
injectAnchoredNudge(message, nudgeText);
|
|
5626
5512
|
}
|
|
5627
|
-
|
|
5628
|
-
|
|
5513
|
+
}
|
|
5514
|
+
function applyMessageModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText, compressionPriorities) {
|
|
5515
|
+
for (const { message, index } of collectAnchoredMessages(anchorMessageIds, messages)) {
|
|
5516
|
+
const priorityGuidance = buildMessagePriorityGuidance(
|
|
5517
|
+
messages,
|
|
5518
|
+
compressionPriorities,
|
|
5519
|
+
index,
|
|
5520
|
+
MESSAGE_MODE_NUDGE_PRIORITY
|
|
5521
|
+
);
|
|
5522
|
+
const nudgeText = appendGuidanceToDcpTag(baseNudgeText, priorityGuidance);
|
|
5523
|
+
injectAnchoredNudge(message, nudgeText);
|
|
5629
5524
|
}
|
|
5630
|
-
|
|
5631
|
-
|
|
5525
|
+
}
|
|
5526
|
+
function buildContextUsageInfo(currentTokens, modelContextLimit) {
|
|
5527
|
+
if (currentTokens === void 0 || modelContextLimit === void 0 || modelContextLimit === 0) {
|
|
5528
|
+
return "";
|
|
5529
|
+
}
|
|
5530
|
+
const percentage = (currentTokens / modelContextLimit * 100).toFixed(1);
|
|
5531
|
+
const formatK = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
5532
|
+
return `
|
|
5632
5533
|
|
|
5633
|
-
${
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5534
|
+
Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%). ACP (Active Context Pruning) threshold: 55%. You ARE the ACP agent \u2014 use the compress tool proactively to manage context quality.`;
|
|
5535
|
+
}
|
|
5536
|
+
function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
|
|
5537
|
+
const contextUsageInfo = buildContextUsageInfo(currentTokens, modelContextLimit);
|
|
5538
|
+
const contextLimitNudgeWithUsage = prompts.contextLimitNudge + contextUsageInfo;
|
|
5539
|
+
const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
|
|
5540
|
+
if (suffixMessage) {
|
|
5541
|
+
const nudgeParts = [];
|
|
5542
|
+
if (config.compress.mode === "message") {
|
|
5543
|
+
if (state.nudges.contextLimitAnchors.size > 0) {
|
|
5544
|
+
for (const { index } of collectAnchoredMessages(state.nudges.contextLimitAnchors, messages)) {
|
|
5545
|
+
const guidance = buildMessagePriorityGuidance(messages, compressionPriorities, index, MESSAGE_MODE_NUDGE_PRIORITY);
|
|
5546
|
+
nudgeParts.push(appendGuidanceToDcpTag(contextLimitNudgeWithUsage, guidance));
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
if (turnNudgeAnchors.size > 0) {
|
|
5550
|
+
for (const { index } of collectAnchoredMessages(turnNudgeAnchors, messages)) {
|
|
5551
|
+
const guidance = buildMessagePriorityGuidance(messages, compressionPriorities, index, MESSAGE_MODE_NUDGE_PRIORITY);
|
|
5552
|
+
nudgeParts.push(appendGuidanceToDcpTag(prompts.turnNudge, guidance));
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
if (state.nudges.iterationNudgeAnchors.size > 0) {
|
|
5556
|
+
for (const { index } of collectAnchoredMessages(state.nudges.iterationNudgeAnchors, messages)) {
|
|
5557
|
+
const guidance = buildMessagePriorityGuidance(messages, compressionPriorities, index, MESSAGE_MODE_NUDGE_PRIORITY);
|
|
5558
|
+
nudgeParts.push(appendGuidanceToDcpTag(prompts.iterationNudge, guidance));
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
} else {
|
|
5562
|
+
if (state.nudges.contextLimitAnchors.size > 0) {
|
|
5563
|
+
nudgeParts.push(contextLimitNudgeWithUsage);
|
|
5564
|
+
}
|
|
5565
|
+
if (turnNudgeAnchors.size > 0) {
|
|
5566
|
+
nudgeParts.push(prompts.turnNudge);
|
|
5567
|
+
}
|
|
5568
|
+
if (state.nudges.iterationNudgeAnchors.size > 0) {
|
|
5569
|
+
nudgeParts.push(prompts.iterationNudge);
|
|
5570
|
+
}
|
|
5641
5571
|
}
|
|
5572
|
+
const combined = nudgeParts.join("\n\n");
|
|
5573
|
+
if (combined.trim()) {
|
|
5574
|
+
injectAnchoredNudge(suffixMessage, combined);
|
|
5575
|
+
}
|
|
5576
|
+
return;
|
|
5642
5577
|
}
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5578
|
+
if (config.compress.mode === "message") {
|
|
5579
|
+
applyMessageModeAnchoredNudge(
|
|
5580
|
+
state.nudges.contextLimitAnchors,
|
|
5581
|
+
messages,
|
|
5582
|
+
contextLimitNudgeWithUsage,
|
|
5583
|
+
compressionPriorities
|
|
5584
|
+
);
|
|
5585
|
+
applyMessageModeAnchoredNudge(
|
|
5586
|
+
turnNudgeAnchors,
|
|
5587
|
+
messages,
|
|
5588
|
+
prompts.turnNudge,
|
|
5589
|
+
compressionPriorities
|
|
5590
|
+
);
|
|
5591
|
+
applyMessageModeAnchoredNudge(
|
|
5592
|
+
state.nudges.iterationNudgeAnchors,
|
|
5593
|
+
messages,
|
|
5594
|
+
prompts.iterationNudge,
|
|
5595
|
+
compressionPriorities
|
|
5596
|
+
);
|
|
5597
|
+
return;
|
|
5648
5598
|
}
|
|
5649
|
-
|
|
5650
|
-
|
|
5599
|
+
applyRangeModeAnchoredNudge(
|
|
5600
|
+
state.nudges.contextLimitAnchors,
|
|
5601
|
+
messages,
|
|
5602
|
+
contextLimitNudgeWithUsage,
|
|
5603
|
+
""
|
|
5604
|
+
);
|
|
5605
|
+
applyRangeModeAnchoredNudge(
|
|
5606
|
+
turnNudgeAnchors,
|
|
5607
|
+
messages,
|
|
5608
|
+
prompts.turnNudge,
|
|
5609
|
+
""
|
|
5610
|
+
);
|
|
5611
|
+
applyRangeModeAnchoredNudge(
|
|
5612
|
+
state.nudges.iterationNudgeAnchors,
|
|
5613
|
+
messages,
|
|
5614
|
+
prompts.iterationNudge,
|
|
5615
|
+
""
|
|
5616
|
+
);
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5619
|
+
// lib/messages/inject/inject.ts
|
|
5620
|
+
var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
|
|
5621
|
+
function createSuffixMessage(messages) {
|
|
5622
|
+
if (messages.length === 0) return null;
|
|
5623
|
+
const base = messages.find((m) => m.info.role === "user") || messages[messages.length - 1];
|
|
5624
|
+
const synthetic = createSyntheticUserMessage(base, "", ACP_SUFFIX_SEED);
|
|
5625
|
+
messages.push(synthetic);
|
|
5626
|
+
return synthetic;
|
|
5627
|
+
}
|
|
5628
|
+
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
|
|
5629
|
+
if (compressPermission(state, config) === "deny") {
|
|
5630
|
+
return;
|
|
5651
5631
|
}
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
}
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5632
|
+
if (state.manualMode) {
|
|
5633
|
+
return;
|
|
5634
|
+
}
|
|
5635
|
+
const lastMessage = findLastNonIgnoredMessage(messages);
|
|
5636
|
+
const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant");
|
|
5637
|
+
if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
|
|
5638
|
+
state.nudges.contextLimitAnchors.clear();
|
|
5639
|
+
state.nudges.turnNudgeAnchors.clear();
|
|
5640
|
+
state.nudges.iterationNudgeAnchors.clear();
|
|
5641
|
+
void saveSessionState(state, logger);
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
const { providerId, modelId } = getModelInfo(messages);
|
|
5645
|
+
let anchorsChanged = false;
|
|
5646
|
+
const { overMaxLimit, overMinLimit, currentTokens, modelContextLimit } = isContextOverLimits(
|
|
5647
|
+
config,
|
|
5648
|
+
state,
|
|
5649
|
+
providerId,
|
|
5650
|
+
modelId,
|
|
5651
|
+
messages
|
|
5658
5652
|
);
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5653
|
+
if (!overMinLimit) {
|
|
5654
|
+
const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
|
|
5655
|
+
const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0;
|
|
5656
|
+
if (hadTurnAnchors || hadIterationAnchors) {
|
|
5657
|
+
state.nudges.turnNudgeAnchors.clear();
|
|
5658
|
+
state.nudges.iterationNudgeAnchors.clear();
|
|
5659
|
+
anchorsChanged = true;
|
|
5665
5660
|
}
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5661
|
+
}
|
|
5662
|
+
if (overMaxLimit) {
|
|
5663
|
+
if (lastMessage) {
|
|
5664
|
+
const interval = getNudgeFrequency(config);
|
|
5665
|
+
const added = addAnchor(
|
|
5666
|
+
state.nudges.contextLimitAnchors,
|
|
5667
|
+
lastMessage.message.info.id,
|
|
5668
|
+
lastMessage.index,
|
|
5669
|
+
messages,
|
|
5670
|
+
interval
|
|
5671
|
+
);
|
|
5672
|
+
if (added) {
|
|
5673
|
+
anchorsChanged = true;
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
} else if (overMinLimit) {
|
|
5677
|
+
const isLastMessageUser = lastMessage?.message.info.role === "user";
|
|
5678
|
+
if (isLastMessageUser && lastAssistantMessage) {
|
|
5679
|
+
const previousSize = state.nudges.turnNudgeAnchors.size;
|
|
5680
|
+
state.nudges.turnNudgeAnchors.add(lastMessage.message.info.id);
|
|
5681
|
+
state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id);
|
|
5682
|
+
if (state.nudges.turnNudgeAnchors.size !== previousSize) {
|
|
5683
|
+
anchorsChanged = true;
|
|
5684
|
+
}
|
|
5685
|
+
}
|
|
5686
|
+
const lastUserMessage = getLastUserMessage(messages);
|
|
5687
|
+
if (lastUserMessage && lastMessage) {
|
|
5688
|
+
const lastUserMessageIndex = messages.findIndex(
|
|
5689
|
+
(message) => message.info.id === lastUserMessage.info.id
|
|
5690
|
+
);
|
|
5691
|
+
if (lastUserMessageIndex >= 0) {
|
|
5692
|
+
const messagesSinceUser = countMessagesAfterIndex(messages, lastUserMessageIndex);
|
|
5693
|
+
const iterationThreshold = getIterationNudgeThreshold(config);
|
|
5694
|
+
if (lastMessage.index > lastUserMessageIndex && messagesSinceUser >= iterationThreshold) {
|
|
5695
|
+
const interval = getNudgeFrequency(config);
|
|
5696
|
+
const added = addAnchor(
|
|
5697
|
+
state.nudges.iterationNudgeAnchors,
|
|
5698
|
+
lastMessage.message.info.id,
|
|
5699
|
+
lastMessage.index,
|
|
5700
|
+
messages,
|
|
5701
|
+
interval
|
|
5702
|
+
);
|
|
5703
|
+
if (added) {
|
|
5704
|
+
anchorsChanged = true;
|
|
5705
|
+
}
|
|
5671
5706
|
}
|
|
5672
5707
|
}
|
|
5673
5708
|
}
|
|
5674
5709
|
}
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
}
|
|
5684
|
-
|
|
5685
|
-
|
|
5710
|
+
const suffixMessage = createSuffixMessage(messages);
|
|
5711
|
+
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
|
|
5712
|
+
injectContextUsage(suffixMessage, currentTokens, modelContextLimit);
|
|
5713
|
+
if (config.compress.mode !== "message") {
|
|
5714
|
+
const blockGuidance = buildCompressedBlockGuidance(state, config.gc, { currentTokens, modelContextLimit });
|
|
5715
|
+
if (blockGuidance.trim() && suffixMessage) {
|
|
5716
|
+
appendToLastTextPart(suffixMessage, "\n\n" + blockGuidance);
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
injectVisibleIdRange(state, messages, suffixMessage);
|
|
5720
|
+
if (anchorsChanged) {
|
|
5721
|
+
void saveSessionState(state, logger);
|
|
5722
|
+
}
|
|
5686
5723
|
};
|
|
5687
|
-
|
|
5724
|
+
function injectContextUsage(target, currentTokens, modelContextLimit) {
|
|
5725
|
+
if (!target) return;
|
|
5726
|
+
if (currentTokens === void 0 || modelContextLimit === void 0 || modelContextLimit === 0) {
|
|
5727
|
+
return;
|
|
5728
|
+
}
|
|
5729
|
+
const percentage = (currentTokens / modelContextLimit * 100).toFixed(1);
|
|
5730
|
+
const formatK = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
5731
|
+
const usageTag = `
|
|
5732
|
+
|
|
5733
|
+
Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%). ACP (Active Context Pruning) threshold: 55%. You ARE the ACP agent \u2014 use the compress tool proactively to manage context quality.`;
|
|
5734
|
+
for (const part of target.parts) {
|
|
5735
|
+
if (part.type === "text") {
|
|
5736
|
+
appendToTextPart(part, usageTag);
|
|
5737
|
+
return;
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
target.parts.push(createSyntheticTextPart(target, usageTag));
|
|
5741
|
+
}
|
|
5742
|
+
function injectVisibleIdRange(state, messages, target) {
|
|
5743
|
+
if (!target) return;
|
|
5744
|
+
const visibleRefs = [];
|
|
5688
5745
|
for (const message of messages) {
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
}
|
|
5693
|
-
if (part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string") {
|
|
5694
|
-
part.state.output = stripHallucinationsFromString(part.state.output);
|
|
5695
|
-
}
|
|
5746
|
+
const ref = state.messageIds.byRawId.get(message.info.id);
|
|
5747
|
+
if (ref) {
|
|
5748
|
+
visibleRefs.push(ref);
|
|
5696
5749
|
}
|
|
5697
5750
|
}
|
|
5698
|
-
|
|
5751
|
+
if (visibleRefs.length === 0) return;
|
|
5752
|
+
visibleRefs.sort();
|
|
5753
|
+
const first = visibleRefs[0];
|
|
5754
|
+
const last = visibleRefs[visibleRefs.length - 1];
|
|
5755
|
+
const rangeTag = `
|
|
5699
5756
|
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5757
|
+
[Visible message IDs: ${first} to ${last} (${visibleRefs.length} messages). Only use IDs in this range for compress.]`;
|
|
5758
|
+
for (const part of target.parts) {
|
|
5759
|
+
if (part.type === "text") {
|
|
5760
|
+
appendToTextPart(part, rangeTag);
|
|
5761
|
+
return;
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
5764
|
+
target.parts.push(createSyntheticTextPart(target, rangeTag));
|
|
5765
|
+
}
|
|
5766
|
+
var injectMessageIds = (state, config, messages, compressionPriorities) => {
|
|
5767
|
+
if (compressPermission(state, config) === "deny") {
|
|
5768
|
+
return;
|
|
5769
|
+
}
|
|
5770
|
+
for (const message of messages) {
|
|
5771
|
+
if (isIgnoredUserMessage(message)) {
|
|
5713
5772
|
continue;
|
|
5714
5773
|
}
|
|
5715
|
-
const
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5774
|
+
const messageRef = state.messageIds.byRawId.get(message.info.id);
|
|
5775
|
+
if (!messageRef) {
|
|
5776
|
+
continue;
|
|
5777
|
+
}
|
|
5778
|
+
const isBlockedMessage = isProtectedUserMessage(config, message);
|
|
5779
|
+
const priority = config.compress.mode === "message" && !isBlockedMessage ? compressionPriorities?.get(message.info.id)?.priority : void 0;
|
|
5780
|
+
const tag = formatMessageIdTag(
|
|
5781
|
+
isBlockedMessage ? "BLOCKED" : messageRef,
|
|
5782
|
+
priority ? { priority } : void 0
|
|
5783
|
+
);
|
|
5784
|
+
if (message.info.role === "user") {
|
|
5785
|
+
let injected = false;
|
|
5786
|
+
for (const part of message.parts) {
|
|
5787
|
+
if (part.type === "text") {
|
|
5788
|
+
injected = appendToTextPart(part, tag) || injected;
|
|
5789
|
+
}
|
|
5725
5790
|
}
|
|
5726
|
-
if (
|
|
5791
|
+
if (injected) {
|
|
5727
5792
|
continue;
|
|
5728
5793
|
}
|
|
5729
|
-
|
|
5794
|
+
message.parts.push(createSyntheticTextPart(message, tag));
|
|
5795
|
+
continue;
|
|
5730
5796
|
}
|
|
5731
|
-
|
|
5732
|
-
};
|
|
5733
|
-
var pruneToolInputs = (state, logger, messages) => {
|
|
5734
|
-
for (const msg of messages) {
|
|
5735
|
-
if (isMessageCompacted(state, msg)) {
|
|
5797
|
+
if (message.info.role !== "assistant") {
|
|
5736
5798
|
continue;
|
|
5737
5799
|
}
|
|
5738
|
-
|
|
5800
|
+
if (!hasContent(message)) {
|
|
5801
|
+
continue;
|
|
5802
|
+
}
|
|
5803
|
+
if (appendToAllToolParts(message, tag)) {
|
|
5804
|
+
continue;
|
|
5805
|
+
}
|
|
5806
|
+
if (appendToLastTextPart(message, tag)) {
|
|
5807
|
+
continue;
|
|
5808
|
+
}
|
|
5809
|
+
const syntheticPart = createSyntheticTextPart(message, tag);
|
|
5810
|
+
const firstToolIndex = message.parts.findIndex((p) => p.type === "tool");
|
|
5811
|
+
if (firstToolIndex === -1) {
|
|
5812
|
+
message.parts.push(syntheticPart);
|
|
5813
|
+
} else {
|
|
5814
|
+
message.parts.splice(firstToolIndex, 0, syntheticPart);
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
};
|
|
5818
|
+
|
|
5819
|
+
// lib/messages/inject/subagent-results.ts
|
|
5820
|
+
async function fetchSubAgentMessages(client, sessionId) {
|
|
5821
|
+
const response = await client.session.messages({
|
|
5822
|
+
path: { id: sessionId }
|
|
5823
|
+
});
|
|
5824
|
+
return filterMessages(response?.data || response);
|
|
5825
|
+
}
|
|
5826
|
+
var injectExtendedSubAgentResults = async (client, state, logger, messages, allowSubAgents) => {
|
|
5827
|
+
if (!allowSubAgents) {
|
|
5828
|
+
return;
|
|
5829
|
+
}
|
|
5830
|
+
for (const message of messages) {
|
|
5831
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
5739
5832
|
for (const part of parts) {
|
|
5740
|
-
if (part.type !== "tool") {
|
|
5833
|
+
if (part.type !== "tool" || part.tool !== "task" || !part.callID) {
|
|
5741
5834
|
continue;
|
|
5742
5835
|
}
|
|
5743
|
-
if (
|
|
5836
|
+
if (state.prune.tools.has(part.callID)) {
|
|
5744
5837
|
continue;
|
|
5745
5838
|
}
|
|
5746
|
-
if (part.state
|
|
5839
|
+
if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
|
|
5747
5840
|
continue;
|
|
5748
5841
|
}
|
|
5749
|
-
|
|
5842
|
+
const cachedResult = state.subAgentResultCache.get(part.callID);
|
|
5843
|
+
if (cachedResult !== void 0) {
|
|
5844
|
+
if (cachedResult) {
|
|
5845
|
+
part.state.output = stripHallucinationsFromString(
|
|
5846
|
+
mergeSubagentResult(part.state.output, cachedResult)
|
|
5847
|
+
);
|
|
5848
|
+
}
|
|
5750
5849
|
continue;
|
|
5751
5850
|
}
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
}
|
|
5755
|
-
}
|
|
5756
|
-
}
|
|
5757
|
-
};
|
|
5758
|
-
var pruneToolErrors = (state, logger, messages) => {
|
|
5759
|
-
for (const msg of messages) {
|
|
5760
|
-
if (isMessageCompacted(state, msg)) {
|
|
5761
|
-
continue;
|
|
5762
|
-
}
|
|
5763
|
-
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
5764
|
-
for (const part of parts) {
|
|
5765
|
-
if (part.type !== "tool") {
|
|
5851
|
+
const subAgentSessionId = getSubAgentId(part);
|
|
5852
|
+
if (!subAgentSessionId) {
|
|
5766
5853
|
continue;
|
|
5767
5854
|
}
|
|
5768
|
-
|
|
5855
|
+
let subAgentMessages = [];
|
|
5856
|
+
try {
|
|
5857
|
+
subAgentMessages = await fetchSubAgentMessages(client, subAgentSessionId);
|
|
5858
|
+
} catch (error) {
|
|
5859
|
+
logger.warn("Failed to fetch subagent session for output expansion", {
|
|
5860
|
+
subAgentSessionId,
|
|
5861
|
+
callID: part.callID,
|
|
5862
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5863
|
+
});
|
|
5769
5864
|
continue;
|
|
5770
5865
|
}
|
|
5771
|
-
|
|
5866
|
+
const subAgentResultText = buildSubagentResultText(subAgentMessages);
|
|
5867
|
+
if (!subAgentResultText) {
|
|
5772
5868
|
continue;
|
|
5773
5869
|
}
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
input[key] = PRUNED_TOOL_ERROR_INPUT_REPLACEMENT;
|
|
5779
|
-
}
|
|
5780
|
-
}
|
|
5781
|
-
}
|
|
5870
|
+
state.subAgentResultCache.set(part.callID, subAgentResultText);
|
|
5871
|
+
part.state.output = stripHallucinationsFromString(
|
|
5872
|
+
mergeSubagentResult(part.state.output, subAgentResultText)
|
|
5873
|
+
);
|
|
5782
5874
|
}
|
|
5783
5875
|
}
|
|
5784
5876
|
};
|
|
5785
|
-
|
|
5786
|
-
|
|
5877
|
+
|
|
5878
|
+
// lib/messages/reasoning-strip.ts
|
|
5879
|
+
function stripStaleMetadata(messages) {
|
|
5880
|
+
const lastUserMessage = getLastUserMessage(messages);
|
|
5881
|
+
if (lastUserMessage?.info.role !== "user") {
|
|
5787
5882
|
return;
|
|
5788
5883
|
}
|
|
5789
|
-
const
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5884
|
+
const modelID = lastUserMessage.info.model.modelID;
|
|
5885
|
+
const providerID = lastUserMessage.info.model.providerID;
|
|
5886
|
+
messages.forEach((message) => {
|
|
5887
|
+
if (message.info.role !== "assistant") {
|
|
5888
|
+
return;
|
|
5889
|
+
}
|
|
5890
|
+
const msgModelID = message.info.modelID;
|
|
5891
|
+
const msgProviderID = message.info.providerID;
|
|
5892
|
+
if (msgModelID === modelID && msgProviderID === providerID) {
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
5895
|
+
message.parts = message.parts.map((part) => {
|
|
5896
|
+
if (part.type !== "text" && part.type !== "tool" && part.type !== "reasoning") {
|
|
5897
|
+
return part;
|
|
5898
|
+
}
|
|
5899
|
+
if (!("metadata" in part)) {
|
|
5900
|
+
return part;
|
|
5901
|
+
}
|
|
5902
|
+
const { metadata: _metadata, ...rest } = part;
|
|
5903
|
+
return rest;
|
|
5904
|
+
});
|
|
5905
|
+
});
|
|
5906
|
+
}
|
|
5907
|
+
|
|
5908
|
+
// lib/commands/compression-targets.ts
|
|
5909
|
+
function byBlockId(a, b) {
|
|
5910
|
+
return a.blockId - b.blockId;
|
|
5911
|
+
}
|
|
5912
|
+
function buildTarget(blocks) {
|
|
5913
|
+
const ordered = [...blocks].sort(byBlockId);
|
|
5914
|
+
const first = ordered[0];
|
|
5915
|
+
if (!first) {
|
|
5916
|
+
throw new Error("Cannot build compression target from empty block list.");
|
|
5917
|
+
}
|
|
5918
|
+
const grouped = first.mode === "message";
|
|
5919
|
+
return {
|
|
5920
|
+
displayId: first.blockId,
|
|
5921
|
+
runId: first.runId,
|
|
5922
|
+
topic: grouped ? first.batchTopic || first.topic : first.topic,
|
|
5923
|
+
compressedTokens: ordered.reduce((total, block) => total + block.compressedTokens, 0),
|
|
5924
|
+
durationMs: ordered.reduce((total, block) => Math.max(total, block.durationMs), 0),
|
|
5925
|
+
grouped,
|
|
5926
|
+
blocks: ordered
|
|
5927
|
+
};
|
|
5928
|
+
}
|
|
5929
|
+
function groupMessageBlocks(blocks) {
|
|
5930
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
5931
|
+
for (const block of blocks) {
|
|
5932
|
+
const existing = grouped.get(block.runId);
|
|
5933
|
+
if (existing) {
|
|
5934
|
+
existing.push(block);
|
|
5935
|
+
continue;
|
|
5936
|
+
}
|
|
5937
|
+
grouped.set(block.runId, [block]);
|
|
5938
|
+
}
|
|
5939
|
+
return Array.from(grouped.values()).map(buildTarget);
|
|
5940
|
+
}
|
|
5941
|
+
function splitTargets(blocks) {
|
|
5942
|
+
const messageBlocks = [];
|
|
5943
|
+
const singleBlocks = [];
|
|
5944
|
+
for (const block of blocks) {
|
|
5945
|
+
if (block.mode === "message") {
|
|
5946
|
+
messageBlocks.push(block);
|
|
5947
|
+
} else {
|
|
5948
|
+
singleBlocks.push(block);
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
const targets = [
|
|
5952
|
+
...singleBlocks.map((block) => buildTarget([block])),
|
|
5953
|
+
...groupMessageBlocks(messageBlocks)
|
|
5954
|
+
];
|
|
5955
|
+
return targets.sort((a, b) => a.displayId - b.displayId);
|
|
5956
|
+
}
|
|
5957
|
+
function getActiveCompressionTargets(messagesState) {
|
|
5958
|
+
const activeBlocks = Array.from(messagesState.activeBlockIds).map((blockId) => messagesState.blocksById.get(blockId)).filter((block) => !!block && block.active);
|
|
5959
|
+
return splitTargets(activeBlocks);
|
|
5960
|
+
}
|
|
5961
|
+
function getRecompressibleCompressionTargets(messagesState, availableMessageIds) {
|
|
5962
|
+
const allBlocks = Array.from(messagesState.blocksById.values()).filter((block) => {
|
|
5963
|
+
return availableMessageIds.has(block.compressMessageId);
|
|
5964
|
+
});
|
|
5965
|
+
const messageGroups = /* @__PURE__ */ new Map();
|
|
5966
|
+
const singleTargets = [];
|
|
5967
|
+
for (const block of allBlocks) {
|
|
5968
|
+
if (block.mode === "message") {
|
|
5969
|
+
const existing = messageGroups.get(block.runId);
|
|
5970
|
+
if (existing) {
|
|
5971
|
+
existing.push(block);
|
|
5801
5972
|
} else {
|
|
5802
|
-
|
|
5803
|
-
const userMessage = getLastUserMessage(messages, msgIndex);
|
|
5804
|
-
const _cleaned = stripStaleMessageRefs(rawSummaryContent);
|
|
5805
|
-
if (userMessage) {
|
|
5806
|
-
const userInfo = userMessage.info;
|
|
5807
|
-
const summaryContent = config.compress.mode === "message" ? replaceBlockIdsWithBlocked(_cleaned) : _cleaned;
|
|
5808
|
-
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
|
|
5809
|
-
result.push(
|
|
5810
|
-
createSyntheticUserMessage(userMessage, summaryContent, summarySeed)
|
|
5811
|
-
);
|
|
5812
|
-
logger.info("Injected compress summary", {
|
|
5813
|
-
anchorMessageId: msgId,
|
|
5814
|
-
summaryLength: summaryContent.length
|
|
5815
|
-
});
|
|
5816
|
-
} else {
|
|
5817
|
-
const anchorInfo = msg.info;
|
|
5818
|
-
const fallbackBase = {
|
|
5819
|
-
info: {
|
|
5820
|
-
id: anchorInfo.id || msgId,
|
|
5821
|
-
sessionID: anchorInfo.sessionID || "",
|
|
5822
|
-
role: "user",
|
|
5823
|
-
agent: anchorInfo.agent || "code",
|
|
5824
|
-
model: anchorInfo.model || {
|
|
5825
|
-
providerID: "",
|
|
5826
|
-
modelID: "",
|
|
5827
|
-
variant: void 0
|
|
5828
|
-
},
|
|
5829
|
-
time: { created: anchorInfo.time?.created || Date.now() }
|
|
5830
|
-
},
|
|
5831
|
-
parts: []
|
|
5832
|
-
};
|
|
5833
|
-
const summaryContent = config.compress.mode === "message" ? replaceBlockIdsWithBlocked(_cleaned) : _cleaned;
|
|
5834
|
-
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
|
|
5835
|
-
result.push(
|
|
5836
|
-
createSyntheticUserMessage(fallbackBase, summaryContent, summarySeed)
|
|
5837
|
-
);
|
|
5838
|
-
logger.info("Injected compress summary (fallback, no preceding user message)", {
|
|
5839
|
-
anchorMessageId: msgId,
|
|
5840
|
-
summaryLength: summaryContent.length
|
|
5841
|
-
});
|
|
5842
|
-
}
|
|
5973
|
+
messageGroups.set(block.runId, [block]);
|
|
5843
5974
|
}
|
|
5844
|
-
}
|
|
5845
|
-
const pruneEntry = state.prune.messages.byMessageId.get(msgId);
|
|
5846
|
-
if (pruneEntry && pruneEntry.activeBlockIds.length > 0) {
|
|
5847
5975
|
continue;
|
|
5848
5976
|
}
|
|
5849
|
-
|
|
5977
|
+
if (block.deactivatedByUser && !block.active) {
|
|
5978
|
+
singleTargets.push(buildTarget([block]));
|
|
5979
|
+
}
|
|
5850
5980
|
}
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
// lib/messages/sync.ts
|
|
5856
|
-
function sortBlocksByCreation(a, b) {
|
|
5857
|
-
const createdAtDiff = a.createdAt - b.createdAt;
|
|
5858
|
-
if (createdAtDiff !== 0) {
|
|
5859
|
-
return createdAtDiff;
|
|
5981
|
+
for (const blocks of messageGroups.values()) {
|
|
5982
|
+
if (blocks.some((block) => block.deactivatedByUser && !block.active)) {
|
|
5983
|
+
singleTargets.push(buildTarget(blocks));
|
|
5984
|
+
}
|
|
5860
5985
|
}
|
|
5861
|
-
return a.
|
|
5986
|
+
return singleTargets.sort((a, b) => a.displayId - b.displayId);
|
|
5862
5987
|
}
|
|
5863
|
-
|
|
5864
|
-
const
|
|
5865
|
-
if (!
|
|
5866
|
-
return;
|
|
5988
|
+
function resolveCompressionTarget(messagesState, blockId) {
|
|
5989
|
+
const block = messagesState.blocksById.get(blockId);
|
|
5990
|
+
if (!block) {
|
|
5991
|
+
return null;
|
|
5867
5992
|
}
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5993
|
+
if (block.mode !== "message") {
|
|
5994
|
+
return buildTarget([block]);
|
|
5995
|
+
}
|
|
5996
|
+
const blocks = Array.from(messagesState.blocksById.values()).filter(
|
|
5997
|
+
(candidate) => candidate.mode === "message" && candidate.runId === block.runId
|
|
5871
5998
|
);
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5999
|
+
if (blocks.length === 0) {
|
|
6000
|
+
return null;
|
|
6001
|
+
}
|
|
6002
|
+
return buildTarget(blocks);
|
|
6003
|
+
}
|
|
6004
|
+
|
|
6005
|
+
// lib/compress/decompress-logic.ts
|
|
6006
|
+
function parseBlockIdArg(arg) {
|
|
6007
|
+
const normalized = arg.trim().toLowerCase();
|
|
6008
|
+
const blockRef = parseBlockRef(normalized);
|
|
6009
|
+
if (blockRef !== null) {
|
|
6010
|
+
return blockRef;
|
|
6011
|
+
}
|
|
6012
|
+
if (!/^[1-9]\d*$/.test(normalized)) {
|
|
6013
|
+
return null;
|
|
6014
|
+
}
|
|
6015
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
6016
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
6017
|
+
}
|
|
6018
|
+
function findActiveParentBlockId(messagesState, block) {
|
|
6019
|
+
const queue = [...block.parentBlockIds];
|
|
6020
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6021
|
+
while (queue.length > 0) {
|
|
6022
|
+
const parentBlockId = queue.shift();
|
|
6023
|
+
if (parentBlockId === void 0 || visited.has(parentBlockId)) {
|
|
5884
6024
|
continue;
|
|
5885
6025
|
}
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
block.deactivatedByBlockId = void 0;
|
|
5891
|
-
continue;
|
|
5892
|
-
}
|
|
6026
|
+
visited.add(parentBlockId);
|
|
6027
|
+
const parent = messagesState.blocksById.get(parentBlockId);
|
|
6028
|
+
if (!parent) {
|
|
6029
|
+
continue;
|
|
5893
6030
|
}
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
continue;
|
|
5897
|
-
}
|
|
5898
|
-
const consumedBlock = messagesState.blocksById.get(consumedBlockId);
|
|
5899
|
-
if (consumedBlock) {
|
|
5900
|
-
consumedBlock.active = false;
|
|
5901
|
-
consumedBlock.deactivatedAt = now;
|
|
5902
|
-
consumedBlock.deactivatedByBlockId = block.blockId;
|
|
5903
|
-
const mappedBlockId = messagesState.activeByAnchorMessageId.get(
|
|
5904
|
-
consumedBlock.anchorMessageId
|
|
5905
|
-
);
|
|
5906
|
-
if (mappedBlockId === consumedBlock.blockId) {
|
|
5907
|
-
messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId);
|
|
5908
|
-
}
|
|
5909
|
-
}
|
|
5910
|
-
messagesState.activeBlockIds.delete(consumedBlockId);
|
|
6031
|
+
if (parent.active) {
|
|
6032
|
+
return parent.blockId;
|
|
5911
6033
|
}
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
if (messageIds.has(block.anchorMessageId)) {
|
|
5917
|
-
messagesState.activeByAnchorMessageId.set(block.anchorMessageId, block.blockId);
|
|
6034
|
+
for (const ancestorId of parent.parentBlockIds) {
|
|
6035
|
+
if (!visited.has(ancestorId)) {
|
|
6036
|
+
queue.push(ancestorId);
|
|
6037
|
+
}
|
|
5918
6038
|
}
|
|
5919
6039
|
}
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
let reactivatedCount = 0;
|
|
5928
|
-
for (const blockId of previousActiveBlockIds) {
|
|
5929
|
-
if (!nextActiveBlockIds.has(blockId)) {
|
|
5930
|
-
deactivatedCount++;
|
|
6040
|
+
return null;
|
|
6041
|
+
}
|
|
6042
|
+
function findActiveAncestorBlockId(messagesState, target) {
|
|
6043
|
+
for (const block of target.blocks) {
|
|
6044
|
+
const activeAncestorBlockId = findActiveParentBlockId(messagesState, block);
|
|
6045
|
+
if (activeAncestorBlockId !== null) {
|
|
6046
|
+
return activeAncestorBlockId;
|
|
5931
6047
|
}
|
|
5932
6048
|
}
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
6049
|
+
return null;
|
|
6050
|
+
}
|
|
6051
|
+
function snapshotActiveMessages(messagesState) {
|
|
6052
|
+
const activeMessages = /* @__PURE__ */ new Map();
|
|
6053
|
+
for (const [messageId, entry] of messagesState.byMessageId) {
|
|
6054
|
+
if (entry.activeBlockIds.length > 0) {
|
|
6055
|
+
activeMessages.set(messageId, entry.tokenCount);
|
|
5936
6056
|
}
|
|
5937
6057
|
}
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
|
|
5952
|
-
const activeAgent = getLastUserMessage(messages)?.info.agent;
|
|
5953
|
-
state.compressPermission = resolveEffectiveCompressPermission(
|
|
5954
|
-
config.compress.permission,
|
|
5955
|
-
hostPermissions,
|
|
5956
|
-
activeAgent
|
|
5957
|
-
);
|
|
5958
|
-
};
|
|
5959
|
-
|
|
5960
|
-
// lib/prompts/extensions/nudge.ts
|
|
5961
|
-
function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
5962
|
-
const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
|
|
5963
|
-
const refs = activeBlockIds.map((id) => `b${id}`);
|
|
5964
|
-
const blockCount = refs.length;
|
|
5965
|
-
const blockList = blockCount > 0 ? refs.join(", ") : "none";
|
|
5966
|
-
const lines = [
|
|
5967
|
-
"Compressed block context:",
|
|
5968
|
-
`- Active compressed blocks: ${blockCount} (${blockList})`,
|
|
5969
|
-
"- If your selected compression range includes any listed block, include each required placeholder exactly once in the summary using `(bN)`."
|
|
5970
|
-
];
|
|
5971
|
-
const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
|
|
5972
|
-
if (gcConfig && usageRatio > 0.5) {
|
|
5973
|
-
const promotionThreshold = gcConfig.promotionThreshold;
|
|
5974
|
-
const agingBlocks = [];
|
|
5975
|
-
for (const blockId of activeBlockIds) {
|
|
5976
|
-
const block = state.prune.messages.blocksById.get(blockId);
|
|
5977
|
-
if (!block) continue;
|
|
5978
|
-
const survived = block.survivedCount ?? 0;
|
|
5979
|
-
const gen = block.generation ?? "young";
|
|
5980
|
-
const sizeK = (block.summary.length / 1e3).toFixed(1);
|
|
5981
|
-
const preview = block.summary.slice(0, 120).replace(/\n/g, " ");
|
|
5982
|
-
if (gen === "old" || survived >= promotionThreshold - 2) {
|
|
5983
|
-
agingBlocks.push(
|
|
5984
|
-
` b${blockId}: age=${survived}/${promotionThreshold}, gen=${gen}, size=${sizeK}K chars \u2014 ${preview}...`
|
|
5985
|
-
);
|
|
6058
|
+
return activeMessages;
|
|
6059
|
+
}
|
|
6060
|
+
function deactivateCompressionTarget(messagesState, target) {
|
|
6061
|
+
const deactivatedAt = Date.now();
|
|
6062
|
+
for (const block of target.blocks) {
|
|
6063
|
+
block.active = false;
|
|
6064
|
+
block.deactivatedByUser = true;
|
|
6065
|
+
block.deactivatedAt = deactivatedAt;
|
|
6066
|
+
block.deactivatedByBlockId = void 0;
|
|
6067
|
+
for (const consumedId of block.consumedBlockIds) {
|
|
6068
|
+
const consumedBlock = messagesState.blocksById.get(consumedId);
|
|
6069
|
+
if (consumedBlock) {
|
|
6070
|
+
consumedBlock.deactivatedByUser = true;
|
|
5986
6071
|
}
|
|
5987
6072
|
}
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
function computeRestoredMessages(messagesState, activeMessagesBefore) {
|
|
6076
|
+
let restoredMessageCount = 0;
|
|
6077
|
+
let restoredTokens = 0;
|
|
6078
|
+
for (const [messageId, tokenCount] of activeMessagesBefore) {
|
|
6079
|
+
const entry = messagesState.byMessageId.get(messageId);
|
|
6080
|
+
const isActiveNow = entry ? entry.activeBlockIds.length > 0 : false;
|
|
6081
|
+
if (!isActiveNow) {
|
|
6082
|
+
restoredMessageCount++;
|
|
6083
|
+
restoredTokens += tokenCount;
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
return { restoredMessageCount, restoredTokens };
|
|
6087
|
+
}
|
|
6088
|
+
function computeReactivatedBlockIds(messagesState, activeBlockIdsBefore) {
|
|
6089
|
+
return Array.from(messagesState.activeBlockIds).filter((blockId) => !activeBlockIdsBefore.has(blockId)).sort((a, b) => a - b);
|
|
6090
|
+
}
|
|
6091
|
+
var MAX_PREVIEW_LENGTH = 2e3;
|
|
6092
|
+
var MAX_MESSAGE_PREVIEW_LENGTH = 200;
|
|
6093
|
+
function buildRestoredContentPreview(messages, activeMessagesBefore, messagesState) {
|
|
6094
|
+
const restoredMessages = [];
|
|
6095
|
+
for (const msg of messages) {
|
|
6096
|
+
const msgId = msg.info.id;
|
|
6097
|
+
if (activeMessagesBefore.has(msgId)) {
|
|
6098
|
+
const entry = messagesState.byMessageId.get(msgId);
|
|
6099
|
+
const isActiveNow = entry ? entry.activeBlockIds.length > 0 : false;
|
|
6100
|
+
if (!isActiveNow) {
|
|
6101
|
+
restoredMessages.push(msg);
|
|
6102
|
+
}
|
|
5995
6103
|
}
|
|
5996
6104
|
}
|
|
6105
|
+
if (restoredMessages.length === 0) {
|
|
6106
|
+
return "";
|
|
6107
|
+
}
|
|
6108
|
+
const lines = [];
|
|
6109
|
+
let totalLength = 0;
|
|
6110
|
+
for (const msg of restoredMessages) {
|
|
6111
|
+
if (totalLength >= MAX_PREVIEW_LENGTH) break;
|
|
6112
|
+
const role = msg.info.role ?? "unknown";
|
|
6113
|
+
const textContent = extractTextContent(msg);
|
|
6114
|
+
const truncated = textContent.length > MAX_MESSAGE_PREVIEW_LENGTH ? textContent.slice(0, MAX_MESSAGE_PREVIEW_LENGTH) + "..." : textContent;
|
|
6115
|
+
const line = `[${role}] ${truncated}`;
|
|
6116
|
+
lines.push(line);
|
|
6117
|
+
totalLength += line.length + 1;
|
|
6118
|
+
}
|
|
5997
6119
|
return lines.join("\n");
|
|
5998
6120
|
}
|
|
5999
|
-
function
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
"Message priority context:",
|
|
6003
|
-
"- Higher-priority older messages consume more context and should be compressed right away if it is safe to do so.",
|
|
6004
|
-
`- ${priorityLabel}-priority message IDs before this point: ${refList}`
|
|
6005
|
-
].join("\n");
|
|
6006
|
-
}
|
|
6007
|
-
function appendGuidanceToDcpTag(nudgeText, guidance) {
|
|
6008
|
-
if (!guidance.trim()) {
|
|
6009
|
-
return nudgeText;
|
|
6121
|
+
function extractTextContent(msg) {
|
|
6122
|
+
if (!msg.parts || msg.parts.length === 0) {
|
|
6123
|
+
return "";
|
|
6010
6124
|
}
|
|
6011
|
-
const
|
|
6012
|
-
const
|
|
6013
|
-
|
|
6014
|
-
|
|
6125
|
+
const textParts = [];
|
|
6126
|
+
for (const part of msg.parts) {
|
|
6127
|
+
if (typeof part === "object" && part !== null) {
|
|
6128
|
+
if ("text" in part && typeof part.text === "string") {
|
|
6129
|
+
textParts.push(part.text);
|
|
6130
|
+
} else if ("type" in part && part.type === "tool") {
|
|
6131
|
+
const toolName = "tool" in part && typeof part.tool === "string" ? part.tool : "tool";
|
|
6132
|
+
const state = part.state;
|
|
6133
|
+
if (state && typeof state.output === "string") {
|
|
6134
|
+
const output = state.output.length > 80 ? state.output.slice(0, 80) + "..." : state.output;
|
|
6135
|
+
textParts.push(`[${toolName}] ${output}`);
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6015
6139
|
}
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
return `${beforeClose}
|
|
6140
|
+
return textParts.join(" ").replace(/\s+/g, " ").trim();
|
|
6141
|
+
}
|
|
6019
6142
|
|
|
6020
|
-
|
|
6021
|
-
|
|
6143
|
+
// lib/compress/decompress.ts
|
|
6144
|
+
async function prepareDecompressSession(ctx, toolCtx) {
|
|
6145
|
+
await toolCtx.ask({
|
|
6146
|
+
permission: "compress",
|
|
6147
|
+
patterns: ["*"],
|
|
6148
|
+
always: ["*"],
|
|
6149
|
+
metadata: {}
|
|
6150
|
+
});
|
|
6151
|
+
toolCtx.metadata({ title: "Decompress" });
|
|
6152
|
+
const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
|
|
6153
|
+
await ensureSessionInitialized(
|
|
6154
|
+
ctx.client,
|
|
6155
|
+
ctx.state,
|
|
6156
|
+
toolCtx.sessionID,
|
|
6157
|
+
ctx.logger,
|
|
6158
|
+
rawMessages,
|
|
6159
|
+
ctx.config.manualMode.enabled
|
|
6160
|
+
);
|
|
6161
|
+
assignMessageRefs(ctx.state, rawMessages);
|
|
6162
|
+
return { rawMessages };
|
|
6163
|
+
}
|
|
6164
|
+
async function finalizeDecompressSession(ctx) {
|
|
6165
|
+
await saveSessionState(ctx.state, ctx.logger);
|
|
6022
6166
|
}
|
|
6167
|
+
var TOOL_DESCRIPTION = `Restores previously compressed content identified by a block ID.
|
|
6023
6168
|
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6169
|
+
Use this tool when you need exact details from a compressed block that the summary cannot provide.
|
|
6170
|
+
The tool returns a condensed preview of the restored content so you can reason about it immediately.
|
|
6171
|
+
|
|
6172
|
+
Argument: blockId \u2014 the block reference to decompress (e.g., "b0", "b2")
|
|
6173
|
+
|
|
6174
|
+
IMPORTANT:
|
|
6175
|
+
- Decompressing inflates context. Check context usage before decompressing.
|
|
6176
|
+
- Message-mode blocks from the same batch (same runId) are restored together.
|
|
6177
|
+
- After decompression, the restored messages will appear in full in your next context window.
|
|
6178
|
+
- Do NOT call this tool in parallel with compress \u2014 their state mutations may conflict.`;
|
|
6179
|
+
function buildSchema3() {
|
|
6180
|
+
return {
|
|
6181
|
+
blockId: tool3.schema.string().describe('Block reference to decompress (e.g., "b0", "b2")')
|
|
6182
|
+
};
|
|
6183
|
+
}
|
|
6184
|
+
function createDecompressTool(ctx) {
|
|
6185
|
+
return tool3({
|
|
6186
|
+
description: TOOL_DESCRIPTION,
|
|
6187
|
+
args: buildSchema3(),
|
|
6188
|
+
async execute(args, toolCtx) {
|
|
6189
|
+
const { rawMessages } = await prepareDecompressSession(ctx, toolCtx);
|
|
6190
|
+
const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
|
|
6191
|
+
getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
|
|
6192
|
+
) : void 0;
|
|
6193
|
+
const targetBlockId = parseBlockIdArg(args.blockId);
|
|
6194
|
+
if (targetBlockId === null) {
|
|
6195
|
+
return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
|
|
6196
|
+
}
|
|
6197
|
+
const messagesState = ctx.state.prune.messages;
|
|
6198
|
+
const target = resolveCompressionTarget(messagesState, targetBlockId);
|
|
6199
|
+
if (!target) {
|
|
6200
|
+
return `Error: Block ${targetBlockId} does not exist. No compression found with that ID.`;
|
|
6201
|
+
}
|
|
6202
|
+
const activeBlocks = target.blocks.filter((block) => block.active);
|
|
6203
|
+
if (activeBlocks.length === 0) {
|
|
6204
|
+
const activeAncestorBlockId = findActiveAncestorBlockId(messagesState, target);
|
|
6205
|
+
if (activeAncestorBlockId !== null) {
|
|
6206
|
+
return `Error: Block ${target.displayId} is nested inside active block ${activeAncestorBlockId}. Decompress block ${activeAncestorBlockId} first.`;
|
|
6207
|
+
}
|
|
6208
|
+
return `Error: Block ${target.displayId} is not active. It may have already been decompressed.`;
|
|
6209
|
+
}
|
|
6210
|
+
const activeMessagesBefore = snapshotActiveMessages(messagesState);
|
|
6211
|
+
const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
|
|
6212
|
+
deactivateCompressionTarget(messagesState, target);
|
|
6213
|
+
syncCompressionBlocks(ctx.state, ctx.logger, rawMessages);
|
|
6214
|
+
const { restoredMessageCount, restoredTokens } = computeRestoredMessages(
|
|
6215
|
+
messagesState,
|
|
6216
|
+
activeMessagesBefore
|
|
6217
|
+
);
|
|
6218
|
+
const reactivatedBlockIds = computeReactivatedBlockIds(
|
|
6219
|
+
messagesState,
|
|
6220
|
+
activeBlockIdsBefore
|
|
6221
|
+
);
|
|
6222
|
+
ctx.state.stats.totalPruneTokens = Math.max(
|
|
6223
|
+
0,
|
|
6224
|
+
ctx.state.stats.totalPruneTokens - restoredTokens
|
|
6225
|
+
);
|
|
6226
|
+
const contextUsageAfter = ctx.state.modelContextLimit ? Math.round(
|
|
6227
|
+
getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
|
|
6228
|
+
) : void 0;
|
|
6229
|
+
await finalizeDecompressSession(ctx);
|
|
6230
|
+
const restoredContentPreview = buildRestoredContentPreview(
|
|
6231
|
+
rawMessages,
|
|
6232
|
+
activeMessagesBefore,
|
|
6233
|
+
messagesState
|
|
6234
|
+
);
|
|
6235
|
+
const lines = [];
|
|
6236
|
+
lines.push(
|
|
6237
|
+
`Decompressed block b${target.displayId}. Restored ${restoredMessageCount} message(s) (~${formatTokenCount(restoredTokens)}).`
|
|
6238
|
+
);
|
|
6239
|
+
if (contextUsageBefore !== void 0 && contextUsageAfter !== void 0) {
|
|
6240
|
+
lines.push(`Context usage: ${contextUsageBefore}% \u2192 ${contextUsageAfter}%.`);
|
|
6241
|
+
}
|
|
6242
|
+
if (reactivatedBlockIds.length > 0) {
|
|
6243
|
+
const refs = reactivatedBlockIds.map((id) => `b${id}`).join(", ");
|
|
6244
|
+
lines.push(`Also restored nested block(s): ${refs}.`);
|
|
6245
|
+
}
|
|
6246
|
+
if (restoredContentPreview) {
|
|
6247
|
+
lines.push("");
|
|
6248
|
+
lines.push("RESTORED CONTENT (condensed):");
|
|
6249
|
+
lines.push(restoredContentPreview);
|
|
6250
|
+
}
|
|
6251
|
+
ctx.logger.info("Decompress tool completed", {
|
|
6252
|
+
targetBlockId: target.displayId,
|
|
6253
|
+
targetRunId: target.runId,
|
|
6254
|
+
restoredMessageCount,
|
|
6255
|
+
restoredTokens,
|
|
6256
|
+
reactivatedBlockIds
|
|
6257
|
+
});
|
|
6258
|
+
return lines.join("\n");
|
|
6035
6259
|
}
|
|
6036
|
-
|
|
6037
|
-
|
|
6260
|
+
});
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
// lib/logger.ts
|
|
6264
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
6265
|
+
import { join as join3 } from "path";
|
|
6266
|
+
import { existsSync as existsSync3 } from "fs";
|
|
6267
|
+
import { homedir as homedir3 } from "os";
|
|
6268
|
+
var Logger = class {
|
|
6269
|
+
logDir;
|
|
6270
|
+
enabled;
|
|
6271
|
+
constructor(enabled) {
|
|
6272
|
+
this.enabled = enabled;
|
|
6273
|
+
const configHome = process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config");
|
|
6274
|
+
this.logDir = join3(configHome, "opencode", "logs", "acp");
|
|
6275
|
+
}
|
|
6276
|
+
async ensureLogDir() {
|
|
6277
|
+
if (!existsSync3(this.logDir)) {
|
|
6278
|
+
await mkdir2(this.logDir, { recursive: true });
|
|
6038
6279
|
}
|
|
6039
|
-
|
|
6040
|
-
|
|
6280
|
+
}
|
|
6281
|
+
formatData(data) {
|
|
6282
|
+
if (!data) return "";
|
|
6283
|
+
const parts = [];
|
|
6284
|
+
for (const [key, value] of Object.entries(data)) {
|
|
6285
|
+
if (value === void 0 || value === null) continue;
|
|
6286
|
+
if (Array.isArray(value)) {
|
|
6287
|
+
if (value.length === 0) continue;
|
|
6288
|
+
parts.push(
|
|
6289
|
+
`${key}=[${value.slice(0, 3).join(",")}${value.length > 3 ? `...+${value.length - 3}` : ""}]`
|
|
6290
|
+
);
|
|
6291
|
+
} else if (typeof value === "object") {
|
|
6292
|
+
const str = JSON.stringify(value);
|
|
6293
|
+
if (str.length < 50) {
|
|
6294
|
+
parts.push(`${key}=${str}`);
|
|
6295
|
+
}
|
|
6296
|
+
} else {
|
|
6297
|
+
parts.push(`${key}=${value}`);
|
|
6298
|
+
}
|
|
6041
6299
|
}
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6300
|
+
return parts.join(" ");
|
|
6301
|
+
}
|
|
6302
|
+
getCallerFile(skipFrames = 3) {
|
|
6303
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
6304
|
+
try {
|
|
6305
|
+
const err = new Error();
|
|
6306
|
+
Error.prepareStackTrace = (_, stack2) => stack2;
|
|
6307
|
+
const stack = err.stack;
|
|
6308
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
6309
|
+
for (let i = skipFrames; i < stack.length; i++) {
|
|
6310
|
+
const filename = stack[i]?.getFileName();
|
|
6311
|
+
if (filename && !filename.includes("/logger.")) {
|
|
6312
|
+
const match = filename.match(/([^/\\]+)\.[tj]s$/);
|
|
6313
|
+
return match ? match[1] : filename;
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
return "unknown";
|
|
6317
|
+
} catch {
|
|
6318
|
+
return "unknown";
|
|
6045
6319
|
}
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6320
|
+
}
|
|
6321
|
+
async write(level, component, message, data) {
|
|
6322
|
+
if (!this.enabled) return;
|
|
6323
|
+
try {
|
|
6324
|
+
await this.ensureLogDir();
|
|
6325
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
6326
|
+
const dataStr = this.formatData(data);
|
|
6327
|
+
const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? " | " + dataStr : ""}
|
|
6328
|
+
`;
|
|
6329
|
+
const dailyLogDir = join3(this.logDir, "daily");
|
|
6330
|
+
if (!existsSync3(dailyLogDir)) {
|
|
6331
|
+
await mkdir2(dailyLogDir, { recursive: true });
|
|
6332
|
+
}
|
|
6333
|
+
const logFile = join3(dailyLogDir, `${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
|
|
6334
|
+
await writeFile2(logFile, logLine, { flag: "a" });
|
|
6335
|
+
} catch (error) {
|
|
6049
6336
|
}
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6337
|
+
}
|
|
6338
|
+
info(message, data) {
|
|
6339
|
+
if (!this.enabled) return;
|
|
6340
|
+
const component = this.getCallerFile(2);
|
|
6341
|
+
return this.write("INFO", component, message, data);
|
|
6342
|
+
}
|
|
6343
|
+
debug(message, data) {
|
|
6344
|
+
if (!this.enabled) return;
|
|
6345
|
+
const component = this.getCallerFile(2);
|
|
6346
|
+
return this.write("DEBUG", component, message, data);
|
|
6347
|
+
}
|
|
6348
|
+
warn(message, data) {
|
|
6349
|
+
if (!this.enabled) return;
|
|
6350
|
+
const component = this.getCallerFile(2);
|
|
6351
|
+
return this.write("WARN", component, message, data);
|
|
6352
|
+
}
|
|
6353
|
+
error(message, data) {
|
|
6354
|
+
if (!this.enabled) return;
|
|
6355
|
+
const component = this.getCallerFile(2);
|
|
6356
|
+
return this.write("ERROR", component, message, data);
|
|
6357
|
+
}
|
|
6358
|
+
/**
|
|
6359
|
+
* Strips unnecessary metadata from messages for cleaner debug logs.
|
|
6360
|
+
*
|
|
6361
|
+
* Removed:
|
|
6362
|
+
* - All IDs (id, sessionID, messageID, parentID)
|
|
6363
|
+
* - summary, path, cost, model, agent, mode, finish, providerID, modelID
|
|
6364
|
+
* - step-start and step-finish parts entirely
|
|
6365
|
+
* - snapshot fields
|
|
6366
|
+
* - ignored text parts
|
|
6367
|
+
*
|
|
6368
|
+
* Kept:
|
|
6369
|
+
* - role, time (created only), tokens (input, output, reasoning, cache)
|
|
6370
|
+
* - text, reasoning, tool parts with content
|
|
6371
|
+
* - tool calls with: tool, callID, input, output, metadata
|
|
6372
|
+
*/
|
|
6373
|
+
minimizeForDebug(messages) {
|
|
6374
|
+
return messages.map((msg) => {
|
|
6375
|
+
const minimized = {
|
|
6376
|
+
role: msg.info?.role
|
|
6377
|
+
};
|
|
6378
|
+
if (msg.info?.time?.created) {
|
|
6379
|
+
minimized.time = msg.info.time.created;
|
|
6380
|
+
}
|
|
6381
|
+
if (msg.info?.tokens) {
|
|
6382
|
+
minimized.tokens = {
|
|
6383
|
+
input: msg.info.tokens.input,
|
|
6384
|
+
output: msg.info.tokens.output,
|
|
6385
|
+
reasoning: msg.info.tokens.reasoning,
|
|
6386
|
+
cache: msg.info.tokens.cache
|
|
6387
|
+
};
|
|
6388
|
+
}
|
|
6389
|
+
if (msg.parts) {
|
|
6390
|
+
minimized.parts = msg.parts.map((part) => {
|
|
6391
|
+
if (part.type === "step-start" || part.type === "step-finish") {
|
|
6392
|
+
return null;
|
|
6393
|
+
}
|
|
6394
|
+
if (part.type === "text") {
|
|
6395
|
+
if (part.ignored) return null;
|
|
6396
|
+
const textPart = { type: "text", text: part.text };
|
|
6397
|
+
if (part.metadata) textPart.metadata = part.metadata;
|
|
6398
|
+
return textPart;
|
|
6399
|
+
}
|
|
6400
|
+
if (part.type === "reasoning") {
|
|
6401
|
+
const reasoningPart = { type: "reasoning", text: part.text };
|
|
6402
|
+
if (part.metadata) reasoningPart.metadata = part.metadata;
|
|
6403
|
+
return reasoningPart;
|
|
6404
|
+
}
|
|
6405
|
+
if (part.type === "tool") {
|
|
6406
|
+
const toolPart = {
|
|
6407
|
+
type: "tool",
|
|
6408
|
+
tool: part.tool,
|
|
6409
|
+
callID: part.callID
|
|
6410
|
+
};
|
|
6411
|
+
if (part.state?.status) {
|
|
6412
|
+
toolPart.status = part.state.status;
|
|
6413
|
+
}
|
|
6414
|
+
if (part.state?.input) {
|
|
6415
|
+
toolPart.input = part.state.input;
|
|
6416
|
+
}
|
|
6417
|
+
if (part.state?.output) {
|
|
6418
|
+
toolPart.output = part.state.output;
|
|
6419
|
+
}
|
|
6420
|
+
if (part.state?.error) {
|
|
6421
|
+
toolPart.error = part.state.error;
|
|
6422
|
+
}
|
|
6423
|
+
if (part.metadata) {
|
|
6424
|
+
toolPart.metadata = part.metadata;
|
|
6425
|
+
}
|
|
6426
|
+
if (part.state?.metadata) {
|
|
6427
|
+
toolPart.metadata = {
|
|
6428
|
+
...toolPart.metadata || {},
|
|
6429
|
+
...part.state.metadata
|
|
6430
|
+
};
|
|
6431
|
+
}
|
|
6432
|
+
if (part.state?.title) {
|
|
6433
|
+
toolPart.title = part.state.title;
|
|
6434
|
+
}
|
|
6435
|
+
return toolPart;
|
|
6436
|
+
}
|
|
6437
|
+
return null;
|
|
6438
|
+
}).filter(Boolean);
|
|
6439
|
+
}
|
|
6440
|
+
return minimized;
|
|
6055
6441
|
});
|
|
6056
6442
|
}
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
const upperBound = Math.max(0, Math.min(anchorIndex, messages.length));
|
|
6072
|
-
for (let index = 0; index < upperBound; index++) {
|
|
6073
|
-
const rawMessageId = messages[index]?.info.id;
|
|
6074
|
-
if (typeof rawMessageId !== "string") {
|
|
6075
|
-
continue;
|
|
6076
|
-
}
|
|
6077
|
-
const entry = priorities.get(rawMessageId);
|
|
6078
|
-
if (!entry || entry.priority !== priority || seen.has(entry.ref)) {
|
|
6079
|
-
continue;
|
|
6443
|
+
async saveContext(sessionId, messages) {
|
|
6444
|
+
if (!this.enabled) return;
|
|
6445
|
+
try {
|
|
6446
|
+
const contextDir = join3(this.logDir, "context", sessionId);
|
|
6447
|
+
if (!existsSync3(contextDir)) {
|
|
6448
|
+
await mkdir2(contextDir, { recursive: true });
|
|
6449
|
+
}
|
|
6450
|
+
const minimized = this.minimizeForDebug(messages).filter(
|
|
6451
|
+
(msg) => msg.parts && msg.parts.length > 0
|
|
6452
|
+
);
|
|
6453
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6454
|
+
const contextFile = join3(contextDir, `${timestamp}.json`);
|
|
6455
|
+
await writeFile2(contextFile, JSON.stringify(minimized, null, 2));
|
|
6456
|
+
} catch (error) {
|
|
6080
6457
|
}
|
|
6081
|
-
seen.add(entry.ref);
|
|
6082
|
-
refs.push(entry.ref);
|
|
6083
6458
|
}
|
|
6084
|
-
|
|
6085
|
-
}
|
|
6459
|
+
};
|
|
6086
6460
|
|
|
6087
|
-
// lib/
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6461
|
+
// lib/prompts/store.ts
|
|
6462
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync as statSync2, cpSync as cpSync2 } from "fs";
|
|
6463
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
6464
|
+
import { homedir as homedir4 } from "os";
|
|
6465
|
+
|
|
6466
|
+
// lib/prompts/system.ts
|
|
6467
|
+
var SYSTEM = `
|
|
6468
|
+
You operate in a context-constrained environment. Manage context continuously to avoid buildup and preserve retrieval quality. Efficient context management is paramount for your agentic performance.
|
|
6469
|
+
|
|
6470
|
+
The tools you have for context management are \`compress\` and \`decompress\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details.
|
|
6471
|
+
|
|
6472
|
+
\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
6473
|
+
|
|
6474
|
+
THE PHILOSOPHY OF COMPRESS
|
|
6475
|
+
\`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.
|
|
6476
|
+
|
|
6477
|
+
Think of compression as phase transitions: raw exploration becomes refined understanding. The original context served its purpose; your summary now carries that understanding forward.
|
|
6478
|
+
|
|
6479
|
+
COMPRESS WHEN
|
|
6480
|
+
|
|
6481
|
+
A section is genuinely closed and the raw conversation has served its purpose:
|
|
6482
|
+
|
|
6483
|
+
- Research concluded and findings are clear
|
|
6484
|
+
- Implementation finished and verified
|
|
6485
|
+
- Exploration exhausted and patterns understood
|
|
6486
|
+
- Dead-end noise can be discarded without waiting for a whole chapter to close
|
|
6487
|
+
|
|
6488
|
+
DO NOT COMPRESS IF
|
|
6489
|
+
|
|
6490
|
+
- Raw context is still relevant and needed for edits or precise references
|
|
6491
|
+
- The target content is still actively in progress
|
|
6492
|
+
- You may need exact code, error messages, or file contents in the immediate next steps
|
|
6493
|
+
|
|
6494
|
+
Before compressing, ask: _"Is this section closed enough to become summary-only right now?"_
|
|
6495
|
+
|
|
6496
|
+
Evaluate conversation signal-to-noise REGULARLY. Use \`compress\` deliberately with quality-first summaries. Prioritize stale content intelligently to maintain a high-signal context window that supports your agency.
|
|
6497
|
+
|
|
6498
|
+
It is of your responsibility to keep a sharp, high-quality context window for optimal performance.
|
|
6499
|
+
`;
|
|
6500
|
+
|
|
6501
|
+
// lib/prompts/compress-range.ts
|
|
6502
|
+
var COMPRESS_RANGE = `Collapse a range in the conversation into a detailed summary.
|
|
6503
|
+
|
|
6504
|
+
THE SUMMARY
|
|
6505
|
+
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings... EVERYTHING that maintains context integrity. This is not a brief note - it is an authoritative record so faithful that the original conversation adds no value.
|
|
6506
|
+
|
|
6507
|
+
USER INTENT FIDELITY
|
|
6508
|
+
When the compressed range includes user messages, preserve the user's intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
|
|
6509
|
+
Directly quote user messages when they are short enough to include safely. Direct quotes are preferred when they best preserve exact meaning.
|
|
6510
|
+
|
|
6511
|
+
Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool outputs, back-and-forth exploration. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
|
|
6512
|
+
|
|
6513
|
+
COMPRESSED BLOCK PLACEHOLDERS
|
|
6514
|
+
When the selected range includes previously compressed blocks, use this exact placeholder format when referencing one:
|
|
6515
|
+
|
|
6516
|
+
- \`(bN)\`
|
|
6517
|
+
|
|
6518
|
+
Compressed block sections in context are clearly marked with a header:
|
|
6519
|
+
|
|
6520
|
+
- \`[Compressed conversation section]\`
|
|
6521
|
+
|
|
6522
|
+
Compressed block IDs always use the \`bN\` form (never \`mNNNNN\`) and are represented in the same XML metadata tag format.
|
|
6523
|
+
|
|
6524
|
+
Rules:
|
|
6525
|
+
|
|
6526
|
+
- Include every required block placeholder exactly once.
|
|
6527
|
+
- Do not invent placeholders for blocks outside the selected range.
|
|
6528
|
+
- Treat \`(bN)\` placeholders as RESERVED TOKENS. Do not emit \`(bN)\` text anywhere except intentional placeholders.
|
|
6529
|
+
- If you need to mention a block in prose, use plain text like \`compressed bN\` (not as a placeholder).
|
|
6530
|
+
- Preflight check before finalizing: the set of \`(bN)\` placeholders in your summary must exactly match the required set, with no duplicates.
|
|
6531
|
+
|
|
6532
|
+
These placeholders are semantic references. They will be replaced with the full stored compressed block content when the tool processes your output.
|
|
6533
|
+
|
|
6534
|
+
FLOW PRESERVATION WITH PLACEHOLDERS
|
|
6535
|
+
When you use compressed block placeholders, write the surrounding summary text so it still reads correctly AFTER placeholder expansion.
|
|
6536
|
+
|
|
6537
|
+
- Treat each placeholder as a stand-in for a full conversation segment, not as a short label.
|
|
6538
|
+
- Ensure transitions before and after each placeholder preserve chronology and causality.
|
|
6539
|
+
- Do not write text that depends on the placeholder staying literal (for example, "as noted in \`(b2)\`").
|
|
6540
|
+
- Your final meaning must be coherent once each placeholder is replaced with its full compressed block content.
|
|
6541
|
+
|
|
6542
|
+
BOUNDARY IDS
|
|
6543
|
+
You specify boundaries by ID using the injected IDs visible in the conversation:
|
|
6544
|
+
|
|
6545
|
+
- \`mNNNNN\` IDs identify raw messages
|
|
6546
|
+
- \`bN\` IDs identify previously compressed blocks
|
|
6547
|
+
|
|
6548
|
+
Each message has an ID inside XML metadata tags like \`<dcp-message-id>...</dcp-message-id>\`.
|
|
6549
|
+
The same ID tag appears in every tool output of the message it belongs to \u2014 each unique ID identifies one complete message.
|
|
6550
|
+
Treat these tags as boundary metadata only, not as tool result content.
|
|
6551
|
+
|
|
6552
|
+
Rules:
|
|
6553
|
+
|
|
6554
|
+
- Pick \`startId\` and \`endId\` directly from injected IDs in context.
|
|
6555
|
+
- IDs must exist in the current visible context. If you cannot see an ID in the messages above, it is stale and will fail.
|
|
6556
|
+
- \`startId\` must appear before \`endId\`.
|
|
6557
|
+
- Do not invent IDs. Use only IDs that are present in context.
|
|
6558
|
+
- NEVER use IDs from compressed block summaries, previous nudges, or your own memory \u2014 only IDs currently visible as XML metadata tags in the conversation.
|
|
6559
|
+
|
|
6560
|
+
BATCHING
|
|
6561
|
+
When multiple independent ranges are ready and their boundaries do not overlap, include all of them as separate entries in the \`content\` array of a single tool call. Each entry should have its own \`startId\`, \`endId\`, and \`summary\`.
|
|
6562
|
+
`;
|
|
6563
|
+
|
|
6564
|
+
// lib/prompts/compress-message.ts
|
|
6565
|
+
var COMPRESS_MESSAGE = `Collapse selected individual messages in the conversation into detailed summaries.
|
|
6566
|
+
|
|
6567
|
+
THE SUMMARY
|
|
6568
|
+
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings, tool outcomes, and user intent details that matter... EVERYTHING that preserves the value of the selected message after the raw message is removed.
|
|
6569
|
+
|
|
6570
|
+
USER INTENT FIDELITY
|
|
6571
|
+
When a selected message contains user intent, preserve that intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
|
|
6572
|
+
Directly quote short user instructions when that best preserves exact meaning.
|
|
6573
|
+
|
|
6574
|
+
Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool output, and repetition. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
|
|
6575
|
+
If a message contains no significant technical decisions, code changes, or user requirements, produce a minimal one-line summary rather than a detailed one.
|
|
6576
|
+
|
|
6577
|
+
MESSAGE IDS
|
|
6578
|
+
You specify individual raw messages by ID using the injected IDs visible in the conversation:
|
|
6579
|
+
|
|
6580
|
+
- \`mNNNNN\` IDs identify raw messages
|
|
6581
|
+
|
|
6582
|
+
Each message has an ID inside XML metadata tags like \`<dcp-message-id priority="high">m0007</dcp-message-id>\`.
|
|
6583
|
+
The same ID tag appears in every tool output of the message it belongs to \u2014 each unique ID identifies one complete message.
|
|
6584
|
+
Treat these tags as message metadata only, not as content to summarize. Use only the inner \`mNNNNN\` value as the \`messageId\`.
|
|
6585
|
+
The \`priority\` attribute indicates relative context cost. You MUST compress high-priority messages when their full text is no longer necessary for the active task.
|
|
6586
|
+
If prior compress-tool results are present, always compress and summarize them minimally only as part of a broader compression pass. Do not invoke the compress tool solely to re-compress an earlier compression result.
|
|
6587
|
+
Messages marked as \`<dcp-message-id>BLOCKED</dcp-message-id>\` cannot be compressed.
|
|
6588
|
+
|
|
6589
|
+
Rules:
|
|
6590
|
+
|
|
6591
|
+
- Pick each \`messageId\` directly from injected IDs visible in context.
|
|
6592
|
+
- Only use raw message IDs of the form \`mNNNNN\`.
|
|
6593
|
+
- Ignore XML attributes such as \`priority\` when copying the ID; use only the inner \`mNNNNN\` value.
|
|
6594
|
+
- Do not invent IDs. Use only IDs that are present in context.
|
|
6595
|
+
|
|
6596
|
+
BATCHING
|
|
6597
|
+
Select MANY messages in a single tool call when they are safe to compress.
|
|
6598
|
+
Each entry should summarize exactly one message, and the tool can receive as many entries as needed in one batch.
|
|
6599
|
+
|
|
6600
|
+
GENERAL CLEANUP
|
|
6601
|
+
Use the topic "general cleanup" for broad cleanup passes.
|
|
6602
|
+
During general cleanup, compress all medium and high-priority messages that are not relevant to the active task.
|
|
6603
|
+
Optimize for reducing context footprint, not for grouping messages by topic.
|
|
6604
|
+
Do not compress away still-active instructions, unresolved questions, or constraints that are likely to matter soon.
|
|
6605
|
+
Prioritize the earliest messages in the context as they will be the least relevant to the active task.
|
|
6606
|
+
General cleanup should be done periodically between other normal compression tool passes, not as the primary form of compression.
|
|
6607
|
+
`;
|
|
6608
|
+
|
|
6609
|
+
// lib/prompts/context-limit-nudge.ts
|
|
6610
|
+
var CONTEXT_LIMIT_NUDGE = `
|
|
6611
|
+
<system-reminder>
|
|
6612
|
+
\u26A0\uFE0F CRITICAL: Context limit reached. You MUST use the \`compress\` tool NOW.
|
|
6613
|
+
|
|
6614
|
+
If mid-atomic-operation, finish that step first, then compress immediately.
|
|
6615
|
+
|
|
6616
|
+
HOW TO CALL COMPRESS:
|
|
6617
|
+
{
|
|
6618
|
+
"topic": "Short Label",
|
|
6619
|
+
"content": [
|
|
6620
|
+
{
|
|
6621
|
+
"startId": "<ID from early in this conversation>",
|
|
6622
|
+
"endId": "<ID from later in this conversation>",
|
|
6623
|
+
"summary": "Complete technical summary of everything in the range"
|
|
6111
6624
|
}
|
|
6112
|
-
|
|
6113
|
-
}
|
|
6114
|
-
return count;
|
|
6115
|
-
}
|
|
6116
|
-
function getModelInfo(messages) {
|
|
6117
|
-
const lastUserMessage = getLastUserMessage(messages);
|
|
6118
|
-
if (!lastUserMessage) {
|
|
6119
|
-
return {
|
|
6120
|
-
providerId: void 0,
|
|
6121
|
-
modelId: void 0
|
|
6122
|
-
};
|
|
6123
|
-
}
|
|
6124
|
-
const userInfo = lastUserMessage.info;
|
|
6125
|
-
return {
|
|
6126
|
-
providerId: userInfo.model.providerID,
|
|
6127
|
-
modelId: userInfo.model.modelID
|
|
6128
|
-
};
|
|
6625
|
+
]
|
|
6129
6626
|
}
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
if (modelLimit !== void 0) {
|
|
6154
|
-
return parseLimitValue(modelLimit);
|
|
6155
|
-
}
|
|
6156
|
-
}
|
|
6157
|
-
const globalLimit = threshold === "max" ? config.compress.maxContextLimit : config.compress.minContextLimit;
|
|
6158
|
-
return parseLimitValue(globalLimit);
|
|
6627
|
+
|
|
6628
|
+
\u26A0\uFE0F ID RULES \u2014 MOST COMMON CAUSE OF ERRORS:
|
|
6629
|
+
- ONLY use IDs you can see in <dcp-message-id> tags in the messages ABOVE.
|
|
6630
|
+
- Do NOT copy IDs from this example. Do NOT invent IDs.
|
|
6631
|
+
- Do NOT use IDs from compressed block summaries \u2014 they are stale.
|
|
6632
|
+
- startId must appear BEFORE endId in the conversation.
|
|
6633
|
+
|
|
6634
|
+
SUMMARY RULES:
|
|
6635
|
+
- Capture ALL essential details: file paths, decisions, constraints, key findings.
|
|
6636
|
+
- Preserve user intent exactly. Direct-quote short user messages.
|
|
6637
|
+
- Prefer one large range over multiple small ones.
|
|
6638
|
+
- Compress OLDER resolved history first. Keep recent active work.
|
|
6639
|
+
</system-reminder>
|
|
6640
|
+
`;
|
|
6641
|
+
|
|
6642
|
+
// lib/prompts/turn-nudge.ts
|
|
6643
|
+
var TURN_NUDGE = `
|
|
6644
|
+
<system-reminder>
|
|
6645
|
+
Context is getting full. Compress closed/older conversation ranges now.
|
|
6646
|
+
|
|
6647
|
+
{
|
|
6648
|
+
"topic": "Short Label",
|
|
6649
|
+
"content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
|
|
6159
6650
|
}
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
const overMinLimit = minContextLimit === void 0 ? false : currentTokens >= minContextLimit;
|
|
6174
|
-
if (overMaxLimit) {
|
|
6175
|
-
const recentCompressCount = 3;
|
|
6176
|
-
const recentMessages = messages.slice(-recentCompressCount);
|
|
6177
|
-
for (const msg of recentMessages) {
|
|
6178
|
-
if (msg.info.role === "assistant" && msg.parts) {
|
|
6179
|
-
for (const part of msg.parts) {
|
|
6180
|
-
if (part.type === "tool-invocation" && part.toolInvocation?.toolName === "compress") {
|
|
6181
|
-
overMaxLimit = false;
|
|
6182
|
-
break;
|
|
6183
|
-
}
|
|
6184
|
-
}
|
|
6185
|
-
}
|
|
6186
|
-
if (!overMaxLimit) break;
|
|
6187
|
-
}
|
|
6188
|
-
}
|
|
6189
|
-
return {
|
|
6190
|
-
overMaxLimit,
|
|
6191
|
-
overMinLimit,
|
|
6192
|
-
currentTokens,
|
|
6193
|
-
modelContextLimit: state.modelContextLimit
|
|
6194
|
-
};
|
|
6651
|
+
|
|
6652
|
+
\u26A0\uFE0F ONLY use IDs from <dcp-message-id> tags visible above. Do NOT invent or copy example IDs.
|
|
6653
|
+
</system-reminder>
|
|
6654
|
+
`;
|
|
6655
|
+
|
|
6656
|
+
// lib/prompts/iteration-nudge.ts
|
|
6657
|
+
var ITERATION_NUDGE = `
|
|
6658
|
+
<system-reminder>
|
|
6659
|
+
You've been iterating for a while. If any earlier work is closed and unlikely to be referenced, compress it now.
|
|
6660
|
+
|
|
6661
|
+
{
|
|
6662
|
+
"topic": "Short Label",
|
|
6663
|
+
"content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
|
|
6195
6664
|
}
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6665
|
+
|
|
6666
|
+
\u26A0\uFE0F ONLY use IDs from <dcp-message-id> tags visible above. Do NOT invent or copy example IDs.
|
|
6667
|
+
</system-reminder>
|
|
6668
|
+
`;
|
|
6669
|
+
|
|
6670
|
+
// lib/prompts/extensions/system.ts
|
|
6671
|
+
var MANUAL_MODE_SYSTEM_EXTENSION = `<dcp-system-reminder>
|
|
6672
|
+
Manual mode is enabled. Do NOT use compress unless the user has explicitly triggered it through a manual marker.
|
|
6673
|
+
|
|
6674
|
+
Only use the compress tool after seeing \`<compress triggered manually>\` in the current user instruction context.
|
|
6675
|
+
|
|
6676
|
+
Issue exactly ONE compress tool per manual trigger. Do NOT launch multiple compress tools in parallel. Each trigger grants a single compression; after it completes, wait for the next trigger.
|
|
6677
|
+
|
|
6678
|
+
After completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input.
|
|
6679
|
+
</dcp-system-reminder>
|
|
6680
|
+
`;
|
|
6681
|
+
var SUBAGENT_SYSTEM_EXTENSION = `<dcp-system-reminder>
|
|
6682
|
+
You are operating in a subagent environment.
|
|
6683
|
+
|
|
6684
|
+
The initial subagent instruction is imperative and must be followed exactly.
|
|
6685
|
+
It is the only user message intentionally not assigned a message ID, and therefore is not eligible for compression.
|
|
6686
|
+
All subsequent messages in the session will have IDs.
|
|
6687
|
+
</dcp-system-reminder>
|
|
6688
|
+
`;
|
|
6689
|
+
function buildProtectedToolsExtension(protectedTools) {
|
|
6690
|
+
if (protectedTools.length === 0) {
|
|
6691
|
+
return "";
|
|
6210
6692
|
}
|
|
6211
|
-
const
|
|
6212
|
-
|
|
6213
|
-
|
|
6693
|
+
const toolList = protectedTools.map((t) => `\`${t}\``).join(", ");
|
|
6694
|
+
return `<dcp-system-reminder>
|
|
6695
|
+
The following tools are environment-managed: ${toolList}.
|
|
6696
|
+
Their outputs are automatically preserved during compression.
|
|
6697
|
+
Do not include their content in compress tool summaries \u2014 the environment retains it independently.
|
|
6698
|
+
</dcp-system-reminder>`;
|
|
6214
6699
|
}
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6700
|
+
var DECOMPRESS_SYSTEM_EXTENSION = `<dcp-system-reminder>
|
|
6701
|
+
THE PHILOSOPHY OF DECOMPRESS
|
|
6702
|
+
\`decompress\` restores previously compressed content. Use it when you need exact details
|
|
6703
|
+
that were lost in compression.
|
|
6704
|
+
|
|
6705
|
+
DECOMPRESS WHEN
|
|
6706
|
+
- You need exact code, error messages, or file contents from a compressed block
|
|
6707
|
+
- A summary lacks the precision needed for your next step
|
|
6708
|
+
- You discovered the compressed content is still relevant
|
|
6709
|
+
|
|
6710
|
+
DO NOT DECOMPRESS IF
|
|
6711
|
+
- Context usage is already high (>70%) \u2014 decompressing inflates context
|
|
6712
|
+
- The summary is sufficient for your needs
|
|
6713
|
+
- You plan to immediately recompress the same content
|
|
6714
|
+
|
|
6715
|
+
Before decompressing, check context usage. Decompressing restores full messages,
|
|
6716
|
+
which can significantly increase context size.
|
|
6717
|
+
|
|
6718
|
+
NOTE: Message-mode blocks created in the same batch (same runId) are restored together.
|
|
6719
|
+
Decompressing one block from a batch restores all blocks in that batch.
|
|
6720
|
+
</dcp-system-reminder>
|
|
6721
|
+
`;
|
|
6722
|
+
|
|
6723
|
+
// lib/prompts/store.ts
|
|
6724
|
+
var PROMPT_DEFINITIONS = [
|
|
6725
|
+
{
|
|
6726
|
+
key: "system",
|
|
6727
|
+
fileName: "system.md",
|
|
6728
|
+
label: "System",
|
|
6729
|
+
description: "Core system-level ACP instruction block",
|
|
6730
|
+
usage: "Injected into the model system prompt on every request",
|
|
6731
|
+
runtimeField: "system"
|
|
6732
|
+
},
|
|
6733
|
+
{
|
|
6734
|
+
key: "compress-range",
|
|
6735
|
+
fileName: "compress-range.md",
|
|
6736
|
+
label: "Compress Range",
|
|
6737
|
+
description: "range-mode compress tool instructions and summary constraints",
|
|
6738
|
+
usage: "Registered as the range-mode compress tool description",
|
|
6739
|
+
runtimeField: "compressRange"
|
|
6740
|
+
},
|
|
6741
|
+
{
|
|
6742
|
+
key: "compress-message",
|
|
6743
|
+
fileName: "compress-message.md",
|
|
6744
|
+
label: "Compress Message",
|
|
6745
|
+
description: "message-mode compress tool instructions and summary constraints",
|
|
6746
|
+
usage: "Registered as the message-mode compress tool description",
|
|
6747
|
+
runtimeField: "compressMessage"
|
|
6748
|
+
},
|
|
6749
|
+
{
|
|
6750
|
+
key: "context-limit-nudge",
|
|
6751
|
+
fileName: "context-limit-nudge.md",
|
|
6752
|
+
label: "Context Limit Nudge",
|
|
6753
|
+
description: "High-priority nudge when context is over max threshold",
|
|
6754
|
+
usage: "Injected when context usage is beyond configured max limits",
|
|
6755
|
+
runtimeField: "contextLimitNudge"
|
|
6756
|
+
},
|
|
6757
|
+
{
|
|
6758
|
+
key: "turn-nudge",
|
|
6759
|
+
fileName: "turn-nudge.md",
|
|
6760
|
+
label: "Turn Nudge",
|
|
6761
|
+
description: "Nudge to compress closed ranges at turn boundaries",
|
|
6762
|
+
usage: "Injected when context is between min and max limits at a new user turn",
|
|
6763
|
+
runtimeField: "turnNudge"
|
|
6764
|
+
},
|
|
6765
|
+
{
|
|
6766
|
+
key: "iteration-nudge",
|
|
6767
|
+
fileName: "iteration-nudge.md",
|
|
6768
|
+
label: "Iteration Nudge",
|
|
6769
|
+
description: "Nudge after many iterations without user input",
|
|
6770
|
+
usage: "Injected when iteration threshold is crossed",
|
|
6771
|
+
runtimeField: "iterationNudge"
|
|
6218
6772
|
}
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6773
|
+
];
|
|
6774
|
+
var HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
|
|
6775
|
+
var LEGACY_INLINE_COMMENT_LINE_REGEX = /^[ \t]*\/\/.*?\/\/[ \t]*$/gm;
|
|
6776
|
+
var DCP_SYSTEM_REMINDER_TAG_REGEX = /^\s*<dcp-system-reminder\b[^>]*>[\s\S]*<\/dcp-system-reminder>\s*$/i;
|
|
6777
|
+
var DEFAULTS_README_FILE = "README.md";
|
|
6778
|
+
var BUNDLED_EDITABLE_PROMPTS = {
|
|
6779
|
+
system: SYSTEM,
|
|
6780
|
+
compressRange: COMPRESS_RANGE,
|
|
6781
|
+
compressMessage: COMPRESS_MESSAGE,
|
|
6782
|
+
contextLimitNudge: CONTEXT_LIMIT_NUDGE,
|
|
6783
|
+
turnNudge: TURN_NUDGE,
|
|
6784
|
+
iterationNudge: ITERATION_NUDGE
|
|
6785
|
+
};
|
|
6786
|
+
var INTERNAL_PROMPT_EXTENSIONS = {
|
|
6787
|
+
manualExtension: MANUAL_MODE_SYSTEM_EXTENSION,
|
|
6788
|
+
subagentExtension: SUBAGENT_SYSTEM_EXTENSION,
|
|
6789
|
+
decompressExtension: DECOMPRESS_SYSTEM_EXTENSION
|
|
6790
|
+
};
|
|
6791
|
+
function createBundledRuntimePrompts() {
|
|
6792
|
+
return {
|
|
6793
|
+
system: BUNDLED_EDITABLE_PROMPTS.system,
|
|
6794
|
+
compressRange: BUNDLED_EDITABLE_PROMPTS.compressRange,
|
|
6795
|
+
compressMessage: BUNDLED_EDITABLE_PROMPTS.compressMessage,
|
|
6796
|
+
contextLimitNudge: BUNDLED_EDITABLE_PROMPTS.contextLimitNudge,
|
|
6797
|
+
turnNudge: BUNDLED_EDITABLE_PROMPTS.turnNudge,
|
|
6798
|
+
iterationNudge: BUNDLED_EDITABLE_PROMPTS.iterationNudge,
|
|
6799
|
+
manualExtension: INTERNAL_PROMPT_EXTENSIONS.manualExtension,
|
|
6800
|
+
subagentExtension: INTERNAL_PROMPT_EXTENSIONS.subagentExtension,
|
|
6801
|
+
decompressExtension: INTERNAL_PROMPT_EXTENSIONS.decompressExtension
|
|
6802
|
+
};
|
|
6222
6803
|
}
|
|
6223
|
-
function
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
}
|
|
6234
|
-
if (message.info.role !== "assistant") {
|
|
6235
|
-
return;
|
|
6236
|
-
}
|
|
6237
|
-
if (!hasContent(message)) {
|
|
6238
|
-
return;
|
|
6239
|
-
}
|
|
6240
|
-
for (const part of message.parts) {
|
|
6241
|
-
if (part.type === "text") {
|
|
6242
|
-
if (appendToTextPart(part, nudgeText)) {
|
|
6243
|
-
return;
|
|
6804
|
+
function findOpencodeDir2(startDir) {
|
|
6805
|
+
let current = startDir;
|
|
6806
|
+
while (current !== "/") {
|
|
6807
|
+
const candidate = join4(current, ".opencode");
|
|
6808
|
+
if (existsSync4(candidate)) {
|
|
6809
|
+
try {
|
|
6810
|
+
if (statSync2(candidate).isDirectory()) {
|
|
6811
|
+
return candidate;
|
|
6812
|
+
}
|
|
6813
|
+
} catch {
|
|
6244
6814
|
}
|
|
6245
6815
|
}
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
if (firstToolIndex === -1) {
|
|
6250
|
-
message.parts.push(syntheticPart);
|
|
6251
|
-
} else {
|
|
6252
|
-
message.parts.splice(firstToolIndex, 0, syntheticPart);
|
|
6253
|
-
}
|
|
6254
|
-
}
|
|
6255
|
-
function collectAnchoredMessages(anchorMessageIds, messages) {
|
|
6256
|
-
const anchoredMessages = [];
|
|
6257
|
-
for (const anchorMessageId of anchorMessageIds) {
|
|
6258
|
-
const index = messages.findIndex((message) => message.info.id === anchorMessageId);
|
|
6259
|
-
if (index === -1) {
|
|
6260
|
-
continue;
|
|
6816
|
+
const parent = dirname2(current);
|
|
6817
|
+
if (parent === current) {
|
|
6818
|
+
break;
|
|
6261
6819
|
}
|
|
6262
|
-
|
|
6263
|
-
message: messages[index],
|
|
6264
|
-
index
|
|
6265
|
-
});
|
|
6820
|
+
current = parent;
|
|
6266
6821
|
}
|
|
6267
|
-
return
|
|
6822
|
+
return null;
|
|
6268
6823
|
}
|
|
6269
|
-
function
|
|
6270
|
-
const
|
|
6271
|
-
const
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6824
|
+
function resolvePromptPaths(workingDirectory) {
|
|
6825
|
+
const configHome = process.env.XDG_CONFIG_HOME || join4(homedir4(), ".config");
|
|
6826
|
+
const globalRoot = join4(configHome, "opencode", "acp-prompts");
|
|
6827
|
+
const legacyGlobalRoot = join4(configHome, "opencode", "dcp-prompts");
|
|
6828
|
+
if (!existsSync4(globalRoot) && existsSync4(legacyGlobalRoot)) {
|
|
6829
|
+
try {
|
|
6830
|
+
cpSync2(legacyGlobalRoot, globalRoot, { recursive: true });
|
|
6831
|
+
console.log("[ACP] Migrated prompts from dcp-prompts to acp-prompts");
|
|
6832
|
+
} catch (e) {
|
|
6833
|
+
console.warn(`[ACP] Prompts migration failed: ${e.message}`);
|
|
6276
6834
|
}
|
|
6277
6835
|
}
|
|
6278
|
-
|
|
6836
|
+
const defaultsDir = join4(globalRoot, "defaults");
|
|
6837
|
+
const globalOverridesDir = join4(globalRoot, "overrides");
|
|
6838
|
+
const configDirOverridesDir = process.env.OPENCODE_CONFIG_DIR ? join4(process.env.OPENCODE_CONFIG_DIR, "acp-prompts", "overrides") : null;
|
|
6839
|
+
const opencodeDir = findOpencodeDir2(workingDirectory);
|
|
6840
|
+
const projectOverridesDir = opencodeDir ? join4(opencodeDir, "acp-prompts", "overrides") : null;
|
|
6841
|
+
return {
|
|
6842
|
+
defaultsDir,
|
|
6843
|
+
globalOverridesDir,
|
|
6844
|
+
configDirOverridesDir,
|
|
6845
|
+
projectOverridesDir
|
|
6846
|
+
};
|
|
6279
6847
|
}
|
|
6280
|
-
function
|
|
6281
|
-
const
|
|
6282
|
-
|
|
6283
|
-
return;
|
|
6284
|
-
}
|
|
6285
|
-
for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
|
|
6286
|
-
injectAnchoredNudge(message, nudgeText);
|
|
6287
|
-
}
|
|
6848
|
+
function stripConditionalTag(content, tagName) {
|
|
6849
|
+
const regex = new RegExp(`<${tagName}>[\\s\\S]*?</${tagName}>`, "gi");
|
|
6850
|
+
return content.replace(regex, "");
|
|
6288
6851
|
}
|
|
6289
|
-
function
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
compressionPriorities,
|
|
6294
|
-
index,
|
|
6295
|
-
MESSAGE_MODE_NUDGE_PRIORITY
|
|
6296
|
-
);
|
|
6297
|
-
const nudgeText = appendGuidanceToDcpTag(baseNudgeText, priorityGuidance);
|
|
6298
|
-
injectAnchoredNudge(message, nudgeText);
|
|
6852
|
+
function unwrapDcpTagIfWrapped(content) {
|
|
6853
|
+
const trimmed = content.trim();
|
|
6854
|
+
if (DCP_SYSTEM_REMINDER_TAG_REGEX.test(trimmed)) {
|
|
6855
|
+
return trimmed.replace(/^\s*<dcp-system-reminder\b[^>]*>\s*/i, "").replace(/\s*<\/dcp-system-reminder>\s*$/i, "").trim();
|
|
6299
6856
|
}
|
|
6857
|
+
return trimmed;
|
|
6300
6858
|
}
|
|
6301
|
-
function
|
|
6302
|
-
|
|
6859
|
+
function normalizeReminderPromptContent(content) {
|
|
6860
|
+
const normalized = content.trim();
|
|
6861
|
+
if (!normalized) {
|
|
6303
6862
|
return "";
|
|
6304
6863
|
}
|
|
6305
|
-
const
|
|
6306
|
-
const
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%). ACP (Active Context Pruning) threshold: 55%. You ARE the ACP agent \u2014 use the compress tool proactively to manage context quality.`;
|
|
6310
|
-
}
|
|
6311
|
-
function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit) {
|
|
6312
|
-
const contextUsageInfo = buildContextUsageInfo(currentTokens, modelContextLimit);
|
|
6313
|
-
const contextLimitNudgeWithUsage = prompts.contextLimitNudge + contextUsageInfo;
|
|
6314
|
-
const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
|
|
6315
|
-
if (config.compress.mode === "message") {
|
|
6316
|
-
applyMessageModeAnchoredNudge(
|
|
6317
|
-
state.nudges.contextLimitAnchors,
|
|
6318
|
-
messages,
|
|
6319
|
-
contextLimitNudgeWithUsage,
|
|
6320
|
-
compressionPriorities
|
|
6321
|
-
);
|
|
6322
|
-
applyMessageModeAnchoredNudge(
|
|
6323
|
-
turnNudgeAnchors,
|
|
6324
|
-
messages,
|
|
6325
|
-
prompts.turnNudge,
|
|
6326
|
-
compressionPriorities
|
|
6327
|
-
);
|
|
6328
|
-
applyMessageModeAnchoredNudge(
|
|
6329
|
-
state.nudges.iterationNudgeAnchors,
|
|
6330
|
-
messages,
|
|
6331
|
-
prompts.iterationNudge,
|
|
6332
|
-
compressionPriorities
|
|
6333
|
-
);
|
|
6334
|
-
return;
|
|
6335
|
-
}
|
|
6336
|
-
applyRangeModeAnchoredNudge(
|
|
6337
|
-
state.nudges.contextLimitAnchors,
|
|
6338
|
-
messages,
|
|
6339
|
-
contextLimitNudgeWithUsage,
|
|
6340
|
-
""
|
|
6341
|
-
);
|
|
6342
|
-
applyRangeModeAnchoredNudge(
|
|
6343
|
-
turnNudgeAnchors,
|
|
6344
|
-
messages,
|
|
6345
|
-
prompts.turnNudge,
|
|
6346
|
-
""
|
|
6347
|
-
);
|
|
6348
|
-
applyRangeModeAnchoredNudge(
|
|
6349
|
-
state.nudges.iterationNudgeAnchors,
|
|
6350
|
-
messages,
|
|
6351
|
-
prompts.iterationNudge,
|
|
6352
|
-
""
|
|
6353
|
-
);
|
|
6354
|
-
}
|
|
6355
|
-
|
|
6356
|
-
// lib/messages/inject/inject.ts
|
|
6357
|
-
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
|
|
6358
|
-
if (compressPermission(state, config) === "deny") {
|
|
6359
|
-
return;
|
|
6360
|
-
}
|
|
6361
|
-
if (state.manualMode) {
|
|
6362
|
-
return;
|
|
6363
|
-
}
|
|
6364
|
-
const lastMessage = findLastNonIgnoredMessage(messages);
|
|
6365
|
-
const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant");
|
|
6366
|
-
if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
|
|
6367
|
-
state.nudges.contextLimitAnchors.clear();
|
|
6368
|
-
state.nudges.turnNudgeAnchors.clear();
|
|
6369
|
-
state.nudges.iterationNudgeAnchors.clear();
|
|
6370
|
-
void saveSessionState(state, logger);
|
|
6371
|
-
return;
|
|
6372
|
-
}
|
|
6373
|
-
const { providerId, modelId } = getModelInfo(messages);
|
|
6374
|
-
let anchorsChanged = false;
|
|
6375
|
-
const { overMaxLimit, overMinLimit, currentTokens, modelContextLimit } = isContextOverLimits(
|
|
6376
|
-
config,
|
|
6377
|
-
state,
|
|
6378
|
-
providerId,
|
|
6379
|
-
modelId,
|
|
6380
|
-
messages
|
|
6381
|
-
);
|
|
6382
|
-
if (!overMinLimit) {
|
|
6383
|
-
const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
|
|
6384
|
-
const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0;
|
|
6385
|
-
if (hadTurnAnchors || hadIterationAnchors) {
|
|
6386
|
-
state.nudges.turnNudgeAnchors.clear();
|
|
6387
|
-
state.nudges.iterationNudgeAnchors.clear();
|
|
6388
|
-
anchorsChanged = true;
|
|
6389
|
-
}
|
|
6390
|
-
}
|
|
6391
|
-
if (overMaxLimit) {
|
|
6392
|
-
if (lastMessage) {
|
|
6393
|
-
const interval = getNudgeFrequency(config);
|
|
6394
|
-
const added = addAnchor(
|
|
6395
|
-
state.nudges.contextLimitAnchors,
|
|
6396
|
-
lastMessage.message.info.id,
|
|
6397
|
-
lastMessage.index,
|
|
6398
|
-
messages,
|
|
6399
|
-
interval
|
|
6400
|
-
);
|
|
6401
|
-
if (added) {
|
|
6402
|
-
anchorsChanged = true;
|
|
6403
|
-
}
|
|
6404
|
-
}
|
|
6405
|
-
} else if (overMinLimit) {
|
|
6406
|
-
const isLastMessageUser = lastMessage?.message.info.role === "user";
|
|
6407
|
-
if (isLastMessageUser && lastAssistantMessage) {
|
|
6408
|
-
const previousSize = state.nudges.turnNudgeAnchors.size;
|
|
6409
|
-
state.nudges.turnNudgeAnchors.add(lastMessage.message.info.id);
|
|
6410
|
-
state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id);
|
|
6411
|
-
if (state.nudges.turnNudgeAnchors.size !== previousSize) {
|
|
6412
|
-
anchorsChanged = true;
|
|
6413
|
-
}
|
|
6414
|
-
}
|
|
6415
|
-
const lastUserMessage = getLastUserMessage(messages);
|
|
6416
|
-
if (lastUserMessage && lastMessage) {
|
|
6417
|
-
const lastUserMessageIndex = messages.findIndex(
|
|
6418
|
-
(message) => message.info.id === lastUserMessage.info.id
|
|
6419
|
-
);
|
|
6420
|
-
if (lastUserMessageIndex >= 0) {
|
|
6421
|
-
const messagesSinceUser = countMessagesAfterIndex(messages, lastUserMessageIndex);
|
|
6422
|
-
const iterationThreshold = getIterationNudgeThreshold(config);
|
|
6423
|
-
if (lastMessage.index > lastUserMessageIndex && messagesSinceUser >= iterationThreshold) {
|
|
6424
|
-
const interval = getNudgeFrequency(config);
|
|
6425
|
-
const added = addAnchor(
|
|
6426
|
-
state.nudges.iterationNudgeAnchors,
|
|
6427
|
-
lastMessage.message.info.id,
|
|
6428
|
-
lastMessage.index,
|
|
6429
|
-
messages,
|
|
6430
|
-
interval
|
|
6431
|
-
);
|
|
6432
|
-
if (added) {
|
|
6433
|
-
anchorsChanged = true;
|
|
6434
|
-
}
|
|
6435
|
-
}
|
|
6436
|
-
}
|
|
6437
|
-
}
|
|
6438
|
-
}
|
|
6439
|
-
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit);
|
|
6440
|
-
injectContextUsage(messages, currentTokens, modelContextLimit);
|
|
6441
|
-
if (config.compress.mode !== "message") {
|
|
6442
|
-
const blockGuidance = buildCompressedBlockGuidance(state, config.gc, { currentTokens, modelContextLimit });
|
|
6443
|
-
if (blockGuidance.trim()) {
|
|
6444
|
-
const lastUser = getLastUserMessage(messages);
|
|
6445
|
-
if (lastUser) appendToLastTextPart(lastUser, "\n\n" + blockGuidance);
|
|
6446
|
-
}
|
|
6864
|
+
const startsWrapped = /^\s*<dcp-system-reminder\b[^>]*>/i.test(normalized);
|
|
6865
|
+
const endsWrapped = /<\/dcp-system-reminder>\s*$/i.test(normalized);
|
|
6866
|
+
if (startsWrapped !== endsWrapped) {
|
|
6867
|
+
return "";
|
|
6447
6868
|
}
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6869
|
+
return unwrapDcpTagIfWrapped(normalized);
|
|
6870
|
+
}
|
|
6871
|
+
function stripPromptComments(content) {
|
|
6872
|
+
return content.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").replace(HTML_COMMENT_REGEX, "").replace(LEGACY_INLINE_COMMENT_LINE_REGEX, "");
|
|
6873
|
+
}
|
|
6874
|
+
function toEditablePromptText(definition, rawContent) {
|
|
6875
|
+
let normalized = stripPromptComments(rawContent).trim();
|
|
6876
|
+
if (!normalized) {
|
|
6877
|
+
return "";
|
|
6451
6878
|
}
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
return;
|
|
6879
|
+
if (definition.key === "system") {
|
|
6880
|
+
normalized = stripConditionalTag(normalized, "manual");
|
|
6881
|
+
normalized = stripConditionalTag(normalized, "subagent");
|
|
6456
6882
|
}
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
const percentage = (currentTokens / modelContextLimit * 100).toFixed(1);
|
|
6460
|
-
const formatK = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6461
|
-
const usageTag = `
|
|
6462
|
-
|
|
6463
|
-
Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%). ACP (Active Context Pruning) threshold: 55%. You ARE the ACP agent \u2014 use the compress tool proactively to manage context quality.`;
|
|
6464
|
-
for (const part of lastUser.parts) {
|
|
6465
|
-
if (part.type === "text") {
|
|
6466
|
-
appendToTextPart(part, usageTag);
|
|
6467
|
-
return;
|
|
6468
|
-
}
|
|
6883
|
+
if (definition.key !== "compress-range" && definition.key !== "compress-message") {
|
|
6884
|
+
normalized = normalizeReminderPromptContent(normalized);
|
|
6469
6885
|
}
|
|
6470
|
-
|
|
6886
|
+
return normalized.trim();
|
|
6471
6887
|
}
|
|
6472
|
-
function
|
|
6473
|
-
const
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
if (ref) {
|
|
6477
|
-
visibleRefs.push(ref);
|
|
6478
|
-
}
|
|
6888
|
+
function wrapRuntimePromptContent(definition, editableText) {
|
|
6889
|
+
const trimmed = editableText.trim();
|
|
6890
|
+
if (!trimmed) {
|
|
6891
|
+
return "";
|
|
6479
6892
|
}
|
|
6480
|
-
if (
|
|
6481
|
-
|
|
6482
|
-
const first = visibleRefs[0];
|
|
6483
|
-
const last = visibleRefs[visibleRefs.length - 1];
|
|
6484
|
-
const rangeTag = `
|
|
6485
|
-
|
|
6486
|
-
[Visible message IDs: ${first} to ${last} (${visibleRefs.length} messages). Only use IDs in this range for compress.]`;
|
|
6487
|
-
const lastUser = getLastUserMessage(messages);
|
|
6488
|
-
if (!lastUser) return;
|
|
6489
|
-
for (const part of lastUser.parts) {
|
|
6490
|
-
if (part.type === "text") {
|
|
6491
|
-
appendToTextPart(part, rangeTag);
|
|
6492
|
-
return;
|
|
6493
|
-
}
|
|
6893
|
+
if (definition.key === "compress-range" || definition.key === "compress-message") {
|
|
6894
|
+
return trimmed;
|
|
6494
6895
|
}
|
|
6495
|
-
|
|
6896
|
+
return `<dcp-system-reminder>
|
|
6897
|
+
${trimmed}
|
|
6898
|
+
</dcp-system-reminder>`;
|
|
6496
6899
|
}
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6900
|
+
function buildDefaultPromptFileContent(bundledEditableText) {
|
|
6901
|
+
return `${bundledEditableText.trim()}
|
|
6902
|
+
`;
|
|
6903
|
+
}
|
|
6904
|
+
function buildDefaultsReadmeContent() {
|
|
6905
|
+
const lines = [];
|
|
6906
|
+
lines.push("# ACP Prompt Defaults");
|
|
6907
|
+
lines.push("");
|
|
6908
|
+
lines.push("This directory stores the ACP prompts.");
|
|
6909
|
+
lines.push("Each prompt file here should contain plain text only (no XML wrappers).");
|
|
6910
|
+
lines.push("");
|
|
6911
|
+
lines.push("## Creating Overrides");
|
|
6912
|
+
lines.push("");
|
|
6913
|
+
lines.push(
|
|
6914
|
+
"1. Copy a prompt file from this directory into an overrides directory using the same filename."
|
|
6915
|
+
);
|
|
6916
|
+
lines.push("2. Edit the copied file using plain text.");
|
|
6917
|
+
lines.push("3. Restart OpenCode.");
|
|
6918
|
+
lines.push("");
|
|
6919
|
+
lines.push("To reset an override, delete the matching file from your overrides directory.");
|
|
6920
|
+
lines.push("");
|
|
6921
|
+
lines.push(
|
|
6922
|
+
"Do not edit the default prompt files directly, they are just for reference, only files in the overrides directory are used."
|
|
6923
|
+
);
|
|
6924
|
+
lines.push("");
|
|
6925
|
+
lines.push("Override precedence (highest first):");
|
|
6926
|
+
lines.push("1. `.opencode/acp-prompts/overrides/` (project)");
|
|
6927
|
+
lines.push("2. `$OPENCODE_CONFIG_DIR/acp-prompts/overrides/` (config dir)");
|
|
6928
|
+
lines.push("3. `~/.config/opencode/acp-prompts/overrides/` (global)");
|
|
6929
|
+
lines.push("");
|
|
6930
|
+
lines.push("## Prompt Files");
|
|
6931
|
+
lines.push("");
|
|
6932
|
+
for (const definition of PROMPT_DEFINITIONS) {
|
|
6933
|
+
lines.push(`- \`${definition.fileName}\``);
|
|
6934
|
+
lines.push(` - Purpose: ${definition.description}.`);
|
|
6935
|
+
lines.push(` - Runtime use: ${definition.usage}.`);
|
|
6500
6936
|
}
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6937
|
+
return `${lines.join("\n")}
|
|
6938
|
+
`;
|
|
6939
|
+
}
|
|
6940
|
+
function readFileIfExists(filePath) {
|
|
6941
|
+
if (!existsSync4(filePath)) {
|
|
6942
|
+
return null;
|
|
6943
|
+
}
|
|
6944
|
+
try {
|
|
6945
|
+
return readFileSync2(filePath, "utf-8");
|
|
6946
|
+
} catch {
|
|
6947
|
+
return null;
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
var PromptStore = class {
|
|
6951
|
+
logger;
|
|
6952
|
+
paths;
|
|
6953
|
+
customPromptsEnabled;
|
|
6954
|
+
runtimePrompts;
|
|
6955
|
+
constructor(logger, workingDirectory, customPromptsEnabled = false) {
|
|
6956
|
+
this.logger = logger;
|
|
6957
|
+
this.paths = resolvePromptPaths(workingDirectory);
|
|
6958
|
+
this.customPromptsEnabled = customPromptsEnabled;
|
|
6959
|
+
this.runtimePrompts = createBundledRuntimePrompts();
|
|
6960
|
+
if (this.customPromptsEnabled) {
|
|
6961
|
+
this.ensureDefaultFiles();
|
|
6504
6962
|
}
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6963
|
+
this.reload();
|
|
6964
|
+
}
|
|
6965
|
+
getRuntimePrompts() {
|
|
6966
|
+
return { ...this.runtimePrompts };
|
|
6967
|
+
}
|
|
6968
|
+
reload() {
|
|
6969
|
+
const nextPrompts = createBundledRuntimePrompts();
|
|
6970
|
+
if (!this.customPromptsEnabled) {
|
|
6971
|
+
this.runtimePrompts = nextPrompts;
|
|
6972
|
+
return;
|
|
6508
6973
|
}
|
|
6509
|
-
const
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
injected = appendToTextPart(part, tag) || injected;
|
|
6974
|
+
for (const definition of PROMPT_DEFINITIONS) {
|
|
6975
|
+
const bundledSource = BUNDLED_EDITABLE_PROMPTS[definition.runtimeField];
|
|
6976
|
+
const bundledEditable = toEditablePromptText(definition, bundledSource);
|
|
6977
|
+
const bundledRuntime = wrapRuntimePromptContent(definition, bundledEditable);
|
|
6978
|
+
const fallbackValue = bundledRuntime || bundledSource.trim();
|
|
6979
|
+
let effectiveValue = fallbackValue;
|
|
6980
|
+
for (const candidate of this.getOverrideCandidates(definition.fileName)) {
|
|
6981
|
+
const rawOverride = readFileIfExists(candidate.path);
|
|
6982
|
+
if (rawOverride === null) {
|
|
6983
|
+
continue;
|
|
6520
6984
|
}
|
|
6985
|
+
const editableOverride = toEditablePromptText(definition, rawOverride);
|
|
6986
|
+
if (!editableOverride) {
|
|
6987
|
+
this.logger.warn("Prompt override is empty or invalid after normalization", {
|
|
6988
|
+
key: definition.key,
|
|
6989
|
+
path: candidate.path
|
|
6990
|
+
});
|
|
6991
|
+
continue;
|
|
6992
|
+
}
|
|
6993
|
+
const wrappedOverride = wrapRuntimePromptContent(definition, editableOverride);
|
|
6994
|
+
if (!wrappedOverride) {
|
|
6995
|
+
this.logger.warn("Prompt override could not be wrapped for runtime", {
|
|
6996
|
+
key: definition.key,
|
|
6997
|
+
path: candidate.path
|
|
6998
|
+
});
|
|
6999
|
+
continue;
|
|
7000
|
+
}
|
|
7001
|
+
effectiveValue = wrappedOverride;
|
|
7002
|
+
break;
|
|
6521
7003
|
}
|
|
6522
|
-
|
|
6523
|
-
continue;
|
|
6524
|
-
}
|
|
6525
|
-
message.parts.push(createSyntheticTextPart(message, tag));
|
|
6526
|
-
continue;
|
|
6527
|
-
}
|
|
6528
|
-
if (message.info.role !== "assistant") {
|
|
6529
|
-
continue;
|
|
6530
|
-
}
|
|
6531
|
-
if (!hasContent(message)) {
|
|
6532
|
-
continue;
|
|
6533
|
-
}
|
|
6534
|
-
if (appendToAllToolParts(message, tag)) {
|
|
6535
|
-
continue;
|
|
7004
|
+
nextPrompts[definition.runtimeField] = effectiveValue;
|
|
6536
7005
|
}
|
|
6537
|
-
|
|
6538
|
-
|
|
7006
|
+
this.runtimePrompts = nextPrompts;
|
|
7007
|
+
}
|
|
7008
|
+
getOverrideCandidates(fileName) {
|
|
7009
|
+
const candidates = [];
|
|
7010
|
+
if (this.paths.projectOverridesDir) {
|
|
7011
|
+
candidates.push({
|
|
7012
|
+
path: join4(this.paths.projectOverridesDir, fileName)
|
|
7013
|
+
});
|
|
6539
7014
|
}
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
} else {
|
|
6545
|
-
message.parts.splice(firstToolIndex, 0, syntheticPart);
|
|
7015
|
+
if (this.paths.configDirOverridesDir) {
|
|
7016
|
+
candidates.push({
|
|
7017
|
+
path: join4(this.paths.configDirOverridesDir, fileName)
|
|
7018
|
+
});
|
|
6546
7019
|
}
|
|
7020
|
+
candidates.push({
|
|
7021
|
+
path: join4(this.paths.globalOverridesDir, fileName)
|
|
7022
|
+
});
|
|
7023
|
+
return candidates;
|
|
6547
7024
|
}
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
}
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
continue;
|
|
6569
|
-
}
|
|
6570
|
-
if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
|
|
6571
|
-
continue;
|
|
6572
|
-
}
|
|
6573
|
-
const cachedResult = state.subAgentResultCache.get(part.callID);
|
|
6574
|
-
if (cachedResult !== void 0) {
|
|
6575
|
-
if (cachedResult) {
|
|
6576
|
-
part.state.output = stripHallucinationsFromString(
|
|
6577
|
-
mergeSubagentResult(part.state.output, cachedResult)
|
|
6578
|
-
);
|
|
6579
|
-
}
|
|
6580
|
-
continue;
|
|
6581
|
-
}
|
|
6582
|
-
const subAgentSessionId = getSubAgentId(part);
|
|
6583
|
-
if (!subAgentSessionId) {
|
|
6584
|
-
continue;
|
|
6585
|
-
}
|
|
6586
|
-
let subAgentMessages = [];
|
|
7025
|
+
ensureDefaultFiles() {
|
|
7026
|
+
try {
|
|
7027
|
+
mkdirSync2(this.paths.defaultsDir, { recursive: true });
|
|
7028
|
+
mkdirSync2(this.paths.globalOverridesDir, { recursive: true });
|
|
7029
|
+
} catch {
|
|
7030
|
+
this.logger.warn("Failed to initialize prompt directories", {
|
|
7031
|
+
defaultsDir: this.paths.defaultsDir,
|
|
7032
|
+
globalOverridesDir: this.paths.globalOverridesDir
|
|
7033
|
+
});
|
|
7034
|
+
return;
|
|
7035
|
+
}
|
|
7036
|
+
for (const definition of PROMPT_DEFINITIONS) {
|
|
7037
|
+
const bundledEditable = toEditablePromptText(
|
|
7038
|
+
definition,
|
|
7039
|
+
BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
|
|
7040
|
+
);
|
|
7041
|
+
const managedContent = buildDefaultPromptFileContent(
|
|
7042
|
+
bundledEditable || BUNDLED_EDITABLE_PROMPTS[definition.runtimeField]
|
|
7043
|
+
);
|
|
7044
|
+
const filePath = join4(this.paths.defaultsDir, definition.fileName);
|
|
6587
7045
|
try {
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
7046
|
+
const existing = readFileIfExists(filePath);
|
|
7047
|
+
if (existing === managedContent) {
|
|
7048
|
+
continue;
|
|
7049
|
+
}
|
|
7050
|
+
writeFileSync2(filePath, managedContent, "utf-8");
|
|
7051
|
+
} catch {
|
|
7052
|
+
this.logger.warn("Failed to write default prompt file", {
|
|
7053
|
+
key: definition.key,
|
|
7054
|
+
path: filePath
|
|
6594
7055
|
});
|
|
6595
|
-
continue;
|
|
6596
7056
|
}
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
7057
|
+
}
|
|
7058
|
+
const readmePath = join4(this.paths.defaultsDir, DEFAULTS_README_FILE);
|
|
7059
|
+
const readmeContent = buildDefaultsReadmeContent();
|
|
7060
|
+
try {
|
|
7061
|
+
const existing = readFileIfExists(readmePath);
|
|
7062
|
+
if (existing !== readmeContent) {
|
|
7063
|
+
writeFileSync2(readmePath, readmeContent, "utf-8");
|
|
6600
7064
|
}
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
);
|
|
7065
|
+
} catch {
|
|
7066
|
+
this.logger.warn("Failed to write defaults README", {
|
|
7067
|
+
path: readmePath
|
|
7068
|
+
});
|
|
6605
7069
|
}
|
|
6606
7070
|
}
|
|
6607
7071
|
};
|
|
6608
7072
|
|
|
6609
|
-
// lib/messages/reasoning-strip.ts
|
|
6610
|
-
function stripStaleMetadata(messages) {
|
|
6611
|
-
const lastUserMessage = getLastUserMessage(messages);
|
|
6612
|
-
if (lastUserMessage?.info.role !== "user") {
|
|
6613
|
-
return;
|
|
6614
|
-
}
|
|
6615
|
-
const modelID = lastUserMessage.info.model.modelID;
|
|
6616
|
-
const providerID = lastUserMessage.info.model.providerID;
|
|
6617
|
-
messages.forEach((message) => {
|
|
6618
|
-
if (message.info.role !== "assistant") {
|
|
6619
|
-
return;
|
|
6620
|
-
}
|
|
6621
|
-
const msgModelID = message.info.modelID;
|
|
6622
|
-
const msgProviderID = message.info.providerID;
|
|
6623
|
-
if (msgModelID === modelID && msgProviderID === providerID) {
|
|
6624
|
-
return;
|
|
6625
|
-
}
|
|
6626
|
-
message.parts = message.parts.map((part) => {
|
|
6627
|
-
if (part.type !== "text" && part.type !== "tool" && part.type !== "reasoning") {
|
|
6628
|
-
return part;
|
|
6629
|
-
}
|
|
6630
|
-
if (!("metadata" in part)) {
|
|
6631
|
-
return part;
|
|
6632
|
-
}
|
|
6633
|
-
const { metadata: _metadata, ...rest } = part;
|
|
6634
|
-
return rest;
|
|
6635
|
-
});
|
|
6636
|
-
});
|
|
6637
|
-
}
|
|
6638
|
-
|
|
6639
7073
|
// lib/prompts/index.ts
|
|
6640
7074
|
function renderSystemPrompt(prompts, protectedToolsExtension, manual, subagent) {
|
|
6641
7075
|
const extensions = [];
|
|
@@ -6648,6 +7082,7 @@ function renderSystemPrompt(prompts, protectedToolsExtension, manual, subagent)
|
|
|
6648
7082
|
if (subagent) {
|
|
6649
7083
|
extensions.push(prompts.subagentExtension.trim());
|
|
6650
7084
|
}
|
|
7085
|
+
extensions.push(prompts.decompressExtension.trim());
|
|
6651
7086
|
return [prompts.system.trim(), ...extensions].filter(Boolean).join("\n\n").replace(/\n([ \t]*\n)+/g, "\n\n").trim();
|
|
6652
7087
|
}
|
|
6653
7088
|
|
|
@@ -6837,158 +7272,7 @@ async function handleContextCommand(ctx) {
|
|
|
6837
7272
|
await sendIgnoredMessage(client, sessionId, message, params, logger);
|
|
6838
7273
|
}
|
|
6839
7274
|
|
|
6840
|
-
// lib/commands/compression-targets.ts
|
|
6841
|
-
function byBlockId(a, b) {
|
|
6842
|
-
return a.blockId - b.blockId;
|
|
6843
|
-
}
|
|
6844
|
-
function buildTarget(blocks) {
|
|
6845
|
-
const ordered = [...blocks].sort(byBlockId);
|
|
6846
|
-
const first = ordered[0];
|
|
6847
|
-
if (!first) {
|
|
6848
|
-
throw new Error("Cannot build compression target from empty block list.");
|
|
6849
|
-
}
|
|
6850
|
-
const grouped = first.mode === "message";
|
|
6851
|
-
return {
|
|
6852
|
-
displayId: first.blockId,
|
|
6853
|
-
runId: first.runId,
|
|
6854
|
-
topic: grouped ? first.batchTopic || first.topic : first.topic,
|
|
6855
|
-
compressedTokens: ordered.reduce((total, block) => total + block.compressedTokens, 0),
|
|
6856
|
-
durationMs: ordered.reduce((total, block) => Math.max(total, block.durationMs), 0),
|
|
6857
|
-
grouped,
|
|
6858
|
-
blocks: ordered
|
|
6859
|
-
};
|
|
6860
|
-
}
|
|
6861
|
-
function groupMessageBlocks(blocks) {
|
|
6862
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
6863
|
-
for (const block of blocks) {
|
|
6864
|
-
const existing = grouped.get(block.runId);
|
|
6865
|
-
if (existing) {
|
|
6866
|
-
existing.push(block);
|
|
6867
|
-
continue;
|
|
6868
|
-
}
|
|
6869
|
-
grouped.set(block.runId, [block]);
|
|
6870
|
-
}
|
|
6871
|
-
return Array.from(grouped.values()).map(buildTarget);
|
|
6872
|
-
}
|
|
6873
|
-
function splitTargets(blocks) {
|
|
6874
|
-
const messageBlocks = [];
|
|
6875
|
-
const singleBlocks = [];
|
|
6876
|
-
for (const block of blocks) {
|
|
6877
|
-
if (block.mode === "message") {
|
|
6878
|
-
messageBlocks.push(block);
|
|
6879
|
-
} else {
|
|
6880
|
-
singleBlocks.push(block);
|
|
6881
|
-
}
|
|
6882
|
-
}
|
|
6883
|
-
const targets = [
|
|
6884
|
-
...singleBlocks.map((block) => buildTarget([block])),
|
|
6885
|
-
...groupMessageBlocks(messageBlocks)
|
|
6886
|
-
];
|
|
6887
|
-
return targets.sort((a, b) => a.displayId - b.displayId);
|
|
6888
|
-
}
|
|
6889
|
-
function getActiveCompressionTargets(messagesState) {
|
|
6890
|
-
const activeBlocks = Array.from(messagesState.activeBlockIds).map((blockId) => messagesState.blocksById.get(blockId)).filter((block) => !!block && block.active);
|
|
6891
|
-
return splitTargets(activeBlocks);
|
|
6892
|
-
}
|
|
6893
|
-
function getRecompressibleCompressionTargets(messagesState, availableMessageIds) {
|
|
6894
|
-
const allBlocks = Array.from(messagesState.blocksById.values()).filter((block) => {
|
|
6895
|
-
return availableMessageIds.has(block.compressMessageId);
|
|
6896
|
-
});
|
|
6897
|
-
const messageGroups = /* @__PURE__ */ new Map();
|
|
6898
|
-
const singleTargets = [];
|
|
6899
|
-
for (const block of allBlocks) {
|
|
6900
|
-
if (block.mode === "message") {
|
|
6901
|
-
const existing = messageGroups.get(block.runId);
|
|
6902
|
-
if (existing) {
|
|
6903
|
-
existing.push(block);
|
|
6904
|
-
} else {
|
|
6905
|
-
messageGroups.set(block.runId, [block]);
|
|
6906
|
-
}
|
|
6907
|
-
continue;
|
|
6908
|
-
}
|
|
6909
|
-
if (block.deactivatedByUser && !block.active) {
|
|
6910
|
-
singleTargets.push(buildTarget([block]));
|
|
6911
|
-
}
|
|
6912
|
-
}
|
|
6913
|
-
for (const blocks of messageGroups.values()) {
|
|
6914
|
-
if (blocks.some((block) => block.deactivatedByUser && !block.active)) {
|
|
6915
|
-
singleTargets.push(buildTarget(blocks));
|
|
6916
|
-
}
|
|
6917
|
-
}
|
|
6918
|
-
return singleTargets.sort((a, b) => a.displayId - b.displayId);
|
|
6919
|
-
}
|
|
6920
|
-
function resolveCompressionTarget(messagesState, blockId) {
|
|
6921
|
-
const block = messagesState.blocksById.get(blockId);
|
|
6922
|
-
if (!block) {
|
|
6923
|
-
return null;
|
|
6924
|
-
}
|
|
6925
|
-
if (block.mode !== "message") {
|
|
6926
|
-
return buildTarget([block]);
|
|
6927
|
-
}
|
|
6928
|
-
const blocks = Array.from(messagesState.blocksById.values()).filter(
|
|
6929
|
-
(candidate) => candidate.mode === "message" && candidate.runId === block.runId
|
|
6930
|
-
);
|
|
6931
|
-
if (blocks.length === 0) {
|
|
6932
|
-
return null;
|
|
6933
|
-
}
|
|
6934
|
-
return buildTarget(blocks);
|
|
6935
|
-
}
|
|
6936
|
-
|
|
6937
7275
|
// lib/commands/decompress.ts
|
|
6938
|
-
function parseBlockIdArg(arg) {
|
|
6939
|
-
const normalized = arg.trim().toLowerCase();
|
|
6940
|
-
const blockRef = parseBlockRef(normalized);
|
|
6941
|
-
if (blockRef !== null) {
|
|
6942
|
-
return blockRef;
|
|
6943
|
-
}
|
|
6944
|
-
if (!/^[1-9]\d*$/.test(normalized)) {
|
|
6945
|
-
return null;
|
|
6946
|
-
}
|
|
6947
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
6948
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
6949
|
-
}
|
|
6950
|
-
function findActiveParentBlockId(messagesState, block) {
|
|
6951
|
-
const queue = [...block.parentBlockIds];
|
|
6952
|
-
const visited = /* @__PURE__ */ new Set();
|
|
6953
|
-
while (queue.length > 0) {
|
|
6954
|
-
const parentBlockId = queue.shift();
|
|
6955
|
-
if (parentBlockId === void 0 || visited.has(parentBlockId)) {
|
|
6956
|
-
continue;
|
|
6957
|
-
}
|
|
6958
|
-
visited.add(parentBlockId);
|
|
6959
|
-
const parent = messagesState.blocksById.get(parentBlockId);
|
|
6960
|
-
if (!parent) {
|
|
6961
|
-
continue;
|
|
6962
|
-
}
|
|
6963
|
-
if (parent.active) {
|
|
6964
|
-
return parent.blockId;
|
|
6965
|
-
}
|
|
6966
|
-
for (const ancestorId of parent.parentBlockIds) {
|
|
6967
|
-
if (!visited.has(ancestorId)) {
|
|
6968
|
-
queue.push(ancestorId);
|
|
6969
|
-
}
|
|
6970
|
-
}
|
|
6971
|
-
}
|
|
6972
|
-
return null;
|
|
6973
|
-
}
|
|
6974
|
-
function findActiveAncestorBlockId(messagesState, target) {
|
|
6975
|
-
for (const block of target.blocks) {
|
|
6976
|
-
const activeAncestorBlockId = findActiveParentBlockId(messagesState, block);
|
|
6977
|
-
if (activeAncestorBlockId !== null) {
|
|
6978
|
-
return activeAncestorBlockId;
|
|
6979
|
-
}
|
|
6980
|
-
}
|
|
6981
|
-
return null;
|
|
6982
|
-
}
|
|
6983
|
-
function snapshotActiveMessages(messagesState) {
|
|
6984
|
-
const activeMessages = /* @__PURE__ */ new Map();
|
|
6985
|
-
for (const [messageId, entry] of messagesState.byMessageId) {
|
|
6986
|
-
if (entry.activeBlockIds.length > 0) {
|
|
6987
|
-
activeMessages.set(messageId, entry.tokenCount);
|
|
6988
|
-
}
|
|
6989
|
-
}
|
|
6990
|
-
return activeMessages;
|
|
6991
|
-
}
|
|
6992
7276
|
function formatDecompressMessage(target, restoredMessageCount, restoredTokens, reactivatedBlockIds) {
|
|
6993
7277
|
const lines = [];
|
|
6994
7278
|
lines.push(`Restored compression ${target.displayId}.`);
|
|
@@ -7097,32 +7381,14 @@ async function handleDecompressCommand(ctx) {
|
|
|
7097
7381
|
}
|
|
7098
7382
|
const activeMessagesBefore = snapshotActiveMessages(messagesState);
|
|
7099
7383
|
const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
|
|
7100
|
-
|
|
7101
|
-
for (const block of target.blocks) {
|
|
7102
|
-
block.active = false;
|
|
7103
|
-
block.deactivatedByUser = true;
|
|
7104
|
-
block.deactivatedAt = deactivatedAt;
|
|
7105
|
-
block.deactivatedByBlockId = void 0;
|
|
7106
|
-
for (const consumedId of block.consumedBlockIds) {
|
|
7107
|
-
const consumedBlock = messagesState.blocksById.get(consumedId);
|
|
7108
|
-
if (consumedBlock) {
|
|
7109
|
-
consumedBlock.deactivatedByUser = true;
|
|
7110
|
-
}
|
|
7111
|
-
}
|
|
7112
|
-
}
|
|
7384
|
+
deactivateCompressionTarget(messagesState, target);
|
|
7113
7385
|
syncCompressionBlocks(state, logger, messages);
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
const isActiveNow = entry ? entry.activeBlockIds.length > 0 : false;
|
|
7119
|
-
if (!isActiveNow) {
|
|
7120
|
-
restoredMessageCount++;
|
|
7121
|
-
restoredTokens += tokenCount;
|
|
7122
|
-
}
|
|
7123
|
-
}
|
|
7386
|
+
const { restoredMessageCount, restoredTokens } = computeRestoredMessages(
|
|
7387
|
+
messagesState,
|
|
7388
|
+
activeMessagesBefore
|
|
7389
|
+
);
|
|
7124
7390
|
state.stats.totalPruneTokens = Math.max(0, state.stats.totalPruneTokens - restoredTokens);
|
|
7125
|
-
const reactivatedBlockIds =
|
|
7391
|
+
const reactivatedBlockIds = computeReactivatedBlockIds(messagesState, activeBlockIdsBefore);
|
|
7126
7392
|
await saveSessionState(state, logger);
|
|
7127
7393
|
const message = formatDecompressMessage(
|
|
7128
7394
|
target,
|
|
@@ -7196,7 +7462,7 @@ var COMPRESS_TRIGGER_PROMPT = [
|
|
|
7196
7462
|
"Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
|
|
7197
7463
|
"Return after compress with a brief explanation of what content was compressed."
|
|
7198
7464
|
].join("\n\n");
|
|
7199
|
-
function getTriggerPrompt(
|
|
7465
|
+
function getTriggerPrompt(tool4, state, config, userFocus) {
|
|
7200
7466
|
const base = COMPRESS_TRIGGER_PROMPT;
|
|
7201
7467
|
const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
|
|
7202
7468
|
const sections = [base, compressedBlockGuidance];
|
|
@@ -7225,8 +7491,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
|
|
|
7225
7491
|
);
|
|
7226
7492
|
logger.info("Manual mode toggled", { manualMode: state.manualMode });
|
|
7227
7493
|
}
|
|
7228
|
-
async function handleManualTriggerCommand(ctx,
|
|
7229
|
-
return getTriggerPrompt(
|
|
7494
|
+
async function handleManualTriggerCommand(ctx, tool4, userFocus) {
|
|
7495
|
+
return getTriggerPrompt(tool4, ctx.state, ctx.config, userFocus);
|
|
7230
7496
|
}
|
|
7231
7497
|
function applyPendingManualTrigger(state, messages, logger) {
|
|
7232
7498
|
const pending = state.pendingManualTrigger;
|
|
@@ -8102,7 +8368,7 @@ function configureClientAuth(client) {
|
|
|
8102
8368
|
import { readFile as readFile2, rm } from "fs/promises";
|
|
8103
8369
|
import { basename, dirname as dirname3, join as join5 } from "path";
|
|
8104
8370
|
import { fileURLToPath } from "url";
|
|
8105
|
-
var PACKAGE_NAME = "
|
|
8371
|
+
var PACKAGE_NAME = "opencode-acp";
|
|
8106
8372
|
function startAutoUpdate(ctx, enabled) {
|
|
8107
8373
|
if (!enabled) return;
|
|
8108
8374
|
const controller = new AbortController();
|
|
@@ -8297,7 +8563,8 @@ var server = (async (ctx) => {
|
|
|
8297
8563
|
event: createEventHandler(state, logger),
|
|
8298
8564
|
tool: {
|
|
8299
8565
|
...config.compress.permission !== "deny" && {
|
|
8300
|
-
compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext)
|
|
8566
|
+
compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
|
|
8567
|
+
decompress: createDecompressTool(compressToolContext)
|
|
8301
8568
|
}
|
|
8302
8569
|
},
|
|
8303
8570
|
config: async (opencodeConfig) => {
|
|
@@ -8313,7 +8580,7 @@ var server = (async (ctx) => {
|
|
|
8313
8580
|
}
|
|
8314
8581
|
const toolsToAdd = [];
|
|
8315
8582
|
if (config.compress.permission !== "deny" && !config.experimental.allowSubAgents) {
|
|
8316
|
-
toolsToAdd.push("compress");
|
|
8583
|
+
toolsToAdd.push("compress", "decompress");
|
|
8317
8584
|
}
|
|
8318
8585
|
if (toolsToAdd.length > 0) {
|
|
8319
8586
|
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];
|