opencode-gbk-tools 0.1.23 → 0.1.25

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  为 OpenCode 提供一套自动识别编码的文本工具,以及面向 `GBK` / `GB18030` 的专用工具。
4
4
 
5
- 解决 OpenCode 内置工具难以稳定处理非 UTF-8 文本文件的问题,并通过 plugin 让所有 agents 默认获得这些工具与规则,无需单独切换专属 GBK agent。
5
+ 解决 OpenCode 内置工具难以稳定处理非 UTF-8 文本文件的问题,并通过 plugin 让所有 agents 默认获得这些工具与规则;同时提供一个只允许调用 `text_*` / `gbk_*` 自定义工具的 `gbk-engine` agent。
6
6
 
7
7
  ---
8
8
 
@@ -18,6 +18,7 @@
18
18
  | `gbk_edit` | 精确替换 GBK 文件中的指定文本块 |
19
19
  | `gbk_search` | 在 GBK 文件中搜索关键词,返回行号和上下文 |
20
20
  | 本地/全局 plugin 规则 | 给所有 agents 注入“优先使用 `text_*`”的系统提示,并统一开放 `gbk_*` 工具 |
21
+ | `gbk-engine` agent | 禁用 OpenCode 内置读写编辑工具,只保留 `text_*` / `gbk_*` 自定义工具 |
21
22
 
22
23
  ---
23
24
 
@@ -34,8 +35,9 @@ npx opencode-gbk-tools install
34
35
  - `text_read` / `text_write` / `text_edit`
35
36
  - `gbk_read` / `gbk_write` / `gbk_edit` / `gbk_search`
36
37
  - 对文本文件优先使用 `text_*` 的系统提示
38
+ - `.opencode/agents/gbk-engine.md` 或 `~/.config/opencode/agents/gbk-engine.md`
37
39
 
38
- 不再需要单独安装或切换专属 `gbk-engine` agent。
40
+ 如果你希望强制禁用内置 `read` / `write` / `edit` / `apply_patch`,可以直接切换到专属 `gbk-engine` agent。
39
41
 
40
42
  ---
41
43
 
@@ -205,6 +207,7 @@ A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,
205
207
 
206
208
  | 版本 | 说明 |
207
209
  |------|------|
210
+ | 0.1.25 | 恢复并正式发布专属 `gbk-engine` agent;通过 agent 白名单禁用内置 `read` / `write` / `edit` / `apply_patch`,只保留 `text_*` / `gbk_*` 自定义工具,并把 agent 纳入构建、安装与 release manifest |
208
211
  | 0.1.17 | 修复 OpenCode 以 npm plugin 方式加载包时缺少 `./server` 导出导致的插件加载失败问题;补充 `main` 与 `./server` 入口兼容性 |
209
212
  | 0.1.16 | 去掉专属 `gbk-engine` agent 安装链路,改为通过 plugin + tools 让全部 agents 统一支持 `text_*` / `gbk_*`;同步更新安装说明 |
210
213
  | 0.1.15 | 为 GBK 大文件统一引入行字节索引与流式读/搜/改路径,提升局部编辑、搜索与大块修改时的性能、稳定性与准确性 |
@@ -0,0 +1,33 @@
1
+ ---
2
+ description: 仅使用 opencode-gbk-tools 自定义工具处理自动编码文本与 GBK/GB18030 文件
3
+ mode: primary
4
+ temperature: 0
5
+ tools:
6
+ "*": false
7
+ read: false
8
+ write: false
9
+ edit: false
10
+ apply_patch: false
11
+ text_read: true
12
+ text_write: true
13
+ text_edit: true
14
+ gbk_read: true
15
+ gbk_write: true
16
+ gbk_edit: true
17
+ gbk_search: true
18
+ permission:
19
+ task:
20
+ "*": deny
21
+ ---
22
+ 你是 `gbk-engine`。
23
+
24
+ 你只能使用 `opencode-gbk-tools` 提供的 `text_*` 与 `gbk_*` 工具,不允许退回到 OpenCode 内置的 `read`、`write`、`edit`、`apply_patch` 或其他内置工具。
25
+
26
+ 工作规则:
27
+
28
+ - 通用文本文件优先使用 `text_read`、`text_write`、`text_edit`
29
+ - 只有在用户明确指定 `GBK` / `GB18030` 文件,或自动编码检测失败时,才优先使用 `gbk_*` 工具
30
+ - 编辑前先读取目标文件;对大文件先搜索,再局部读取
31
+ - 插入内容优先使用 `mode="insertAfter"` 或 `mode="insertBefore"` 搭配 `anchor` / `content`
32
+ - 只有在精确替换现有内容时,才使用 `oldString` / `newString`
33
+ - 如果自定义工具不可用,要明确说明当前 OpenCode 没有加载 `opencode-gbk-tools` plugin
package/dist/cli/index.js CHANGED
@@ -60,7 +60,7 @@ function resolveTargetBase(target, cwd) {
60
60
  // src/cli/install.ts
61
61
  async function installCommand(args) {
62
62
  const targetBase = resolveTargetBase(args.target, args.cwd);
63
- const allowedArtifacts = new Set(args.artifacts ?? ["plugin"]);
63
+ const allowedArtifacts = new Set(args.artifacts ?? ["plugin", "agent"]);
64
64
  const releaseManifest = JSON.parse(await fs3.readFile(path3.join(args.packageRoot, "dist", "release-manifest.json"), "utf8"));
65
65
  const selectedArtifacts = releaseManifest.artifacts.filter((artifact) => allowedArtifacts.has(artifact.kind));
66
66
  const existingManifest = await loadInstalledManifest(targetBase);
@@ -190,7 +190,7 @@ async function setupCommand(args) {
190
190
  await fs6.mkdir(configBase, { recursive: true });
191
191
  const configPath = await resolveConfigFile(configBase);
192
192
  await ensurePluginConfigured(configPath, pluginName);
193
- const installResult = await installCommand({ ...args, force: true, artifacts: ["plugin"] });
193
+ const installResult = await installCommand({ ...args, force: true, artifacts: ["plugin", "agent"] });
194
194
  return {
195
195
  configPath,
196
196
  targetBase: installResult.targetBase
@@ -16238,6 +16238,48 @@ function tool(input) {
16238
16238
  }
16239
16239
  tool.schema = external_exports;
16240
16240
 
16241
+ // src/lib/model-context.ts
16242
+ var FALLBACK_MAX_OUTPUT_CHARS = 8e3;
16243
+ var MIN_BASE_MAX_OUTPUT_CHARS = 4e3;
16244
+ var MAX_BASE_OUTPUT_CHARS = 32e3;
16245
+ var MIN_PRESSURED_OUTPUT_CHARS = 1500;
16246
+ var sessionStates = /* @__PURE__ */ new Map();
16247
+ function getSessionState(sessionID) {
16248
+ if (!sessionID) return null;
16249
+ return sessionStates.get(sessionID) ?? null;
16250
+ }
16251
+ function getCompactionPressureFactor(compactionCount) {
16252
+ if (compactionCount >= 3) return 0.35;
16253
+ if (compactionCount === 2) return 0.5;
16254
+ if (compactionCount === 1) return 0.75;
16255
+ return 1;
16256
+ }
16257
+ function getBaseMaxOutputChars(contextTokens, fallback) {
16258
+ if (contextTokens === null) return fallback;
16259
+ const computed = Math.round(contextTokens * 0.01 * 4);
16260
+ return Math.max(MIN_BASE_MAX_OUTPUT_CHARS, Math.min(MAX_BASE_OUTPUT_CHARS, computed));
16261
+ }
16262
+ function buildTruncationSuffix(sessionID, maxChars) {
16263
+ const state = getSessionState(sessionID);
16264
+ if (!state || state.compactionCount === 0) {
16265
+ return `\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650`;
16266
+ }
16267
+ return `\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650\uFF1B\u5F53\u524D\u4F1A\u8BDD\u5DF2\u538B\u7F29 ${state.compactionCount} \u6B21\uFF0C\u5DF2\u81EA\u52A8\u6536\u7D27\u5DE5\u5177\u8F93\u51FA\u9884\u7B97`;
16268
+ }
16269
+ function getMaxOutputChars(sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16270
+ const state = getSessionState(sessionID);
16271
+ const base = getBaseMaxOutputChars(state?.contextTokens ?? null, fallback);
16272
+ const pressured = Math.round(base * getCompactionPressureFactor(state?.compactionCount ?? 0));
16273
+ return Math.max(MIN_PRESSURED_OUTPUT_CHARS, pressured);
16274
+ }
16275
+ function truncateToolOutput(content, sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16276
+ const maxChars = getMaxOutputChars(sessionID, fallback);
16277
+ if (content.length <= maxChars) return content;
16278
+ const truncated = content.slice(0, maxChars);
16279
+ return `${truncated}
16280
+ \x1B[2m... (${buildTruncationSuffix(sessionID, maxChars)})\x1B[0m`;
16281
+ }
16282
+
16241
16283
  // src/lib/gbk-file.ts
16242
16284
  var import_iconv_lite = __toESM(require_lib(), 1);
16243
16285
  import crypto from "crypto";
@@ -17314,22 +17356,7 @@ async function replaceGbkFileText(input) {
17314
17356
  };
17315
17357
  }
17316
17358
 
17317
- // src/lib/model-context.ts
17318
- var _currentContextTokens = null;
17319
- function getMaxOutputChars(fallback = 8e3) {
17320
- if (_currentContextTokens === null) return fallback;
17321
- const computed = Math.round(_currentContextTokens * 0.01 * 4);
17322
- return Math.max(4e3, Math.min(32e3, computed));
17323
- }
17324
-
17325
17359
  // src/tools/gbk_edit.ts
17326
- function truncateToolOutput(content) {
17327
- const maxChars = getMaxOutputChars();
17328
- if (content.length <= maxChars) return content;
17329
- const truncated = content.slice(0, maxChars);
17330
- return truncated + `
17331
- \x1B[2m... (\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
17332
- }
17333
17360
  var gbk_edit_default = tool({
17334
17361
  description: `Edit GBK/GB18030 encoded text files with exact string replacement.
17335
17362
 
@@ -17399,8 +17426,8 @@ Insert mode:
17399
17426
  diffPreview
17400
17427
  }
17401
17428
  });
17402
- if (diffPreview) return truncateToolOutput(diffPreview);
17403
- return truncateToolOutput(JSON.stringify(result, null, 2));
17429
+ if (diffPreview) return truncateToolOutput(diffPreview, context.sessionID);
17430
+ return truncateToolOutput(JSON.stringify(result, null, 2), context.sessionID);
17404
17431
  }
