@andyqiu/codeforge 0.3.9 → 0.3.11

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
@@ -9007,7 +9007,7 @@ var handler5 = autoLearningServer;
9007
9007
 
9008
9008
  // plugins/channels.ts
9009
9009
  init_opencode_plugin_helpers();
9010
- import { promises as fs2 } from "node:fs";
9010
+ import { promises as fs2, statSync as statSync2 } from "node:fs";
9011
9011
  import * as path4 from "node:path";
9012
9012
 
9013
9013
  // lib/channels.ts
@@ -9285,6 +9285,13 @@ function transformSlackToWebhook(ch, ev) {
9285
9285
  text: { type: "plain_text", text: titleText.slice(0, 150), emoji: true }
9286
9286
  }
9287
9287
  ];
9288
+ const sevLabel = ch.show_severity_label !== false ? severityToLabel(ev.severity) : "";
9289
+ if (sevLabel) {
9290
+ blocks.push({
9291
+ type: "section",
9292
+ text: { type: "mrkdwn", text: sevLabel }
9293
+ });
9294
+ }
9288
9295
  if (message.rendered.trim()) {
9289
9296
  blocks.push({
9290
9297
  type: "section",
@@ -9297,6 +9304,56 @@ function transformSlackToWebhook(ch, ev) {
9297
9304
  elements: [{ type: "mrkdwn", text: mentionText }]
9298
9305
  });
9299
9306
  }
9307
+ const showFields = ch.show_fields ?? [...DEFAULT_SHOW_FIELDS];
9308
+ const labels = { ...DEFAULT_FIELD_LABELS, ...ch.field_labels ?? {} };
9309
+ const fieldLines = pickFieldLines(ev.data, showFields, labels);
9310
+ if (fieldLines.length > 0) {
9311
+ blocks.push({ type: "divider" });
9312
+ blocks.push({
9313
+ type: "section",
9314
+ text: { type: "mrkdwn", text: fieldLines.join(`
9315
+ `) }
9316
+ });
9317
+ }
9318
+ if (ev.severity !== undefined && ev.severity >= 20) {
9319
+ const errMsg = ev.data?.["error"];
9320
+ const stack = ev.data?.["stack"];
9321
+ if (typeof errMsg === "string" && errMsg) {
9322
+ let errContent = `**❌ Error**: ${errMsg.slice(0, 500)}`;
9323
+ if (typeof stack === "string" && stack) {
9324
+ const stackHead = stack.split(`
9325
+ `).slice(0, 5).join(`
9326
+ `);
9327
+ errContent += "\n```\n" + stackHead.slice(0, 1000) + "\n```";
9328
+ }
9329
+ blocks.push({ type: "divider" });
9330
+ blocks.push({
9331
+ type: "section",
9332
+ text: { type: "mrkdwn", text: errContent }
9333
+ });
9334
+ }
9335
+ }
9336
+ const sessionUrl = ev.data?.["session_url"];
9337
+ const logsUrl = ev.data?.["logs_url"];
9338
+ const slackActions = [];
9339
+ if (isValidCtaUrl(sessionUrl)) {
9340
+ slackActions.push({
9341
+ type: "button",
9342
+ text: { type: "plain_text", text: "查看会话", emoji: true },
9343
+ url: sessionUrl,
9344
+ style: "primary"
9345
+ });
9346
+ }
9347
+ if (isValidCtaUrl(logsUrl)) {
9348
+ slackActions.push({
9349
+ type: "button",
9350
+ text: { type: "plain_text", text: "查看日志", emoji: true },
9351
+ url: logsUrl
9352
+ });
9353
+ }
9354
+ if (slackActions.length > 0) {
9355
+ blocks.push({ type: "actions", elements: slackActions });
9356
+ }
9300
9357
  const footerParts = [`event=\`${ev.event}\``];
9301
9358
  if (ev.session_id)
9302
9359
  footerParts.push(`session=\`${ev.session_id.slice(0, 8)}\``);
@@ -9387,6 +9444,13 @@ function transformLarkToWebhook(ch, ev) {
9387
9444
  }).join(" ");
9388
9445
  const headerTemplate = severityToLarkHeader(ev.severity);
9389
9446
  const elements = [];
