dsh-plugin-om 0.0.22 → 0.0.24

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.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { scopeOf } from "@deepseek-ai/dsh-scope";
2
+ import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
1
3
  import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
4
  import { homedir } from "node:os";
3
5
  import path from "node:path";
@@ -11,7 +13,7 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
11
13
  /**
12
14
  * 共享常量:插件级魔法字符串,集中定义避免散落各模块。
13
15
  * 导出 PLUGIN_LABEL / HISTORY_TAG / HISTORY_TIP / COMPLETE_MESSAGE_DEFINITION /
14
- * COMPACT_CHECKPOINT_PLUGIN / isPluginOwnedSource。
16
+ * COMPACT_CHECKPOINT_PLUGIN / COMPACTION_ABORTED_ERROR / isPluginOwnedSource。
15
17
  */
16
18
  /** 插件标识:压缩消息 source.plugin 取值与日志前缀。 */
17
19
  const PLUGIN_LABEL = "dsh-plugin-om";
@@ -23,13 +25,65 @@ const HISTORY_TIP = "当前块是历史消息的压缩产物,不要复述";
23
25
  * 「完整消息」定义(提示词 / 工具描述 / 块顶注释共用)。
24
26
  * 完整消息是摘要日志与 recall 共用的定位单位;首条 index 为 0,按会话顺序递增、全局稳定。
25
27
  */
26
- const COMPLETE_MESSAGE_DEFINITION = "`完整消息`被定义为`用户消息`,`系统消息`,`模型输出文本`和`具有result的toolcall`的集合。首条`完整消息`的index0,后续的index递增。";
28
+ const COMPLETE_MESSAGE_DEFINITION = "`完整消息`指一条`用户消息`、`系统消息`、`模型输出文本`或`具有result的toolcall`;首条 index0,按会话顺序递增。";
29
+ /**
30
+ * 压缩因 signal 中止而放弃时 compaction/end 的 error 标识(服务端写入、客户端过滤):
31
+ * 中止不是失败,客户端据此隐藏失败行(宿主不变量要求无 summary 的 end 必须带 error)。
32
+ */
33
+ const COMPACTION_ABORTED_ERROR = "压缩已中止(signal aborted)";
27
34
  /** 判定 user/message 的 source 是否为本插件自产(压缩日志消息;含旧宿主 checkpoint 标记兼容)。这类消息不占完整消息 index。 */
28
35
  function isPluginOwnedSource(source) {
29
36
  if (source?.kind !== "plugin") return false;
30
37
  return source.plugin === "dsh-plugin-om" || source.plugin === "compact";
31
38
  }
32
39
  //#endregion
40
+ //#region src/degrade.ts
41
+ /**
42
+ * 降级报告:压缩流程中的挂载失败统一出口。
43
+ * 导出 reportDegrade / DEGRADE_PROBLEMS / DegradedProblem。
44
+ *
45
+ * - 挂载失败类问题(服务缺失、服务调用异常)始终 console.warn 到宿主进程外部输出,
46
+ * 并向会话日志追加 log-only 的 `om/warning` 事件(客户端渲染为「功能降级」警告行);
47
+ * 同一会话同一问题只报告一次(按日志扫描去重,重启不重复)
48
+ * - 报告动作自身绝不抛错:追加失败只记日志,不阻塞压缩
49
+ * - 辅助函数的普通运行时报错(组装失败、请求头读取失败)不走本模块,仅记日志
50
+ */
51
+ /** 各降级问题面向用户的简短说明(om/warning 载荷 message;客户端警告行直接展示)。 */
52
+ const DEGRADE_PROBLEMS = {
53
+ "systemPrompt-missing": "系统提示词服务未挂载,上下文压力估算不扣除系统提示词 tokens(压缩触发会偏早)",
54
+ "tokenMeter-unavailable": "token 计量服务异常,上下文压力估算降级(可能跳过压缩或按 0 计)"
55
+ };
56
+ /** 判定事件是否为指定 problem 的已有 om/warning 记录(会话内去重依据)。 */
57
+ function isWarningFor(event, problem) {
58
+ if (event.type !== "om/warning") return false;
59
+ return event.data?.problem === problem;
60
+ }
61
+ /**
62
+ * 报告一次挂载失败类降级:console.warn 始终输出到宿主进程外部;同会话同问题首次
63
+ * 出现时,向会话日志追加 log-only `om/warning` 事件(客户端渲染警告行,每会话最多
64
+ * 一次)并输出 logger.warn(避免每个 pre-step 重复刷日志)。
65
+ * 日志扫描/事件追加失败只记日志,绝不抛错、不阻塞压缩。
66
+ */
67
+ function reportDegrade(session, logger, problem) {
68
+ const message = DEGRADE_PROBLEMS[problem];
69
+ let first = true;
70
+ try {
71
+ first = !session.events.some((event) => isWarningFor(event, problem));
72
+ } catch {}
73
+ if (!first) return;
74
+ console.warn(`${PLUGIN_LABEL}: ${message}`);
75
+ logger.warn(message);
76
+ try {
77
+ session.append("om/warning", {
78
+ problem,
79
+ message
80
+ });
81
+ } catch (error) {
82
+ const text = error instanceof Error ? error.message : String(error);
83
+ logger.warn(`om/warning 事件追加失败: ${text}`);
84
+ }
85
+ }
86
+ //#endregion
33
87
  //#region src/utils.ts
34
88
  /** 判断值是否为普通对象(非 null、非数组)。 */