17405
17432
  });
17406
17433
  export {
@@ -16238,6 +16238,48 @@ function tool(input) {
16238
16238
  }
16239
16239
  tool.schema = external_exports;
16240
16240
 
16241
+ // src/lib/model-context.ts
16242
+ var FALLBACK_MAX_OUTPUT_CHARS = 8e3;
16243
+ var MIN_BASE_MAX_OUTPUT_CHARS = 4e3;
16244
+ var MAX_BASE_OUTPUT_CHARS = 32e3;
16245
+ var MIN_PRESSURED_OUTPUT_CHARS = 1500;
16246
+ var sessionStates = /* @__PURE__ */ new Map();
16247
+ function getSessionState(sessionID) {
16248
+ if (!sessionID) return null;
16249
+ return sessionStates.get(sessionID) ?? null;
16250
+ }
16251
+ function getCompactionPressureFactor(compactionCount) {
16252
+ if (compactionCount >= 3) return 0.35;
16253
+ if (compactionCount === 2) return 0.5;
16254
+ if (compactionCount === 1) return 0.75;
16255
+ return 1;
16256
+ }
16257
+ function getBaseMaxOutputChars(contextTokens, fallback) {
16258
+ if (contextTokens === null) return fallback;
16259
+ const computed = Math.round(contextTokens * 0.01 * 4);
16260
+ return Math.max(MIN_BASE_MAX_OUTPUT_CHARS, Math.min(MAX_BASE_OUTPUT_CHARS, computed));
16261
+ }
16262
+ function buildTruncationSuffix(sessionID, maxChars) {
16263
+ const state = getSessionState(sessionID);
16264
+ if (!state || state.compactionCount === 0) {
16265
+ return `\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650`;
16266
+ }
16267
+ return `\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650\uFF1B\u5F53\u524D\u4F1A\u8BDD\u5DF2\u538B\u7F29 ${state.compactionCount} \u6B21\uFF0C\u5DF2\u81EA\u52A8\u6536\u7D27\u5DE5\u5177\u8F93\u51FA\u9884\u7B97`;
16268
+ }
16269
+ function getMaxOutputChars(sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16270
+ const state = getSessionState(sessionID);
16271
+ const base = getBaseMaxOutputChars(state?.contextTokens ?? null, fallback);
16272
+ const pressured = Math.round(base * getCompactionPressureFactor(state?.compactionCount ?? 0));
16273
+ return Math.max(MIN_PRESSURED_OUTPUT_CHARS, pressured);
16274
+ }
16275
+ function truncateToolOutput(content, sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16276
+ const maxChars = getMaxOutputChars(sessionID, fallback);
16277
+ if (content.length <= maxChars) return content;
16278
+ const truncated = content.slice(0, maxChars);
16279
+ return `${truncated}
16280
+ \x1B[2m... (${buildTruncationSuffix(sessionID, maxChars)})\x1B[0m`;
16281
+ }
16282
+
16241
16283
  // src/lib/text-file.ts
16242
16284
  var import_iconv_lite2 = __toESM(require_lib(), 1);
16243
16285
  import crypto2 from "crypto";
@@ -16990,22 +17032,7 @@ async function replaceTextFileText(input) {
16990
17032
  };
16991
17033
  }
16992
17034
 
16993
- // src/lib/model-context.ts
16994
- var _currentContextTokens = null;
16995
- function getMaxOutputChars(fallback = 8e3) {
16996
- if (_currentContextTokens === null) return fallback;
16997
- const computed = Math.round(_currentContextTokens * 0.01 * 4);
16998
- return Math.max(4e3, Math.min(32e3, computed));
16999
- }
17000
-
17001
17035
  // src/tools/text_edit.ts
17002
- function truncateToolOutput(content) {
17003
- const maxChars = getMaxOutputChars();
17004
- if (content.length <= maxChars) return content;
17005
- const truncated = content.slice(0, maxChars);
17006
- return truncated + `
17007
- \x1B[2m... (\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
17008
- }
17009
17036
  var text_edit_default = tool({
17010
17037
  description: `Edit text files with automatic encoding detection and preservation.
17011
17038
 
@@ -17056,8 +17083,8 @@ var text_edit_default = tool({
17056
17083
  diffPreview
17057
17084
  }
17058
17085
  });