9447
+ const sevLabel = ch.show_severity_label !== false ? severityToLabel(ev.severity) : "";
9448
+ if (sevLabel) {
9449
+ elements.push({
9450
+ tag: "div",
9451
+ text: { tag: "lark_md", content: sevLabel }
9452
+ });
9453
+ }
9390
9454
  if (messageText || mentionMarkdown) {
9391
9455
  const fullMsg = [messageText, mentionMarkdown].filter(Boolean).join(`
9392
9456
 
@@ -9396,6 +9460,57 @@ function transformLarkToWebhook(ch, ev) {
9396
9460
  text: { tag: "lark_md", content: fullMsg.slice(0, 3000) }
9397
9461
  });
9398
9462
  }
9463
+ const showFields = ch.show_fields ?? [...DEFAULT_SHOW_FIELDS];
9464
+ const labels = { ...DEFAULT_FIELD_LABELS, ...ch.field_labels ?? {} };
9465
+ const fieldLines = pickFieldLines(ev.data, showFields, labels);
9466
+ if (fieldLines.length > 0) {
9467
+ elements.push({ tag: "hr" });
9468
+ elements.push({
9469
+ tag: "div",
9470
+ text: { tag: "lark_md", content: fieldLines.join(`
9471
+ `) }
9472
+ });
9473
+ }
9474
+ if (ev.severity !== undefined && ev.severity >= 20) {
9475
+ const errMsg = ev.data?.["error"];
9476
+ const stack = ev.data?.["stack"];
9477
+ if (typeof errMsg === "string" && errMsg) {
9478
+ let errContent = `**❌ Error**: ${errMsg.slice(0, 500)}`;
9479
+ if (typeof stack === "string" && stack) {
9480
+ const stackHead = stack.split(`
9481
+ `).slice(0, 5).join(`
9482
+ `);
9483
+ errContent += "\n```\n" + stackHead.slice(0, 1000) + "\n```";
9484
+ }
9485
+ elements.push({ tag: "hr" });
9486
+ elements.push({
9487
+ tag: "div",
9488
+ text: { tag: "lark_md", content: errContent }
9489
+ });
9490
+ }
9491
+ }
9492
+ const sessionUrl = ev.data?.["session_url"];
9493
+ const logsUrl = ev.data?.["logs_url"];
9494
+ const larkActions = [];
9495
+ if (isValidCtaUrl(sessionUrl)) {
9496
+ larkActions.push({
9497
+ tag: "button",
9498
+ text: { tag: "plain_text", content: "查看会话" },
9499
+ type: "primary",
9500
+ url: sessionUrl
9501
+ });
9502
+ }
9503
+ if (isValidCtaUrl(logsUrl)) {
9504
+ larkActions.push({
9505
+ tag: "button",
9506
+ text: { tag: "plain_text", content: "查看日志" },
9507
+ type: "default",
9508
+ url: logsUrl
9509
+ });
9510
+ }
9511
+ if (larkActions.length > 0) {
9512
+ elements.push({ tag: "action", actions: larkActions });
9513
+ }
9399
9514
  const footer = [
9400
9515
  `**event**: \`${ev.event}\``,
9401
9516
  ev.session_id ? `**session**: \`${ev.session_id.slice(0, 8)}\`` : null,
@@ -9431,6 +9546,60 @@ function severityToLarkHeader(sev) {
9431
9546
  return "blue";
9432
9547
  return "grey";
9433
9548
  }
9549
+ function severityToLabel(sev) {
9550
+ if (sev === undefined)
9551
+ return "";
9552
+ if (sev >= 40)
9553
+ return "\uD83D\uDEA8 CRITICAL";
9554
+ if (sev >= 30)
9555
+ return "\uD83D\uDD34 ERROR";
9556
+ if (sev >= 20)
9557
+ return "\uD83D\uDFE0 WARN";
9558
+ if (sev >= 10)
9559
+ return "\uD83D\uDD35 INFO";
9560
+ return "";
9561
+ }
9562
+ var DEFAULT_SHOW_FIELDS = [
9563
+ "agent",
9564
+ "model",
9565
+ "duration_ms",
9566
+ "cost",
9567
+ "files_changed",
9568
+ "status"
9569
+ ];
9570
+ var DEFAULT_FIELD_LABELS = {
9571
+ agent: "Agent",
9572
+ model: "Model",
9573
+ duration_ms: "耗时",
9574
+ cost: "费用",
9575
+ files_changed: "改动文件数",
9576
+ status: "状态",
9577
+ error: "错误"
9578
+ };
9579
+ function pickFieldLines(data, showFields, labels) {
9580
+ if (!data)
9581
+ return [];
9582
+ const out = [];
9583
+ for (const key of showFields) {
9584
+ const v = data[key];
9585
+ if (v === undefined || v === null || v === "")
9586
+ continue;
9587
+ const label = labels[key] ?? key;
9588
+ let valueStr;
9589
+ if (key === "duration_ms" && typeof v === "number") {
9590
+ valueStr = v >= 60000 ? `${(v / 60000).toFixed(1)}m` : `${(v / 1000).toFixed(1)}s`;
9591
+ } else if (key === "cost" && typeof v === "number") {
9592
+ valueStr = `$${v.toFixed(4)}`;
9593
+ } else {
9594
+ valueStr = String(v);
9595
+ }
9596
+ out.push(`**${label}**: ${valueStr}`);
9597
+ }
9598
+ return out;
9599
+ }
9600
+ function isValidCtaUrl(url) {
9601
+ return typeof url === "string" && url.startsWith("https://");
9602
+ }
9434
9603
  function computeLarkSign(secret, timestampSec) {
9435
9604
  const stringToSign = `${timestampSec}
9436
9605
  ${secret}`;
@@ -9524,11 +9693,88 @@ function makeChannelEvent(event, data = {}) {
9524
9693
  }
9525
9694
 
9526
9695
  // plugins/channels.ts
9696
+ init_global_config();
9527
9697
  var PLUGIN_NAME6 = "channels";
9528
9698
  logLifecycle(PLUGIN_NAME6, "import", {});
9529
9699
  var fallbackLog = makePluginLogger(PLUGIN_NAME6);
9530
9700
  var _channelsCache = null;
9531
- function loadChannelsFromEnv() {
9701
+ var _activatedDirectory;
9702
+ var KNOWN_TYPES = new Set([
9703
+ "webhook",
9704
+ "file",
9705
+ "exec",
9706
+ "kh",
9707
+ "mcp",
9708
+ "slack",
9709
+ "lark"
9710
+ ]);
9711
+ var REQUIRED_FIELDS = {
9712
+ webhook: ["url"],
9713
+ file: ["path"],
9714
+ exec: [],
9715
+ kh: [],
9716
+ mcp: ["server", "tool"],
9717
+ slack: ["webhook_url"],
9718
+ lark: ["webhook_url"]
9719
+ };
9720
+ function safePeek(o) {
9721
+ const out = {};
9722
+ for (const k of ["type", "name"]) {
9723
+ if (k in o)
9724
+ out[k] = o[k];
9725
+ }
9726
+ return out;
9727
+ }
9728
+ function isChannel(x, log4) {
9729
+ if (!x || typeof x !== "object")
9730
+ return false;
9731
+ const o = x;
9732
+ const t = o["type"];
9733
+ const n = o["name"];
9734
+ if (typeof n !== "string" || n.length === 0) {
9735
+ log4?.warn(`[channels] 配置被忽略:缺少 name 字段`, { sample: safePeek(o) });
9736
+ return false;
9737
+ }
9738
+ if (typeof t !== "string" || !KNOWN_TYPES.has(t)) {
9739
+ log4?.warn(`[channels] 配置 '${n}' 被忽略:未知 type='${String(t)}',合法值=${[...KNOWN_TYPES].join(",")}`);
9740
+ return false;
9741
+ }
9742
+ const required = REQUIRED_FIELDS[t] ?? [];
9743
+ for (const f of required) {
9744
+ const v = o[f];
9745
+ if (typeof v !== "string" || v.length === 0) {
9746
+ log4?.warn(`[channels] 配置 '${n}' (type=${t}) 被忽略:缺少必填字段 '${f}'`);
9747
+ return false;
9748
+ }
9749
+ }
9750
+ if (t === "exec") {
9751
+ const hasArgv = Array.isArray(o["argv"]) && o["argv"].length > 0;
9752
+ const hasCmd = typeof o["command"] === "string" && o["command"].length > 0;
9753
+ if (!hasArgv && !hasCmd) {
9754
+ log4?.warn(`[channels] 配置 '${n}' (type=exec) 被忽略:argv 与 command 均为空`);
9755
+ return false;
9756
+ }
9757
+ }
9758
+ return true;
9759
+ }
9760
+ function resolveGlobalConfigPath() {
9761
+ return path4.join(globalConfigDir(), "channels.json");
9762
+ }
9763
+ function resolveProjectConfigPaths(root) {
9764
+ const r = root ?? _activatedDirectory ?? process.cwd();
9765
+ return {
9766
+ recommended: path4.join(projectConfigDir(r), "channels.json"),
9767
+ legacy: path4.join(projectConfigDir(r), "config", "channels.json")
9768
+ };
9769
+ }
9770
+ var _warnedKeys2 = new Set;
9771
+ function warnOnce3(key, msg, log4) {
9772
+ if (_warnedKeys2.has(key))
9773
+ return;
9774
+ _warnedKeys2.add(key);
9775
+ log4.warn(msg);
9776
+ }
9777
+ function loadChannelsFromEnv(log4 = fallbackLog) {
9532
9778
  const raw = process.env.CODEFORGE_CHANNELS_JSON;
9533
9779
  if (!raw)
9534
9780
  return [];
@@ -9536,22 +9782,103 @@ function loadChannelsFromEnv() {
9536
9782
  const parsed = JSON.parse(raw);
9537
9783
  if (!Array.isArray(parsed))
9538
9784
  return [];
9539
- return parsed.filter((c) => isChannel(c));
9785
+ return parsed.filter((c) => isChannel(c, log4));
9540
9786
  } catch {
9787
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON JSON 解析失败,忽略整段 env 配置`);
9541
9788
  return [];
9542
9789
  }
9543
9790
  }
9544
- function isChannel(x) {
9545
- if (!x || typeof x !== "object")
9546
- return false;
9547
- const t = x.type;
9548
- const n = x.name;
9549
- return typeof n === "string" && (t === "webhook" || t === "file" || t === "exec" || t === "kh" || t === "mcp");
9791
+ function loadChannelsFromGlobal(log4 = fallbackLog) {
9792
+ const filePath = resolveGlobalConfigPath();
9793
+ const raw = loadJsonIfExists(filePath);
9794
+ if (!raw)
9795
+ return [];
9796
+ try {
9797
+ const st = statSync2(filePath);
9798
+ const perm = st.mode & 511;
9799
+ if ((perm & 63) !== 0) {
9800
+ warnOnce3(`global-perm:${filePath}`, `[channels] 全局 channels.json 权限 0${perm.toString(8)} 含 group/other 可读位,` + `含 webhook 时建议 chmod 600 ${filePath}`, log4);
9801
+ }
9802
+ } catch {}
9803
+ const arr = Array.isArray(raw) ? raw : raw.channels;
9804
+ if (!Array.isArray(arr))
9805
+ return [];
9806
+ return arr.filter((c) => isChannel(c, log4));
9807
+ }
9808
+ function loadChannelsFromFile(root, log4 = fallbackLog) {
9809
+ const { recommended, legacy } = resolveProjectConfigPaths(root);
9810
+ let raw = loadJsonIfExists(recommended);
9811
+ if (raw === null) {
9812
+ const legacyRaw = loadJsonIfExists(legacy);
9813
+ if (legacyRaw !== null) {
9814
+ log4.warn(`[channels] 检测到 0.3.10 兼容路径 ${legacy},建议迁移到 0.3.11 推荐路径 ${recommended}` + `(与 KH 配置惯例统一)。两条路径同时存在时只读推荐路径。`);
9815
+ raw = legacyRaw;
9816
+ }
9817
+ }
9818
+ if (!raw)
9819
+ return [];
9820
+ const arr = Array.isArray(raw) ? raw : raw.channels;
9821
+ if (!Array.isArray(arr))
9822
+ return [];
9823
+ return arr.filter((c) => isChannel(c, log4));
9550
9824
  }
9551
- function ensureChannels() {
9825
+ function parseRawEnvLength(raw) {
9826
+ if (raw === undefined || raw === "")
9827
+ return "not-set";
9828
+ try {
9829
+ const p = JSON.parse(raw);
9830
+ return Array.isArray(p) ? p.length : "invalid";
9831
+ } catch {
9832
+ return "invalid";
9833
+ }
9834
+ }
9835
+ function mergeByName(layers, log4) {
9836
+ const map = new Map;
9837
+ for (const { layer, items } of layers) {
9838
+ for (const c of items) {
9839
+ const prev = map.get(c.name);
9840
+ if (prev) {
9841
+ const typeNote = prev.channel.type !== c.type ? `(type: ${prev.channel.type} → ${c.type})` : "";
9842
+ log4.warn(`[channels] channel '${c.name}' (${layer} 层) 覆盖 ${prev.layer} 层同名配置${typeNote}`);
9843
+ }
9844
+ map.set(c.name, { layer, channel: c });
9845
+ }
9846
+ }
9847
+ return [...map.values()].map((e) => e.channel);
9848
+ }
9849
+ function ensureChannels(log4 = fallbackLog) {
9552
9850
  if (_channelsCache)
9553
9851
  return _channelsCache;
9554
- _channelsCache = loadChannelsFromEnv();
9852
+ const rawEnv = process.env.CODEFORGE_CHANNELS_JSON;
9853
+ const rawLen = parseRawEnvLength(rawEnv);
9854
+ if (rawLen === 0) {
9855
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON 显式为空数组,跳过 global + project 两层(全部通知已禁用)`);
9856
+ _channelsCache = [];
9857
+ return _channelsCache;
9858
+ }
9859
+ const fromGlobal = loadChannelsFromGlobal(log4);
9860
+ const fromFile = loadChannelsFromFile(undefined, log4);
9861
+ const fromEnv = rawLen === "not-set" ? [] : loadChannelsFromEnv(log4);
9862
+ if (rawLen === "not-set") {
9863
+ _channelsCache = mergeByName([
9864
+ { layer: "global", items: fromGlobal },
9865
+ { layer: "project", items: fromFile }
9866
+ ], log4);
9867
+ return _channelsCache;
9868
+ }
9869
+ if (fromEnv.length === 0) {
9870
+ log4.warn(`[channels] CODEFORGE_CHANNELS_JSON 含 ${rawLen === "invalid" ? "非数组" : rawLen} 项但全部被过滤,` + `退回 global + project 两层(global ${fromGlobal.length} 项 + project ${fromFile.length} 项)`);
9871
+ _channelsCache = mergeByName([
9872
+ { layer: "global", items: fromGlobal },
9873
+ { layer: "project", items: fromFile }
9874
+ ], log4);
9875
+ return _channelsCache;
9876
+ }
9877
+ _channelsCache = mergeByName([
9878
+ { layer: "global", items: fromGlobal },
9879
+ { layer: "project", items: fromFile },
9880
+ { layer: "env", items: fromEnv }
9881
+ ], log4);
9555
9882
  return _channelsCache;
9556
9883
  }
9557
9884
  var _limiter = null;
@@ -9698,6 +10025,7 @@ var TRIGGER_EVENT_TYPES3 = new Set([
9698
10025
  "subtasks.completed"
9699
10026
  ]);
9700
10027
  var channelsServer = async (ctx) => {
10028
+ _activatedDirectory = ctx.directory;
9701
10029
  logLifecycle(PLUGIN_NAME6, "activate", {
9702
10030
  directory: ctx.directory,
9703
10031
  triggerEventTypes: [...TRIGGER_EVENT_TYPES3],
@@ -10769,6 +11097,14 @@ function applyAnchor(content, edit, eol, beforeHash) {
10769
11097
  if (!edit.anchor) {
10770
11098
  return { ok: false, reason: "invalid_input", message: "anchor 不能为空" };
10771
11099
  }
11100
+ if (edit.anchor.includes(`
11101
+ `)) {
11102
+ return {
11103
+ ok: false,
11104
+ reason: "invalid_input",
11105
+ message: "anchor 不能含换行符(仅支持单行匹配);多行改动请改用 pending_changes.stage 整文件暂存"
11106
+ };
11107
+ }
10772
11108
  const lines = splitLines(content);
10773
11109
  const hits = findAnchorLines(lines, edit.anchor, edit.regex === true);
10774
11110
  if (hits.length === 0) {
@@ -10971,6 +11307,7 @@ function finish(before, after, beforeHash, affected) {
10971
11307
  // tools/ast-edit.ts
10972
11308
  var description16 = [
10973
11309
  "AST 风格的精确文件编辑:anchor + 哈希校验,避免 LLM 「整文件重写」误改无关代码。",
11310
+ "**anchor 仅支持单行**:含 `\\n` 的多行 anchor 会被直接拒绝(reason=invalid_input);多行改动请改用 `pending_changes.stage` 整文件暂存。",
10974
11311
  "**何时调用**:",
10975
11312
  "- 需要在指定 anchor(行特征)后插入代码(hook 注入、import 添加)",
10976
11313
  "- 重命名一个标识符(rename_symbol,全文件词边界匹配)",
@@ -10978,6 +11315,7 @@ var description16 = [
10978
11315
  "**何时不需要**:",
10979
11316
  "- 整文件重写更合理(直接 pending-changes.stage 整文件)",
10980
11317
  "- 只是 1-2 个字符的 typo(用 edit 工具更直接)",
11318
+ "- 多行 anchor / 跨行匹配(请用 pending_changes.stage 整文件)",
10981
11319
  "**安全约束**:",
10982
11320
  "- 必须传 before_hash(如果文件已存在),不一致会被拒绝",
10983
11321
  "- 默认 auto_stage=true:结果直接进 pending-changes 等待人审,不立刻落盘"
@@ -10995,7 +11333,7 @@ var AnchorAction = Common.extend({
10995
11333
  "insert_after_anchor",
10996
11334
  "insert_before_anchor"
10997
11335
  ]),
10998
- anchor: z17.string().min(1).describe("anchor 文本子串或正则源"),
11336
+ anchor: z17.string().min(1).describe("anchor 文本子串或正则源(必须单行;含 \\n 会被拒绝,多行改动请用 pending_changes.stage)"),
10999
11337
  regex: z17.boolean().optional().describe("anchor 是否按 RegExp 解释,默认 false"),
11000
11338
  occurrence: z17.number().int().min(1).optional().describe("第几次匹配(1-based),默认 1;多次命中时必须显式指定"),
11001
11339
  payload: z17.string().describe("要写入的内容;引擎会按文件原 EOL 处理")
@@ -12201,6 +12539,11 @@ import { z as z26 } from "zod";
12201
12539
  // lib/model-config.ts
12202
12540
  import { promises as fs7 } from "node:fs";
12203
12541
  import * as path10 from "node:path";
12542
+
12543
+ // lib/model-tier.ts
12544
+ var TIER_ORDER = ["quick", "balanced", "deep", "ultra"];
12545
+
12546
+ // lib/model-config.ts
12204
12547
  var CONFIG_FILE = "codeforge.json";
12205
12548
  var DEFAULT_RUNTIME_FALLBACK = {
12206
12549
  enabled: true,
@@ -12305,11 +12648,29 @@ function validateConfig(input) {
12305
12648
  return { ok: false, warnings, error: r.error };
12306
12649
  runtime = r.cfg;
12307
12650
  }
12651
+ let tiers;
12652
+ if (obj.tiers !== undefined) {
12653
+ const r = normalizeTiers(obj.tiers);
12654
+ if (!r.ok)
12655
+ return { ok: false, warnings, error: r.error };
12656
+ tiers = r.cfg;
12657
+ }
12308
12658
  for (const [name, agent] of Object.entries(agents)) {
12309
12659
  if (agent.category && !categories?.[agent.category]) {
12310
12660
  warnings.push(`agent[${name}].category="${agent.category}" 未在 categories 定义,将忽略 category 链`);
12311
12661
  }
12312
12662
  }
12663
+ for (const [name, agent] of Object.entries(agents)) {
12664
+ if (!agent.tier)
12665
+ continue;
12666
+ const mappedCat = tiers?.category_map?.[agent.tier];
12667
+ const hasOverride = agent.tier_overrides?.[agent.tier] !== undefined;
12668
+ if (!mappedCat && !hasOverride) {
12669
+ warnings.push(`agent[${name}].tier="${agent.tier}" 既无 models.tiers.category_map["${agent.tier}"] 映射,` + `也无 tier_overrides["${agent.tier}"];该 agent 将不参与 tier 体系(adapter 返 null)。`);
12670
+ } else if (mappedCat && !categories?.[mappedCat] && !hasOverride) {
12671
+ warnings.push(`agent[${name}].tier="${agent.tier}" 通过 category_map 映射到 "${mappedCat}",` + `但 categories.${mappedCat} 不存在;该 agent 将不参与 tier 体系。`);
12672
+ }
12673
+ }
12313
12674
  return {
12314
12675
  ok: true,
12315
12676
  warnings,
@@ -12318,7 +12679,8 @@ function validateConfig(input) {
12318
12679
  _doc: typeof obj._doc === "string" ? obj._doc : undefined,
12319
12680
  agents,
12320
12681
  categories,
12321
- runtime_fallback: runtime
12682
+ runtime_fallback: runtime,
12683
+ tiers
12322
12684
  }
12323
12685
  };
12324
12686
  }
@@ -12339,6 +12701,23 @@ function normalizeAgent(name, raw) {
12339
12701
  const thinking = o.thinking !== undefined ? normalizeThinking(`agent[${name}]`, o.thinking) : undefined;
12340
12702
  if (thinking && !thinking.ok)
12341
12703
  return { ok: false, error: thinking.error };
12704
+ let tier;
12705
+ if (o.tier !== undefined) {
12706
+ if (typeof o.tier !== "string" || !isValidTierLevel(o.tier)) {
12707
+ return {
12708
+ ok: false,
12709
+ error: `agent[${name}].tier="${String(o.tier)}" 不是合法 TierLevel (期望 ${TIER_ORDER.join("/")})`
12710
+ };
12711
+ }
12712
+ tier = o.tier;
12713
+ }
12714
+ let tierOverrides;
12715
+ if (o.tier_overrides !== undefined) {
12716
+ const r = normalizeTierOverrides(name, o.tier_overrides);
12717
+ if (!r.ok)
12718
+ return { ok: false, error: r.error };
12719
+ tierOverrides = r.value;
12720
+ }
12342
12721
  return {
12343
12722
  ok: true,
12344
12723
  binding: {
@@ -12347,6 +12726,8 @@ function normalizeAgent(name, raw) {
12347
12726
  category: typeof o.category === "string" ? o.category : undefined,
12348
12727
  thinking: thinking?.value,
12349
12728
  fallback_models: fallbacks.value,
12729
+ tier,
12730
+ tier_overrides: tierOverrides,
12350
12731
  _doc: typeof o._doc === "string" ? o._doc : undefined
12351
12732
  }
12352
12733
  };
@@ -12443,6 +12824,99 @@ function normalizeRuntime(raw) {
12443
12824
  cfg._doc = o._doc;
12444
12825
  return { ok: true, cfg };
12445
12826
  }
12827
+ function isValidTierLevel(x) {
12828
+ return typeof x === "string" && TIER_ORDER.includes(x);
12829
+ }
12830
+ function normalizeTierOverrides(agentName, raw) {
12831
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
12832
+ return {
12833
+ ok: false,
12834
+ error: `agent[${agentName}].tier_overrides 必须是 object(不是 array / null / 其他类型)`
12835
+ };
12836
+ }
12837
+ const o = raw;
12838
+ const result = {};
12839
+ for (const [key, value] of Object.entries(o)) {
12840
+ if (!isValidTierLevel(key)) {
12841
+ return {
12842
+ ok: false,
12843
+ error: `agent[${agentName}].tier_overrides.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
12844
+ };
12845
+ }
12846
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
12847
+ return {
12848
+ ok: false,
12849
+ error: `agent[${agentName}].tier_overrides.${key}: 必须是 object`
12850
+ };
12851
+ }
12852
+ const ov = value;
12853
+ const partial = {};
12854
+ if (ov.level !== undefined) {
12855
+ if (ov.level !== key) {
12856
+ return {
12857
+ ok: false,
12858
+ error: `agent[${agentName}].tier_overrides.${key}.level="${String(ov.level)}" 与 key "${key}" 错配`
12859
+ };
12860
+ }
12861
+ partial.level = key;
12862
+ }
12863
+ if (ov.model !== undefined) {
12864
+ if (typeof ov.model !== "string" || !PROVIDER_MODEL_RE.test(ov.model)) {
12865
+ return {
12866
+ ok: false,
12867
+ error: `agent[${agentName}].tier_overrides.${key}.model="${String(ov.model)}" 格式非法 (期望 <provider>/<id>)`
12868
+ };
12869
+ }
12870
+ partial.model = ov.model;
12871
+ }
12872
+ if (ov.thinking !== undefined) {
12873
+ const t = normalizeThinking(`agent[${agentName}].tier_overrides.${key}`, ov.thinking);
12874
+ if (!t.ok)
12875
+ return { ok: false, error: t.error };
12876
+ partial.thinking = t.value;
12877
+ }
12878
+ if (ov.fallback_models !== undefined) {
12879
+ const f = normalizeFallbackList(`agent[${agentName}].tier_overrides.${key}.fallback_models`, ov.fallback_models);
12880
+ if (!f.ok)
12881
+ return { ok: false, error: f.error };
12882
+ partial.fallback_models = f.value;
12883
+ }
12884
+ result[key] = partial;
12885
+ }
12886
+ return { ok: true, value: result };
12887
+ }
12888
+ function normalizeTiers(raw) {
12889
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
12890
+ return { ok: false, error: "models.tiers 必须是 object(不是 array / null)" };
12891
+ }
12892
+ const o = raw;
12893
+ const cfg = {};
12894
+ if (o.category_map !== undefined) {
12895
+ if (!o.category_map || typeof o.category_map !== "object" || Array.isArray(o.category_map)) {
12896
+ return { ok: false, error: "models.tiers.category_map 必须是 object" };
12897
+ }
12898
+ const map = {};
12899
+ for (const [key, value] of Object.entries(o.category_map)) {
12900
+ if (!isValidTierLevel(key)) {
12901
+ return {
12902
+ ok: false,
12903
+ error: `models.tiers.category_map.${key}: 非法 TierLevel key (期望 ${TIER_ORDER.join("/")})`
12904
+ };
12905
+ }
12906
+ if (typeof value !== "string" || value.length === 0) {
12907
+ return {
12908
+ ok: false,
12909
+ error: `models.tiers.category_map.${key}: 必须是非空字符串(category 名)`
12910
+ };
12911
+ }
12912
+ map[key] = value;
12913
+ }
12914
+ cfg.category_map = map;
12915
+ }
12916
+ if (typeof o._doc === "string")
12917
+ cfg._doc = o._doc;
12918
+ return { ok: true, cfg };
12919
+ }
12446
12920
  function resolveAgentModel(config, agent) {
12447
12921
  const a = config.agents[agent];
12448
12922
  if (!a)
@@ -13517,76 +13991,146 @@ var codeforgeToolsServer = async (ctx) => {
13517
13991
  };
13518
13992
  var handler8 = codeforgeToolsServer;
13519
13993
 
13520
- // plugins/hello-world.ts
13521
- init_opencode_plugin_helpers();
13522
- var PLUGIN_NAME9 = "hello-world";
13523
- var PLUGIN_VERSION = "1.0.0";
13524
- try {
13525
- logLifecycle(PLUGIN_NAME9, "import", { version: PLUGIN_VERSION });
13526
- safeWriteLog(PLUGIN_NAME9, { phase: "import", version: PLUGIN_VERSION });
13527
- } catch (importErr) {
13528
- console.error(`[${PLUGIN_NAME9}] import lifecycle log failed:`, importErr);
13529
- }
13530
- var helloWorldServer = async (ctx) => {
13531
- logLifecycle(PLUGIN_NAME9, "activate", {
13532
- version: PLUGIN_VERSION,
13533
- directory: ctx.directory,
13534
- worktree: ctx.worktree,
13535
- projectId: ctx.project?.id
13536
- });
13537
- await safeAsync(PLUGIN_NAME9, "client.app.log", async () => {
13538
- await ctx.client.app.log({
13539
- body: {
13540
- service: PLUGIN_NAME9,
13541
- level: "info",
13542
- message: `${PLUGIN_NAME9} v${PLUGIN_VERSION} activated in ${ctx.directory}`
13543
- }
13994
+ // plugins/kh-auto-context.ts
13995
+ init_kh_client();
13996
+
13997
+ // lib/kh-shared-context.ts
13998
+ var DEFAULT_MAX_PER_SESSION = 20;
13999
+ var DEFAULT_TTL_MS = 5 * 60 * 1000;
14000
+
14001
+ class SessionScopedCache {
14002
+ cache = new Map;
14003
+ inflight = new Map;
14004
+ generation = new Map;
14005
+ ttlMs;
14006
+ maxPerSession;
14007
+ now;
14008
+ constructor(opts = {}) {
14009
+ this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
14010
+ this.maxPerSession = opts.maxPerSession ?? DEFAULT_MAX_PER_SESSION;
14011
+ this.now = opts.now ?? (() => Date.now());
14012
+ }
14013
+ async searchOrGet(args) {
14014
+ const { client, sessionId, query, limit, timeoutMs } = args;
14015
+ const queryHash = this.hashQuery(sessionId, query);
14016
+ const inflightKey = `${sessionId}::${queryHash}`;
14017
+ const sessionMap = this.cache.get(sessionId);
14018
+ if (sessionMap) {
14019
+ const entry = sessionMap.get(queryHash);
14020
+ if (entry && this.now() - entry.cachedAt < this.ttlMs) {
14021
+ sessionMap.delete(queryHash);
14022
+ sessionMap.set(queryHash, entry);
14023
+ return entry.result;
14024
+ }
14025
+ if (entry)
14026
+ sessionMap.delete(queryHash);
14027
+ }
14028
+ const pending = this.inflight.get(inflightKey);
14029
+ if (pending)
14030
+ return pending;
14031
+ const startGen = this.generation.get(sessionId) ?? 0;
14032
+ const promise = this.doSearchAndMaybeCache(client, sessionId, queryHash, query, limit, timeoutMs, startGen).finally(() => {
14033
+ this.inflight.delete(inflightKey);
13544
14034
  });
13545
- });
13546
- return {
13547
- event: async ({ event }) => {
13548
- await safeAsync(PLUGIN_NAME9, "event", async () => {
13549
- const e = event;
13550
- const propKeys = e.properties && typeof e.properties === "object" ? Object.keys(e.properties).slice(0, 10) : [];
13551
- safeWriteLog(PLUGIN_NAME9, {
13552
- hook: "event",
13553
- type: e.type ?? "unknown",
13554
- propKeys
13555
- });
13556
- });
13557
- },
13558
- "chat.message": async (input, output) => {
13559
- await safeAsync(PLUGIN_NAME9, "chat.message", async () => {
13560
- const text = extractUserText(output);
13561
- safeWriteLog(PLUGIN_NAME9, {
13562
- hook: "chat.message",
13563
- sessionID: input.sessionID,
13564
- agent: input.agent,
13565
- messageID: input.messageID,
13566
- textPreview: text.slice(0, 80),
13567
- textLen: text.length,
13568
- partsCount: output.parts?.length ?? 0
13569
- });
13570
- });
13571
- },
13572
- "tool.execute.before": async (input, _output) => {
13573
- await safeAsync(PLUGIN_NAME9, "tool.execute.before", async () => {
13574
- safeWriteLog(PLUGIN_NAME9, {
13575
- hook: "tool.execute.before",
13576
- tool: input.tool,
13577
- sessionID: input.sessionID,
13578
- callID: input.callID
13579
- });
14035
+ this.inflight.set(inflightKey, promise);
14036
+ return promise;
14037
+ }
14038
+ onSessionEnd(sessionId) {
14039
+ this.generation.set(sessionId, (this.generation.get(sessionId) ?? 0) + 1);
14040
+ this.cache.delete(sessionId);
14041
+ const prefix = `${sessionId}::`;
14042
+ for (const key of this.inflight.keys()) {
14043
+ if (key.startsWith(prefix)) {
14044
+ this.inflight.delete(key);
14045
+ }
14046
+ }
14047
+ }
14048
+ _snapshot() {
14049
+ const perSession = {};
14050
+ let total = 0;
14051
+ for (const [sid, map] of this.cache.entries()) {
14052
+ perSession[sid] = map.size;
14053
+ total += map.size;
14054
+ }
14055
+ const generations = {};
14056
+ for (const [sid, g] of this.generation.entries()) {
14057
+ generations[sid] = g;
14058
+ }
14059
+ return {
14060
+ cacheSize: total,
14061
+ inflightSize: this.inflight.size,
14062
+ sessions: Array.from(this.cache.keys()),
14063
+ perSession,
14064
+ generations
14065
+ };
14066
+ }
14067
+ _reset() {
14068
+ this.cache.clear();
14069
+ this.inflight.clear();
14070
+ this.generation.clear();
14071
+ }
14072
+ hashQuery(sessionId, query) {
14073
+ return `${sessionId}::${query}`;
14074
+ }
14075
+ async doSearchAndMaybeCache(client, sessionId, queryHash, query, limit, timeoutMs, startGen) {
14076
+ const result = await this.doSearch(client, query, limit, timeoutMs);
14077
+ const currentGen = this.generation.get(sessionId) ?? 0;
14078
+ if (currentGen === startGen && result.ok) {
14079
+ this.writeCache(sessionId, queryHash, query, result);
14080
+ }
14081
+ return result;
14082
+ }
14083
+ async doSearch(client, query, limit, timeoutMs) {
14084
+ const callPromise = (async () => {
14085
+ try {
14086
+ return await client.search({ query, limit });
14087
+ } catch (err) {
14088
+ return {
14089
+ ok: false,
14090
+ reason: "kh_returned_error",
14091
+ message: err instanceof Error ? err.message : String(err)
14092
+ };
14093
+ }
14094
+ })();
14095
+ if (timeoutMs === undefined || timeoutMs <= 0) {
14096
+ return callPromise;
14097
+ }
14098
+ let timer = null;
14099
+ try {
14100
+ const racer = new Promise((res) => {
14101
+ timer = setTimeout(() => res({
14102
+ ok: false,
14103
+ reason: "kh_returned_error",
14104
+ message: `kh search timeout after ${timeoutMs}ms`
14105
+ }), timeoutMs);
13580
14106
  });
14107
+ return await Promise.race([callPromise, racer]);
14108
+ } finally {
14109
+ if (timer)
14110
+ clearTimeout(timer);
13581
14111
  }
13582
- };
13583
- };
13584
- var handler9 = helloWorldServer;
14112
+ }
14113
+ writeCache(sessionId, queryHash, query, result) {
14114
+ let sessionMap = this.cache.get(sessionId);
14115
+ if (!sessionMap) {
14116
+ sessionMap = new Map;
14117
+ this.cache.set(sessionId, sessionMap);
14118
+ }
14119
+ sessionMap.delete(queryHash);
14120
+ sessionMap.set(queryHash, { query, result, cachedAt: this.now() });
14121
+ while (sessionMap.size > this.maxPerSession) {
14122
+ const oldest = sessionMap.keys().next().value;
14123
+ if (oldest === undefined)
14124
+ break;
14125
+ sessionMap.delete(oldest);
14126
+ }
14127
+ }
14128
+ }
14129
+ var sharedKhCache = new SessionScopedCache;
13585
14130
 
13586
14131
  // plugins/kh-auto-context.ts
13587
- init_kh_client();
13588
14132
  init_opencode_plugin_helpers();
13589
- var PLUGIN_NAME10 = "kh-auto-context";
14133
+ var PLUGIN_NAME9 = "kh-auto-context";
13590
14134
  var INJECTION_MODE = "observe-only";
13591
14135
  function resolveInjectionMode(client) {
13592
14136
  return client.hasTransport() ? "system-injected" : "observe-only";
@@ -13685,7 +14229,7 @@ var CODEFORGE_CONSTRAINTS = [
13685
14229
  ];
13686
14230
  async function seedConstraints(client, log6) {
13687
14231
  if (!client.hasTransport()) {
13688
- log6?.debug?.(`[${PLUGIN_NAME10}] seedConstraints: transport 不可用,跳过`, {});
14232
+ log6?.debug?.(`[${PLUGIN_NAME9}] seedConstraints: transport 不可用,跳过`, {});
13689
14233
  return { ok: false, reason: "transport_unavailable" };
13690
14234
  }
13691
14235
  try {
@@ -13699,7 +14243,7 @@ async function seedConstraints(client, log6) {
13699
14243
  });
13700
14244
  if (result && typeof result === "object" && "ok" in result && result.ok === false) {
13701
14245
  const r = result;
13702
- log6?.warn(`[${PLUGIN_NAME10}] seedConstraints 降级`, {
14246
+ log6?.warn(`[${PLUGIN_NAME9}] seedConstraints 降级`, {
13703
14247
  reason: r.reason,
13704
14248
  message: r.message
13705
14249
  });
@@ -13709,11 +14253,11 @@ async function seedConstraints(client, log6) {
13709
14253
  message: r.message
13710
14254
  };
13711
14255
  }
13712
- log6?.info(`[${PLUGIN_NAME10}] seedConstraints: 已写入 ${CODEFORGE_CONSTRAINTS.length} 条 constraints`, { count: CODEFORGE_CONSTRAINTS.length });
14256
+ log6?.info(`[${PLUGIN_NAME9}] seedConstraints: 已写入 ${CODEFORGE_CONSTRAINTS.length} 条 constraints`, { count: CODEFORGE_CONSTRAINTS.length });
13713
14257
  return { ok: true, itemsWritten: CODEFORGE_CONSTRAINTS.length };
13714
14258
  } catch (err) {
13715
14259
  const message = err instanceof Error ? err.message : String(err);
13716
- log6?.warn(`[${PLUGIN_NAME10}] seedConstraints 失败(已静默)`, { error: message });
14260
+ log6?.warn(`[${PLUGIN_NAME9}] seedConstraints 失败(已静默)`, { error: message });
13717
14261
  return { ok: false, reason: "exception", message };
13718
14262
  }
13719
14263
  }
@@ -13728,7 +14272,7 @@ function createSystemInjectedHook(client, sessionId, log6) {
13728
14272
  });
13729
14273
  if (result && typeof result === "object" && "ok" in result && result.ok === false) {
13730
14274
  const r = result;
13731
- log6?.warn(`[${PLUGIN_NAME10}] system-injected 降级到 observe-only:${r.reason ?? "unknown"}`, {
14275
+ log6?.warn(`[${PLUGIN_NAME9}] system-injected 降级到 observe-only:${r.reason ?? "unknown"}`, {
13732
14276
  sessionId,
13733
14277
  section,
13734
14278
  preview: markdown.slice(0, 200),
@@ -13737,12 +14281,12 @@ function createSystemInjectedHook(client, sessionId, log6) {
13737
14281
  });
13738
14282
  return;
13739
14283
  }
13740
- log6?.info(`[${PLUGIN_NAME10}] system-injected: 写入 KH working_memory 成功 (${markdown.length} chars)`, {
14284
+ log6?.info(`[${PLUGIN_NAME9}] system-injected: 写入 KH working_memory 成功 (${markdown.length} chars)`, {
13741
14285
  sessionId,
13742
14286
  section
13743
14287
  });
13744
14288
  } catch (err) {
13745
- log6?.warn(`[${PLUGIN_NAME10}] system-injected 抛异常,降级到 observe-only`, {
14289
+ log6?.warn(`[${PLUGIN_NAME9}] system-injected 抛异常,降级到 observe-only`, {
13746
14290
  sessionId,
13747
14291
  section,
13748
14292
  preview: markdown.slice(0, 200),
@@ -13751,23 +14295,16 @@ function createSystemInjectedHook(client, sessionId, log6) {
13751
14295
  }
13752
14296
  };
13753
14297
  }
13754
- async function handleMessage2(raw, opts) {
14298
+ var inflight2 = new Set;
14299
+ var INFLIGHT_CAP = 5;
14300
+ function inflightKey(sessionId, query) {
14301
+ return `${sessionId ?? "global"}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
14302
+ }
14303
+ async function runKhSearchAndInject(args) {
14304
+ const { query, ctx, opts, mode } = args;
13755
14305
  const cfg = opts.config ?? DEFAULT_CONFIG5;
13756
- const mode = opts.mode ?? INJECTION_MODE;
13757
- const ctx = raw ?? {};
13758
14306
  const log6 = ctx.log;
13759
- const text = (ctx.content ?? "").trim();
13760
- if (!shouldInject(text, cfg)) {
13761
- log6?.debug?.(`[${PLUGIN_NAME10}] skip (filter)`, { textLen: text.length });
13762
- return null;
13763
- }
13764
- const query = extractQuery(text);
13765
- if (!query)
13766
- return null;
13767
- if (opts.cache.shouldSkip(query)) {
13768
- log6?.debug?.(`[${PLUGIN_NAME10}] cache hit, skip`, { query });
13769
- return null;
13770
- }
14307
+ const startedAt = Date.now();
13771
14308
  const racer = opts.scheduler?.raceTimeout ?? (async (p, ms) => {
13772
14309
  let timer = null;
13773
14310
  try {
@@ -13782,20 +14319,54 @@ async function handleMessage2(raw, opts) {
13782
14319
  clearTimeout(timer);
13783
14320
  }
13784
14321
  });
13785
- const result = await racer(opts.client.search({ query, limit: cfg.limit }), cfg.timeoutMs);
14322
+ let result;
14323
+ try {
14324
+ const searchPromise = sharedKhCache.searchOrGet({
14325
+ client: opts.client,
14326
+ sessionId: ctx.sessionId ?? "global",
14327
+ query,
14328
+ limit: cfg.limit
14329
+ });
14330
+ result = await racer(searchPromise, cfg.timeoutMs);
14331
+ } catch (err) {
14332
+ log6?.warn(`[${PLUGIN_NAME9}] client.search threw (sync or async), return null`, {
14333
+ query,
14334
+ elapsedMs: Date.now() - startedAt,
14335
+ sessionId: ctx.sessionId,
14336
+ error: err instanceof Error ? err.message : String(err)
14337
+ });
14338
+ opts.cache.record(query, []);
14339
+ return null;
14340
+ }
13786
14341
  if (result === "__timeout__") {
13787
- log6?.warn(`[${PLUGIN_NAME10}] timeout`, { query, ms: cfg.timeoutMs });
14342
+ log6?.warn(`[${PLUGIN_NAME9}] timeout`, {
14343
+ query,
14344
+ ms: cfg.timeoutMs,
14345
+ elapsedMs: Date.now() - startedAt,
14346
+ sessionId: ctx.sessionId
14347
+ });
13788
14348
  opts.cache.record(query, []);
13789
14349
  return null;
13790
14350
  }
13791
14351
  if (!result.ok) {
13792
- log6?.debug?.(`[${PLUGIN_NAME10}] kh degraded`, { reason: result.reason });
14352
+ log6?.warn(`[${PLUGIN_NAME9}] kh degraded`, {
14353
+ reason: result.reason,
14354
+ query,
14355
+ elapsedMs: Date.now() - startedAt,
14356
+ sessionId: ctx.sessionId
14357
+ });
13793
14358
  opts.cache.record(query, []);
13794
14359
  return null;
13795
14360
  }
13796
14361
  const filtered = result.insights.filter((i) => (i.confidence ?? 0) >= cfg.minConfidence);
13797
14362
  if (filtered.length === 0) {
13798
14363
  opts.cache.record(query, []);
14364
+ log6?.debug?.(`[${PLUGIN_NAME9}] no candidate above threshold`, {
14365
+ query,
14366
+ rawCount: result.insights.length,
14367
+ elapsedMs: Date.now() - startedAt,
14368
+ sessionId: ctx.sessionId
14369
+ });
13799
14370
  return null;
13800
14371
  }
13801
14372
  const payload = formatInjection(query, filtered, mode);
@@ -13803,22 +14374,68 @@ async function handleMessage2(raw, opts) {
13803
14374
  try {
13804
14375
  await ctx.injectContext(payload.markdown);
13805
14376
  } catch (err) {
13806
- log6?.warn(`[${PLUGIN_NAME10}] injectContext threw`, {
13807
- error: err instanceof Error ? err.message : String(err)
14377
+ log6?.warn(`[${PLUGIN_NAME9}] injectContext threw`, {
14378
+ error: err instanceof Error ? err.message : String(err),
14379
+ query,
14380
+ sessionId: ctx.sessionId
13808
14381
  });
13809
14382
  }
13810
14383
  }
13811
14384
  opts.cache.record(query, filtered);
13812
- log6?.info(`[${PLUGIN_NAME10}] ${mode}: ${filtered.length} candidate insights ready`, { query, mode });
14385
+ log6?.info(`[${PLUGIN_NAME9}] inject complete (${mode})`, {
14386
+ query,
14387
+ mode,
14388
+ candidateCount: filtered.length,
14389
+ elapsedMs: Date.now() - startedAt,
14390
+ sessionId: ctx.sessionId
14391
+ });
13813
14392
  return payload;
13814
14393
  }
13815
- logLifecycle(PLUGIN_NAME10, "import");
14394
+ async function handleMessage2(raw, opts) {
14395
+ const cfg = opts.config ?? DEFAULT_CONFIG5;
14396
+ const mode = opts.mode ?? INJECTION_MODE;
14397
+ const ctx = raw ?? {};
14398
+ const log6 = ctx.log;
14399
+ const text = (ctx.content ?? "").trim();
14400
+ if (!shouldInject(text, cfg)) {
14401
+ log6?.debug?.(`[${PLUGIN_NAME9}] skip (filter)`, { textLen: text.length });
14402
+ return;
14403
+ }
14404
+ const query = extractQuery(text);
14405
+ if (!query)
14406
+ return;
14407
+ if (opts.cache.shouldSkip(query)) {
14408
+ log6?.debug?.(`[${PLUGIN_NAME9}] cache hit, skip`, { query });
14409
+ return;
14410
+ }
14411
+ if (inflight2.size >= INFLIGHT_CAP) {
14412
+ log6?.warn(`[${PLUGIN_NAME9}] inflight cap reached, skip`, {
14413
+ query,
14414
+ inflightSize: inflight2.size,
14415
+ cap: INFLIGHT_CAP,
14416
+ sessionId: ctx.sessionId
14417
+ });
14418
+ return;
14419
+ }
14420
+ const key = inflightKey(ctx.sessionId, query);
14421
+ inflight2.add(key);
14422
+ runKhSearchAndInject({ query, ctx, opts, mode }).catch((err) => {
14423
+ log6?.warn(`[${PLUGIN_NAME9}] runKhSearchAndInject 顶层兜底捕获`, {
14424
+ error: err instanceof Error ? err.message : String(err),
14425
+ query,
14426
+ sessionId: ctx.sessionId
14427
+ });
14428
+ }).finally(() => {
14429
+ inflight2.delete(key);
14430
+ });
14431
+ }
14432
+ logLifecycle(PLUGIN_NAME9, "import");
13816
14433
  var sharedClient2 = new KhClient;
13817
14434
  var sharedCache = new QueryCache(DEFAULT_CONFIG5.cacheTtlMs);
13818
14435
  var khAutoContextServer = async (ctx) => {
13819
- const log6 = makePluginLogger(PLUGIN_NAME10);
14436
+ const log6 = makePluginLogger(PLUGIN_NAME9);
13820
14437
  const runtimeMode = resolveInjectionMode(sharedClient2);
13821
- logLifecycle(PLUGIN_NAME10, "activate", {
14438
+ logLifecycle(PLUGIN_NAME9, "activate", {
13822
14439
  directory: ctx.directory,
13823
14440
  minConfidence: DEFAULT_CONFIG5.minConfidence,
13824
14441
  timeoutMs: DEFAULT_CONFIG5.timeoutMs,
@@ -13832,7 +14449,7 @@ var khAutoContextServer = async (ctx) => {
13832
14449
  });
13833
14450
  return {
13834
14451
  "chat.message": async (input, output) => {
13835
- await safeAsync(PLUGIN_NAME10, "chat.message", async () => {
14452
+ await safeAsync(PLUGIN_NAME9, "chat.message", async () => {
13836
14453
  const text = extractUserText(output);
13837
14454
  if (!text)
13838
14455
  return;
@@ -13850,10 +14467,25 @@ var khAutoContextServer = async (ctx) => {
13850
14467
  log: log6
13851
14468
  }, { client: sharedClient2, cache: sharedCache, mode: runtimeMode });
13852
14469
  });
14470
+ },
14471
+ event: async ({ event }) => {
14472
+ await safeAsync(PLUGIN_NAME9, "event", async () => {
14473
+ const e = event;
14474
+ if (e.type !== "session.idle")
14475
+ return;
14476
+ const props = e.properties;
14477
+ const sid = props?.sessionID;
14478
+ if (typeof sid !== "string" || !sid)
14479
+ return;
14480
+ sharedKhCache.onSessionEnd(sid);
14481
+ log6.debug?.(`[${PLUGIN_NAME9}] session.idle: cleared shared cache`, {
14482
+ sessionID: sid
14483
+ });
14484
+ });
13853
14485
  }
13854
14486
  };
13855
14487
  };
13856
- var handler10 = khAutoContextServer;
14488
+ var handler9 = khAutoContextServer;
13857
14489
 
13858
14490
  // plugins/kh-reminder.ts
13859
14491
  init_opencode_plugin_helpers();
@@ -13974,8 +14606,8 @@ async function condense(input, opts = {}) {
13974
14606
  }
13975
14607
 
13976
14608
  // plugins/kh-reminder.ts
13977
- var PLUGIN_NAME11 = "kh-reminder";
13978
- logLifecycle(PLUGIN_NAME11, "import", {});
14609
+ var PLUGIN_NAME10 = "kh-reminder";
14610
+ logLifecycle(PLUGIN_NAME10, "import", {});
13979
14611
  var TRIGGER_WORDS_ZH = [
13980
14612
  "怎么",
13981
14613
  "怎样",
@@ -14129,7 +14761,7 @@ function handleObserve(raw, log6) {
14129
14761
  const result = evaluate(ctx);
14130
14762
  if (result.triggered && result.reason) {
14131
14763
  const reasonStr = formatReason(result.reason);
14132
- log6?.info(`[${PLUGIN_NAME11}] ⚡ KH 提醒(observe-only) · session=${result.sessionId} · ${reasonStr}`, {
14764
+ log6?.info(`[${PLUGIN_NAME10}] ⚡ KH 提醒(observe-only) · session=${result.sessionId} · ${reasonStr}`, {
14133
14765
  sessionId: result.sessionId,
14134
14766
  reason: result.reason,
14135
14767
  suggestion: "调用 smart_search 查项目历史;完成后用 save_chat_insight 沉淀"
@@ -14137,7 +14769,7 @@ function handleObserve(raw, log6) {
14137
14769
  }
14138
14770
  return result;
14139
14771
  } catch (err) {
14140
- log6?.warn(`[${PLUGIN_NAME11}] evaluate 异常(已隔离)`, {
14772
+ log6?.warn(`[${PLUGIN_NAME10}] evaluate 异常(已隔离)`, {
14141
14773
  error: err instanceof Error ? err.message : String(err)
14142
14774
  });
14143
14775
  return null;
@@ -14153,9 +14785,9 @@ function formatReason(r) {
14153
14785
  return `no-search-in-recent-rounds (last ${r.rounds})`;
14154
14786
  }
14155
14787
  }
14156
- var log6 = makePluginLogger(PLUGIN_NAME11);
14788
+ var log6 = makePluginLogger(PLUGIN_NAME10);
14157
14789
  var khReminderServer = async (ctx) => {
14158
- logLifecycle(PLUGIN_NAME11, "activate", {
14790
+ logLifecycle(PLUGIN_NAME10, "activate", {
14159
14791
  directory: ctx.directory,
14160
14792
  threshold: DEFAULT_THRESHOLD,
14161
14793
  cooldown_ms: COOLDOWN_MS,
@@ -14163,7 +14795,7 @@ var khReminderServer = async (ctx) => {
14163
14795
  });
14164
14796
  return {
14165
14797
  "experimental.chat.messages.transform": async (_input, output) => {
14166
- await safeAsync(PLUGIN_NAME11, "experimental.chat.messages.transform", async () => {
14798
+ await safeAsync(PLUGIN_NAME10, "experimental.chat.messages.transform", async () => {
14167
14799
  const list = output.messages;
14168
14800
  if (!Array.isArray(list) || list.length === 0)
14169
14801
  return;
@@ -14182,7 +14814,7 @@ var khReminderServer = async (ctx) => {
14182
14814
  const result = handleObserve({ messages: flat, sessionId }, log6);
14183
14815
  if (!result)
14184
14816
  return;
14185
- safeWriteLog(PLUGIN_NAME11, {
14817
+ safeWriteLog(PLUGIN_NAME10, {
14186
14818
  hook: "experimental.chat.messages.transform",
14187
14819
  mode: "observe-only",
14188
14820
  sessionId: result.sessionId,
@@ -14195,7 +14827,7 @@ var khReminderServer = async (ctx) => {
14195
14827
  }
14196
14828
  };
14197
14829
  };
14198
- var handler11 = khReminderServer;
14830
+ var handler10 = khReminderServer;
14199
14831
 
14200
14832
  // lib/memories.ts
14201
14833
  import { promises as fs8 } from "node:fs";
@@ -14397,7 +15029,7 @@ function bagOfWordsScore(query, doc) {
14397
15029
 
14398
15030
  // plugins/memories-context.ts
14399
15031
  init_opencode_plugin_helpers();
14400
- var PLUGIN_NAME12 = "memories-context";
15032
+ var PLUGIN_NAME11 = "memories-context";
14401
15033
  var INJECTION_MODE2 = "observe-only";
14402
15034
  var DEFAULT_CONFIG6 = {
14403
15035
  minTextLength: 8,
@@ -14495,14 +15127,14 @@ async function handleMessage3(raw, opts) {
14495
15127
  return await handleDirective(dir, ctx, opts.memCfg, log7);
14496
15128
  }
14497
15129
  if (!shouldRecall(text, cfg)) {
14498
- log7?.debug?.(`[${PLUGIN_NAME12}] skip (filter)`, { textLen: text.length });
15130
+ log7?.debug?.(`[${PLUGIN_NAME11}] skip (filter)`, { textLen: text.length });
14499
15131
  return { kind: "noop", reason: "filtered", mode: INJECTION_MODE2 };
14500
15132
  }
14501
15133
  const query = extractQuery2(text);
14502
15134
  if (!query)
14503
15135
  return { kind: "noop", reason: "empty_query", mode: INJECTION_MODE2 };
14504
15136
  if (cache2.shouldSkip(query)) {
14505
- log7?.debug?.(`[${PLUGIN_NAME12}] cache hit`, { query });
15137
+ log7?.debug?.(`[${PLUGIN_NAME11}] cache hit`, { query });
14506
15138
  return { kind: "noop", reason: "cache", mode: INJECTION_MODE2 };
14507
15139
  }
14508
15140
  const racer = opts.scheduler?.raceTimeout ?? (async (p, ms) => {
@@ -14527,7 +15159,7 @@ async function handleMessage3(raw, opts) {
14527
15159
  }, opts.memCfg);
14528
15160
  const result = await racer(injectPromise, cfg.timeoutMs);
14529
15161
  if (result === "__timeout__") {
14530
- log7?.warn(`[${PLUGIN_NAME12}] timeout`, { query, ms: cfg.timeoutMs });
15162
+ log7?.warn(`[${PLUGIN_NAME11}] timeout`, { query, ms: cfg.timeoutMs });
14531
15163
  cache2.record(query, 0);
14532
15164
  return { kind: "noop", reason: "timeout", mode: INJECTION_MODE2 };
14533
15165
  }
@@ -14539,13 +15171,13 @@ async function handleMessage3(raw, opts) {
14539
15171
  try {
14540
15172
  await ctx.injectContext(result.text);
14541
15173
  } catch (err) {
14542
- log7?.warn(`[${PLUGIN_NAME12}] injectContext threw`, {
15174
+ log7?.warn(`[${PLUGIN_NAME11}] injectContext threw`, {
14543
15175
  error: err instanceof Error ? err.message : String(err)
14544
15176
  });
14545
15177
  }
14546
15178
  }
14547
15179
  cache2.record(query, result.recalled);
14548
- log7?.info(`[${PLUGIN_NAME12}] observe-only: ${result.used}/${result.recalled} memories ready`, { query, mode: INJECTION_MODE2 });
15180
+ log7?.info(`[${PLUGIN_NAME11}] observe-only: ${result.used}/${result.recalled} memories ready`, { query, mode: INJECTION_MODE2 });
14549
15181
  return { kind: "injected", payload: result, mode: INJECTION_MODE2 };
14550
15182
  }
14551
15183
  async function handleDirective(dir, ctx, memCfg, log7) {
@@ -14554,14 +15186,14 @@ async function handleDirective(dir, ctx, memCfg, log7) {
14554
15186
  if (r.ok) {
14555
15187
  const target = r.written_to === "kh" ? "KH" : "本地";
14556
15188
  await safeReply(ctx, `\uD83E\uDDE0 已记住(${dir.scope} / ${target},id=${r.id})`);
14557
- log7?.info(`[${PLUGIN_NAME12}] /remember ok`, {
15189
+ log7?.info(`[${PLUGIN_NAME11}] /remember ok`, {
14558
15190
  scope: dir.scope,
14559
15191
  id: r.id,
14560
15192
  mode: INJECTION_MODE2
14561
15193
  });
14562
15194
  } else {
14563
15195
  await safeReply(ctx, `❌ 记忆失败:${r.error ?? "unknown"}`);
14564
- log7?.warn(`[${PLUGIN_NAME12}] /remember failed`, { error: r.error });
15196
+ log7?.warn(`[${PLUGIN_NAME11}] /remember failed`, { error: r.error });
14565
15197
  }
14566
15198
  return { kind: "remembered", result: r, mode: INJECTION_MODE2 };
14567
15199
  }
@@ -14592,22 +15224,22 @@ async function safeReply(ctx, text) {
14592
15224
  await ctx.reply(text);
14593
15225
  } catch {}
14594
15226
  }
14595
- logLifecycle(PLUGIN_NAME12, "import");
15227
+ logLifecycle(PLUGIN_NAME11, "import");
14596
15228
  var sharedCache2 = new QueryCache2(DEFAULT_CONFIG6.cacheTtlMs);
14597
15229
  function buildMemCfg(directory) {
14598
15230
  return { projectRoot: directory };
14599
15231
  }
14600
15232
  var memoriesContextServer = async (ctx) => {
14601
- const log7 = makePluginLogger(PLUGIN_NAME12);
15233
+ const log7 = makePluginLogger(PLUGIN_NAME11);
14602
15234
  const memCfg = buildMemCfg(ctx.directory);
14603
- logLifecycle(PLUGIN_NAME12, "activate", {
15235
+ logLifecycle(PLUGIN_NAME11, "activate", {
14604
15236
  directory: ctx.directory,
14605
15237
  projectRoot: memCfg.projectRoot,
14606
15238
  mode: INJECTION_MODE2
14607
15239
  });
14608
15240
  return {
14609
15241
  "chat.message": async (input, output) => {
14610
- await safeAsync(PLUGIN_NAME12, "chat.message", async () => {
15242
+ await safeAsync(PLUGIN_NAME11, "chat.message", async () => {
14611
15243
  const text = extractUserText(output);
14612
15244
  if (!text)
14613
15245
  return;
@@ -14633,18 +15265,18 @@ var memoriesContextServer = async (ctx) => {
14633
15265
  }
14634
15266
  };
14635
15267
  };
14636
- var handler12 = memoriesContextServer;
15268
+ var handler11 = memoriesContextServer;
14637
15269
 
14638
15270
  // plugins/model-fallback.ts
14639
15271
  init_opencode_plugin_helpers();
14640
- var PLUGIN_NAME13 = "model-fallback";
15272
+ var PLUGIN_NAME12 = "model-fallback";
14641
15273
  var state2 = {
14642
15274
  config: null,
14643
15275
  configPath: null,
14644
15276
  warnings: [],
14645
15277
  error: null
14646
15278
  };
14647
- logLifecycle(PLUGIN_NAME13, "import");
15279
+ logLifecycle(PLUGIN_NAME12, "import");
14648
15280
  function loadOnce(root) {
14649
15281
  if (state2.config !== null || state2.error !== null)
14650
15282
  return;
@@ -14654,11 +15286,11 @@ function loadOnce(root) {
14654
15286
  state2.configPath = r.path ?? null;
14655
15287
  state2.warnings = r.warnings;
14656
15288
  if (r.warnings.length > 0) {
14657
- safeWriteLog(PLUGIN_NAME13, { phase: "load.warnings", warnings: r.warnings });
15289
+ safeWriteLog(PLUGIN_NAME12, { phase: "load.warnings", warnings: r.warnings });
14658
15290
  }
14659
15291
  } else {
14660
15292
  state2.error = r.error ?? "unknown_load_error";
14661
- safeWriteLog(PLUGIN_NAME13, { phase: "load.failed", error: state2.error, path: r.path });
15293
+ safeWriteLog(PLUGIN_NAME12, { phase: "load.failed", error: state2.error, path: r.path });
14662
15294
  }
14663
15295
  }
14664
15296
  var MODEL_ERR_PATTERNS = [
@@ -14703,9 +15335,9 @@ fallback 链已用尽:${meta.chain.join(" → ")}`;
14703
15335
  完整链:${meta.chain.join(" → ")}`;
14704
15336
  }
14705
15337
  var modelFallbackServer = async (ctx) => {
14706
- const log7 = makePluginLogger(PLUGIN_NAME13);
15338
+ const log7 = makePluginLogger(PLUGIN_NAME12);
14707
15339
  loadOnce(ctx.directory ?? process.cwd());
14708
- logLifecycle(PLUGIN_NAME13, "activate", {
15340
+ logLifecycle(PLUGIN_NAME12, "activate", {
14709
15341
  directory: ctx.directory,
14710
15342
  config_path: state2.configPath,
14711
15343
  config_loaded: state2.config !== null,
@@ -14715,7 +15347,7 @@ var modelFallbackServer = async (ctx) => {
14715
15347
  });
14716
15348
  return {
14717
15349
  "chat.params": async (input, output) => {
14718
- await safeAsync(PLUGIN_NAME13, "chat.params", async () => {
15350
+ await safeAsync(PLUGIN_NAME12, "chat.params", async () => {
14719
15351
  if (!state2.config)
14720
15352
  return;
14721
15353
  const agent = input.agent;
@@ -14736,7 +15368,7 @@ var modelFallbackServer = async (ctx) => {
14736
15368
  next: meta.next_fallback,
14737
15369
  source: meta.source
14738
15370
  };
14739
- safeWriteLog(PLUGIN_NAME13, {
15371
+ safeWriteLog(PLUGIN_NAME12, {
14740
15372
  hook: "chat.params",
14741
15373
  agent,
14742
15374
  model: currentModel,
@@ -14746,7 +15378,7 @@ var modelFallbackServer = async (ctx) => {
14746
15378
  });
14747
15379
  },
14748
15380
  event: async ({ event }) => {
14749
- await safeAsync(PLUGIN_NAME13, "event", async () => {
15381
+ await safeAsync(PLUGIN_NAME12, "event", async () => {
14750
15382
  if (!state2.config)
14751
15383
  return;
14752
15384
  const e = event;
@@ -14762,8 +15394,8 @@ var modelFallbackServer = async (ctx) => {
14762
15394
  const model = props.model ?? "unknown/unknown";
14763
15395
  const meta = buildFallbackMeta(state2.config, agent, model);
14764
15396
  const suggestion = meta ? buildSuggestion(meta, String(message)) : `⚠️ ${agent}/${model} 失败:${message}`;
14765
- log7.warn(`[${PLUGIN_NAME13}] ${suggestion}`);
14766
- safeWriteLog(PLUGIN_NAME13, {
15397
+ log7.warn(`[${PLUGIN_NAME12}] ${suggestion}`);
15398
+ safeWriteLog(PLUGIN_NAME12, {
14767
15399
  hook: "event.error",
14768
15400
  eventType: e.type,
14769
15401
  agent,
@@ -14775,19 +15407,24 @@ var modelFallbackServer = async (ctx) => {
14775
15407
  }
14776
15408
  };
14777
15409
  };
14778
- var handler13 = modelFallbackServer;
15410
+ var handler12 = modelFallbackServer;
14779
15411
 
14780
15412
  // plugins/subtask-heartbeat.ts
14781
15413
  init_opencode_plugin_helpers();
14782
- var PLUGIN_NAME14 = "subtask-heartbeat";
14783
- logLifecycle(PLUGIN_NAME14, "import", {});
15414
+ var PLUGIN_NAME13 = "subtask-heartbeat";
15415
+ logLifecycle(PLUGIN_NAME13, "import", {});
14784
15416
  var HEARTBEAT_INTERVAL_MS2 = 30000;
14785
15417
  var HEARTBEAT_DEBOUNCE_MS = 25000;
14786
15418
  var TOAST_DURATION_MS3 = 5000;
14787
15419
  var START_TOAST_DURATION_MS = 2000;
14788
- var inflight2 = new Map;
15420
+ var PENDING_TASK_TTL_MS = 60000;
15421
+ var PENDING_TASK_MAX_PARENTS = 64;
15422
+ var PENDING_TASK_MAX_PER_PARENT = 16;
15423
+ var DESCRIPTION_MAX_LEN = 60;
15424
+ var inflight3 = new Map;
15425
+ var pendingTask = new Map;
14789
15426
  function _snapshotInflight() {
14790
- return [...inflight2.values()].map((r) => ({ ...r }));
15427
+ return [...inflight3.values()].map((r) => ({ ...r }));
14791
15428
  }
14792
15429
  function getInflightSnapshot() {
14793
15430
  return _snapshotInflight();
@@ -14829,20 +15466,89 @@ function extractEndedSessionID(event) {
14829
15466
  }
14830
15467
  return null;
14831
15468
  }
15469
+ function extractTaskArgs(args) {
15470
+ if (!args || typeof args !== "object")
15471
+ return null;
15472
+ const a = args;
15473
+ const rawDesc = typeof a["description"] === "string" ? a["description"] : null;
15474
+ const rawPrompt = typeof a["prompt"] === "string" ? a["prompt"] : null;
15475
+ const description26 = rawDesc ?? (rawPrompt ? rawPrompt.slice(0, 60) : null);
15476
+ const subagentType = typeof a["subagent_type"] === "string" && a["subagent_type"] || typeof a["agent"] === "string" && a["agent"] || typeof a["agentType"] === "string" && a["agentType"] || typeof a["agent_type"] === "string" && a["agent_type"] || null;
15477
+ if (!description26 && !subagentType)
15478
+ return null;
15479
+ return { description: description26, subagentType };
15480
+ }
15481
+ function enqueuePendingTask(parentID, entry, now = Date.now()) {
15482
+ const ts = entry.ts ?? now;
15483
+ let bucket = pendingTask.get(parentID);
15484
+ if (!bucket) {
15485
+ if (pendingTask.size >= PENDING_TASK_MAX_PARENTS) {
15486
+ let oldestKey = null;
15487
+ let oldestTs = Number.POSITIVE_INFINITY;
15488
+ for (const [k, v] of pendingTask.entries()) {
15489
+ const headTs = v[0]?.ts ?? Number.POSITIVE_INFINITY;
15490
+ if (headTs < oldestTs) {
15491
+ oldestTs = headTs;
15492
+ oldestKey = k;
15493
+ }
15494
+ }
15495
+ if (oldestKey !== null)
15496
+ pendingTask.delete(oldestKey);
15497
+ }
15498
+ bucket = [];
15499
+ pendingTask.set(parentID, bucket);
15500
+ }
15501
+ bucket.push({ agent: entry.agent, description: entry.description, ts });
15502
+ while (bucket.length > PENDING_TASK_MAX_PER_PARENT) {
15503
+ bucket.shift();
15504
+ }
15505
+ }
15506
+ function dequeuePendingTask(parentID, now = Date.now()) {
15507
+ const bucket = pendingTask.get(parentID);
15508
+ if (!bucket || bucket.length === 0) {
15509
+ if (bucket)
15510
+ pendingTask.delete(parentID);
15511
+ return null;
15512
+ }
15513
+ while (bucket.length > 0 && now - bucket[0].ts > PENDING_TASK_TTL_MS) {
15514
+ bucket.shift();
15515
+ }
15516
+ if (bucket.length === 0) {
15517
+ pendingTask.delete(parentID);
15518
+ return null;
15519
+ }
15520
+ const entry = bucket.shift();
15521
+ if (bucket.length === 0)
15522
+ pendingTask.delete(parentID);
15523
+ return entry;
15524
+ }
15525
+ function sweepExpiredPendingTasks(now = Date.now()) {
15526
+ let removed = 0;
15527
+ for (const [parentID, bucket] of [...pendingTask.entries()]) {
15528
+ while (bucket.length > 0 && now - bucket[0].ts > PENDING_TASK_TTL_MS) {
15529
+ bucket.shift();
15530
+ removed++;
15531
+ }
15532
+ if (bucket.length === 0)
15533
+ pendingTask.delete(parentID);
15534
+ }
15535
+ return removed;
15536
+ }
14832
15537
  function registerInflight(payload, now = Date.now()) {
14833
15538
  const r = {
14834
15539
  childID: payload.childID,
14835
15540
  parentID: payload.parentID,
14836
15541
  agent: payload.agent,
15542
+ description: payload.description ?? null,
14837
15543
  startedAt: now,
14838
15544
  lastBeatAt: now,
14839
15545
  lastTool: null
14840
15546
  };
14841
- inflight2.set(payload.childID, r);
15547
+ inflight3.set(payload.childID, r);
14842
15548
  return r;
14843
15549
  }
14844
15550
  function recordToolBeat(sessionID, tool2, now = Date.now()) {
14845
- const r = inflight2.get(sessionID);
15551
+ const r = inflight3.get(sessionID);
14846
15552
  if (!r)
14847
15553
  return null;
14848
15554
  r.lastBeatAt = now;
@@ -14850,15 +15556,15 @@ function recordToolBeat(sessionID, tool2, now = Date.now()) {
14850
15556
  return r;
14851
15557
  }
14852
15558
  function clearInflight2(sessionID) {
14853
- const r = inflight2.get(sessionID);
15559
+ const r = inflight3.get(sessionID);
14854
15560
  if (!r)
14855
15561
  return null;
14856
- inflight2.delete(sessionID);
15562
+ inflight3.delete(sessionID);
14857
15563
  return r;
14858
15564
  }
14859
15565
  function pickHeartbeats(now = Date.now()) {
14860
15566
  const out = [];
14861
- for (const r of inflight2.values()) {
15567
+ for (const r of inflight3.values()) {
14862
15568
  if (now - r.lastBeatAt >= HEARTBEAT_DEBOUNCE_MS)
14863
15569
  out.push(r);
14864
15570
  }
@@ -14870,10 +15576,32 @@ function fmtElapsed(ms) {
14870
15576
  const s = total % 60;
14871
15577
  return m > 0 ? `${m}m${s.toString().padStart(2, "0")}s` : `${s}s`;
14872
15578
  }
15579
+ function titleCase(s) {
15580
+ if (!s)
15581
+ return s;
15582
+ return s.charAt(0).toUpperCase() + s.slice(1);
15583
+ }
15584
+ function sanitizeDescription(s) {
15585
+ const collapsed = s.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
15586
+ if (collapsed.length <= DESCRIPTION_MAX_LEN)
15587
+ return collapsed;
15588
+ return collapsed.slice(0, DESCRIPTION_MAX_LEN) + "…";
15589
+ }
14873
15590
  function buildStartToast(r) {
14874
- const who = r.agent ?? "subagent";
15591
+ if (r.agent && r.description) {
15592
+ return {
15593
+ message: `\uD83D\uDE80 ${titleCase(r.agent)} 启动 — ${sanitizeDescription(r.description)}`,
15594
+ variant: "info"
15595
+ };
15596
+ }
15597
+ if (r.agent) {
15598
+ return {
15599
+ message: `\uD83D\uDE80 ${titleCase(r.agent)} 启动`,
15600
+ variant: "info"
15601
+ };
15602
+ }
14875
15603
  return {
14876
- message: `\uD83D\uDE80 子 session 启动: ${who}`,
15604
+ message: `\uD83D\uDE80 子 session 启动: subagent`,
14877
15605
  variant: "info"
14878
15606
  };
14879
15607
  }
@@ -14886,15 +15614,32 @@ function buildHeartbeatToast(r, now = Date.now()) {
14886
15614
  };
14887
15615
  }
14888
15616
  function buildEndToast(r, type, now = Date.now()) {
14889
- const who = r.agent ?? "subagent";
14890
15617
  const elapsed = fmtElapsed(now - r.startedAt);
14891
- if (type === "session.error") {
14892
- return { message: `❌ ${who} 失败 (${elapsed})`, variant: "error" };
15618
+ const variant = type === "session.error" || type === "session.deleted" ? "error" : "success";
15619
+ const emoji = type === "session.error" ? "❌" : type === "session.deleted" ? "\uD83D\uDDD1️" : "";
15620
+ const verb = type === "session.error" ? "失败" : type === "session.deleted" ? "被取消" : "完成";
15621
+ if (r.agent && r.description) {
15622
+ if (type === "session.idle" || type !== "session.error" && type !== "session.deleted") {
15623
+ return {
15624
+ message: `${emoji} ${titleCase(r.agent)} Task — ${sanitizeDescription(r.description)} (${elapsed})`,
15625
+ variant
15626
+ };
15627
+ }
15628
+ return {
15629
+ message: `${emoji} ${titleCase(r.agent)} ${verb} — ${sanitizeDescription(r.description)} (${elapsed})`,
15630
+ variant
15631
+ };
14893
15632
  }
14894
- if (type === "session.deleted") {
14895
- return { message: `\uD83D\uDDD1️ ${who} 被取消 (${elapsed})`, variant: "error" };
15633
+ if (r.agent) {
15634
+ return {
15635
+ message: `${emoji} ${titleCase(r.agent)} ${verb} (${elapsed})`,
15636
+ variant
15637
+ };
14896
15638
  }
14897
- return { message: `✅ ${who} 完成 (${elapsed})`, variant: "success" };
15639
+ return {
15640
+ message: `${emoji} subagent ${verb} (${elapsed})`,
15641
+ variant
15642
+ };
14898
15643
  }
14899
15644
  function normalizeVariant3(raw) {
14900
15645
  if (raw === "info" || raw === "warning")
@@ -14923,22 +15668,26 @@ async function showToast3(client, payload, log7) {
14923
15668
  return false;
14924
15669
  }
14925
15670
  }
14926
- var log7 = makePluginLogger(PLUGIN_NAME14);
15671
+ var log7 = makePluginLogger(PLUGIN_NAME13);
14927
15672
  var subtaskHeartbeatServer = async (ctx) => {
14928
- logLifecycle(PLUGIN_NAME14, "activate", {
15673
+ logLifecycle(PLUGIN_NAME13, "activate", {
14929
15674
  directory: ctx.directory,
14930
15675
  intervalMs: HEARTBEAT_INTERVAL_MS2
14931
15676
  });
14932
15677
  const client = ctx.client;
14933
15678
  const interval = setInterval(() => {
14934
- safeAsync(PLUGIN_NAME14, "interval", async () => {
15679
+ safeAsync(PLUGIN_NAME13, "interval", async () => {
15680
+ const swept = sweepExpiredPendingTasks();
15681
+ if (swept > 0) {
15682
+ safeWriteLog(PLUGIN_NAME13, { hook: "interval", pending_task_swept: swept });
15683
+ }
14935
15684
  const beats = pickHeartbeats();
14936
15685
  if (beats.length === 0)
14937
15686
  return;
14938
15687
  for (const r of beats) {
14939
15688
  const t = buildHeartbeatToast(r);
14940
15689
  const sent = await showToast3(client, t, log7);
14941
- safeWriteLog(PLUGIN_NAME14, {
15690
+ safeWriteLog(PLUGIN_NAME13, {
14942
15691
  hook: "interval",
14943
15692
  child: r.childID,
14944
15693
  parent: r.parentID,
@@ -14955,23 +15704,33 @@ var subtaskHeartbeatServer = async (ctx) => {
14955
15704
  }
14956
15705
  return {
14957
15706
  event: async ({ event }) => {
14958
- await safeAsync(PLUGIN_NAME14, "event", async () => {
15707
+ await safeAsync(PLUGIN_NAME13, "event", async () => {
14959
15708
  const created = extractCreatedChild(event);
14960
15709
  if (created) {
14961
- const record = registerInflight(created);
14962
- safeWriteLog(PLUGIN_NAME14, {
15710
+ const pending = dequeuePendingTask(created.parentID);
15711
+ const record = registerInflight({
15712
+ childID: created.childID,
15713
+ parentID: created.parentID,
15714
+ agent: pending?.agent ?? created.agent,
15715
+ description: pending?.description ?? null
15716
+ });
15717
+ safeWriteLog(PLUGIN_NAME13, {
14963
15718
  hook: "event",
14964
15719
  type: "session.created",
14965
15720
  child: created.childID,
14966
- parent: created.parentID
15721
+ parent: created.parentID,
15722
+ pending_task_matched: pending !== null,
15723
+ agent: record.agent,
15724
+ description_len: record.description?.length ?? 0
14967
15725
  });
14968
15726
  const startToast = buildStartToast(record);
14969
15727
  const sent = await showToast3(client, { ...startToast, duration: START_TOAST_DURATION_MS }, log7);
14970
- safeWriteLog(PLUGIN_NAME14, {
15728
+ safeWriteLog(PLUGIN_NAME13, {
14971
15729
  hook: "event",
14972
15730
  type: "session.created.toast",
14973
15731
  child: created.childID,
14974
- toast_sent: sent
15732
+ toast_sent: sent,
15733
+ start_toast_message: startToast.message
14975
15734
  });
14976
15735
  return;
14977
15736
  }
@@ -14981,7 +15740,7 @@ var subtaskHeartbeatServer = async (ctx) => {
14981
15740
  if (r) {
14982
15741
  const t = buildEndToast(r, ended.type);
14983
15742
  const sent = await showToast3(client, t, log7);
14984
- safeWriteLog(PLUGIN_NAME14, {
15743
+ safeWriteLog(PLUGIN_NAME13, {
14985
15744
  hook: "event",
14986
15745
  type: ended.type,
14987
15746
  child: r.childID,
@@ -14993,21 +15752,48 @@ var subtaskHeartbeatServer = async (ctx) => {
14993
15752
  }
14994
15753
  });
14995
15754
  },
14996
- "tool.execute.before": async (input) => {
14997
- await safeAsync(PLUGIN_NAME14, "tool.execute.before", async () => {
15755
+ "tool.execute.before": async (input, output) => {
15756
+ const isTaskTool = input?.tool === "task";
15757
+ if (inflight3.size === 0 && !isTaskTool)
15758
+ return;
15759
+ await safeAsync(PLUGIN_NAME13, "tool.execute.before", async () => {
14998
15760
  if (!input || typeof input.sessionID !== "string" || typeof input.tool !== "string")
14999
15761
  return;
15000
- recordToolBeat(input.sessionID, input.tool);
15762
+ if (isTaskTool) {
15763
+ const args = output?.args ?? null;
15764
+ safeWriteLog(PLUGIN_NAME13, {
15765
+ hook: "tool.execute.before.task",
15766
+ sessionID: input.sessionID,
15767
+ args_keys: args && typeof args === "object" ? Object.keys(args) : null,
15768
+ args_raw: args
15769
+ });
15770
+ const extracted = extractTaskArgs(args);
15771
+ if (extracted) {
15772
+ enqueuePendingTask(input.sessionID, {
15773
+ agent: extracted.subagentType,
15774
+ description: extracted.description
15775
+ });
15776
+ safeWriteLog(PLUGIN_NAME13, {
15777
+ hook: "tool.execute.before.task.enqueued",
15778
+ parent: input.sessionID,
15779
+ agent: extracted.subagentType,
15780
+ description_len: extracted.description?.length ?? 0
15781
+ });
15782
+ }
15783
+ }
15784
+ if (inflight3.has(input.sessionID)) {
15785
+ recordToolBeat(input.sessionID, input.tool);
15786
+ }
15001
15787
  });
15002
15788
  }
15003
15789
  };
15004
15790
  };
15005
- var handler14 = subtaskHeartbeatServer;
15791
+ var handler13 = subtaskHeartbeatServer;
15006
15792
 
15007
15793
  // plugins/parallel-status.ts
15008
15794
  init_opencode_plugin_helpers();
15009
- var PLUGIN_NAME15 = "parallel-status";
15010
- logLifecycle(PLUGIN_NAME15, "import");
15795
+ var PLUGIN_NAME14 = "parallel-status";
15796
+ logLifecycle(PLUGIN_NAME14, "import");
15011
15797
  var ID_MAX_LEN = 16;
15012
15798
  var ID_KEEP_LEN = 13;
15013
15799
  function shortId(s) {
@@ -15039,8 +15825,8 @@ function formatInflightMarkdown(snapshot, now = Date.now()) {
15039
15825
  `);
15040
15826
  }
15041
15827
  var parallelStatusServer = async (ctx) => {
15042
- const log8 = makePluginLogger(PLUGIN_NAME15);
15043
- logLifecycle(PLUGIN_NAME15, "activate", { directory: ctx.directory });
15828
+ const log8 = makePluginLogger(PLUGIN_NAME14);
15829
+ logLifecycle(PLUGIN_NAME14, "activate", { directory: ctx.directory });
15044
15830
  return {
15045
15831
  "command.execute.before": async (input, output) => {
15046
15832
  try {
@@ -15059,21 +15845,21 @@ var parallelStatusServer = async (ctx) => {
15059
15845
  synthetic: false
15060
15846
  });
15061
15847
  }
15062
- log8.info(`[${PLUGIN_NAME15}] 已回写 ${snapshot.length} 条 inflight`);
15848
+ log8.info(`[${PLUGIN_NAME14}] 已回写 ${snapshot.length} 条 inflight`);
15063
15849
  } catch (err) {
15064
- log8.error(`[${PLUGIN_NAME15}] command.execute.before 异常(已隔离)`, {
15850
+ log8.error(`[${PLUGIN_NAME14}] command.execute.before 异常(已隔离)`, {
15065
15851
  error: err instanceof Error ? err.message : String(err)
15066
15852
  });
15067
15853
  }
15068
15854
  }
15069
15855
  };
15070
15856
  };
15071
- var handler15 = parallelStatusServer;
15857
+ var handler14 = parallelStatusServer;
15072
15858
 
15073
15859
  // plugins/pwsh-utf8.ts
15074
15860
  init_opencode_plugin_helpers();
15075
- var PLUGIN_NAME16 = "pwsh-utf8";
15076
- logLifecycle(PLUGIN_NAME16, "import", {});
15861
+ var PLUGIN_NAME15 = "pwsh-utf8";
15862
+ logLifecycle(PLUGIN_NAME15, "import", {});
15077
15863
  var PRELUDE = "chcp 65001 *> $null; " + "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); " + "$OutputEncoding = [System.Text.UTF8Encoding]::new(); ";
15078
15864
  function prependUtf8Prelude(command) {
15079
15865
  if (typeof command !== "string")
@@ -15086,14 +15872,15 @@ function prependUtf8Prelude(command) {
15086
15872
  return command;
15087
15873
  return PRELUDE + command;
15088
15874
  }
15089
- var handler16 = async (_ctx) => {
15875
+ var handler15 = async (_ctx) => {
15090
15876
  const enabled = process.platform === "win32" && process.env.CODEFORGE_DISABLE_PWSH_UTF8 !== "1";
15091
- logLifecycle(PLUGIN_NAME16, "activate", { enabled, platform: process.platform });
15877
+ const reason = enabled ? "win32" : process.platform !== "win32" ? "non-win32" : "disabled-by-env";
15878
+ logLifecycle(PLUGIN_NAME15, "activate", { enabled, platform: process.platform, reason });
15092
15879
  if (!enabled)
15093
15880
  return {};
15094
15881
  return {
15095
15882
  "tool.execute.before": async (input, output) => {
15096
- await safeAsync(PLUGIN_NAME16, "tool.execute.before", async () => {
15883
+ await safeAsync(PLUGIN_NAME15, "tool.execute.before", async () => {
15097
15884
  if (input.tool !== "bash")
15098
15885
  return;
15099
15886
  const args = output.args ?? {};
@@ -15102,7 +15889,7 @@ var handler16 = async (_ctx) => {
15102
15889
  if (next !== undefined && next !== original) {
15103
15890
  args["command"] = next;
15104
15891
  output.args = args;
15105
- safeWriteLog(PLUGIN_NAME16, {
15892
+ safeWriteLog(PLUGIN_NAME15, {
15106
15893
  hook: "tool.execute.before",
15107
15894
  tool: input.tool,
15108
15895
  callID: input.callID,
@@ -15295,7 +16082,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
15295
16082
  });
15296
16083
  }
15297
16084
  }
15298
- const inflight3 = [...toolMap.values()].sort((a, b) => b.last_ts - a.last_ts);
16085
+ const inflight4 = [...toolMap.values()].sort((a, b) => b.last_ts - a.last_ts);
15299
16086
  const proposed = window.some((t) => PENDING_CHANGES_TOOLS.has(t.tool));
15300
16087
  const applied = window.some((t) => APPLY_TOOLS.has(t.tool) && t.ok);
15301
16088
  const pending_changes_likely = proposed && !applied;
@@ -15319,7 +16106,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
15319
16106
  idleMs,
15320
16107
  lastUser,
15321
16108
  lastAgent,
15322
- inflight: inflight3,
16109
+ inflight: inflight4,
15323
16110
  pending_changes_likely,
15324
16111
  open_subtasks_likely,
15325
16112
  reason
@@ -15330,7 +16117,7 @@ function buildRecoveryPlan(events, meta = { id: null }, opts = {}) {
15330
16117
  idle_ms: idleMs,
15331
16118
  last_user_intent: lastUser,
15332
16119
  last_agent: lastAgent,
15333
- inflight_tools: inflight3,
16120
+ inflight_tools: inflight4,
15334
16121
  pending_changes_likely,
15335
16122
  open_subtasks_likely,
15336
16123
  summary
@@ -15435,8 +16222,8 @@ function isRecoveryWorthShowing(plan) {
15435
16222
  }
15436
16223
 
15437
16224
  // plugins/session-recovery.ts
15438
- var PLUGIN_NAME17 = "session-recovery";
15439
- logLifecycle(PLUGIN_NAME17, "import", {});
16225
+ var PLUGIN_NAME16 = "session-recovery";
16226
+ logLifecycle(PLUGIN_NAME16, "import", {});
15440
16227
  async function processSessionStart(currentSessionId, opts = {}) {
15441
16228
  if (opts.disabled) {
15442
16229
  return { ok: true, injected: false, reason: "disabled" };
@@ -15446,7 +16233,7 @@ async function processSessionStart(currentSessionId, opts = {}) {
15446
16233
  excludeIds.add(currentSessionId);
15447
16234
  const r = await scanLastSession({ ...opts, excludeIds: [...excludeIds] });
15448
16235
  if (!r.ok) {
15449
- opts.log?.warn?.(`[${PLUGIN_NAME17}] 扫描失败:${r.error}`);
16236
+ opts.log?.warn?.(`[${PLUGIN_NAME16}] 扫描失败:${r.error}`);
15450
16237
  return { ok: false, injected: false, reason: "scan_error", error: r.error };
15451
16238
  }
15452
16239
  const plan = r.plan;
@@ -15460,7 +16247,7 @@ async function processSessionStart(currentSessionId, opts = {}) {
15460
16247
  await opts.injectRecovery(injection);
15461
16248
  } catch (err) {
15462
16249
  const msg = err instanceof Error ? err.message : String(err);
15463
- opts.log?.warn?.(`[${PLUGIN_NAME17}] injectRecovery 异常:${msg}`);
16250
+ opts.log?.warn?.(`[${PLUGIN_NAME16}] injectRecovery 异常:${msg}`);
15464
16251
  return { ok: false, injected: false, plan, reason: "inject_error", error: msg };
15465
16252
  }
15466
16253
  }
@@ -15489,13 +16276,13 @@ function renderPrompt(plan) {
15489
16276
  return lines.join(`
15490
16277
  `);
15491
16278
  }
15492
- var log8 = makePluginLogger(PLUGIN_NAME17);
16279
+ var log8 = makePluginLogger(PLUGIN_NAME16);
15493
16280
  var _lastInjection = null;
15494
16281
  var sessionRecoveryServer = async (ctx) => {
15495
- logLifecycle(PLUGIN_NAME17, "activate", { directory: ctx.directory });
16282
+ logLifecycle(PLUGIN_NAME16, "activate", { directory: ctx.directory });
15496
16283
  return {
15497
16284
  event: async ({ event }) => {
15498
- await safeAsync(PLUGIN_NAME17, "event", async () => {
16285
+ await safeAsync(PLUGIN_NAME16, "event", async () => {
15499
16286
  const e = event;
15500
16287
  if (!e || typeof e.type !== "string")
15501
16288
  return;
@@ -15510,7 +16297,7 @@ var sessionRecoveryServer = async (ctx) => {
15510
16297
  _lastInjection = inj;
15511
16298
  }
15512
16299
  });
15513
- safeWriteLog(PLUGIN_NAME17, {
16300
+ safeWriteLog(PLUGIN_NAME16, {
15514
16301
  hook: "event",
15515
16302
  type: "session.start",
15516
16303
  ok: r.ok,
@@ -15519,13 +16306,13 @@ var sessionRecoveryServer = async (ctx) => {
15519
16306
  last_session_id: r.plan?.last_session_id
15520
16307
  });
15521
16308
  if (r.injected && r.plan) {
15522
- log8.info(`[${PLUGIN_NAME17}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
16309
+ log8.info(`[${PLUGIN_NAME16}] 注入恢复提示(last=${r.plan.last_session_id?.slice(0, 8) ?? "?"}, reason=${r.plan.reason})`);
15523
16310
  }
15524
16311
  });
15525
16312
  }
15526
16313
  };
15527
16314
  };
15528
- var handler17 = sessionRecoveryServer;
16315
+ var handler16 = sessionRecoveryServer;
15529
16316
 
15530
16317
  // plugins/subtasks.ts
15531
16318
  import { promises as fs10 } from "node:fs";
@@ -16050,7 +16837,7 @@ async function sendParentNotice(client, sessionID, text, opts = {}) {
16050
16837
  // plugins/subtasks.ts
16051
16838
  init_opencode_plugin_helpers();
16052
16839
  init_runtime_paths();
16053
- var PLUGIN_NAME18 = "subtasks";
16840
+ var PLUGIN_NAME17 = "subtasks";
16054
16841
  function getLogFile(root = process.cwd()) {
16055
16842
  return path13.join(runtimeDir(root), "logs", "subtasks.log");
16056
16843
  }
@@ -16129,7 +16916,7 @@ async function handleParallelCommand(raw) {
16129
16916
  specs = splitDescriptions(args.description, { parentId });
16130
16917
  }
16131
16918
  if (specs.length === 0) {
16132
- log9?.warn(`[${PLUGIN_NAME18}] /parallel 缺有效子任务`);
16919
+ log9?.warn(`[${PLUGIN_NAME17}] /parallel 缺有效子任务`);
16133
16920
  await safeReply2(ctx, "⚠ /parallel 需要至少 1 个子任务(用 ; 或换行分隔)");
16134
16921
  return { ok: false, reason: "no_subtasks" };
16135
16922
  }
@@ -16187,7 +16974,7 @@ async function handleParallelCommand(raw) {
16187
16974
  });
16188
16975
  } catch (err) {
16189
16976
  const msg = err instanceof Error ? err.message : String(err);
16190
- log9?.error(`[${PLUGIN_NAME18}] schedule 抛错`, { error: msg });
16977
+ log9?.error(`[${PLUGIN_NAME17}] schedule 抛错`, { error: msg });
16191
16978
  await safeReply2(ctx, `❌ 并发调度失败:${msg}`);
16192
16979
  return { ok: false, reason: msg };
16193
16980
  }
@@ -16195,7 +16982,7 @@ async function handleParallelCommand(raw) {
16195
16982
  const summaryLines = [head, "", "```", result.digest.text, "```"];
16196
16983
  await safeReply2(ctx, summaryLines.join(`
16197
16984
  `));
16198
- log9?.info(`[${PLUGIN_NAME18}] schedule 完成`, {
16985
+ log9?.info(`[${PLUGIN_NAME17}] schedule 完成`, {
16199
16986
  parentId,
16200
16987
  success: result.digest.success,
16201
16988
  failed: result.digest.failed,
@@ -16205,7 +16992,7 @@ async function handleParallelCommand(raw) {
16205
16992
  try {
16206
16993
  await ctx.onCompleted(result);
16207
16994
  } catch (err) {
16208
- log9?.warn(`[${PLUGIN_NAME18}] onCompleted hook 抛错(已隔离)`, {
16995
+ log9?.warn(`[${PLUGIN_NAME17}] onCompleted hook 抛错(已隔离)`, {
16209
16996
  error: err instanceof Error ? err.message : String(err)
16210
16997
  });
16211
16998
  }
@@ -16246,7 +17033,7 @@ async function writeLog(level, msg, data) {
16246
17033
  const line = JSON.stringify({
16247
17034
  ts: new Date().toISOString(),
16248
17035
  level,
16249
- plugin: PLUGIN_NAME18,
17036
+ plugin: PLUGIN_NAME17,
16250
17037
  msg,
16251
17038
  data
16252
17039
  }) + `
@@ -16257,11 +17044,11 @@ async function writeLog(level, msg, data) {
16257
17044
  await fs10.appendFile(logFile, line, "utf8");
16258
17045
  } catch {}
16259
17046
  }
16260
- logLifecycle(PLUGIN_NAME18, "import");
17047
+ logLifecycle(PLUGIN_NAME17, "import");
16261
17048
  var subtasksServer = async (ctx) => {
16262
- const log9 = makePluginLogger(PLUGIN_NAME18);
17049
+ const log9 = makePluginLogger(PLUGIN_NAME17);
16263
17050
  const client = ctx?.client ?? undefined;
16264
- logLifecycle(PLUGIN_NAME18, "activate", {
17051
+ logLifecycle(PLUGIN_NAME17, "activate", {
16265
17052
  directory: ctx.directory,
16266
17053
  hasClient: Boolean(client)
16267
17054
  });
@@ -16321,20 +17108,20 @@ var subtasksServer = async (ctx) => {
16321
17108
  });
16322
17109
  }
16323
17110
  } catch (err) {
16324
- log9.error(`[${PLUGIN_NAME18}] command.execute.before 异常(已隔离)`, {
17111
+ log9.error(`[${PLUGIN_NAME17}] command.execute.before 异常(已隔离)`, {
16325
17112
  error: err instanceof Error ? err.message : String(err)
16326
17113
  });
16327
17114
  }
16328
17115
  }
16329
17116
  };
16330
17117
  };
16331
- var handler18 = subtasksServer;
17118
+ var handler17 = subtasksServer;
16332
17119
 
16333
17120
  // plugins/terminal-monitor.ts
16334
17121
  init_opencode_plugin_helpers();
16335
17122
  import * as crypto5 from "node:crypto";
16336
- var PLUGIN_NAME19 = "terminal-monitor";
16337
- logLifecycle(PLUGIN_NAME19, "import", {});
17123
+ var PLUGIN_NAME18 = "terminal-monitor";
17124
+ logLifecycle(PLUGIN_NAME18, "import", {});
16338
17125
  var DEFAULT_CONFIG7 = {
16339
17126
  minScore: 0.6,
16340
17127
  cooldownMs: 30000,
@@ -16616,17 +17403,17 @@ function describeError(err) {
16616
17403
  return String(err);
16617
17404
  }
16618
17405
  }
16619
- var log9 = makePluginLogger(PLUGIN_NAME19);
17406
+ var log9 = makePluginLogger(PLUGIN_NAME18);
16620
17407
  var lru = new FingerprintLRU2;
16621
17408
  var _lastNotification = null;
16622
17409
  var terminalMonitorServer = async (ctx) => {
16623
- logLifecycle(PLUGIN_NAME19, "activate", {
17410
+ logLifecycle(PLUGIN_NAME18, "activate", {
16624
17411
  directory: ctx.directory,
16625
17412
  triggerEventTypes: ["terminal.output", "terminal.exit"]
16626
17413
  });
16627
17414
  return {
16628
17415
  event: async ({ event }) => {
16629
- await safeAsync(PLUGIN_NAME19, "event", async () => {
17416
+ await safeAsync(PLUGIN_NAME18, "event", async () => {
16630
17417
  const e = event;
16631
17418
  if (!e || typeof e.type !== "string")
16632
17419
  return;
@@ -16640,7 +17427,7 @@ var terminalMonitorServer = async (ctx) => {
16640
17427
  _lastNotification = msg;
16641
17428
  }
16642
17429
  });
16643
- safeWriteLog(PLUGIN_NAME19, {
17430
+ safeWriteLog(PLUGIN_NAME18, {
16644
17431
  hook: "event",
16645
17432
  type: e.type,
16646
17433
  notified: r.notified,
@@ -16648,18 +17435,18 @@ var terminalMonitorServer = async (ctx) => {
16648
17435
  findings: r.notification?.findings.length ?? 0
16649
17436
  });
16650
17437
  if (r.notified && r.notification) {
16651
- log9.info(`[${PLUGIN_NAME19}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
17438
+ log9.info(`[${PLUGIN_NAME18}] 反馈 ${r.notification.findings.length} 条 → ${r.notification.summary}`);
16652
17439
  }
16653
17440
  });
16654
17441
  }
16655
17442
  };
16656
17443
  };
16657
- var handler19 = terminalMonitorServer;
17444
+ var handler18 = terminalMonitorServer;
16658
17445
 
16659
17446
  // plugins/token-manager.ts
16660
17447
  init_opencode_plugin_helpers();
16661
- var PLUGIN_NAME20 = "token-manager";
16662
- logLifecycle(PLUGIN_NAME20, "import", {});
17448
+ var PLUGIN_NAME19 = "token-manager";
17449
+ logLifecycle(PLUGIN_NAME19, "import", {});
16663
17450
  async function handleMessageBefore(raw, log10, defaults) {
16664
17451
  const ctx = raw ?? {};
16665
17452
  if (!Array.isArray(ctx.messages) || ctx.messages.length === 0)
@@ -16679,21 +17466,21 @@ async function handleMessageBefore(raw, log10, defaults) {
16679
17466
  };
16680
17467
  if (r.compressed) {
16681
17468
  ctx.messages = r.messages;
16682
- log10?.info(`[${PLUGIN_NAME20}] 压缩 ${r.before.count}→${r.after.count} 条 / ${r.before.tokens}→${r.after.tokens} tokens`);
17469
+ log10?.info(`[${PLUGIN_NAME19}] 压缩 ${r.before.count}→${r.after.count} 条 / ${r.before.tokens}→${r.after.tokens} tokens`);
16683
17470
  }
16684
17471
  return r;
16685
17472
  } catch (err) {
16686
- log10?.warn(`[${PLUGIN_NAME20}] 压缩异常(已隔离)`, {
17473
+ log10?.warn(`[${PLUGIN_NAME19}] 压缩异常(已隔离)`, {
16687
17474
  error: err instanceof Error ? err.message : String(err)
16688
17475
  });
16689
17476
  return null;
16690
17477
  }
16691
17478
  }
16692
- var log10 = makePluginLogger(PLUGIN_NAME20);
17479
+ var log10 = makePluginLogger(PLUGIN_NAME19);
16693
17480
  var tokenManagerServer = async (ctx) => {
16694
17481
  const rt = loadRuntimeSync();
16695
17482
  const threshold = rt.runtime.context.condenser_threshold_ratio;
16696
- logLifecycle(PLUGIN_NAME20, "activate", {
17483
+ logLifecycle(PLUGIN_NAME19, "activate", {
16697
17484
  directory: ctx.directory,
16698
17485
  threshold,
16699
17486
  target: DEFAULT_CONDENSE.target,
@@ -16702,7 +17489,7 @@ var tokenManagerServer = async (ctx) => {
16702
17489
  });
16703
17490
  return {
16704
17491
  "experimental.chat.messages.transform": async (_input, output) => {
16705
- await safeAsync(PLUGIN_NAME20, "experimental.chat.messages.transform", async () => {
17492
+ await safeAsync(PLUGIN_NAME19, "experimental.chat.messages.transform", async () => {
16706
17493
  const list = output.messages;
16707
17494
  if (!Array.isArray(list) || list.length === 0)
16708
17495
  return;
@@ -16715,7 +17502,7 @@ var tokenManagerServer = async (ctx) => {
16715
17502
  const r = await handleMessageBefore({ messages: flat }, log10, { threshold });
16716
17503
  if (!r)
16717
17504
  return;
16718
- safeWriteLog(PLUGIN_NAME20, {
17505
+ safeWriteLog(PLUGIN_NAME19, {
16719
17506
  hook: "experimental.chat.messages.transform",
16720
17507
  mode: "observe-only",
16721
17508
  before_msgs: r.before.count,
@@ -16726,13 +17513,13 @@ var tokenManagerServer = async (ctx) => {
16726
17513
  reason: r.reason
16727
17514
  });
16728
17515
  if (r.compressed) {
16729
- log10.warn(`[${PLUGIN_NAME20}] advise condense: ${r.before.count}→${r.after.count} msgs / ${r.before.tokens}→${r.after.tokens} tokens (observe-only, no write-back to avoid opencode message schema mismatch)`);
17516
+ log10.warn(`[${PLUGIN_NAME19}] advise condense: ${r.before.count}→${r.after.count} msgs / ${r.before.tokens}→${r.after.tokens} tokens (observe-only, no write-back to avoid opencode message schema mismatch)`);
16730
17517
  }
16731
17518
  });
16732
17519
  }
16733
17520
  };
16734
17521
  };
16735
- var handler20 = tokenManagerServer;
17522
+ var handler19 = tokenManagerServer;
16736
17523
 
16737
17524
  // plugins/tool-policy.ts
16738
17525
  init_opencode_plugin_helpers();
@@ -16975,10 +17762,42 @@ function checkFileAccess(acl, file, op) {
16975
17762
  }
16976
17763
 
16977
17764
  // plugins/tool-policy.ts
16978
- var PLUGIN_NAME21 = "tool-policy";
16979
- logLifecycle(PLUGIN_NAME21, "import", {});
17765
+ var PLUGIN_NAME20 = "tool-policy";
17766
+ logLifecycle(PLUGIN_NAME20, "import", {});
16980
17767
  var EMPTY_ACL = { whitelistMode: false };
16981
- function decideToolCall(ctx, cfg = {}) {
17768
+ var SUBAGENT_APPLY_DENY_LIST = new Set([
17769
+ "coder",
17770
+ "planner",
17771
+ "reviewer"
17772
+ ]);
17773
+ function isSubagentApplyBlocked(tool2, args, currentAgent) {
17774
+ if (tool2 !== "pending_changes")
17775
+ return false;
17776
+ const action = args?.action;
17777
+ if (action !== "apply" && action !== "apply_all")
17778
+ return false;
17779
+ if (!currentAgent)
17780
+ return false;
17781
+ return SUBAGENT_APPLY_DENY_LIST.has(currentAgent);
17782
+ }
17783
+ function decideToolCall(ctx, cfg = {}, currentAgent) {
17784
+ if (isSubagentApplyBlocked(ctx.tool, ctx.args, currentAgent)) {
17785
+ const action2 = String(ctx.args?.action);
17786
+ return {
17787
+ action: "deny",
17788
+ reasons: [
17789
+ `[ADR-0061] subagent '${currentAgent}' 禁止调 pending_changes.${action2}` + ` — apply 必须由 codeforge orchestrator 或用户拍板`,
17790
+ `替代路径:stage 完成后回报 codeforge,由其决定 apply 后通过 task_id 复用启动你跑测试`
17791
+ ],
17792
+ autonomy: {
17793
+ effective_mode: ctx.mode ?? cfg.defaultMode ?? DEFAULT_RUNTIME.autonomy.default_mode,
17794
+ action: "confirm",
17795
+ downgraded: false,
17796
+ detected_risks: ["subagent_apply_blocked"],
17797
+ reason: `subagent '${currentAgent}' 自 apply 越权 (ADR-0061)`
17798
+ }
17799
+ };
17800
+ }
16982
17801
  const fallbackMode = cfg.defaultMode ?? DEFAULT_RUNTIME.autonomy.default_mode;
16983
17802
  const mode = ctx.mode ?? fallbackMode;
16984
17803
  const a = evaluate2(mode, {
@@ -17028,13 +17847,39 @@ function classifyToolKind(toolName) {
17028
17847
  return "webfetch";
17029
17848
  return "other";
17030
17849
  }
17031
- var log11 = makePluginLogger(PLUGIN_NAME21);
17850
+ async function resolveCurrentAgent(client, sessionID, log11) {
17851
+ try {
17852
+ const sessionApi = client?.session;
17853
+ if (!sessionApi || typeof sessionApi.get !== "function") {
17854
+ log11.warn(`client.session.get unavailable`, { sessionID });
17855
+ return;
17856
+ }
17857
+ const res = await sessionApi.get({ path: { id: sessionID } });
17858
+ const data = res?.data ?? res;
17859
+ const rawAgent = data?.agent;
17860
+ if (typeof rawAgent !== "string" || rawAgent === "") {
17861
+ log11.warn(`client.session.get returned no string agent (保守放行)`, {
17862
+ sessionID,
17863
+ dataKeys: data && typeof data === "object" ? Object.keys(data) : null
17864
+ });
17865
+ return;
17866
+ }
17867
+ return rawAgent;
17868
+ } catch (err) {
17869
+ log11.warn(`client.session.get failed (保守放行)`, {
17870
+ sessionID,
17871
+ error: err instanceof Error ? err.message : String(err)
17872
+ });
17873
+ return;
17874
+ }
17875
+ }
17876
+ var log11 = makePluginLogger(PLUGIN_NAME20);
17032
17877
  var toolPolicyServer = async (ctx) => {
17033
17878
  const directory = ctx.directory ?? process.cwd();
17034
17879
  const cfg = await loadPolicy(directory);
17035
17880
  const rt = loadRuntimeSync();
17036
17881
  cfg.defaultMode = rt.runtime.autonomy.default_mode;
17037
- logLifecycle(PLUGIN_NAME21, "activate", {
17882
+ logLifecycle(PLUGIN_NAME20, "activate", {
17038
17883
  directory,
17039
17884
  acl_loaded: !!cfg.acl,
17040
17885
  default_mode: cfg.defaultMode,
@@ -17042,9 +17887,15 @@ var toolPolicyServer = async (ctx) => {
17042
17887
  });
17043
17888
  return {
17044
17889
  "tool.execute.before": async (input, output) => {
17045
- await safeAsync(PLUGIN_NAME21, "tool.execute.before", async () => {
17890
+ let denied;
17891
+ await safeAsync(PLUGIN_NAME20, "tool.execute.before", async () => {
17046
17892
  const toolName = input.tool;
17047
17893
  const argsObj = output.args ?? {};
17894
+ const needsAgentDetection = toolName === "pending_changes" && (argsObj.action === "apply" || argsObj.action === "apply_all");
17895
+ let currentAgent = undefined;
17896
+ if (needsAgentDetection) {
17897
+ currentAgent = await resolveCurrentAgent(ctx.client, input.sessionID, log11);
17898
+ }
17048
17899
  const files = [];
17049
17900
  const filePath = argsObj["path"] ?? argsObj["filePath"] ?? argsObj["file"];
17050
17901
  if (typeof filePath === "string") {
@@ -17057,31 +17908,39 @@ var toolPolicyServer = async (ctx) => {
17057
17908
  args: argsObj,
17058
17909
  mode: cfg.defaultMode,
17059
17910
  files: files.length ? files : undefined
17060
- }, cfg);
17061
- safeWriteLog(PLUGIN_NAME21, {
17911
+ }, cfg, currentAgent);
17912
+ safeWriteLog(PLUGIN_NAME20, {
17062
17913
  hook: "tool.execute.before",
17063
17914
  tool: toolName,
17064
17915
  callID: input.callID,
17065
17916
  sessionID: input.sessionID,
17066
17917
  action: decision.action,
17067
- reasons: decision.reasons
17918
+ reasons: decision.reasons,
17919
+ currentAgent
17068
17920
  });
17069
17921
  if (decision.action === "deny") {
17070
- log11.warn(`[${PLUGIN_NAME21}] DENY ${toolName}: ${decision.reasons.join("; ")}`);
17922
+ log11.warn(`[${PLUGIN_NAME20}] DENY ${toolName}`, {
17923
+ action: argsObj.action,
17924
+ currentAgent,
17925
+ reasons: decision.reasons
17926
+ });
17927
+ denied = new Error(`[tool-policy] DENIED: ${decision.reasons.join(" / ")}`);
17071
17928
  } else if (decision.action === "confirm") {
17072
- log11.info(`[${PLUGIN_NAME21}] CONFIRM ${toolName}: ${decision.reasons.join("; ")}`);
17929
+ log11.info(`[${PLUGIN_NAME20}] CONFIRM ${toolName}: ${decision.reasons.join("; ")}`);
17073
17930
  }
17074
17931
  });
17932
+ if (denied)
17933
+ throw denied;
17075
17934
  }
17076
17935
  };
17077
17936
  };
17078
- var handler21 = toolPolicyServer;
17937
+ var handler20 = toolPolicyServer;
17079
17938
 
17080
17939
  // plugins/update-checker.ts
17081
17940
  init_opencode_plugin_helpers();
17082
17941
  import { existsSync as existsSync5 } from "node:fs";
17083
17942
  import { homedir as homedir7 } from "node:os";
17084
- import { join as join15 } from "node:path";
17943
+ import { join as join16 } from "node:path";
17085
17944
 
17086
17945
  // lib/update-checker-impl.ts
17087
17946
  import { createHash as createHash6 } from "node:crypto";
@@ -17093,12 +17952,12 @@ import {
17093
17952
  readFileSync as readFileSync4,
17094
17953
  readdirSync,
17095
17954
  renameSync,
17096
- statSync as statSync2,
17955
+ statSync as statSync3,
17097
17956
  unlinkSync,
17098
17957
  writeFileSync as writeFileSync2
17099
17958
  } from "node:fs";
17100
17959
  import { homedir as homedir6, tmpdir } from "node:os";
17101
- import { dirname as dirname7, join as join14 } from "node:path";
17960
+ import { dirname as dirname7, join as join15 } from "node:path";
17102
17961
  import { fileURLToPath } from "node:url";
17103
17962
  import * as https from "node:https";
17104
17963
  import * as zlib from "node:zlib";
@@ -17106,7 +17965,7 @@ import * as zlib from "node:zlib";
17106
17965
  // lib/version-injected.ts
17107
17966
  function getInjectedVersion() {
17108
17967
  try {
17109
- const v = "0.3.9";
17968
+ const v = "0.3.11";
17110
17969
  if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
17111
17970
  return v;
17112
17971
  }
@@ -17196,17 +18055,17 @@ function readLocalVersion() {
17196
18055
  try {
17197
18056
  const here = fileURLToPath(import.meta.url);
17198
18057
  const root = dirname7(dirname7(here));
17199
- const pkg = JSON.parse(readFileSync4(join14(root, "package.json"), "utf8"));
18058
+ const pkg = JSON.parse(readFileSync4(join15(root, "package.json"), "utf8"));
17200
18059
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
17201
18060
  } catch {
17202
18061
  return "0.0.0";
17203
18062
  }
17204
18063
  }
17205
18064
  function defaultCacheDir() {
17206
- return process.env["CODEFORGE_CACHE_DIR"] ?? join14(homedir6(), ".cache", "codeforge");
18065
+ return process.env["CODEFORGE_CACHE_DIR"] ?? join15(homedir6(), ".cache", "codeforge");
17207
18066
  }
17208
18067
  function defaultCacheFile() {
17209
- return join14(defaultCacheDir(), "update-check.json");
18068
+ return join15(defaultCacheDir(), "update-check.json");
17210
18069
  }
17211
18070
  function readCache(file) {
17212
18071
  try {
@@ -17362,14 +18221,14 @@ function defaultHttpFetcher(url, timeoutMs) {
17362
18221
  });
17363
18222
  }
17364
18223
  async function downloadAndExtractBundle(opts) {
17365
- const tmpRoot = opts.tmpDir ?? mkdtempSync(join14(tmpdir(), "codeforge-update-"));
18224
+ const tmpRoot = opts.tmpDir ?? mkdtempSync(join15(tmpdir(), "codeforge-update-"));
17366
18225
  mkdirSync3(tmpRoot, { recursive: true });
17367
18226
  const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
17368
18227
  const tarballBuf = await fetcher(opts.tarballUrl);
17369
18228
  verifyIntegrity(tarballBuf, opts.expectedIntegrity);
17370
18229
  const tarBuf = zlib.gunzipSync(tarballBuf);
17371
18230
  extractTarToDir(tarBuf, tmpRoot);
17372
- const bundlePath = join14(tmpRoot, "package", "dist", "index.js");
18231
+ const bundlePath = join15(tmpRoot, "package", "dist", "index.js");
17373
18232
  if (!existsSync4(bundlePath)) {
17374
18233
  throw new Error(`bundle_not_found: ${bundlePath}`);
17375
18234
  }
@@ -17409,11 +18268,11 @@ function extractTarToDir(tarBuf, destRoot) {
17409
18268
  offset += 512;
17410
18269
  if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
17411
18270
  const fileBuf = tarBuf.subarray(offset, offset + size);
17412
- const dest = join14(destRoot, fullName);
18271
+ const dest = join15(destRoot, fullName);
17413
18272
  mkdirSync3(dirname7(dest), { recursive: true });
17414
18273
  writeFileSync2(dest, fileBuf);
17415
18274
  } else if (typeFlag === "5") {
17416
- mkdirSync3(join14(destRoot, fullName), { recursive: true });
18275
+ mkdirSync3(join15(destRoot, fullName), { recursive: true });
17417
18276
  }
17418
18277
  offset += Math.ceil(size / 512) * 512;
17419
18278
  }
@@ -17522,10 +18381,10 @@ function cleanupOldBackups(target, keep) {
17522
18381
  const base = target.substring(dir.length + 1);
17523
18382
  const prefix = `${base}.bak.`;
17524
18383
  const all = readdirSync(dir).filter((f) => f.startsWith(prefix)).map((f) => {
17525
- const full = join14(dir, f);
18384
+ const full = join15(dir, f);
17526
18385
  let mtimeMs = 0;
17527
18386
  try {
17528
- mtimeMs = statSync2(full).mtimeMs;
18387
+ mtimeMs = statSync3(full).mtimeMs;
17529
18388
  } catch {}
17530
18389
  return { full, mtimeMs };
17531
18390
  }).sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -17544,7 +18403,7 @@ function loadCompatibility(opts) {
17544
18403
  const root = opts?.cwd ?? inferPluginRoot();
17545
18404
  if (!root)
17546
18405
  return null;
17547
- file = join14(root, "compatibility.json");
18406
+ file = join15(root, "compatibility.json");
17548
18407
  }
17549
18408
  if (!existsSync4(file))
17550
18409
  return null;
@@ -17609,20 +18468,20 @@ function compareOpencodeVersion(opts) {
17609
18468
  }
17610
18469
 
17611
18470
  // plugins/update-checker.ts
17612
- var PLUGIN_NAME22 = "update-checker";
17613
- var PLUGIN_VERSION2 = "2.0.0";
17614
- logLifecycle(PLUGIN_NAME22, "import", { version: PLUGIN_VERSION2 });
18471
+ var PLUGIN_NAME21 = "update-checker";
18472
+ var PLUGIN_VERSION = "2.0.0";
18473
+ logLifecycle(PLUGIN_NAME21, "import", { version: PLUGIN_VERSION });
17615
18474
  var updateCheckerServer = async (ctx) => {
17616
18475
  const yieldResult = shouldYieldToLocalPlugin({ directory: ctx.directory });
17617
18476
  if (yieldResult.yield) {
17618
- safeWriteLog(PLUGIN_NAME22, {
18477
+ safeWriteLog(PLUGIN_NAME21, {
17619
18478
  level: "info",
17620
18479
  msg: "dev_mode_yield_skip",
17621
18480
  reason: yieldResult.reason,
17622
18481
  markerPath: yieldResult.markerPath,
17623
18482
  detail: formatYieldLog(yieldResult)
17624
18483
  });
17625
- logLifecycle(PLUGIN_NAME22, "activate", {
18484
+ logLifecycle(PLUGIN_NAME21, "activate", {
17626
18485
  yield_to_local: true,
17627
18486
  yield_reason: yieldResult.reason,
17628
18487
  skipped: "auto_install + background_check"
@@ -17631,8 +18490,8 @@ var updateCheckerServer = async (ctx) => {
17631
18490
  }
17632
18491
  const rt = loadRuntimeSync();
17633
18492
  const u = rt.runtime.update;
17634
- logLifecycle(PLUGIN_NAME22, "activate", {
17635
- version: PLUGIN_VERSION2,
18493
+ logLifecycle(PLUGIN_NAME21, "activate", {
18494
+ version: PLUGIN_VERSION,
17636
18495
  auto_check_enabled: u.auto_check_enabled,
17637
18496
  interval_hours: u.interval_hours,
17638
18497
  package: u.package,
@@ -17643,14 +18502,14 @@ var updateCheckerServer = async (ctx) => {
17643
18502
  repo_fallback: u.repo,
17644
18503
  config_source: "codeforge.json"
17645
18504
  });
17646
- await safeAsync(PLUGIN_NAME22, "opencode_version_check", async () => {
18505
+ await safeAsync(PLUGIN_NAME21, "opencode_version_check", async () => {
17647
18506
  const compat = loadCompatibility();
17648
18507
  const opencodeVer = detectOpencodeVersion();
17649
18508
  const verdict = compareOpencodeVersion({
17650
18509
  currentOpencodeVer: opencodeVer,
17651
18510
  compat
17652
18511
  });
17653
- safeWriteLog(PLUGIN_NAME22, {
18512
+ safeWriteLog(PLUGIN_NAME21, {
17654
18513
  level: "info",
17655
18514
  msg: "opencode_version_check",
17656
18515
  opencodeVer,
@@ -17667,14 +18526,14 @@ var updateCheckerServer = async (ctx) => {
17667
18526
  }
17668
18527
  });
17669
18528
  if (!u.auto_check_enabled) {
17670
- safeWriteLog(PLUGIN_NAME22, {
18529
+ safeWriteLog(PLUGIN_NAME21, {
17671
18530
  level: "info",
17672
18531
  msg: "auto-check disabled (codeforge.json update.auto_check_enabled=false)"
17673
18532
  });
17674
18533
  return {};
17675
18534
  }
17676
18535
  setImmediate(() => {
17677
- safeAsync(PLUGIN_NAME22, "checkAndMaybeUpdate", async () => {
18536
+ safeAsync(PLUGIN_NAME21, "checkAndMaybeUpdate", async () => {
17678
18537
  const local = readLocalVersion();
17679
18538
  let npmResult = null;
17680
18539
  try {
@@ -17685,7 +18544,7 @@ var updateCheckerServer = async (ctx) => {
17685
18544
  timeoutMs: 5000
17686
18545
  });
17687
18546
  } catch (e) {
17688
- safeWriteLog(PLUGIN_NAME22, {
18547
+ safeWriteLog(PLUGIN_NAME21, {
17689
18548
  level: "warn",
17690
18549
  msg: "npm_fetch_failed",
17691
18550
  error: e.message
@@ -17694,7 +18553,7 @@ var updateCheckerServer = async (ctx) => {
17694
18553
  return;
17695
18554
  }
17696
18555
  if (!npmResult) {
17697
- safeWriteLog(PLUGIN_NAME22, {
18556
+ safeWriteLog(PLUGIN_NAME21, {
17698
18557
  level: "info",
17699
18558
  msg: "npm_no_release",
17700
18559
  package: u.package,
@@ -17703,7 +18562,7 @@ var updateCheckerServer = async (ctx) => {
17703
18562
  return;
17704
18563
  }
17705
18564
  const hasUpdate = cmpVersion(local, npmResult.version) < 0;
17706
- safeWriteLog(PLUGIN_NAME22, {
18565
+ safeWriteLog(PLUGIN_NAME21, {
17707
18566
  level: "info",
17708
18567
  msg: "npm_check_result",
17709
18568
  local,
@@ -17718,10 +18577,10 @@ var updateCheckerServer = async (ctx) => {
17718
18577
  更新命令:npx ${u.package} install --global`);
17719
18578
  return;
17720
18579
  }
17721
- await safeAsync(PLUGIN_NAME22, "auto_install_bundle", async () => {
18580
+ await safeAsync(PLUGIN_NAME21, "auto_install_bundle", async () => {
17722
18581
  const target = getOpencodeBundlePath();
17723
18582
  if (!target) {
17724
- safeWriteLog(PLUGIN_NAME22, {
18583
+ safeWriteLog(PLUGIN_NAME21, {
17725
18584
  level: "warn",
17726
18585
  msg: "auto_install_skip",
17727
18586
  reason: "无法定位 opencode bundle 路径"
@@ -17740,7 +18599,7 @@ var updateCheckerServer = async (ctx) => {
17740
18599
  oldVersion: local,
17741
18600
  keepBackups: u.backup_keep
17742
18601
  });
17743
- safeWriteLog(PLUGIN_NAME22, {
18602
+ safeWriteLog(PLUGIN_NAME21, {
17744
18603
  level: "info",
17745
18604
  msg: "auto_install_success",
17746
18605
  local,
@@ -17764,14 +18623,14 @@ function detectOpencodeVersion() {
17764
18623
  }
17765
18624
  function getOpencodeBundlePath() {
17766
18625
  const candidates = [];
17767
- candidates.push(join15(homedir7(), ".config", "opencode", "codeforge", "index.js"));
18626
+ candidates.push(join16(homedir7(), ".config", "opencode", "codeforge", "index.js"));
17768
18627
  if (process.platform === "win32") {
17769
18628
  const appData = process.env["APPDATA"];
17770
18629
  if (appData)
17771
- candidates.push(join15(appData, "opencode", "codeforge", "index.js"));
18630
+ candidates.push(join16(appData, "opencode", "codeforge", "index.js"));
17772
18631
  const localAppData = process.env["LOCALAPPDATA"];
17773
18632
  if (localAppData)
17774
- candidates.push(join15(localAppData, "opencode", "codeforge", "index.js"));
18633
+ candidates.push(join16(localAppData, "opencode", "codeforge", "index.js"));
17775
18634
  }
17776
18635
  for (const c of candidates) {
17777
18636
  if (existsSync5(c))
@@ -17780,7 +18639,7 @@ function getOpencodeBundlePath() {
17780
18639
  return candidates[0] ?? null;
17781
18640
  }
17782
18641
  async function fallbackToGitHubReleases(ctx, u) {
17783
- await safeAsync(PLUGIN_NAME22, "github_fallback", async () => {
18642
+ await safeAsync(PLUGIN_NAME21, "github_fallback", async () => {
17784
18643
  const result = await checkUpdateOnce({
17785
18644
  repo: u.repo,
17786
18645
  intervalMs: u.interval_hours * 3600 * 1000
@@ -17790,7 +18649,7 @@ async function fallbackToGitHubReleases(ctx, u) {
17790
18649
  }
17791
18650
  async function reportLegacyResult(ctx, result, repo) {
17792
18651
  if (result.error && !result.fromCache) {
17793
- safeWriteLog(PLUGIN_NAME22, {
18652
+ safeWriteLog(PLUGIN_NAME21, {
17794
18653
  level: "warn",
17795
18654
  msg: `update check failed: ${result.error}`,
17796
18655
  ...result
@@ -17798,7 +18657,7 @@ async function reportLegacyResult(ctx, result, repo) {
17798
18657
  return;
17799
18658
  }
17800
18659
  if (!result.hasUpdate) {
17801
- safeWriteLog(PLUGIN_NAME22, {
18660
+ safeWriteLog(PLUGIN_NAME21, {
17802
18661
  level: "info",
17803
18662
  msg: `up-to-date (local=${result.local}, remote=${result.remote}${result.fromCache ? ", from_cache" : ""})`,
17804
18663
  ...result
@@ -17808,7 +18667,7 @@ async function reportLegacyResult(ctx, result, repo) {
17808
18667
  const updateCmd = `bunx --bun github:${repo} install`;
17809
18668
  const toast = `[CodeForge] 有新版本:${result.local} → ${result.remote}
17810
18669
  更新命令:${updateCmd}`;
17811
- safeWriteLog(PLUGIN_NAME22, {
18670
+ safeWriteLog(PLUGIN_NAME21, {
17812
18671
  level: "info",
17813
18672
  msg: "new_version_available_github_fallback",
17814
18673
  local: result.local,
@@ -17819,17 +18678,17 @@ async function reportLegacyResult(ctx, result, repo) {
17819
18678
  await postToast(ctx, toast);
17820
18679
  }
17821
18680
  async function postToast(ctx, message) {
17822
- await safeAsync(PLUGIN_NAME22, "client.app.log", async () => {
18681
+ await safeAsync(PLUGIN_NAME21, "client.app.log", async () => {
17823
18682
  await ctx.client.app.log({
17824
18683
  body: {
17825
- service: PLUGIN_NAME22,
18684
+ service: PLUGIN_NAME21,
17826
18685
  level: "info",
17827
18686
  message
17828
18687
  }
17829
18688
  });
17830
18689
  });
17831
18690
  }
17832
- var handler22 = updateCheckerServer;
18691
+ var handler21 = updateCheckerServer;
17833
18692
 
17834
18693
  // plugins/workflow-engine.ts
17835
18694
  init_opencode_plugin_helpers();
@@ -18333,9 +19192,9 @@ async function runStepAutoFeedback(step, adapter) {
18333
19192
  }
18334
19193
 
18335
19194
  // plugins/workflow-engine.ts
18336
- var PLUGIN_NAME23 = "workflow-engine";
18337
- logLifecycle(PLUGIN_NAME23, "import", {});
18338
- var fallbackLog2 = makePluginLogger(PLUGIN_NAME23);
19195
+ var PLUGIN_NAME22 = "workflow-engine";
19196
+ logLifecycle(PLUGIN_NAME22, "import", {});
19197
+ var fallbackLog2 = makePluginLogger(PLUGIN_NAME22);
18339
19198
  var _registry = null;
18340
19199
  async function loadRegistry(workflowsDir) {
18341
19200
  const { loaded, failed } = await loadWorkflowsFromDir(workflowsDir);
@@ -18355,32 +19214,32 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
18355
19214
  const log12 = ctx.log ?? fallbackLog2;
18356
19215
  const command = typeof ctx.command === "string" ? ctx.command : null;
18357
19216
  if (!command) {
18358
- log12.warn(`[${PLUGIN_NAME23}] command.invoked 缺 command 字段`, ctx);
19217
+ log12.warn(`[${PLUGIN_NAME22}] command.invoked 缺 command 字段`, ctx);
18359
19218
  return null;
18360
19219
  }
18361
19220
  const reg = await ensureRegistry(workflowsDir);
18362
19221
  if (reg.errors.length) {
18363
- log12.warn(`[${PLUGIN_NAME23}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
19222
+ log12.warn(`[${PLUGIN_NAME22}] 有 ${reg.errors.length} 个 workflow 加载失败`, reg.errors);
18364
19223
  }
18365
19224
  const wf = reg.workflows.find((w) => matchesTrigger(w, command));
18366
19225
  if (!wf) {
18367
- log12.info(`[${PLUGIN_NAME23}] no workflow matches "${command}"`);
19226
+ log12.info(`[${PLUGIN_NAME22}] no workflow matches "${command}"`);
18368
19227
  return null;
18369
19228
  }
18370
- log12.info(`[${PLUGIN_NAME23}] dispatch "${command}" → workflow "${wf.name}"`);
19229
+ log12.info(`[${PLUGIN_NAME22}] dispatch "${command}" → workflow "${wf.name}"`);
18371
19230
  try {
18372
19231
  const result = await run(wf, {
18373
19232
  mode: ctx.adapter ? "real" : "dry_run",
18374
19233
  autonomy: ctx.autonomy ?? "semi",
18375
19234
  adapter: ctx.adapter
18376
19235
  });
18377
- log12.info(`[${PLUGIN_NAME23}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
19236
+ log12.info(`[${PLUGIN_NAME22}] workflow "${wf.name}" 完成 (${result.plan.mode})`, {
18378
19237
  steps: result.plan.steps.length,
18379
19238
  results: result.results.length
18380
19239
  });
18381
19240
  return result;
18382
19241
  } catch (err) {
18383
- log12.error(`[${PLUGIN_NAME23}] workflow "${wf.name}" 执行失败`, {
19242
+ log12.error(`[${PLUGIN_NAME22}] workflow "${wf.name}" 执行失败`, {
18384
19243
  error: err instanceof Error ? err.message : String(err)
18385
19244
  });
18386
19245
  throw err;
@@ -18389,15 +19248,15 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
18389
19248
  var workflowEngineServer = async (ctx) => {
18390
19249
  const directory = ctx.directory ?? process.cwd();
18391
19250
  const workflowsDir = path17.join(directory, "workflows");
18392
- ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
19251
+ ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME22}] preload workflows failed`, {
18393
19252
  error: err instanceof Error ? err.message : String(err)
18394
19253
  }));
18395
- logLifecycle(PLUGIN_NAME23, "activate", { directory, workflowsDir });
19254
+ logLifecycle(PLUGIN_NAME22, "activate", { directory, workflowsDir });
18396
19255
  return {
18397
19256
  "command.execute.before": async (input, output) => {
18398
- await safeAsync(PLUGIN_NAME23, "command.execute.before", async () => {
19257
+ await safeAsync(PLUGIN_NAME22, "command.execute.before", async () => {
18399
19258
  const cmd = input.command.startsWith("/") ? input.command : `/${input.command}`;
18400
- safeWriteLog(PLUGIN_NAME23, {
19259
+ safeWriteLog(PLUGIN_NAME22, {
18401
19260
  hook: "command.execute.before",
18402
19261
  command: cmd,
18403
19262
  sessionID: input.sessionID
@@ -18412,14 +19271,14 @@ var workflowEngineServer = async (ctx) => {
18412
19271
  });
18413
19272
  },
18414
19273
  "chat.message": async (input, output) => {
18415
- await safeAsync(PLUGIN_NAME23, "chat.message", async () => {
19274
+ await safeAsync(PLUGIN_NAME22, "chat.message", async () => {
18416
19275
  const text = extractUserText(output).trim();
18417
19276
  if (!text.startsWith("/"))
18418
19277
  return;
18419
19278
  const cmd = text.split(/\s+/)[0];
18420
19279
  if (!cmd)
18421
19280
  return;
18422
- safeWriteLog(PLUGIN_NAME23, {
19281
+ safeWriteLog(PLUGIN_NAME22, {
18423
19282
  hook: "chat.message",
18424
19283
  command: cmd,
18425
19284
  sessionID: input.sessionID
@@ -18429,7 +19288,7 @@ var workflowEngineServer = async (ctx) => {
18429
19288
  }
18430
19289
  };
18431
19290
  };
18432
- var handler23 = workflowEngineServer;
19291
+ var handler22 = workflowEngineServer;
18433
19292
 
18434
19293
  // src/index.ts
18435
19294
  var PLUGIN_ID = "codeforge";
@@ -18443,22 +19302,21 @@ var HANDLERS = [
18443
19302
  { name: "auto-learning", init: handler5 },
18444
19303
  { name: "channels", init: handler6 },
18445
19304
  { name: "codeforge-tools", init: handler8 },
18446
- { name: "hello-world", init: handler9 },
18447
- { name: "kh-auto-context", init: handler10 },
18448
- { name: "kh-reminder", init: handler11 },
18449
- { name: "memories-context", init: handler12 },
18450
- { name: "model-fallback", init: handler13 },
18451
- { name: "pwsh-utf8", init: handler16 },
18452
- { name: "session-recovery", init: handler17 },
18453
- { name: "subtask-heartbeat", init: handler14 },
18454
- { name: "subtasks", init: handler18 },
18455
- { name: "parallel-status", init: handler15 },
18456
- { name: "terminal-monitor", init: handler19 },
18457
- { name: "token-manager", init: handler20 },
19305
+ { name: "kh-auto-context", init: handler9 },
19306
+ { name: "kh-reminder", init: handler10 },
19307
+ { name: "memories-context", init: handler11 },
19308
+ { name: "model-fallback", init: handler12 },
19309
+ { name: "pwsh-utf8", init: handler15 },
19310
+ { name: "session-recovery", init: handler16 },
19311
+ { name: "subtask-heartbeat", init: handler13 },
19312
+ { name: "subtasks", init: handler17 },
19313
+ { name: "parallel-status", init: handler14 },
19314
+ { name: "terminal-monitor", init: handler18 },
19315
+ { name: "token-manager", init: handler19 },
18458
19316
  { name: "tool-heartbeat", init: handler7 },
18459
- { name: "tool-policy", init: handler21 },
18460
- { name: "update-checker", init: handler22 },
18461
- { name: "workflow-engine", init: handler23 }
19317
+ { name: "tool-policy", init: handler20 },
19318
+ { name: "update-checker", init: handler21 },
19319
+ { name: "workflow-engine", init: handler22 }
18462
19320
  ];
18463
19321
  function makeSerialHook(hookName, fns) {
18464
19322
  return async (input, output) => {