opencode-acp 1.1.1 → 1.2.0

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