17059
- if (diffPreview) return truncateToolOutput(diffPreview);
17060
- return truncateToolOutput(JSON.stringify(result, null, 2));
17086
+ if (diffPreview) return truncateToolOutput(diffPreview, context.sessionID);
17087
+ return truncateToolOutput(JSON.stringify(result, null, 2), context.sessionID);
17061
17088
  }
17062
17089
  });
17063
17090
  export {
@@ -3816,6 +3816,100 @@ var require_lib = __commonJS({
3816
3816
  }
3817
3817
  });
3818
3818
 
3819
+ // src/lib/session-pressure.ts
3820
+ var AUTO_SUMMARIZE_PRESSURE_RATIO = 0.85;
3821
+ var AUTO_SUMMARIZE_COOLDOWN_MS = 6e4;
3822
+ var PRESSURE_CHECK_INTERVAL_MS = 15e3;
3823
+ var SESSION_PRESSURE_MESSAGE_LIMIT = 200;
3824
+ function estimateUnknownChars(value) {
3825
+ if (typeof value === "string") return value.length;
3826
+ if (typeof value === "number" || typeof value === "boolean") return String(value).length;
3827
+ if (Array.isArray(value)) {
3828
+ return value.reduce((total, item) => total + estimateUnknownChars(item), 0);
3829
+ }
3830
+ if (!value || typeof value !== "object") return 0;
3831
+ try {
3832
+ return JSON.stringify(value).length;
3833
+ } catch {
3834
+ return 0;
3835
+ }
3836
+ }
3837
+ function estimateDiffChars(summary) {
3838
+ let chars = (summary.title?.length ?? 0) + (summary.body?.length ?? 0);
3839
+ for (const diff of summary.diffs ?? []) {
3840
+ chars += diff.file.length + diff.before.length + diff.after.length;
3841
+ }
3842
+ return chars;
3843
+ }
3844
+ function estimateToolPartChars(part) {
3845
+ let chars = part.tool.length + estimateUnknownChars(part.metadata);
3846
+ switch (part.state.status) {
3847
+ case "pending":
3848
+ chars += estimateUnknownChars(part.state.input) + part.state.raw.length;
3849
+ break;
3850
+ case "running":
3851
+ chars += estimateUnknownChars(part.state.input);
3852
+ chars += part.state.title?.length ?? 0;
3853
+ chars += estimateUnknownChars(part.state.metadata);
3854
+ break;
3855
+ case "completed":
3856
+ chars += estimateUnknownChars(part.state.input);
3857
+ chars += part.state.output.length + part.state.title.length;
3858
+ chars += estimateUnknownChars(part.state.metadata);
3859
+ break;
3860
+ case "error":
3861
+ chars += estimateUnknownChars(part.state.input);
3862
+ chars += part.state.error.length + estimateUnknownChars(part.state.metadata);
3863
+ break;
3864
+ }
3865
+ return chars;
3866
+ }
3867
+ function estimatePartChars(part) {
3868
+ switch (part.type) {
3869
+ case "text":
3870
+ return part.text.length;
3871
+ case "reasoning":
3872
+ return Math.round(part.text.length * 0.25);
3873
+ case "file":
3874
+ return (part.filename?.length ?? 0) + (part.source?.text.value.length ?? 0);
3875
+ case "tool":
3876
+ return estimateToolPartChars(part);
3877
+ case "step-start":
3878
+ return part.snapshot?.length ?? 0;
3879
+ case "step-finish":
3880
+ return part.reason.length + (part.snapshot?.length ?? 0);
3881
+ case "snapshot":
3882
+ return part.snapshot.length;
3883
+ case "patch":
3884
+ return part.hash.length + part.files.join("\n").length;
3885
+ case "agent":
3886
+ return part.name.length + (part.source?.value.length ?? 0);
3887
+ case "retry":
3888
+ return estimateUnknownChars(part.error);
3889
+ case "compaction":
3890
+ return 32;
3891
+ case "subtask":
3892
+ return part.prompt.length + part.description.length + part.agent.length;
3893
+ }
3894
+ }
3895
+ function estimateMessageChars(message, parts) {
3896
+ let chars = 0;
3897
+ if (message.role === "user") {
3898
+ chars += message.system?.length ?? 0;
3899
+ if (message.summary) {
3900
+ chars += estimateDiffChars(message.summary);
3901
+ }
3902
+ }
3903
+ for (const part of parts) {
3904
+ chars += estimatePartChars(part);
3905
+ }
3906
+ return chars;
3907
+ }
3908
+ function estimateSessionTokens(messages) {
3909
+ const totalChars = messages.reduce((total, message) => total + estimateMessageChars(message.info, message.parts), 0);
3910
+ return Math.ceil(totalChars / 4);
3911
+ }
3912
+
3819
3913
  // node_modules/zod/v4/classic/external.js
3820
3914
  var external_exports = {};
