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