35
89
  function isRecord(value) {
@@ -7474,27 +7528,24 @@ async function gateRateLimit(waitMs, signal) {
7474
7528
  */
7475
7529
  function buildHistoryPrompt() {
7476
7530
  return [
7477
- "把下方的 <history> 消息记录压缩为一份更紧凑的 <history> 压缩日志。不用工具、不展示思考、不输出多余文字。",
7531
+ "压缩 <history> 消息记录。你应当输出**单个**合法的 <history> 块。",
7478
7532
  "",
7479
7533
  "【history 块定义】",
7480
- "- <history> 是历史消息的记录块:输入与输出都是合法的 <history> 块,块内条目是「模型消息 + index」的表达形式。",
7481
- `- 完整消息:${COMPLETE_MESSAGE_DEFINITION}`,
7482
- "- <user_message index=\"N\">:用户消息条目(N 为该条完整消息的 index;文本原样,图片/文件等以注释补充)。",
7483
- "- <sys type=\"(kind)\" index=\"N\">:系统消息条目(user_message 中非 kind:user 的部分,如宿主注入的上下文),块中为空,内容不进入压缩输入。",
7534
+ "- <history> 是历史消息的记录块。",
7535
+ "- <user_message index=\"N\">:用户消息条目。",
7536
+ "- <sys type=\"(kind)\" index=\"N\">:系统消息条目。",
7484
7537
  "- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。",
7485
7538
  "- <assistant index=\"N\">:单条完整消息(模型输出文本,或 toolcall 及其 result)。",
7486
7539
  "- <assistant start=\"A\" end=\"B\">:多条连续完整消息聚合的模块(A/B 为模块首尾完整消息的 index)。",
7487
7540
  "",
7488
7541
  "【压缩要求】",
7489
- "- 完整保留用户消息:<user_message> 条目逐条保留原文,不概括、不省略。",
7490
- "- 完整保留系统消息:<sys> 条目逐条原样保留(type 与 index 不变),块中为空,不概括、不省略、不填充内容。",
7542
+ "- <user_message> <sys> 条目从输入中逐条保留,不做任何处理。",
7491
7543
  "- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。",
7492
7544
  "- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\"> 块:块内描述模块的目的、行为与结果;涉及的具体文件保留在模块内容中,多个前缀相同的路径合并简写。",
7493
7545
  "- 单条重要的完整消息以 <assistant index=\"\"> 单独呈现,内容不受限制。",
7494
7546
  "- 条目按 index 顺序覆盖本次压缩的全部完整消息:index/start/end 必须连续(区间内 index 连续、相邻条目相接),不跳号、不重叠、不遗漏。",
7495
- "- 条目内容为合法 XML 文本:特殊字符保持转义形式(如 & → &amp;、< → &lt;),用户消息原文的转义形式原样保留,不要反转义。",
7496
7547
  "",
7497
- "【输出格式】只输出一个 <history> 包裹的合法 XML 日志块(不要解释、不要复述规则):",
7548
+ "【输出格式】输出单个合法的 <history> 块,**不包含其他任何内容**:",
7498
7549
  `<${HISTORY_TAG}>`,
7499
7550
  "<user_message index=\"(index)\">",
7500
7551
  "(user 消息原文)",
@@ -7511,6 +7562,13 @@ function buildHistoryPrompt() {
7511
7562
  "【数据源】下方的 <history> 消息记录是本次要压缩的全部消息;压缩结果作为一个新的 <history> 块输出。"
7512
7563
  ].join("\n");
7513
7564
  }
7565
+ /**
7566
+ * 静默 DOMParser:非致命解析问题不再走 xmldom 默认的 console.error 输出
7567
+ * (模型输出非法 XML 时避免刷 console),fatalError 仍抛 ParseError、解析语义不变。
7568
+ */
7569
+ function newQuietParser() {
7570
+ return new import_lib.DOMParser({ onError: () => {} });
7571
+ }
7514
7572
  /** 渲染用户消息条目(DOM 元素):文本块原样;图片/文件等非文本块以注释补充。 */
7515
7573
  function renderUserEntry(doc, session, cm) {
7516
7574
  const seq = cm.seqs[0];
@@ -7555,7 +7613,7 @@ function renderMessages(session, seqs) {
7555
7613
  if (reasonings.length > 0) reasoningBySeq.set(seq, reasonings);
7556
7614
  }
7557
7615
  const emittedReasoning = /* @__PURE__ */ new Set();
7558
- const doc = new import_lib.DOMParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
7616
+ const doc = newQuietParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
7559
7617
  const entries = [];
7560
7618
  for (const cm of indexCompleteMessages(session)) {
7561
7619
  if (!cm.seqs.every((seq) => shadowed.has(seq))) continue;
@@ -7646,7 +7704,7 @@ function buildSummaryOptions(session, instruction, contextText, maxTokens, targe
7646
7704
  return {
7647
7705
  provider: target.provider,
7648
7706
  model: target.model,
7649
- maxTokens,
7707
+ ...maxTokens === void 0 ? {} : { maxTokens },
7650
7708
  sessionId: session.id,
7651
7709
  purpose: "compaction",
7652
7710
  ...signal === void 0 ? {} : { signal },
@@ -7671,7 +7729,7 @@ function intAttr(el, name) {
7671
7729
  function parseHistoryBlock(xml) {
7672
7730
  let doc;
7673
7731
  try {
7674
- doc = new import_lib.DOMParser().parseFromString(xml, "text/xml");
7732
+ doc = newQuietParser().parseFromString(xml, "text/xml");
7675
7733
  } catch {
7676
7734
  return null;
7677
7735
  }
@@ -7830,7 +7888,7 @@ function rebuildHistoryBlock(inner) {
7830
7888
  content: inner.slice(open.contentFrom)
7831
7889
  });
7832
7890
  if (entries.length === 0) return null;
7833
- const doc = new import_lib.DOMParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
7891
+ const doc = newQuietParser().parseFromString(`<${HISTORY_TAG} />`, "text/xml");
7834
7892
  const root = doc.documentElement;
7835
7893
  if (!root) return null;
7836
7894
  for (const e of entries) {
@@ -7844,37 +7902,39 @@ function rebuildHistoryBlock(inner) {
7844
7902
  /**
7845
7903
  * 从 AI 摘要输出中提取合法日志(不信任 AI 的总结结果):
7846
7904
  * 取首个 <history> 开标签(允许带属性)到最后一个 </history> 切为候选块,
7847
- * 找不到或内容过短返回 null;候选块经 XML 结构校验,整块非法时按条目模糊
7905
+ * 找不到或内容过短返回具体原因;候选块经 XML 结构校验,整块非法时按条目模糊
7848
7906
  * 提取重建为合法块(不要求模型输出整体合法 XML);随后统一校验:无 reasoning、
7849
7907
  * index 连续且与 expected 覆盖区间一致;通过后统一改写为带 tip 属性的开标签
7850
- * 并在块顶插入格式说明注释。
7908
+ * 并在块顶插入格式说明注释。所有失败原因均为说明性描述,不携带解析器原始报错。
7851
7909
  */
7852
- function extractSummaryLog(raw, expected) {
7910
+ function extractSummaryDetailed(raw, expected) {
7853
7911
  const closeTag = `</${HISTORY_TAG}>`;
7854
7912
  const openMatch = HISTORY_OPEN_TAG_RE.exec(raw);
7855
7913
  const open = openMatch?.index ?? -1;
7856
7914
  const close = raw.lastIndexOf(closeTag);
7857
- if (openMatch === null || close === -1 || close < open) return null;
7915
+ if (openMatch === null || close === -1 || close < open) return { error: "输出中找不到完整的 <history> 块(缺少 <history> 开标签或 </history> 闭标签)" };
7858
7916
  const inner = raw.slice(open + openMatch[0].length, close);
7859
- if (inner.trim().length < 10) return null;
7917
+ if (inner.trim().length < 10) return { error: "输出中 <history> 块内容过短(少于 10 字符)" };
7860
7918
  const block = raw.slice(open, close + closeTag.length);
7861
7919
  const candidate = parseHistoryBlock(block) === null ? rebuildHistoryBlock(inner) : block;
7862
- if (candidate === null) return null;
7920
+ if (candidate === null) return { error: "输出不是合法的 <history> 块(XML 结构非法,按条目模糊提取也未找到有效条目)" };
7863
7921
  const parsed = parseHistoryBlock(candidate);
7864
- if (parsed === null) return null;
7865
- if (parsed.hasReasoning) return null;
7922
+ if (parsed === null) return { error: "输出不是合法的 <history> 块(条目结构非法:属性缺失或包含未定义元素)" };
7923
+ if (parsed.hasReasoning) return { error: "输出包含 <reasoning> 条目(产物不允许携带思考过程)" };
7866
7924
  const span = historyContinuity(parsed.entries);
7867
- if (span === null) return null;
7868
- if (expected?.start !== void 0 && span.start !== expected.start) return null;
7869
- if (expected?.end !== void 0 && span.end !== expected.end) return null;
7925
+ if (span === null) return { error: "输出条目 index/start/end 不连续(跳号、重叠或乱序)" };
7926
+ if (expected?.start !== void 0 && span.start !== expected.start) return { error: `输出覆盖区间从 ${span.start} 开始,与期望起始 ${expected.start} 不一致` };
7927
+ if (expected?.end !== void 0 && span.end !== expected.end) return { error: `输出覆盖区间止于 ${span.end},与期望结束 ${expected.end} 不一致` };
7870
7928
  const candidateInner = candidate.slice(candidate.indexOf(">") + 1, candidate.length - closeTag.length).trim();
7871
- return `<${HISTORY_TAG} tip="${HISTORY_TIP}">\n${HISTORY_FORMAT_NOTE}\n${candidateInner}\n</${HISTORY_TAG}>`;
7929
+ return { log: `<${HISTORY_TAG} tip="${HISTORY_TIP}">\n${HISTORY_FORMAT_NOTE}\n${candidateInner}\n</${HISTORY_TAG}>` };
7872
7930
  }
7873
7931
  /**
7874
7932
  * 直连 LLM 执行一次摘要(观察或反思),返回文本与可选 token usage。
7875
- * 失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)均记录日志并重试,最多尝试
7876
- * options.maxAttempts 次;全部失败返回 null(不产生任何日志变更)。每次请求发出前
7877
- * 先过全局限流等待门。
7933
+ * 失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)均记录日志并重试,每次尝试的
7934
+ * 结果或报错始终写入日志(成功 info / 失败 warn,不受 debug 影响);全部尝试耗尽
7935
+ * 返回失败结果(携带最后一次尝试的实际报错/具体问题,不产生任何日志变更)。
7936
+ * signal 中止(含限流等待被中止)立即放弃并标记 aborted。每次请求发出前先过全局
7937
+ * 限流等待门。
7878
7938
  */
7879
7939
  async function runSummarySubagent(ctx, agent, instruction, contextText, maxTokens, target, debug, signal, options) {
7880
7940
  const session = agent.session;
@@ -7883,30 +7943,44 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
7883
7943
  let lastFailure = {};
7884
7944
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
7885
7945
  if (signal?.aborted) {
7886
- logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前 signal 已中止),放弃本次摘要`);
7887
- return null;
7946
+ logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前 signal 已中止),放弃本次压缩`);
7947
+ return {
7948
+ ok: false,
7949
+ error: COMPACTION_ABORTED_ERROR,
7950
+ aborted: true
7951
+ };
7888
7952
  }
7889
7953
  const rateLimitWaitMs = options?.rateLimitWaitMs ?? 6e4;
7890
7954
  if (!await gateRateLimit(rateLimitWaitMs, signal)) {
7891
- logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前限流等待被 signal 中止),放弃本次摘要`);
7892
- return null;
7955
+ logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前限流等待被 signal 中止),放弃本次压缩`);
7956
+ return {
7957
+ ok: false,
7958
+ error: COMPACTION_ABORTED_ERROR,
7959
+ aborted: true
7960
+ };
7893
7961
  }
7894
- logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens})`);
7962
+ logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens === void 0 ? "未设置" : String(maxTokens)})`);
7895
7963
  try {
7896
7964
  const requestOptions = buildSummaryOptions(session, instruction, contextText, maxTokens, target, signal);
7897
7965
  const collector = new StreamCollector();
7898
7966
  for await (const chunk of ctx.llm.stream(requestOptions)) collector.push(chunk);
7899
- const text = extractSummaryLog(collector.text, options?.expected);
7967
+ const extracted = extractSummaryDetailed(collector.text, options?.expected);
7900
7968
  const finish = collector.finish;
7901
- if (finish.kind !== "stop" || text === null) {
7902
- const reason = finish.kind === "stop" ? collector.text.trim() === "" ? "无输出" : "缺少合法 <history> 块(含 reasoning 或 index 不连续)" : String(finish.kind);
7903
- lastFailure = { finish: reason };
7904
- logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${reason})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,忽略本次摘要"));
7969
+ if (finish.kind !== "stop") {
7970
+ lastFailure = { reason: `摘要流以 ${String(finish.kind)} 结束(非正常完成)` };
7971
+ logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${lastFailure.reason})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
7905
7972
  continue;
7906
7973
  }
7974
+ if ("error" in extracted) {
7975
+ lastFailure = { reason: extracted.error };
7976
+ logger.warn(`摘要输出未通过校验(第 ${attempt}/${maxAttempts} 次,${extracted.error})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
7977
+ continue;
7978
+ }
7979
+ const text = extracted.log;
7907
7980
  const usage = collector.usage;
7908
- logger.step(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + ")");
7981
+ logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + ")");
7909
7982
  return {
7983
+ ok: true,
7910
7984
  text,
7911
7985
  attemptCount: attempt,
7912
7986
  ...usage === void 0 ? {} : { usage }
@@ -7918,11 +7992,16 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
7918
7992
  logger.warn(`摘要调用触发限流(429,第 ${attempt}/${maxAttempts} 次),下一次请求前至少等待 ${rateLimitWaitMs}ms`);
7919
7993
  }
7920
7994
  lastFailure = { error: message };
7921
- logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,忽略本次摘要"));
7995
+ logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
7922
7996
  }
7923
7997
  }
7924
- logger.warn(`摘要调用最终失败(已尝试 ${maxAttempts} 次` + (lastFailure.error !== void 0 ? `,最后错误:${lastFailure.error}` : "") + (lastFailure.finish !== void 0 ? `,最后结果:${lastFailure.finish}` : "") + "),忽略本次摘要");
7925
- return null;
7998
+ const lastError = lastFailure.error ?? lastFailure.reason ?? "未知原因";
7999
+ logger.warn(`摘要调用最终失败(已尝试 ${maxAttempts} 次,最后错误:${lastError}),拒绝放行本轮 step`);
8000
+ return {
8001
+ ok: false,
8002
+ error: lastError,
8003
+ aborted: false
8004
+ };
7926
8005
  }
7927
8006
  //#endregion
7928
8007
  //#region src/compress.ts
@@ -7932,12 +8011,19 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
7932
8011
  * reflectPass / observePass / maybeCompress。
7933
8012
  *
7934
8013
  * - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要合并为一条
7935
- * - 观察:净压力(上下文压力 − 已压缩块 token 合计)≥ observeThresholdTokens 时,
8014
+ * - 观察:净压力(上下文压力 − 已压缩块 token 合计 系统提示词 token 估算 − 工具定义
8015
+ * token 估算)≥ observeThresholdTokens 时,
7936
8016
  * 摘要未压缩消息为新 <history> 块并精确替换被压缩区间(旧块保留)
7937
8017
  * - 两级在 pre-step 阻塞串行执行(先反思后观察);仅主会话生效;omEnabled=false 关闭
7938
8018
  * - 压缩边界:最后一个合法 <history> 块之后的消息视为未压缩,其前不重复压缩
8019
+ * - 摘要尝试全部耗尽时 pass 返回失败结果(携带最后一次尝试的实际报错),压缩流程
8020
+ * 向上传播,pre-step 据此拒绝本 step 中断当前 turn;signal 中止标记 aborted(不中断)
7939
8021
  * - 提交走宿主 compaction/* 生命周期事件(start 带 phase → summary → 替换消息 → end),
7940
- * 失败补 end(error);替换消息 source 标记插件标识供 UI 认领
8022
+ * 失败补 end(error,实际报错);替换消息 source 标记插件标识供 UI 认领
8023
+ * - 挂载失败类问题(systemPrompt/tokenMeter 服务异常)始终 console 到外部进程,并追加
8024
+ * log-only om/warning 事件(客户端渲染功能降级警告行,每会话同一问题至多一次);
8025
+ * 辅助估算的普通运行时报错仅记日志。降级与报错都不阻塞压缩(tokenMeter 压力数据
8026
+ * 缺失时本轮跳过观察)
7941
8027
  */
7942
8028
  /** 历史文本 token 估算:4 字符 ≈ 1 token(与宿主 dsh-token-meter 启发式一致)。 */
7943
8029
  function estimateTextTokens(text) {
@@ -8084,7 +8170,7 @@ function appendCompactionSummary(session, data) {
8084
8170
  shadowedCharCount: data.shadowedCharCount,
8085
8171
  provider: data.provider,
8086
8172
  model: data.model,
8087
- maxTokens: data.maxTokens,
8173
+ ...data.maxTokens === void 0 ? {} : { maxTokens: data.maxTokens },
8088
8174
  attemptCount: data.attemptCount,
8089
8175
  ...data.usage === void 0 ? {} : { usage: data.usage }
8090
8176
  };
@@ -8120,7 +8206,8 @@ function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, comp
8120
8206
  }
8121
8207
  /**
8122
8208
  * 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要调用把整个块
8123
- * 区段合并替换为一条更紧凑的摘要。失败不产生部分替换。
8209
+ * 区段合并替换为一条更紧凑的摘要。失败不产生部分替换;摘要尝试全部耗尽返回
8210
+ * 失败结果(error = 最后一次尝试的实际报错/具体问题)。
8124
8211
  */
8125
8212
  async function reflectPass(ctx, agent, config, target, signal) {
8126
8213
  const session = agent.session;
@@ -8129,21 +8216,21 @@ async function reflectPass(ctx, agent, config, target, signal) {
8129
8216
  const { blocks } = historySection(session);
8130
8217
  if (blocks.length === 0) {
8131
8218
  logger.step("反思:无 <history> 压缩日志,跳过");
8132
- return;
8219
+ return { failed: false };
8133
8220
  }
8134
8221
  const threshold = config.reflectThresholdTokens;
8135
8222
  const tokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
8136
8223
  const shadowedCharCount = blocks.reduce((total, block) => total + historyInnerText(block.text).length, 0);
8137
8224
  if (tokens < threshold) {
8138
8225
  logger.step(`反思:摘要 ${tokens} tokens < 阈值 ${threshold},跳过`);
8139
- return;
8226
+ return { failed: false };
8140
8227
  }
8141
8228
  logger.step(`反思:摘要 ${tokens} tokens ≥ 阈值 ${threshold},触发精简合并(${blocks.length} 个块)`);
8142
8229
  const first = blocks[0];
8143
8230
  const last = blocks[blocks.length - 1];
8144
8231
  if (first === void 0 || last === void 0) {
8145
8232
  logger.step("反思:块区段缺失,跳过");
8146
- return;
8233
+ return { failed: false };
8147
8234
  }
8148
8235
  const blockSeqs = blocks.map((block) => block.seq);
8149
8236
  const instruction = buildHistoryPrompt();
@@ -8159,7 +8246,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
8159
8246
  } catch (error) {
8160
8247
  const message = error instanceof Error ? error.message : String(error);
8161
8248
  logger.warn(`反思压缩启动失败: ${message}`);
8162
- return;
8249
+ return { failed: false };
8163
8250
  }
8164
8251
  const summaryResult = await runSummarySubagent(ctx, agent, instruction, contextText, config.compressMaxTokens, target, config.debug, signal, {
8165
8252
  maxAttempts: config.compressRetryCount + 1,
@@ -8169,12 +8256,16 @@ async function reflectPass(ctx, agent, config, target, signal) {
8169
8256
  },
8170
8257
  rateLimitWaitMs: config.rateLimitWaitMs
8171
8258
  });
8172
- if (summaryResult === null || summaryResult.text.trim().length === 0) {
8173
- logger.step("反思:摘要调用失败/无输出,追加 compaction/end(error)");
8259
+ if (!summaryResult.ok) {
8260
+ logger.warn(`反思:摘要调用失败(${summaryResult.error}),追加 compaction/end(error)`);
8174
8261
  try {
8175
- appendCompactionEnd(session, lifecycle, "摘要调用失败/无输出");
8262
+ appendCompactionEnd(session, lifecycle, summaryResult.error);
8176
8263
  } catch {}
8177
- return;
8264
+ return {
8265
+ failed: true,
8266
+ error: summaryResult.error,
8267
+ aborted: summaryResult.aborted
8268
+ };
8178
8269
  }
8179
8270
  const report = summaryResult.text;
8180
8271
  try {
@@ -8204,50 +8295,113 @@ async function reflectPass(ctx, agent, config, target, signal) {
8204
8295
  logger.step("反思提交:追加 compaction/end");
8205
8296
  appendCompactionEnd(session, lifecycle);
8206
8297
  logger.info(`反思完成(摘要 ${tokens} tokens ≥ 阈值 ${threshold},合并 ${blocks.length} 个块为一条)`);
8298
+ return { failed: false };
8207
8299
  } catch (error) {
8208
8300
  const message = error instanceof Error ? error.message : String(error);
8209
8301
  logger.warn(`反思提交失败: ${message}`);
8210
8302
  try {
8211
8303
  appendCompactionEnd(session, lifecycle, message);
8212
8304
  } catch {}
8305
+ return { failed: false };
8306
+ }
8307
+ }
8308
+ /**
8309
+ * 估算系统提示词 tokens:按 agent 作用域组装并渲染系统提示词,按长度/4 启发式计。
8310
+ * systemPrompt 服务经 ctx.get 容错读取(ctx 属性访问在服务未挂载时抛错);服务缺失
8311
+ * 或组装/渲染失败时按 0 计——只影响观察触发时机(偏早触发),不阻塞压缩。
8312
+ * 服务缺失属挂载失败:console 外部 + om/warning 事件每会话报告一次;组装失败仅记日志。
8313
+ */
8314
+ async function estimateSystemPromptTokens(ctx, agent, logger, signal) {
8315
+ const systemPrompt = ctx.get("systemPrompt");
8316
+ if (systemPrompt === void 0 || typeof systemPrompt.assemble !== "function") {
8317
+ reportDegrade(agent.session, logger, "systemPrompt-missing");
8318
+ return 0;
8319
+ }
8320
+ try {
8321
+ const agentCtx = agent.ctx;
8322
+ const scope = agentCtx === void 0 ? void 0 : scopeOf(agentCtx);
8323
+ const assembleContext = { agent };
8324
+ if (scope !== void 0) assembleContext.scope = scope;
8325
+ if (signal !== void 0) assembleContext.signal = signal;
8326
+ const assembly = await systemPrompt.assemble(assembleContext);
8327
+ return estimateTextTokens(renderPrompt(assembly));
8328
+ } catch (error) {
8329
+ const message = error instanceof Error ? error.message : String(error);
8330
+ logger.warn(`系统提示词 tokens 估算失败,按 0 计: ${message}`);
8331
+ return 0;
8332
+ }
8333
+ }
8334
+ /**
8335
+ * 估算请求工具定义 tokens:按会话请求头 tools(assembled tool schemas)JSON 序列化
8336
+ * 长度/4 启发式计(与宿主 dsh-token-meter 对工具 schema 的启发式一致)。请求头读取
8337
+ * 失败或缺 tools 时按 0 计——只影响观察触发时机(偏早触发),不阻塞压缩。
8338
+ */
8339
+ function estimateToolsTokens(session, logger) {
8340
+ try {
8341
+ const header = session.requestHeader();
8342
+ if (header?.tools === void 0 || header.tools.length === 0) return 0;
8343
+ return estimateTextTokens(JSON.stringify(header.tools));
8344
+ } catch (error) {
8345
+ const message = error instanceof Error ? error.message : String(error);
8346
+ logger.warn(`工具定义 tokens 估算失败,按 0 计: ${message}`);
8347
+ return 0;
8348
+ }
8349
+ }
8350
+ /**
8351
+ * 读取上下文压力 tokens(tokenMeter.measure 的 totalTokens)。tokenMeter 调用异常时
8352
+ * 记日志并报告降级(console 外部 + om/warning 每会话一次),返回 undefined——本轮
8353
+ * 跳过观察压缩(无压力数据不触发),不阻塞 turn。
8354
+ */
8355
+ function measurePressureTokens(ctx, session, logger) {
8356
+ try {
8357
+ return ctx.tokenMeter.measure(session).totalTokens;
8358
+ } catch (error) {
8359
+ const message = error instanceof Error ? error.message : String(error);
8360
+ logger.warn(`上下文压力读取失败(tokenMeter.measure 抛错),本轮跳过观察压缩: ${message}`);
8361
+ reportDegrade(session, logger, "tokenMeter-unavailable");
8362
+ return;
8213
8363
  }
8214
8364
  }
8215
8365
  /**
8216
- * 观察:净压力 tokens(上下文压力 − 已压缩 <history> 块 token 合计)≥
8217
- * observeThresholdTokens 时,摘要调用把未压缩消息压缩为观察日志,追加到旧摘要并
8218
- * 替换被压缩消息区间。失败不产生部分替换。
8366
+ * 观察:净压力 tokens(上下文压力 − 已压缩 <history> 块 token 合计 − 系统提示词
8367
+ * token 估算 − 工具定义 token 估算)≥ observeThresholdTokens 时,摘要调用把未压缩
8368
+ * 消息压缩为观察日志,追加到旧摘要并替换被压缩消息区间。失败不产生部分替换;
8369
+ * 摘要尝试全部耗尽返回失败结果(error = 最后一次尝试的实际报错/具体问题)。
8219
8370
  */
8220
8371
  async function observePass(ctx, agent, config, tailCount, target, signal) {
8221
8372
  const session = agent.session;
8222
8373
  const logger = makeLogger(ctx, config.debug);
8223
8374
  logger.step(`观察检查(观察阈值 ${config.observeThresholdTokens} tokens,尾部保留 ${tailCount} 条)`);
8224
8375
  const threshold = config.observeThresholdTokens;
8225
- const pressureTokens = ctx.tokenMeter.measure(session).totalTokens;
8376
+ const pressureTokens = measurePressureTokens(ctx, session, logger);
8377
+ if (pressureTokens === void 0) return { failed: false };
8226
8378
  const { blocks } = historySection(session);
8227
8379
  const historyTokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
8228
- const netTokens = pressureTokens - historyTokens;
8380
+ const systemTokens = await estimateSystemPromptTokens(ctx, agent, logger, signal);
8381
+ const toolsTokens = estimateToolsTokens(session, logger);
8382
+ const netTokens = pressureTokens - historyTokens - systemTokens - toolsTokens;
8229
8383
  if (netTokens < threshold) {
8230
- logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})< 阈值 ${threshold},跳过`);
8231
- return;
8384
+ logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens} − 系统提示词 ${systemTokens} − 工具定义 ${toolsTokens})< 阈值 ${threshold},跳过`);
8385
+ return { failed: false };
8232
8386
  }
8233
- logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})≥ 阈值 ${threshold},触发压缩`);
8387
+ logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens} − 系统提示词 ${systemTokens} − 工具定义 ${toolsTokens})≥ 阈值 ${threshold},触发压缩`);
8234
8388
  const range = computeCompressRange(session, tailCount);
8235
8389
  if (!range) {
8236
8390
  logger.step("观察:无可行压缩区间(边界后消息过短或配对无法平衡),跳过");
8237
- return;
8391
+ return { failed: false };
8238
8392
  }
8239
8393
  logger.step(`观察:压缩区间 [${range.start}..${range.end}],遮蔽 ${range.shadowedSeqs.length} 个表层节点`);
8240
8394
  const replaceSeqs = range.shadowedSeqs;
8241
8395
  const replaceStart = replaceSeqs[0];
8242
8396
  if (replaceStart === void 0) {
8243
8397
  logger.step("观察:区间内无新消息(全部为压缩日志块),跳过");
8244
- return;
8398
+ return { failed: false };
8245
8399
  }
8246
8400
  const shadowedSet = new Set(replaceSeqs);
8247
8401
  const inRangeCms = indexCompleteMessages(session).filter((cm) => cm.seqs.every((seq) => shadowedSet.has(seq)));
8248
8402
  if (inRangeCms.length === 0) {
8249
8403
  logger.step("观察:区间内无完整消息,跳过");
8250
- return;
8404
+ return { failed: false };
8251
8405
  }
8252
8406
  const startIndex = inRangeCms[0]?.index ?? 0;
8253
8407
  const endIndex = inRangeCms[inRangeCms.length - 1]?.index ?? startIndex;
@@ -8264,7 +8418,7 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
8264
8418
  } catch (error) {
8265
8419
  const message = error instanceof Error ? error.message : String(error);
8266
8420
  logger.warn(`观察压缩启动失败: ${message}`);
8267
- return;
8421
+ return { failed: false };
8268
8422
  }
8269
8423
  const summaryResult = await runSummarySubagent(ctx, agent, instruction, contextText, config.compressMaxTokens, target, config.debug, signal, {
8270
8424
  maxAttempts: config.compressRetryCount + 1,
@@ -8274,12 +8428,16 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
8274
8428
  },
8275
8429
  rateLimitWaitMs: config.rateLimitWaitMs
8276
8430
  });
8277
- if (summaryResult === null || summaryResult.text.trim().length === 0) {
8278
- logger.step("观察:摘要调用失败/无输出,追加 compaction/end(error)");
8431
+ if (!summaryResult.ok) {
8432
+ logger.warn(`观察:摘要调用失败(${summaryResult.error}),追加 compaction/end(error)`);
8279
8433
  try {
8280
- appendCompactionEnd(session, lifecycle, "摘要调用失败/无输出");
8434
+ appendCompactionEnd(session, lifecycle, summaryResult.error);
8281
8435
  } catch {}
8282
- return;
8436
+ return {
8437
+ failed: true,
8438
+ error: summaryResult.error,
8439
+ aborted: summaryResult.aborted
8440
+ };
8283
8441
  }
8284
8442
  const report = summaryResult.text;
8285
8443
  const attemptCount = summaryResult.attemptCount - 1;
@@ -8287,7 +8445,13 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
8287
8445
  const shadowedTokenCount = replaceSeqs.reduce((total, seq) => {
8288
8446
  const event = session.events[seq];
8289
8447
  const message = event ? session.deriveEventMessage(event) : null;
8290
- return total + (message ? ctx.tokenMeter.estimateMessage(message) : 0);
8448
+ let tokens = 0;
8449
+ if (message) try {
8450
+ tokens = ctx.tokenMeter.estimateMessage(message);
8451
+ } catch {
8452
+ reportDegrade(session, logger, "tokenMeter-unavailable");
8453
+ }
8454
+ return total + tokens;
8291
8455
  }, 0);
8292
8456
  const shadowedCharCount = replaceSeqs.reduce((total, seq) => {
8293
8457
  const event = session.events[seq];
@@ -8321,34 +8485,45 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
8321
8485
  logger.step("观察提交:追加 compaction/end");
8322
8486
  appendCompactionEnd(session, lifecycle);
8323
8487
  logger.info(`观察压缩完成(净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})≥ 阈值 ${threshold},替换 ${replaceSeqs.length} 个表层节点,约 ${shadowedTokenCount} tokens)`);
8488
+ return { failed: false };
8324
8489
  } catch (error) {
8325
8490
  const message = error instanceof Error ? error.message : String(error);
8326
8491
  logger.warn(`观察压缩提交失败: ${message}`);
8327
8492
  try {
8328
8493
  appendCompactionEnd(session, lifecycle, message);
8329
8494
  } catch {}
8495
+ return { failed: false };
8330
8496
  }
8331
8497
  }
8332
- /** 压力检查 + 两级压缩入口:先反思后观察,pre-step 阻塞串行执行;仅主会话生效。 */
8498
+ /**
8499
+ * 压力检查 + 两级压缩入口:先反思后观察,pre-step 阻塞串行执行;仅主会话生效。
8500
+ * 反思摘要耗尽失败时直接返回失败结果(观察无需继续,本轮将被拒绝);观察失败同样
8501
+ * 传播失败结果,由 pre-step 拒绝本 step 中断当前 turn。
8502
+ */
8333
8503
  async function maybeCompress(ctx, agent, config, signal) {
8334
8504
  const session = agent.session;
8335
8505
  const logger = makeLogger(ctx, config.debug);
8336
8506
  if (!config.omEnabled) {
8337
8507
  logger.step("omEnabled=false,跳过压缩");
8338
- return;
8508
+ return { failed: false };
8339
8509
  }
8340
8510
  const target = routedTarget(session);
8341
8511
  if (target === void 0) {
8342
8512
  logger.step("会话未路由(无 provider/model),跳过压缩");
8343
- return;
8513
+ return { failed: false };
8344
8514
  }
8345
8515
  logger.step(`会话路由:provider ${target.provider},model ${target.model}`);
8346
8516
  const tailCount = config.tailMessageCount;
8347
8517
  logger.step("反思 pass 开始");
8348
- await reflectPass(ctx, agent, config, target, signal);
8518
+ const reflect = await reflectPass(ctx, agent, config, target, signal);
8519
+ if (reflect.failed) {
8520
+ logger.step("反思 pass 失败(本轮将被拒绝),跳过观察 pass");
8521
+ return reflect;
8522
+ }
8349
8523
  logger.step("反思 pass 结束,观察 pass 开始");
8350
- await observePass(ctx, agent, config, tailCount, target, signal);
8524
+ const observe = await observePass(ctx, agent, config, tailCount, target, signal);
8351
8525
  logger.step("观察 pass 结束,压缩流程完成");
8526
+ return observe;
8352
8527
  }
8353
8528
  //#endregion
8354
8529
  //#region src/model-download.ts
@@ -8559,9 +8734,9 @@ function cosineSimilarity(a, b) {
8559
8734
  */
8560
8735
  /** 默认配置(冻结对象,resolveConfig 合并的基底;debug 缺省值在解析时按 NODE_ENV 判定)。 */
8561
8736
  const DEFAULT_CONFIG = Object.freeze({
8562
- observeThresholdTokens: 35e3,
8563
- reflectThresholdTokens: 4e4,
8564
- compressMaxTokens: 5e3,
8737
+ observeThresholdTokens: 45e3,
8738
+ reflectThresholdTokens: 12e4,
8739
+ compressMaxTokens: void 0,
8565
8740
  rateLimitWaitMs: 6e4,
8566
8741
  tailMessageCount: 5,
8567
8742
  compressRetryCount: 5,
@@ -8720,9 +8895,9 @@ function renderRecallOutput(value) {
8720
8895
  * 各字段描述经 .describe() 透传到 wire JSON Schema。
8721
8896
  */
8722
8897
  const recallArgsSchema = z.object({
8723
- start: z.number().describe("完整消息序号(index),作为基准边界,和 end 或 offset 配合,指定区间;end 与 offset 至少提供一个(同时给出时 end 优先)。"),
8724
- end: z.number().optional().describe("与 offset 互斥,指定区间的另一个边界。"),
8725
- offset: z.number().optional().describe("与 end 互斥。相对 start 的步数:正数向后、负数向前。")
8898
+ start: z.number().describe("完整消息 index,基准边界,与 end 或 offset 配合指定区间;end 与 offset 至少提供一个(同时给出时 end 优先)。"),
8899
+ end: z.number().optional().describe("与 offset 互斥,指定区间另一边界。"),
8900
+ offset: z.number().optional().describe("与 end 互斥,相对 start 的步数(正向后、负向前)。")
8726
8901
  }).refine((args) => args.end !== void 0 || args.offset !== void 0, { message: "end 与 offset 至少提供一个" });
8727
8902
  /** 解析并校验 recall 调用参数:失败时抛出首个校验问题的可读消息(普通 Error,兼容 SDK 展示)。 */
8728
8903
  function parseRecallArgs(raw) {
@@ -8737,7 +8912,7 @@ function parseRecallArgs(raw) {
8737
8912
  function buildRecallTool(getPruner) {
8738
8913
  return {
8739
8914
  name: "recall",
8740
- description: `${COMPLETE_MESSAGE_DEFINITION}此工具可以精确查询完整消息。用index指定一个区间,返回区间内所有完整消息的内容;完整消息携带的图片附件随结果保留(文本段以 [图片附件:…] 标注,随后附 image 内容块)。`,
8915
+ description: `${COMPLETE_MESSAGE_DEFINITION}index 区间精确返回区间内全部完整消息的内容。`,
8741
8916
  parameters: parametersFromZod(recallArgsSchema),
8742
8917
  output: {
8743
8918
  schema: RECALL_OUTPUT_SCHEMA,
@@ -8804,11 +8979,11 @@ function buildRecallTool(getPruner) {
8804
8979
  */
8805
8980
  /** recall-semantic 工具参数 schema:query 必填;top_k 默认 3(1-10);区间参数均可选。 */
8806
8981
  const semanticRecallArgsSchema = z.object({
8807
- query: z.string().describe("描述要找的内容的自然语言 query(可混用中英文与代码术语,如 \"修复 retry backoff 的逻辑\"),不能为空。"),
8808
- top_k: z.number().int().min(1).max(10).optional().describe("返回最匹配的完整消息条数(1-10,默认 3)。"),
8809
- start: z.number().optional().describe("完整消息序号(index),可选。作为基准边界,和endoffset配合,指定搜索区间。"),
8810
- end: z.number().optional().describe("必须和start配合使用,与 offset 互斥,指定搜索区间的另一个边界。"),
8811
- offset: z.number().optional().describe("必须和start配合使用,与 end 互斥。相对 start 的步数:正数向后、负数向前。")
8982
+ query: z.string().describe("要找的内容的自然语言 query(中英文与代码术语皆可,如 \"修复 retry backoff 的逻辑\"),不能为空。"),
8983
+ top_k: z.number().int().min(1).max(10).optional().describe("返回最匹配条数(1-10,默认 3)。"),
8984
+ start: z.number().optional().describe("完整消息 index,基准边界,与 end/offset 配合指定搜索区间;缺省检索全部。"),
8985
+ end: z.number().optional().describe("须与 start 配合,与 offset 互斥,指定搜索区间另一边界。"),
8986
+ offset: z.number().optional().describe("须与 start 配合,与 end 互斥,相对 start 的步数(正向后、负向前)。")
8812
8987
  }).refine((args) => args.query.trim().length > 0, { message: "query 不能为空" });
8813
8988
  /** 解析并校验参数:失败时抛出可读错误(普通 Error 而非 ZodError)。 */
8814
8989
  function parseSemanticRecallArgs(raw) {
@@ -8888,7 +9063,7 @@ function buildSemanticRecallTool(options) {
8888
9063
  const embed = options?.embedder ?? ((texts) => getEmbedder().then((fn) => fn(texts)));
8889
9064
  return {
8890
9065
  name: "recall-semantic",
8891
- description: `${COMPLETE_MESSAGE_DEFINITION}此工具可以按自然语言含义,检索最符合的完整消息。默认在全消息范围搜索,可以指定区间。注意:语义检索只匹配文本,图片内容不参与匹配(纯图片消息无法命中,也不能按图片内容检索);命中的完整消息携带的图片附件随结果保留(文本段以 [图片附件:…] 标注,随后附 image 内容块),与 recall 相同。`,
9066
+ description: `${COMPLETE_MESSAGE_DEFINITION}按自然语言含义检索最符合的完整消息,默认全量搜索,可指定区间。注意:只匹配文本。`,
8892
9067
  parameters: parametersFromZod(semanticRecallArgsSchema),
8893
9068
  output: {
8894
9069
  schema: RECALL_OUTPUT_SCHEMA,
@@ -8966,7 +9141,8 @@ function buildSemanticRecallTool(options) {
8966
9141
  /**
8967
9142
  * dsh-plugin-om 入口(tsdown 打包入口):导出 name / inject / apply。
8968
9143
  * apply 注册 recall / recall-semantic 工具,并接线 agent/pre-step 自动压缩
8969
- * (先反思后观察,仅主会话生效)。压缩与检索的实现见 compress.ts / recall.ts /
9144
+ * (先反思后观察,仅主会话生效)。压缩摘要尝试全部耗尽时拒绝本 step 中断当前
9145
+ * turn(signal 中止除外)。压缩与检索的实现见 compress.ts / recall.ts /
8970
9146
  * semantic-recall.ts。
8971
9147
  */
8972
9148
  /** 插件名(Loader 识别入口的稳定标识)。 */
@@ -8982,14 +9158,11 @@ const inject = [
8982
9158
  function apply(ctx, config) {
8983
9159
  const resolved = resolveConfig(config);
8984
9160
  const logger = makeLogger(ctx, resolved.debug);
8985
- logger.step(`apply 启动:observeThresholdTokens=${String(resolved.observeThresholdTokens)} reflectThresholdTokens=${String(resolved.reflectThresholdTokens)} compressMaxTokens=${String(resolved.compressMaxTokens)} tailMessageCount=${String(resolved.tailMessageCount)} omEnabled=${String(resolved.omEnabled)} debug=${String(resolved.debug)}`);
9161
+ logger.step(`apply 启动:observeThresholdTokens=${String(resolved.observeThresholdTokens)} reflectThresholdTokens=${String(resolved.reflectThresholdTokens)} compressMaxTokens=${resolved.compressMaxTokens === void 0 ? "未设置" : String(resolved.compressMaxTokens)} tailMessageCount=${String(resolved.tailMessageCount)} omEnabled=${String(resolved.omEnabled)} debug=${String(resolved.debug)}`);
8986
9162
  if (resolved.recallEnabled) ctx.tools.register(buildRecallTool(() => ctx.get("toolResultPruner")));
8987
9163
  if (resolved.semanticRecallEnabled) {
8988
9164
  const warnModel = (message) => ctx.logger.warn(`dsh-plugin-om: ${message}`);
8989
- const logModel = (message) => {
8990
- console.log(message);
8991
- logger.info(message);
8992
- };
9165
+ const logModel = (message) => logger.info(message);
8993
9166
  ensureModelReady(resolved.modelDir, warnModel, void 0, logModel);
8994
9167
  ctx.tools.register(buildSemanticRecallTool({
8995
9168
  getPruner: () => ctx.get("toolResultPruner"),
@@ -8998,16 +9171,21 @@ function apply(ctx, config) {
8998
9171
  }));
8999
9172
  }
9000
9173
  ctx.on("agent/pre-step", async ({ agent, signal }, next) => {
9174
+ let outcome = { failed: false };
9001
9175
  try {
9002
9176
  if (signal.aborted) logger.step("pre-step 已中止(signal aborted),跳过压缩");
9003
9177
  else if (!isMainSession(agent.session)) logger.step("subagent 会话,跳过压缩(仅主会话生效)");
9004
9178
  else {
9005
9179
  logger.step(`pre-step 触发压缩(会话 ${agent.session.id})`);
9006
- await maybeCompress(ctx, agent, resolved, signal);
9180
+ outcome = await maybeCompress(ctx, agent, resolved, signal);
9007
9181
  }
9008
9182
  } catch (error) {
9009
9183
  logger.warn(`pre-step 处理失败: ${error instanceof Error ? error.message : String(error)}`);
9010
9184
  }
9185
+ if (outcome.failed && !outcome.aborted) {
9186
+ logger.warn(`上下文压缩失败,拒绝本 step 中断当前 turn:${outcome.error}`);
9187
+ return { kind: "reject" };
9188
+ }
9011
9189
  return next();
9012
9190
  });
9013
9191
  }