3821
3915
  __export(external_exports, {
@@ -16238,6 +16332,132 @@ function tool(input) {
16238
16332
  }
16239
16333
  tool.schema = external_exports;
16240
16334
 
16335
+ // src/lib/model-context.ts
16336
+ var FALLBACK_MAX_OUTPUT_CHARS = 8e3;
16337
+ var MIN_BASE_MAX_OUTPUT_CHARS = 4e3;
16338
+ var MAX_BASE_OUTPUT_CHARS = 32e3;
16339
+ var MIN_PRESSURED_OUTPUT_CHARS = 1500;
16340
+ var sessionStates = /* @__PURE__ */ new Map();
16341
+ function getOrCreateSessionState(sessionID) {
16342
+ let state = sessionStates.get(sessionID);
16343
+ if (!state) {
16344
+ state = {
16345
+ contextTokens: null,
16346
+ compactionCount: 0,
16347
+ estimatedTokens: null,
16348
+ pressureRatio: null,
16349
+ lastPressureCheckedAt: null,
16350
+ autoSummarizeInFlight: false,
16351
+ lastAutoSummarizeAt: null,
16352
+ autoSummarizeCount: 0
16353
+ };
16354
+ sessionStates.set(sessionID, state);
16355
+ }
16356
+ return state;
16357
+ }
16358
+ function getSessionState(sessionID) {
16359
+ if (!sessionID) return null;
16360
+ return sessionStates.get(sessionID) ?? null;
16361
+ }
16362
+ function getCompactionPressureFactor(compactionCount) {
16363
+ if (compactionCount >= 3) return 0.35;
16364
+ if (compactionCount === 2) return 0.5;
16365
+ if (compactionCount === 1) return 0.75;
16366
+ return 1;
16367
+ }
16368
+ function getBaseMaxOutputChars(contextTokens, fallback) {
16369
+ if (contextTokens === null) return fallback;
16370
+ const computed = Math.round(contextTokens * 0.01 * 4);
16371
+ return Math.max(MIN_BASE_MAX_OUTPUT_CHARS, Math.min(MAX_BASE_OUTPUT_CHARS, computed));
16372
+ }
16373
+ function buildTruncationSuffix(sessionID, maxChars) {
16374
+ const state = getSessionState(sessionID);
16375
+ if (!state || state.compactionCount === 0) {
16376
+ return `\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650`;
16377
+ }
16378
+ return `\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650\uFF1B\u5F53\u524D\u4F1A\u8BDD\u5DF2\u538B\u7F29 ${state.compactionCount} \u6B21\uFF0C\u5DF2\u81EA\u52A8\u6536\u7D27\u5DE5\u5177\u8F93\u51FA\u9884\u7B97`;
16379
+ }
16380
+ function setCurrentContextTokens(sessionID, tokens) {
16381
+ if (!sessionID || tokens <= 0) return;
16382
+ const state = getOrCreateSessionState(sessionID);
16383
+ state.contextTokens = tokens;
16384
+ }
16385
+ function markSessionCompacted(sessionID) {
16386
+ if (!sessionID) return;
16387
+ const state = getOrCreateSessionState(sessionID);
16388
+ state.compactionCount += 1;
16389
+ state.estimatedTokens = null;
16390
+ state.pressureRatio = null;
16391
+ state.lastPressureCheckedAt = null;
16392
+ }
16393
+ function getSessionCompactionCount(sessionID) {
16394
+ return getSessionState(sessionID)?.compactionCount ?? 0;
16395
+ }
16396
+ function updateSessionPressure(sessionID, estimatedTokens, pressureRatio, checkedAt = Date.now()) {
16397
+ if (!sessionID) return;
16398
+ const state = getOrCreateSessionState(sessionID);
16399
+ state.estimatedTokens = estimatedTokens;
16400
+ state.pressureRatio = pressureRatio;
16401
+ state.lastPressureCheckedAt = checkedAt;
16402
+ }
16403
+ function clearSessionPressure(sessionID) {
16404
+ if (!sessionID) return;
16405
+ const state = getOrCreateSessionState(sessionID);
16406
+ state.estimatedTokens = null;
16407
+ state.pressureRatio = null;
16408
+ state.lastPressureCheckedAt = null;
16409
+ }
16410
+ function getSessionPressure(sessionID) {
16411
+ const state = getSessionState(sessionID);
16412
+ if (!state || state.estimatedTokens === null || state.pressureRatio === null || state.lastPressureCheckedAt === null) {
16413
+ return null;
16414
+ }
16415
+ return {
16416
+ estimatedTokens: state.estimatedTokens,
16417
+ pressureRatio: state.pressureRatio,
16418
+ checkedAt: state.lastPressureCheckedAt
16419
+ };
16420
+ }
16421
+ function isAutoSummarizeInFlight(sessionID) {
16422
+ return getSessionState(sessionID)?.autoSummarizeInFlight ?? false;
16423
+ }
16424
+ function markAutoSummarizeStarted(sessionID) {
16425
+ if (!sessionID) return;
16426
+ const state = getOrCreateSessionState(sessionID);
16427
+ state.autoSummarizeInFlight = true;
16428
+ }
16429
+ function markAutoSummarizeFinished(sessionID, summarized, finishedAt = Date.now()) {
16430
+ if (!sessionID) return;
16431
+ const state = getOrCreateSessionState(sessionID);
16432
+ state.autoSummarizeInFlight = false;
16433
+ if (summarized) {
16434
+ state.lastAutoSummarizeAt = finishedAt;
16435
+ state.autoSummarizeCount += 1;
16436
+ state.estimatedTokens = null;
16437
+ state.pressureRatio = null;
16438
+ state.lastPressureCheckedAt = null;
16439
+ }
16440
+ }
16441
+ function getLastAutoSummarizeAt(sessionID) {
16442
+ return getSessionState(sessionID)?.lastAutoSummarizeAt ?? null;
16443
+ }
16444
+ function getAutoSummarizeCount(sessionID) {
16445
+ return getSessionState(sessionID)?.autoSummarizeCount ?? 0;
16446
+ }
16447
+ function getMaxOutputChars(sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16448
+ const state = getSessionState(sessionID);
16449
+ const base = getBaseMaxOutputChars(state?.contextTokens ?? null, fallback);
16450
+ const pressured = Math.round(base * getCompactionPressureFactor(state?.compactionCount ?? 0));
16451
+ return Math.max(MIN_PRESSURED_OUTPUT_CHARS, pressured);
16452
+ }
16453
+ function truncateToolOutput(content, sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16454
+ const maxChars = getMaxOutputChars(sessionID, fallback);
16455
+ if (content.length <= maxChars) return content;
16456
+ const truncated = content.slice(0, maxChars);
16457
+ return `${truncated}
16458
+ \x1B[2m... (${buildTruncationSuffix(sessionID, maxChars)})\x1B[0m`;
16459
+ }
16460
+
16241
16461
  // src/lib/gbk-file.ts
16242
16462
  var import_iconv_lite = __toESM(require_lib(), 1);
16243
16463
  import crypto from "crypto";
@@ -17610,25 +17830,7 @@ async function writeGbkFile(input) {
17610
17830
  }
17611
17831
  }
17612
17832
 
17613
- // src/lib/model-context.ts
17614
- var _currentContextTokens = null;
17615
- function setCurrentContextTokens(tokens) {
17616
- _currentContextTokens = tokens;
17617
- }
17618
- function getMaxOutputChars(fallback = 8e3) {
17619
- if (_currentContextTokens === null) return fallback;
17620
- const computed = Math.round(_currentContextTokens * 0.01 * 4);
17621
- return Math.max(4e3, Math.min(32e3, computed));
17622
- }
17623
-
17624
17833
  // src/tools/gbk_edit.ts
17625
- function truncateToolOutput(content) {
17626
- const maxChars = getMaxOutputChars();
17627
- if (content.length <= maxChars) return content;
17628
- const truncated = content.slice(0, maxChars);
17629
- return truncated + `
17630
- \x1B[2m... (\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
17631
- }
17632
17834
  var gbk_edit_default = tool({
17633
17835
  description: `Edit GBK/GB18030 encoded text files with exact string replacement.
17634
17836
 
@@ -17698,8 +17900,8 @@ Insert mode:
17698
17900
  diffPreview
17699
17901
  }
17700
17902
  });
17701
- if (diffPreview) return truncateToolOutput(diffPreview);
17702
- return truncateToolOutput(JSON.stringify(result, null, 2));
17903
+ if (diffPreview) return truncateToolOutput(diffPreview, context.sessionID);
17904
+ return truncateToolOutput(JSON.stringify(result, null, 2), context.sessionID);
17703
17905
  }
17704
17906
  });
17705
17907
 
@@ -18765,13 +18967,6 @@ async function replaceTextFileText(input) {
18765
18967
  }
18766
18968
 
18767
18969
  // src/tools/text_edit.ts
18768
- function truncateToolOutput2(content) {
18769
- const maxChars = getMaxOutputChars();
18770
- if (content.length <= maxChars) return content;
18771
- const truncated = content.slice(0, maxChars);
18772
- return truncated + `
18773
- \x1B[2m... (\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
18774
- }
18775
18970
  var text_edit_default = tool({
18776
18971
  description: `Edit text files with automatic encoding detection and preservation.
18777
18972
 
@@ -18822,8 +19017,8 @@ var text_edit_default = tool({
18822
19017
  diffPreview
18823
19018
  }
18824
19019
  });
18825
- if (diffPreview) return truncateToolOutput2(diffPreview);
18826
- return truncateToolOutput2(JSON.stringify(result, null, 2));
19020
+ if (diffPreview) return truncateToolOutput(diffPreview, context.sessionID);
19021
+ return truncateToolOutput(JSON.stringify(result, null, 2), context.sessionID);
18827
19022
  }
18828
19023
  });
18829
19024
 
@@ -18904,7 +19099,66 @@ var text_write_default = tool({
18904
19099
  });
18905
19100
 
18906
19101
  // src/plugin/index.ts
18907
- function createOpencodeGbkHooks() {
19102
+ var MANAGED_TOOL_IDS = /* @__PURE__ */ new Set([
19103
+ "gbk_read",
19104
+ "gbk_write",
19105
+ "gbk_edit",
19106
+ "gbk_search",
19107
+ "text_read",
19108
+ "text_write",
19109
+ "text_edit"
19110
+ ]);
19111
+ function truncateMetadataPreview(value, sessionID) {
19112
+ const previewMaxChars = Math.max(800, Math.min(2e3, Math.floor(getMaxOutputChars(sessionID) / 2)));
19113
+ if (value.length <= previewMaxChars) return value;
19114
+ const truncated = value.slice(0, previewMaxChars);
19115
+ return `${truncated}
19116
+ \x1B[2m... (metadata diffPreview \u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${previewMaxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
19117
+ }
19118
+ async function maybeAutoSummarizeSession(client, directory, input) {
19119
+ if (!client?.session?.messages || !client.session.summarize) return;
19120
+ if (isAutoSummarizeInFlight(input.sessionID)) return;
19121
+ const contextTokens = input.model.limit?.context;
19122
+ if (typeof contextTokens !== "number" || contextTokens <= 0) return;
19123
+ const now = Date.now();
19124
+ const lastAutoSummarizeAt = getLastAutoSummarizeAt(input.sessionID);
19125
+ if (lastAutoSummarizeAt !== null && now - lastAutoSummarizeAt < AUTO_SUMMARIZE_COOLDOWN_MS) {
19126
+ return;
19127
+ }
19128
+ let pressure = getSessionPressure(input.sessionID);
19129
+ if (!pressure || now - pressure.checkedAt >= PRESSURE_CHECK_INTERVAL_MS) {
19130
+ const response = await client.session.messages({
19131
+ path: { id: input.sessionID },
19132
+ query: {
19133
+ directory,
19134
+ limit: SESSION_PRESSURE_MESSAGE_LIMIT
19135
+ },
19136
+ throwOnError: true
19137
+ });
19138
+ const estimatedTokens = estimateSessionTokens(Array.isArray(response.data) ? response.data : []);
19139
+ const pressureRatio = estimatedTokens / contextTokens;
19140
+ updateSessionPressure(input.sessionID, estimatedTokens, pressureRatio, now);
19141
+ pressure = getSessionPressure(input.sessionID);
19142
+ }
19143
+ if (!pressure || pressure.pressureRatio < AUTO_SUMMARIZE_PRESSURE_RATIO) return;
19144
+ markAutoSummarizeStarted(input.sessionID);
19145
+ try {
19146
+ await client.session.summarize({
19147
+ path: { id: input.sessionID },
19148
+ body: {
19149
+ providerID: input.model.providerID,
19150
+ modelID: input.model.id
19151
+ },
19152
+ query: { directory },
19153
+ throwOnError: true
19154
+ });
19155
+ clearSessionPressure(input.sessionID);
19156
+ markAutoSummarizeFinished(input.sessionID, true);
19157
+ } catch {
19158
+ markAutoSummarizeFinished(input.sessionID, false);
19159
+ }
19160
+ }
19161
+ function createOpencodeGbkHooks(client, directory) {
18908
19162
  return {
18909
19163
  tool: {
18910
19164
  gbk_read: gbk_read_default,
@@ -18915,21 +19169,63 @@ function createOpencodeGbkHooks() {
18915
19169
  text_write: text_write_default,
18916
19170
  text_edit: text_edit_default
18917
19171
  },
19172
+ async event(input) {
19173
+ if (input.event.type === "session.compacted") {
19174
+ const sessionID = input.event.properties.sessionID;
19175
+ if (sessionID) {
19176
+ markSessionCompacted(sessionID);
19177
+ }
19178
+ }
19179
+ },
18918
19180
  async "chat.params"(input) {
18919
19181
  const contextTokens = input.model?.limit?.context;
18920
19182
  if (typeof contextTokens === "number" && contextTokens > 0) {
18921
- setCurrentContextTokens(contextTokens);
19183
+ setCurrentContextTokens(input.sessionID, contextTokens);
19184
+ await maybeAutoSummarizeSession(client, directory, input);
18922
19185
  }
18923
19186
  },
19187
+ async "tool.execute.after"(input, output) {
19188
+ if (!MANAGED_TOOL_IDS.has(input.tool)) return;
19189
+ const maxOutputChars = getMaxOutputChars(input.sessionID);
19190
+ const compactionCount = getSessionCompactionCount(input.sessionID);
19191
+ const sessionPressure = getSessionPressure(input.sessionID);
19192
+ const autoSummarizeCount = getAutoSummarizeCount(input.sessionID);
19193
+ const metadata = output.metadata && typeof output.metadata === "object" ? { ...output.metadata } : {};
19194
+ const nextOutput = truncateToolOutput(output.output, input.sessionID);
19195
+ if (nextOutput !== output.output) {
19196
+ metadata.outputTruncated = true;
19197
+ output.output = nextOutput;
19198
+ }
19199
+ if (typeof metadata.diffPreview === "string") {
19200
+ metadata.diffPreview = truncateMetadataPreview(metadata.diffPreview, input.sessionID);
19201
+ }
19202
+ metadata.maxOutputChars = maxOutputChars;
19203
+ if (compactionCount > 0) {
19204
+ metadata.sessionCompactions = compactionCount;
19205
+ }
19206
+ if (sessionPressure) {
19207
+ metadata.estimatedSessionTokens = sessionPressure.estimatedTokens;
19208
+ metadata.sessionPressureRatio = Number(sessionPressure.pressureRatio.toFixed(3));
19209
+ }
19210
+ if (autoSummarizeCount > 0) {
19211
+ metadata.autoSummarizeCount = autoSummarizeCount;
19212
+ }
19213
+ output.metadata = metadata;
19214
+ },
18924
19215
  async "experimental.chat.system.transform"(_input, output) {
18925
19216
  appendTextToolSystemPrompt(output.system);
19217
+ },
19218
+ async "experimental.session.compacting"(_input, output) {
19219
+ output.context.push(
19220
+ "Aggressively compress prior tool outputs. Keep only unresolved tasks, final decisions, exact file paths or line ranges, and the smallest snippets needed to continue. Drop repeated raw file content, full JSON payloads, verbose logs, and duplicated diff previews."
19221
+ );
18926
19222
  }
18927
19223
  };
18928
19224
  }
18929
19225
  var pluginModule = {
18930
19226
  id: "opencode-gbk-tools",
18931
- async server() {
18932
- return createOpencodeGbkHooks();
19227
+ async server(ctx) {
19228
+ return createOpencodeGbkHooks(ctx.client, ctx.directory);
18933
19229
  }
18934
19230
  };
18935
19231
  var plugin_default = pluginModule;
@@ -3816,6 +3816,100 @@ var require_lib = __commonJS({
3816
3816
  }
3817
3817
  });
3818
3818
 
3819
+ // src/lib/session-pressure.ts
3820
+ var AUTO_SUMMARIZE_PRESSURE_RATIO = 0.85;
3821
+ var AUTO_SUMMARIZE_COOLDOWN_MS = 6e4;
3822
+ var PRESSURE_CHECK_INTERVAL_MS = 15e3;
3823
+ var SESSION_PRESSURE_MESSAGE_LIMIT = 200;
3824
+ function estimateUnknownChars(value) {
3825
+ if (typeof value === "string") return value.length;
3826
+ if (typeof value === "number" || typeof value === "boolean") return String(value).length;
3827
+ if (Array.isArray(value)) {
3828
+ return value.reduce((total, item) => total + estimateUnknownChars(item), 0);
3829
+ }
3830
+ if (!value || typeof value !== "object") return 0;
3831
+ try {
3832
+ return JSON.stringify(value).length;
3833
+ } catch {
3834
+ return 0;
3835
+ }
3836
+ }
3837
+ function estimateDiffChars(summary) {
3838
+ let chars = (summary.title?.length ?? 0) + (summary.body?.length ?? 0);
3839
+ for (const diff of summary.diffs ?? []) {
3840
+ chars += diff.file.length + diff.before.length + diff.after.length;
3841
+ }
3842
+ return chars;
3843
+ }
3844
+ function estimateToolPartChars(part) {
3845
+ let chars = part.tool.length + estimateUnknownChars(part.metadata);
3846
+ switch (part.state.status) {
3847
+ case "pending":
3848
+ chars += estimateUnknownChars(part.state.input) + part.state.raw.length;
3849
+ break;
3850
+ case "running":
3851
+ chars += estimateUnknownChars(part.state.input);
3852
+ chars += part.state.title?.length ?? 0;
3853
+ chars += estimateUnknownChars(part.state.metadata);
3854
+ break;
3855
+ case "completed":
3856
+ chars += estimateUnknownChars(part.state.input);
3857
+ chars += part.state.output.length + part.state.title.length;
3858
+ chars += estimateUnknownChars(part.state.metadata);
3859
+ break;
3860
+ case "error":
3861
+ chars += estimateUnknownChars(part.state.input);
3862
+ chars += part.state.error.length + estimateUnknownChars(part.state.metadata);
3863
+ break;
3864
+ }
3865
+ return chars;
3866
+ }
3867
+ function estimatePartChars(part) {
3868
+ switch (part.type) {
3869
+ case "text":
3870
+ return part.text.length;
3871
+ case "reasoning":
3872
+ return Math.round(part.text.length * 0.25);
3873
+ case "file":
3874
+ return (part.filename?.length ?? 0) + (part.source?.text.value.length ?? 0);
3875
+ case "tool":
3876
+ return estimateToolPartChars(part);
3877
+ case "step-start":
3878
+ return part.snapshot?.length ?? 0;
3879
+ case "step-finish":
3880
+ return part.reason.length + (part.snapshot?.length ?? 0);
3881
+ case "snapshot":
3882
+ return part.snapshot.length;
3883
+ case "patch":
3884
+ return part.hash.length + part.files.join("\n").length;
3885
+ case "agent":
3886
+ return part.name.length + (part.source?.value.length ?? 0);
3887
+ case "retry":
3888
+ return estimateUnknownChars(part.error);
3889
+ case "compaction":
3890
+ return 32;
3891
+ case "subtask":
3892
+ return part.prompt.length + part.description.length + part.agent.length;
3893
+ }
3894
+ }
3895
+ function estimateMessageChars(message, parts) {
3896
+ let chars = 0;
3897
+ if (message.role === "user") {
3898
+ chars += message.system?.length ?? 0;
3899
+ if (message.summary) {
3900
+ chars += estimateDiffChars(message.summary);
3901
+ }
3902
+ }
3903
+ for (const part of parts) {
3904
+ chars += estimatePartChars(part);
3905
+ }
3906
+ return chars;
3907
+ }
3908
+ function estimateSessionTokens(messages) {
3909
+ const totalChars = messages.reduce((total, message) => total + estimateMessageChars(message.info, message.parts), 0);
3910
+ return Math.ceil(totalChars / 4);
3911
+ }
3912
+
3819
3913
  // node_modules/zod/v4/classic/external.js
3820
3914
  var external_exports = {};
3821
3915
  __export(external_exports, {
@@ -16238,6 +16332,132 @@ function tool(input) {
16238
16332
  }
16239
16333
  tool.schema = external_exports;
16240
16334
 
16335
+ // src/lib/model-context.ts
16336
+ var FALLBACK_MAX_OUTPUT_CHARS = 8e3;
16337
+ var MIN_BASE_MAX_OUTPUT_CHARS = 4e3;
16338
+ var MAX_BASE_OUTPUT_CHARS = 32e3;
16339
+ var MIN_PRESSURED_OUTPUT_CHARS = 1500;
16340
+ var sessionStates = /* @__PURE__ */ new Map();
16341
+ function getOrCreateSessionState(sessionID) {
16342
+ let state = sessionStates.get(sessionID);
16343
+ if (!state) {
16344
+ state = {
16345
+ contextTokens: null,
16346
+ compactionCount: 0,
16347
+ estimatedTokens: null,
16348
+ pressureRatio: null,
16349
+ lastPressureCheckedAt: null,
16350
+ autoSummarizeInFlight: false,
16351
+ lastAutoSummarizeAt: null,
16352
+ autoSummarizeCount: 0
16353
+ };
16354
+ sessionStates.set(sessionID, state);
16355
+ }
16356
+ return state;
16357
+ }
16358
+ function getSessionState(sessionID) {
16359
+ if (!sessionID) return null;
16360
+ return sessionStates.get(sessionID) ?? null;
16361
+ }
16362
+ function getCompactionPressureFactor(compactionCount) {
16363
+ if (compactionCount >= 3) return 0.35;
16364
+ if (compactionCount === 2) return 0.5;
16365
+ if (compactionCount === 1) return 0.75;
16366
+ return 1;
16367
+ }
16368
+ function getBaseMaxOutputChars(contextTokens, fallback) {
16369
+ if (contextTokens === null) return fallback;
16370
+ const computed = Math.round(contextTokens * 0.01 * 4);
16371
+ return Math.max(MIN_BASE_MAX_OUTPUT_CHARS, Math.min(MAX_BASE_OUTPUT_CHARS, computed));
16372
+ }
16373
+ function buildTruncationSuffix(sessionID, maxChars) {
16374
+ const state = getSessionState(sessionID);
16375
+ if (!state || state.compactionCount === 0) {
16376
+ return `\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650`;
16377
+ }
16378
+ return `\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650\uFF1B\u5F53\u524D\u4F1A\u8BDD\u5DF2\u538B\u7F29 ${state.compactionCount} \u6B21\uFF0C\u5DF2\u81EA\u52A8\u6536\u7D27\u5DE5\u5177\u8F93\u51FA\u9884\u7B97`;
16379
+ }
16380
+ function setCurrentContextTokens(sessionID, tokens) {
16381
+ if (!sessionID || tokens <= 0) return;
16382
+ const state = getOrCreateSessionState(sessionID);
16383
+ state.contextTokens = tokens;
16384
+ }
16385
+ function markSessionCompacted(sessionID) {
16386
+ if (!sessionID) return;
16387
+ const state = getOrCreateSessionState(sessionID);
16388
+ state.compactionCount += 1;
16389
+ state.estimatedTokens = null;
16390
+ state.pressureRatio = null;
16391
+ state.lastPressureCheckedAt = null;
16392
+ }
16393
+ function getSessionCompactionCount(sessionID) {
16394
+ return getSessionState(sessionID)?.compactionCount ?? 0;
16395
+ }
16396
+ function updateSessionPressure(sessionID, estimatedTokens, pressureRatio, checkedAt = Date.now()) {
16397
+ if (!sessionID) return;
16398
+ const state = getOrCreateSessionState(sessionID);
16399
+ state.estimatedTokens = estimatedTokens;
16400
+ state.pressureRatio = pressureRatio;
16401
+ state.lastPressureCheckedAt = checkedAt;
16402
+ }
16403
+ function clearSessionPressure(sessionID) {
16404
+ if (!sessionID) return;
16405
+ const state = getOrCreateSessionState(sessionID);
16406
+ state.estimatedTokens = null;
16407
+ state.pressureRatio = null;
16408
+ state.lastPressureCheckedAt = null;
16409
+ }
16410
+ function getSessionPressure(sessionID) {
16411
+ const state = getSessionState(sessionID);
16412
+ if (!state || state.estimatedTokens === null || state.pressureRatio === null || state.lastPressureCheckedAt === null) {
16413
+ return null;
16414
+ }
16415
+ return {
16416
+ estimatedTokens: state.estimatedTokens,
16417
+ pressureRatio: state.pressureRatio,
16418
+ checkedAt: state.lastPressureCheckedAt
16419
+ };
16420
+ }
16421
+ function isAutoSummarizeInFlight(sessionID) {
16422
+ return getSessionState(sessionID)?.autoSummarizeInFlight ?? false;
16423
+ }
16424
+ function markAutoSummarizeStarted(sessionID) {
16425
+ if (!sessionID) return;
16426
+ const state = getOrCreateSessionState(sessionID);
16427
+ state.autoSummarizeInFlight = true;
16428
+ }
16429
+ function markAutoSummarizeFinished(sessionID, summarized, finishedAt = Date.now()) {
16430
+ if (!sessionID) return;
16431
+ const state = getOrCreateSessionState(sessionID);
16432
+ state.autoSummarizeInFlight = false;
16433
+ if (summarized) {
16434
+ state.lastAutoSummarizeAt = finishedAt;
16435
+ state.autoSummarizeCount += 1;
16436
+ state.estimatedTokens = null;
16437
+ state.pressureRatio = null;
16438
+ state.lastPressureCheckedAt = null;
16439
+ }
16440
+ }
16441
+ function getLastAutoSummarizeAt(sessionID) {
16442
+ return getSessionState(sessionID)?.lastAutoSummarizeAt ?? null;
16443
+ }
16444
+ function getAutoSummarizeCount(sessionID) {
16445
+ return getSessionState(sessionID)?.autoSummarizeCount ?? 0;
16446
+ }
16447
+ function getMaxOutputChars(sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16448
+ const state = getSessionState(sessionID);
16449
+ const base = getBaseMaxOutputChars(state?.contextTokens ?? null, fallback);
16450
+ const pressured = Math.round(base * getCompactionPressureFactor(state?.compactionCount ?? 0));
16451
+ return Math.max(MIN_PRESSURED_OUTPUT_CHARS, pressured);
16452
+ }
16453
+ function truncateToolOutput(content, sessionID, fallback = FALLBACK_MAX_OUTPUT_CHARS) {
16454
+ const maxChars = getMaxOutputChars(sessionID, fallback);
16455
+ if (content.length <= maxChars) return content;
16456
+ const truncated = content.slice(0, maxChars);
16457
+ return `${truncated}
16458
+ \x1B[2m... (${buildTruncationSuffix(sessionID, maxChars)})\x1B[0m`;
16459
+ }
16460
+
16241
16461
  // src/lib/gbk-file.ts
16242
16462
  var import_iconv_lite = __toESM(require_lib(), 1);
16243
16463
  import crypto from "crypto";
@@ -17610,25 +17830,7 @@ async function writeGbkFile(input) {
17610
17830
  }
17611
17831
  }
17612
17832
 
17613
- // src/lib/model-context.ts
17614
- var _currentContextTokens = null;
17615
- function setCurrentContextTokens(tokens) {
17616
- _currentContextTokens = tokens;
17617
- }
17618
- function getMaxOutputChars(fallback = 8e3) {
17619
- if (_currentContextTokens === null) return fallback;
17620
- const computed = Math.round(_currentContextTokens * 0.01 * 4);
17621
- return Math.max(4e3, Math.min(32e3, computed));
17622
- }
17623
-
17624
17833
  // src/tools/gbk_edit.ts
17625
- function truncateToolOutput(content) {
17626
- const maxChars = getMaxOutputChars();
17627
- if (content.length <= maxChars) return content;
17628
- const truncated = content.slice(0, maxChars);
17629
- return truncated + `
17630
- \x1B[2m... (\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
17631
- }
17632
17834
  var gbk_edit_default = tool({
17633
17835
  description: `Edit GBK/GB18030 encoded text files with exact string replacement.
17634
17836
 
@@ -17698,8 +17900,8 @@ Insert mode:
17698
17900
  diffPreview
17699
17901
  }
17700
17902
  });
17701
- if (diffPreview) return truncateToolOutput(diffPreview);
17702
- return truncateToolOutput(JSON.stringify(result, null, 2));
17903
+ if (diffPreview) return truncateToolOutput(diffPreview, context.sessionID);
17904
+ return truncateToolOutput(JSON.stringify(result, null, 2), context.sessionID);
17703
17905
  }
17704
17906
  });
17705
17907
 
@@ -18765,13 +18967,6 @@ async function replaceTextFileText(input) {
18765
18967
  }
18766
18968
 
18767
18969
  // src/tools/text_edit.ts
18768
- function truncateToolOutput2(content) {
18769
- const maxChars = getMaxOutputChars();
18770
- if (content.length <= maxChars) return content;
18771
- const truncated = content.slice(0, maxChars);
18772
- return truncated + `
18773
- \x1B[2m... (\u8F93\u51FA\u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${maxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
18774
- }
18775
18970
  var text_edit_default = tool({
18776
18971
  description: `Edit text files with automatic encoding detection and preservation.
18777
18972
 
@@ -18822,8 +19017,8 @@ var text_edit_default = tool({
18822
19017
  diffPreview
18823
19018
  }
18824
19019
  });
18825
- if (diffPreview) return truncateToolOutput2(diffPreview);
18826
- return truncateToolOutput2(JSON.stringify(result, null, 2));
19020
+ if (diffPreview) return truncateToolOutput(diffPreview, context.sessionID);
19021
+ return truncateToolOutput(JSON.stringify(result, null, 2), context.sessionID);
18827
19022
  }
18828
19023
  });
18829
19024
 
@@ -18904,7 +19099,66 @@ var text_write_default = tool({
18904
19099
  });
18905
19100
 
18906
19101
  // src/plugin/index.ts
18907
- function createOpencodeGbkHooks() {
19102
+ var MANAGED_TOOL_IDS = /* @__PURE__ */ new Set([
19103
+ "gbk_read",
19104
+ "gbk_write",
19105
+ "gbk_edit",
19106
+ "gbk_search",
19107
+ "text_read",
19108
+ "text_write",
19109
+ "text_edit"
19110
+ ]);
19111
+ function truncateMetadataPreview(value, sessionID) {
19112
+ const previewMaxChars = Math.max(800, Math.min(2e3, Math.floor(getMaxOutputChars(sessionID) / 2)));
19113
+ if (value.length <= previewMaxChars) return value;
19114
+ const truncated = value.slice(0, previewMaxChars);
19115
+ return `${truncated}
19116
+ \x1B[2m... (metadata diffPreview \u5DF2\u622A\u65AD\uFF0C\u8D85\u51FA ${previewMaxChars} \u5B57\u7B26\u4E0A\u9650)\x1B[0m`;
19117
+ }
19118
+ async function maybeAutoSummarizeSession(client, directory, input) {
19119
+ if (!client?.session?.messages || !client.session.summarize) return;
19120
+ if (isAutoSummarizeInFlight(input.sessionID)) return;
19121
+ const contextTokens = input.model.limit?.context;
19122
+ if (typeof contextTokens !== "number" || contextTokens <= 0) return;
19123
+ const now = Date.now();
19124
+ const lastAutoSummarizeAt = getLastAutoSummarizeAt(input.sessionID);
19125
+ if (lastAutoSummarizeAt !== null && now - lastAutoSummarizeAt < AUTO_SUMMARIZE_COOLDOWN_MS) {
19126
+ return;
19127
+ }
19128
+ let pressure = getSessionPressure(input.sessionID);
19129
+ if (!pressure || now - pressure.checkedAt >= PRESSURE_CHECK_INTERVAL_MS) {
19130
+ const response = await client.session.messages({
19131
+ path: { id: input.sessionID },
19132
+ query: {
19133
+ directory,
19134
+ limit: SESSION_PRESSURE_MESSAGE_LIMIT
19135
+ },
19136
+ throwOnError: true
19137
+ });
19138
+ const estimatedTokens = estimateSessionTokens(Array.isArray(response.data) ? response.data : []);
19139
+ const pressureRatio = estimatedTokens / contextTokens;
19140
+ updateSessionPressure(input.sessionID, estimatedTokens, pressureRatio, now);
19141
+ pressure = getSessionPressure(input.sessionID);
19142
+ }
19143
+ if (!pressure || pressure.pressureRatio < AUTO_SUMMARIZE_PRESSURE_RATIO) return;
19144
+ markAutoSummarizeStarted(input.sessionID);
19145
+ try {
19146
+ await client.session.summarize({
19147
+ path: { id: input.sessionID },
19148
+ body: {
19149
+ providerID: input.model.providerID,
19150
+ modelID: input.model.id
19151
+ },
19152
+ query: { directory },
19153
+ throwOnError: true
19154
+ });
19155
+ clearSessionPressure(input.sessionID);
19156
+ markAutoSummarizeFinished(input.sessionID, true);
19157
+ } catch {
19158
+ markAutoSummarizeFinished(input.sessionID, false);
19159
+ }
19160
+ }
19161
+ function createOpencodeGbkHooks(client, directory) {
18908
19162
  return {
18909
19163
  tool: {
18910
19164
  gbk_read: gbk_read_default,
@@ -18915,21 +19169,63 @@ function createOpencodeGbkHooks() {
18915
19169
  text_write: text_write_default,
18916
19170
  text_edit: text_edit_default
18917
19171
  },
19172
+ async event(input) {
19173
+ if (input.event.type === "session.compacted") {
19174
+ const sessionID = input.event.properties.sessionID;
19175
+ if (sessionID) {
19176
+ markSessionCompacted(sessionID);
19177
+ }
19178
+ }
19179
+ },
18918
19180
  async "chat.params"(input) {
18919
19181
  const contextTokens = input.model?.limit?.context;
18920
19182
  if (typeof contextTokens === "number" && contextTokens > 0) {
18921
- setCurrentContextTokens(contextTokens);
19183
+ setCurrentContextTokens(input.sessionID, contextTokens);
19184
+ await maybeAutoSummarizeSession(client, directory, input);
18922
19185
  }
18923
19186
  },
19187
+ async "tool.execute.after"(input, output) {
19188
+ if (!MANAGED_TOOL_IDS.has(input.tool)) return;
19189
+ const maxOutputChars = getMaxOutputChars(input.sessionID);
19190
+ const compactionCount = getSessionCompactionCount(input.sessionID);
19191
+ const sessionPressure = getSessionPressure(input.sessionID);
19192
+ const autoSummarizeCount = getAutoSummarizeCount(input.sessionID);
19193
+ const metadata = output.metadata && typeof output.metadata === "object" ? { ...output.metadata } : {};
19194
+ const nextOutput = truncateToolOutput(output.output, input.sessionID);
19195
+ if (nextOutput !== output.output) {
19196
+ metadata.outputTruncated = true;
19197
+ output.output = nextOutput;
19198
+ }
19199
+ if (typeof metadata.diffPreview === "string") {
19200
+ metadata.diffPreview = truncateMetadataPreview(metadata.diffPreview, input.sessionID);
19201
+ }
19202
+ metadata.maxOutputChars = maxOutputChars;
19203
+ if (compactionCount > 0) {
19204
+ metadata.sessionCompactions = compactionCount;
19205
+ }
19206
+ if (sessionPressure) {
19207
+ metadata.estimatedSessionTokens = sessionPressure.estimatedTokens;
19208
+ metadata.sessionPressureRatio = Number(sessionPressure.pressureRatio.toFixed(3));
19209
+ }
19210
+ if (autoSummarizeCount > 0) {
19211
+ metadata.autoSummarizeCount = autoSummarizeCount;
19212
+ }
19213
+ output.metadata = metadata;
19214
+ },
18924
19215
  async "experimental.chat.system.transform"(_input, output) {
18925
19216
  appendTextToolSystemPrompt(output.system);
19217
+ },
19218
+ async "experimental.session.compacting"(_input, output) {
19219
+ output.context.push(
19220
+ "Aggressively compress prior tool outputs. Keep only unresolved tasks, final decisions, exact file paths or line ranges, and the smallest snippets needed to continue. Drop repeated raw file content, full JSON payloads, verbose logs, and duplicated diff previews."
19221
+ );
18926
19222
  }
18927
19223
  };
18928
19224
  }
18929
19225
 
18930
19226
  // src/local-plugin/opencode-gbk-tools.ts
18931
- var OpencodeGbkToolsLocalPlugin = async () => {
18932
- return createOpencodeGbkHooks();
19227
+ var OpencodeGbkToolsLocalPlugin = async (ctx) => {
19228
+ return createOpencodeGbkHooks(ctx.client, ctx.directory);
18933
19229
  };
18934
19230
  export {
18935
19231
  OpencodeGbkToolsLocalPlugin
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "manifestVersion": 1,
3
3
  "packageName": "opencode-gbk-tools",
4
- "packageVersion": "0.1.23",
4
+ "packageVersion": "0.1.25",
5
5
  "artifacts": [
6
6
  {
7
7
  "relativePath": "plugins/opencode-gbk-tools.js",
8
8
  "kind": "plugin",
9
- "expectedHash": "28e00665097934ea390c85f9a991f9074f77d31ff7ffe6f118035e834b911d2d",
9
+ "expectedHash": "55f2eae379bf3bd5a9129fbc413f255a4ffd9186d90a4fe26ecb3cc75b5a4c68",
10
+ "hashAlgorithm": "sha256"
11
+ },
12
+ {
13
+ "relativePath": "agents/gbk-engine.md",
14
+ "kind": "agent",
15
+ "expectedHash": "05a97cfe24d72339e45616638d0706d46c0d3782a3bfedefa1e59978adc5c3be",
10
16
  "hashAlgorithm": "sha256"
11
17
  }
12
18
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gbk-tools",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Auto-encoding text tools plus GBK/GB18030 tools for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin/index.js",