dsh-plugin-om 0.0.26 → 0.0.28

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
@@ -39,30 +39,87 @@ function isPluginOwnedSource(source) {
39
39
  return source.plugin === "dsh-plugin-om" || source.plugin === "compact";
40
40
  }
41
41
  //#endregion
42
+ //#region src/om-event.ts
43
+ /** om 信封 text 前缀:标识该 feedback/record 记录由本插件写入。 */
44
+ const OM_EVENT_PREFIX = "om:1:";
45
+ /** 校验载荷字段与类别匹配(运行时守卫,保证返回类型的诚实性)。 */
46
+ function isValidPayload(kind, data) {
47
+ switch (kind) {
48
+ case "om/warning": return typeof data.problem === "string" && typeof data.message === "string";
49
+ case "om/observe-pending": return typeof data.triggerMessageIndex === "number" && Number.isSafeInteger(data.triggerMessageIndex);
50
+ case "om/observe-invalidate": return typeof data.pendingSeq === "number" && Number.isSafeInteger(data.pendingSeq);
51
+ }
52
+ }
53
+ /**
54
+ * 解码一条会话事件为 om 私有事件:仅识别 feedback/record 中带 om 信封前缀的
55
+ * text;前缀缺失、JSON 非法、kind 未知或载荷字段缺失时返回 undefined。
56
+ */
57
+ function readOmEvent(event) {
58
+ if (event === void 0 || event === null || event.type !== "feedback/record") return void 0;
59
+ const text = event.data?.text;
60
+ if (typeof text !== "string" || !text.startsWith("om:1:")) return void 0;
61
+ let envelope;
62
+ try {
63
+ envelope = JSON.parse(text.slice(5));
64
+ } catch {
65
+ return;
66
+ }
67
+ if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) return void 0;
68
+ const { kind, ...rest } = envelope;
69
+ if (typeof kind !== "string") return void 0;
70
+ if (![
71
+ "om/warning",
72
+ "om/observe-pending",
73
+ "om/observe-invalidate"
74
+ ].includes(kind)) return void 0;
75
+ const omKind = kind;
76
+ if (!isValidPayload(omKind, rest)) return void 0;
77
+ return {
78
+ kind: omKind,
79
+ data: rest,
80
+ seq: event.seq
81
+ };
82
+ }
83
+ /** 编码一条 om 私有事件为 feedback/record 的 text 信封。 */
84
+ function encodeOmEvent(kind, data) {
85
+ return `${OM_EVENT_PREFIX}${JSON.stringify({
86
+ kind,
87
+ ...data
88
+ })}`;
89
+ }
90
+ /** 追加一条 om 私有事件(借用 feedback/record,log-only,不进 surface),返回事件 seq。 */
91
+ function appendOmEvent(session, kind, data) {
92
+ return session.append("feedback/record", { text: encodeOmEvent(kind, data) }).seq;
93
+ }
94
+ function findOmEvents(session, kind) {
95
+ const result = [];
96
+ for (const event of session.events) {
97
+ const om = readOmEvent(event);
98
+ if (om !== void 0 && (kind === void 0 || om.kind === kind)) result.push(om);
99
+ }
100
+ return result;
101
+ }
102
+ //#endregion
42
103
  //#region src/degrade.ts
43
104
  /**
44
105
  * 降级报告:压缩流程中的挂载失败统一出口。
45
106
  * 导出 reportDegrade / DEGRADE_PROBLEMS / DegradedProblem。
46
107
  *
47
108
  * - 挂载失败类问题(服务缺失、服务调用异常)始终 console.warn 到宿主进程外部输出,
48
- * 并向会话日志追加 log-only 的 `om/warning` 事件(客户端渲染为「功能降级」警告行);
109
+ * 并向会话日志追加 log-only 的 om 警告事件(借用 feedback/record 的 om 信封,
110
+ * 见 om-event.ts;客户端渲染为「功能降级」警告行);
49
111
  * 同一会话同一问题只报告一次(按日志扫描去重,重启不重复)
50
112
  * - 报告动作自身绝不抛错:追加失败只记日志,不阻塞压缩
51
113
  * - 辅助函数的普通运行时报错(组装失败、请求头读取失败)不走本模块,仅记日志
52
114
  */
53
- /** 各降级问题面向用户的简短说明(om/warning 载荷 message;客户端警告行直接展示)。 */
115
+ /** 各降级问题面向用户的简短说明(om/warning 信封载荷 message;客户端警告行直接展示)。 */
54
116
  const DEGRADE_PROBLEMS = {
55
117
  "systemPrompt-missing": "系统提示词服务未挂载,上下文压力估算不扣除系统提示词 tokens(压缩触发会偏早)",
56
118
  "tokenMeter-unavailable": "token 计量服务异常,上下文压力估算降级(可能跳过压缩或按 0 计)"
57
119
  };
58
- /** 判定事件是否为指定 problem 的已有 om/warning 记录(会话内去重依据)。 */
59
- function isWarningFor(event, problem) {
60
- if (event.type !== "om/warning") return false;
61
- return event.data?.problem === problem;
62
- }
63
120
  /**
64
121
  * 报告一次挂载失败类降级:console.warn 始终输出到宿主进程外部;同会话同问题首次
65
- * 出现时,向会话日志追加 log-only `om/warning` 事件(客户端渲染警告行,每会话最多
122
+ * 出现时,向会话日志追加 log-only om 警告事件(客户端渲染警告行,每会话最多
66
123
  * 一次)并输出 logger.warn(避免每个 pre-step 重复刷日志)。
67
124
  * 日志扫描/事件追加失败只记日志,绝不抛错、不阻塞压缩。
68
125
  */
@@ -70,19 +127,19 @@ function reportDegrade(session, logger, problem) {
70
127
  const message = DEGRADE_PROBLEMS[problem];
71
128
  let first = true;
72
129
  try {
73
- first = !session.events.some((event) => isWarningFor(event, problem));
130
+ first = !findOmEvents(session, "om/warning").some((om) => om.data.problem === problem);
74
131
  } catch {}
75
132
  if (!first) return;
76
133
  console.warn(`${PLUGIN_LABEL}: ${message}`);
77
134
  logger.warn(message);
78
135
  try {
79
- session.append("om/warning", {
136
+ appendOmEvent(session, "om/warning", {
80
137
  problem,
81
138
  message
82
139
  });
83
140
  } catch (error) {
84
141
  const text = error instanceof Error ? error.message : String(error);
85
- logger.warn(`om/warning 事件追加失败: ${text}`);
142
+ logger.warn(`om 警告事件追加失败: ${text}`);
86
143
  }
87
144
  }
88
145
  //#endregion
@@ -7482,9 +7539,9 @@ function phaseLabel(phase) {
7482
7539
  if (phase === "reflect") return "反思";
7483
7540
  return "压缩";
7484
7541
  }
7485
- /** 诊断子会话 label:含压缩阶段与尝试次数。 */
7486
- function compactionLogLabel(phase, attemptCount) {
7487
- return `OM 压缩失败日志(${phaseLabel(phase)} · ${attemptCount} 次尝试)`;
7542
+ /** 诊断子会话 label:含压缩阶段与尝试序号。 */
7543
+ function compactionLogLabel(phase, attemptNo) {
7544
+ return `OM 压缩日志(${phaseLabel(phase)} · ${attemptNo} 次尝试)`;
7488
7545
  }
7489
7546
  /** 追加一次尝试的「提示词 → 原始输出」消息组(surfaceOp append;id 为品牌类型,session.append 运行时校验)。 */
7490
7547
  function appendAttemptMessages(child, attempt, step, target) {
@@ -7521,14 +7578,13 @@ function appendAttemptMessages(child, attempt, step, target) {
7521
7578
  }, { surfaceOp: "append" });
7522
7579
  }
7523
7580
  /**
7524
- * 把一次最终失败的摘要 run 落盘为诊断子会话:ctx.sessions.create 创建子会话
7525
- * (header origin 'subagent'、parentSession 指向主会话、delegationDepth = 父 + 1、
7526
- * cwd 继承主会话),追加 one-shot descriptor(provider om-compaction-log,label
7527
- * 压缩阶段与尝试次数),逐尝试原样追加「提示词 + 原始输出」消息组,flush 持久化
7528
- * 检查点,返回子会话 id。落盘自身绝不抛错:任何失败仅 logger.warn 并返回
7529
- * undefined(不影响压缩失败流程)。
7581
+ * 把一次摘要尝试落盘为诊断子会话:ctx.sessions.create 创建子会话(header origin
7582
+ * 'subagent'、parentSession 指向主会话、delegationDepth = 父 + 1、cwd 继承主会话),
7583
+ * 追加 one-shot descriptor(provider om-compaction-log,label 含压缩阶段与尝试序号),
7584
+ * 原样追加「提示词 + 原始输出」消息组,flush 持久化检查点,返回子会话 id。落盘自身
7585
+ * 绝不抛错:任何失败仅 logger.warn 并返回 undefined(不影响压缩流程)。
7530
7586
  */
7531
- async function recordCompactionFailure(ctx, parentSession, options) {
7587
+ async function recordCompactionAttempt(ctx, parentSession, options) {
7532
7588
  const logger = makeLogger(ctx, options.debug);
7533
7589
  try {
7534
7590
  const header = parentSession.header;
@@ -7542,23 +7598,19 @@ async function recordCompactionFailure(ctx, parentSession, options) {
7542
7598
  version: SUBAGENT_DESCRIPTOR_VERSION,
7543
7599
  mode: "one-shot",
7544
7600
  provider: COMPACTION_LOG_PROVIDER,
7545
- label: compactionLogLabel(options.phase, options.attempts.length)
7601
+ label: compactionLogLabel(options.phase, options.attemptNo)
7546
7602
  });
7547
- for (let i = 0; i < options.attempts.length; i += 1) {
7548
- const attempt = options.attempts[i];
7549
- if (attempt === void 0) continue;
7550
- appendAttemptMessages(child, attempt, i + 1, options.target);
7551
- }
7603
+ appendAttemptMessages(child, options.attempt, 1, options.target);
7552
7604
  try {
7553
7605
  await ctx.sessions.flush(child);
7554
7606
  } catch (error) {
7555
7607
  const message = error instanceof Error ? error.message : String(error);
7556
- logger.warn(`压缩失败诊断子会话 flush 失败(子会话 ${child.id} 已创建): ${message}`);
7608
+ logger.warn(`压缩日志子会话 flush 失败(子会话 ${child.id} 已创建): ${message}`);
7557
7609
  }
7558
7610
  return child.id;
7559
7611
  } catch (error) {
7560
7612
  const message = error instanceof Error ? error.message : String(error);
7561
- logger.warn(`压缩失败诊断子会话落盘失败: ${message}`);
7613
+ logger.warn(`压缩日志子会话落盘失败(第 ${options.attemptNo} 次尝试): ${message}`);
7562
7614
  return;
7563
7615
  }
7564
7616
  }
@@ -7621,8 +7673,10 @@ async function gateRateLimit(waitMs, signal) {
7621
7673
  /**
7622
7674
  * 共享压缩提示词(观察/反思同一套):定义 history 块(模型消息 + index 的表达形式)、
7623
7675
  * 完整消息定义、压缩要求、输出格式与数据源说明。
7676
+ * skipReasoning=true(默认,与 compressSkipReasoning 默认一致)时压缩输入不含
7677
+ * <reasoning> 参考条目,提示词相应省略 <reasoning> 的说明两行。
7624
7678
  */
7625
- function buildHistoryPrompt() {
7679
+ function buildHistoryPrompt(skipReasoning = true) {
7626
7680
  return [
7627
7681
  "压缩 <history> 消息记录。你应当输出**单个**合法的 <history> 块。",
7628
7682
  "",
@@ -7630,17 +7684,18 @@ function buildHistoryPrompt() {
7630
7684
  "- <history> 是历史消息的记录块。",
7631
7685
  "- <user_message index=\"N\">:用户消息条目。",
7632
7686
  "- <sys type=\"(kind)\" index=\"N\">:系统消息条目。",
7633
- "- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。",
7687
+ ...skipReasoning ? [] : ["- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。"],
7634
7688
  "- <assistant index=\"N\">:单条完整消息(模型输出文本,或 toolcall 及其 result)。",
7635
7689
  "- <assistant start=\"A\" end=\"B\">:多条连续完整消息聚合的模块(A/B 为模块首尾完整消息的 index)。",
7636
7690
  "",
7637
7691
  "【压缩要求】",
7638
7692
  "- <user_message> <sys> 条目从输入中逐条保留,不做任何处理。",
7639
- "- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。",
7640
- "- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\"> 块:块内描述模块的目的、行为与结果;涉及的具体文件保留在模块内容中,多个前缀相同的路径合并简写。",
7641
- "- 单条重要的完整消息以 <assistant index=\"\"> 单独呈现,内容不受限制。",
7693
+ ...skipReasoning ? [] : ["- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。"],
7694
+ "- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\"> ",
7695
+ "- 单条重要的完整消息以 <assistant index=\"\"> 单独呈现",
7696
+ "- 压缩后的 <assistant> 块内,应当描述**行为逻辑**,强调关键的**结论、产出和任务**;涉及到的具体文件保留完整路径",
7642
7697
  "- 加载的 skill 属于**关键信息**:应当产出独立块且不过多省略。",
7643
- "- 条目按 index 顺序覆盖本次压缩的全部完整消息:index/start/end 必须连续(区间内 index 连续、相邻条目相接),不跳号、不重叠、不遗漏。",
7698
+ "- 压缩后的消息,区间边界与输入的消息必须完全相同,内部 index/start/end 必须连续,相邻区间的左右界必须相邻,",
7644
7699
  "",
7645
7700
  "【摘要粒度】",
7646
7701
  "- 越往后越细:靠近末尾(最近)的完整消息保留更多细节(关键文件、改动与结论),开头(较早)的完整消息可适当从简。",
@@ -7697,14 +7752,15 @@ function renderUserEntry(doc, session, cm) {
7697
7752
  }
7698
7753
  /**
7699
7754
  * 渲染完整消息记录(观察输入):输出一个合法的 <history> 块——
7700
- * user → <user_message>(文本原样、图片注释)、sys → <sys> 空块、assistant 的
7701
- * reasoning → <reasoning>(参考条目)、assistant/toolcall → <assistant>(原样文本)。
7755
+ * user → <user_message>(文本原样、图片注释)、sys → <sys> 空块、
7756
+ * assistant/toolcall → <assistant>(原样文本);skipReasoning=false 时另把
7757
+ * assistant 的 reasoning → <reasoning>(参考条目)。
7702
7758
  * 文本经 XML 序列化自动转义;仅渲染 seqs 全部落在给定集合内的完整消息。
7703
7759
  */
7704
- function renderMessages(session, seqs) {
7760
+ function renderMessages(session, seqs, skipReasoning = true) {
7705
7761
  const shadowed = new Set(seqs);
7706
7762
  const reasoningBySeq = /* @__PURE__ */ new Map();
7707
- for (const seq of seqs) {
7763
+ if (!skipReasoning) for (const seq of seqs) {
7708
7764
  const event = session.events[seq];
7709
7765
  if (event?.type !== "assistant/message") continue;
7710
7766
  const message = event.data.message;
@@ -7733,7 +7789,7 @@ function renderMessages(session, seqs) {
7733
7789
  } else {
7734
7790
  const callSeq = cm.seqs[0];
7735
7791
  const reasonings = callSeq === void 0 ? void 0 : reasoningBySeq.get(callSeq);
7736
- if (callSeq !== void 0 && reasonings !== void 0 && !emittedReasoning.has(callSeq)) {
7792
+ if (!skipReasoning && callSeq !== void 0 && reasonings !== void 0 && !emittedReasoning.has(callSeq)) {
7737
7793
  emittedReasoning.add(callSeq);
7738
7794
  for (const text of reasonings) {
7739
7795
  const re = doc.createElement("reasoning");
@@ -7815,6 +7871,14 @@ function buildSummaryOptions(session, instruction, contextText, maxTokens, targe
7815
7871
  }
7816
7872
  /** 产出日志后插入首个 <history> 后的格式说明(XML 注释,完整消息定义 + 条目标签语义)。 */
7817
7873
  const HISTORY_FORMAT_NOTE = `<!-- 完整消息:${COMPLETE_MESSAGE_DEFINITION} <TAG index="N">表示单条完整消息,<TAG start="A" end="B"> 表示连续模块,start/end 是首尾完整消息的 index;<sys type="KIND" index="N"> 表示被压缩的系统消息,块中为空 -->`;
7874
+ /**
7875
+ * 剥离 <history> 块内文块首的格式说明注释(HISTORY_FORMAT_NOTE 整体精确匹配,仅块首
7876
+ * 一处);正文条目内出现的同名注释串不动。非块首或不匹配时原样返回。
7877
+ */
7878
+ function stripLeadingFormatNote(inner) {
7879
+ if (!inner.startsWith(HISTORY_FORMAT_NOTE)) return inner;
7880
+ return inner.slice(HISTORY_FORMAT_NOTE.length).replace(/^\s+/, "");
7881
+ }
7818
7882
  /** 读取元素整数属性(非负整数;缺失 / 非数字返回 undefined)。 */
7819
7883
  function intAttr(el, name) {
7820
7884
  const raw = el.getAttribute(name);
@@ -7886,7 +7950,7 @@ function parseHistoryBlock(xml) {
7886
7950
  };
7887
7951
  }
7888
7952
  /**
7889
- * 解析文本中全部 <history> 块内的条目(反思输入为多个块拼接:逐块解析提取)。
7953
+ * 解析文本中全部 <history> 块内的条目(逐块解析提取,兼容多块拼接文本)。
7890
7954
  * 非法块跳过;仅提取不校验顺序(连续性由 historyContinuity 校验)。
7891
7955
  */
7892
7956
  function parseHistoryEntries(text) {
@@ -8031,95 +8095,109 @@ function extractSummaryDetailed(raw, expected) {
8031
8095
  }
8032
8096
  /**
8033
8097
  * 直连 LLM 执行一次摘要(观察或反思),返回文本与可选 token usage。
8034
- * 失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)均记录日志并重试,每次尝试的
8035
- * 结果或报错始终写入日志(成功 info / 失败 warn,不受 debug 影响);全部尝试耗尽
8036
- * 返回失败结果(携带最后一次尝试的实际报错/具体问题)。最终失败(耗尽或 signal
8037
- * 中止)时把每次尝试的完整提示词与模型原始输出原样落盘为诊断子会话(phase 标注
8038
- * 观察或反思),诊断子会话 id 随失败结果返回。signal 中止(含限流等待被中止)
8039
- * 立即放弃并标记 aborted。每次请求发出前先过全局限流等待门。
8098
+ * 每次实际发出的 LLM 调用(无论成功、校验不通过、非 stop 结束还是异常)完成后,
8099
+ * 立即把该次尝试的完整提示词与模型原始输出原样落盘为诊断子会话(phase 标注观察
8100
+ * 或反思),子会话 id 写入该次尝试的主会话日志;请求发出前被中止的尝试(无实际
8101
+ * 调用)不落盘。失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)记录日志并重试;
8102
+ * 全部尝试耗尽返回失败结果(携带最后一次尝试的实际报错/具体问题与其诊断子会话 id)。
8103
+ * signal 中止(含限流等待被中止)立即放弃并标记 aborted。每次请求发出前先过全局
8104
+ * 限流等待门。
8040
8105
  */
8041
8106
  async function runSummarySubagent(ctx, agent, instruction, contextText, maxTokens, target, debug, signal, options) {
8042
8107
  const session = agent.session;
8043
8108
  const logger = makeLogger(ctx, debug);
8044
8109
  const maxAttempts = options?.maxAttempts ?? 11;
8045
8110
  let lastFailure = {};
8046
- /** 逐尝试的完整记录(提示词 + 原始输出原样收集,仅最终失败时落盘)。 */
8047
- const attempts = [];
8048
- /** 最终失败的统一出口:落盘诊断子会话并携带其 id 返回。 */
8049
- const finishFailure = async (error, aborted) => {
8050
- const diagnosticSessionId = await recordCompactionFailure(ctx, session, {
8051
- phase: options?.phase,
8052
- target,
8053
- attempts,
8054
- debug
8055
- });
8056
- return {
8057
- ok: false,
8058
- error,
8059
- aborted,
8060
- ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8061
- };
8062
- };
8063
8111
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
8064
8112
  if (signal?.aborted) {
8065
8113
  logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前 signal 已中止),放弃本次压缩`);
8066
- return await finishFailure(COMPACTION_ABORTED_ERROR, true);
8114
+ return {
8115
+ ok: false,
8116
+ error: COMPACTION_ABORTED_ERROR,
8117
+ aborted: true,
8118
+ ...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
8119
+ };
8067
8120
  }
8068
8121
  const rateLimitWaitMs = options?.rateLimitWaitMs ?? 6e4;
8069
8122
  if (!await gateRateLimit(rateLimitWaitMs, signal)) {
8070
8123
  logger.warn(`摘要调用中止(第 ${attempt}/${maxAttempts} 次尝试前限流等待被 signal 中止),放弃本次压缩`);
8071
- return await finishFailure(COMPACTION_ABORTED_ERROR, true);
8124
+ return {
8125
+ ok: false,
8126
+ error: COMPACTION_ABORTED_ERROR,
8127
+ aborted: true,
8128
+ ...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
8129
+ };
8072
8130
  }
8073
8131
  logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens === void 0 ? "未设置" : String(maxTokens)})`);
8074
8132
  const prompt = `${instruction}\n\n${contextText ?? ""}`;
8075
8133
  const collector = new StreamCollector();
8076
8134
  let streamCompleted = false;
8135
+ /** 本次尝试的诊断子会话 id(调用完成后立即落盘;落盘失败时缺失)。 */
8136
+ const logAttempt = async () => recordCompactionAttempt(ctx, session, {
8137
+ phase: options?.phase,
8138
+ target,
8139
+ attempt: {
8140
+ prompt,
8141
+ rawOutput: collector.text
8142
+ },
8143
+ attemptNo: attempt,
8144
+ debug
8145
+ });
8077
8146
  try {
8078
8147
  const requestOptions = buildSummaryOptions(session, instruction, contextText, maxTokens, target, signal);
8079
8148
  for await (const chunk of ctx.llm.stream(requestOptions)) collector.push(chunk);
8080
8149
  streamCompleted = true;
8081
- attempts.push({
8082
- prompt,
8083
- rawOutput: collector.text
8084
- });
8150
+ const diagnosticSessionId = await logAttempt();
8085
8151
  const extracted = extractSummaryDetailed(collector.text, options?.expected);
8086
8152
  const finish = collector.finish;
8087
8153
  if (finish.kind !== "stop") {
8088
- lastFailure = { reason: `摘要流以 ${String(finish.kind)} 结束(非正常完成)` };
8089
- logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${lastFailure.reason})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8154
+ lastFailure = {
8155
+ reason: `摘要流以 ${String(finish.kind)} 结束(非正常完成)`,
8156
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8157
+ };
8158
+ logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${lastFailure.reason},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8090
8159
  continue;
8091
8160
  }
8092
8161
  if ("error" in extracted) {
8093
- lastFailure = { reason: extracted.error };
8094
- logger.warn(`摘要输出未通过校验(第 ${attempt}/${maxAttempts} 次,${extracted.error})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8162
+ lastFailure = {
8163
+ reason: extracted.error,
8164
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8165
+ };
8166
+ logger.warn(`摘要输出未通过校验(第 ${attempt}/${maxAttempts} 次,${extracted.error},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8095
8167
  continue;
8096
8168
  }
8097
8169
  const text = extracted.log;
8098
8170
  const usage = collector.usage;
8099
- logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + "");
8171
+ logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + `,子会话 ${diagnosticSessionId ?? "未落盘"})`);
8100
8172
  return {
8101
8173
  ok: true,
8102
8174
  text,
8103
8175
  attemptCount: attempt,
8104
- ...usage === void 0 ? {} : { usage }
8176
+ ...usage === void 0 ? {} : { usage },
8177
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8105
8178
  };
8106
8179
  } catch (error) {
8107
- if (!streamCompleted) attempts.push({
8108
- prompt,
8109
- rawOutput: collector.text
8110
- });
8180
+ const diagnosticSessionId = streamCompleted ? void 0 : await logAttempt();
8111
8181
  const message = error instanceof Error ? error.message : String(error);
8112
8182
  if (isRateLimitError(message)) {
8113
8183
  noteRateLimit();
8114
8184
  logger.warn(`摘要调用触发限流(429,第 ${attempt}/${maxAttempts} 次),下一次请求前至少等待 ${rateLimitWaitMs}ms`);
8115
8185
  }
8116
- lastFailure = { error: message };
8117
- logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8186
+ lastFailure = {
8187
+ error: message,
8188
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8189
+ };
8190
+ logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8118
8191
  }
8119
8192
  }
8120
8193
  const lastError = lastFailure.error ?? lastFailure.reason ?? "未知原因";
8121
8194
  logger.warn(`摘要调用最终失败(已尝试 ${maxAttempts} 次,最后错误:${lastError}),拒绝放行本轮 step`);
8122
- return await finishFailure(lastError, false);
8195
+ return {
8196
+ ok: false,
8197
+ error: lastError,
8198
+ aborted: false,
8199
+ ...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
8200
+ };
8123
8201
  }
8124
8202
  //#endregion
8125
8203
  //#region src/compress.ts
@@ -8128,15 +8206,17 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
8128
8206
  * 导出 estimateTextTokens / isPairBalancedAfter / computeCompressRange / historySection /
8129
8207
  * findObservePending / reflectPass / observePass / maybeCompress。
8130
8208
  *
8131
- * - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要合并为一条
8209
+ * - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,块内文拼合为单个
8210
+ * <history> 块输入摘要,合并为一条
8132
8211
  * - 观察(触发 → 待定 → 延迟执行):净压力(上下文压力 − 已压缩块 token 合计 − 系统提示词
8133
8212
  * token 估算 − 工具定义 token 估算)首次 ≥ observeThresholdTokens 时记录待定标记
8134
8213
  * (触发点 = 当时的最后一条完整消息 index),本次不压缩;待定后新增完整消息数 ≥
8135
8214
  * tailMessageCount 时,把压缩边界至触发点的全部消息摘要为新 <history> 块并精确替换
8136
8215
  * 被压缩区间(旧块保留),等待期间的新消息成为下一轮未压缩尾部(延迟窗口内压力允许
8137
8216
  * 短暂超阈值);tailMessageCount=0 时触发当轮直接执行(不落待定标记)
8138
- * - 待定标记以 log-only om/observe-pending / om/observe-invalidate 事件持久化在会话
8139
- * 日志中(重启后从日志恢复);摘要失败保留待定,下个 pre-step 直接重试执行
8217
+ * - 待定标记以 log-only om 信封事件(借用 feedback/record,kind om/observe-pending /
8218
+ * om/observe-invalidate,见 om-event.ts)持久化在会话日志中(重启后从日志恢复);
8219
+ * 摘要失败保留待定,下个 pre-step 直接重试执行
8140
8220
  * - 两级在 pre-step 阻塞串行执行(先反思后观察);仅主会话生效;omEnabled=false 关闭
8141
8221
  * - 压缩边界:最后一个合法 <history> 块之后的消息视为未压缩,其前不重复压缩
8142
8222
  * - 摘要尝试全部耗尽时 pass 返回失败结果(携带最后一次尝试的实际报错),压缩流程
@@ -8144,7 +8224,8 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
8144
8224
  * - 提交走宿主 compaction/* 生命周期事件(start 带 phase → summary → 替换消息 → end),
8145
8225
  * 失败补 end(error,实际报错 + 诊断子会话 sessionId);替换消息 source 标记插件标识供 UI 认领
8146
8226
  * - 挂载失败类问题(systemPrompt/tokenMeter 服务异常)始终 console 到外部进程,并追加
8147
- * log-only om/warning 事件(客户端渲染功能降级警告行,每会话同一问题至多一次);
8227
+ * log-only om 警告事件(借用 feedback/record,客户端渲染功能降级警告行,每会话同一
8228
+ * 问题至多一次);
8148
8229
  * 辅助估算的普通运行时报错仅记日志。降级与报错都不阻塞压缩(tokenMeter 压力数据
8149
8230
  * 缺失时本轮跳过观察)
8150
8231
  */
@@ -8192,6 +8273,14 @@ function historyInnerText(text) {
8192
8273
  return text.slice(gt + 1, close).trim();
8193
8274
  }
8194
8275
  /**
8276
+ * 反思输入拼合:全部块内文(historyInnerText 去掉开标签属性,块首格式说明注释剥离)
8277
+ * 按序合并进单个 <history> 块。正文条目内出现的属性/注释同名串原样保留。
8278
+ */
8279
+ function mergeHistoryBlocks(blocks) {
8280
+ const inner = blocks.map((block) => stripLeadingFormatNote(historyInnerText(block.text))).join("\n");
8281
+ return `<${HISTORY_TAG}>\n${inner}\n</${HISTORY_TAG}>`;
8282
+ }
8283
+ /**
8195
8284
  * 判定表层节点 seq 之后的切点是否 tool-call/result 配对平衡:按表层顺序折叠未闭合的
8196
8285
  * 工具调用数,处理到 seq 后计数为 0 即平衡(防止把 tool-call 与其结果切到两侧)。
8197
8286
  */
@@ -8266,40 +8355,35 @@ function historySection(session) {
8266
8355
  };
8267
8356
  }
8268
8357
  /**
8269
- * 查找当前活跃的观察压缩待定标记:按日志顺序取最后一条 om/observe-pending,其后须无
8270
- * 引用它的 om/observe-invalidate(已失效),且其后的压缩边界 seq 不大于标记 seq(边界
8271
- * 后移说明标记期间已发生过压缩,标记过期——兜底「执行成功但失效标记未写出」的崩溃
8272
- * 窗口)。无活跃标记返回 undefined。
8358
+ * 查找当前活跃的观察压缩待定标记:按日志顺序取最后一条 om/observe-pending 信封事件,
8359
+ * 其后须无引用它的 om/observe-invalidate(已失效),且其后的压缩边界 seq 不大于标记
8360
+ * seq(边界后移说明标记期间已发生过压缩,标记过期——兜底「执行成功但失效标记未写出」
8361
+ * 的崩溃窗口)。无活跃标记返回 undefined。
8273
8362
  */
8274
8363
  function findObservePending(session) {
8275
8364
  let pending;
8276
8365
  for (let seq = 0; seq < session.events.length; seq += 1) {
8277
8366
  const event = session.events[seq];
8278
8367
  if (!event) continue;
8279
- if (event.type === "om/observe-pending") pending = {
8368
+ const om = readOmEvent(event);
8369
+ if (om?.kind === "om/observe-pending") pending = {
8280
8370
  seq,
8281
- triggerMessageIndex: event.data.triggerMessageIndex
8371
+ triggerMessageIndex: om.data.triggerMessageIndex
8282
8372
  };
8283
- else if (event.type === "om/observe-invalidate" && pending !== void 0 && event.data.pendingSeq === pending.seq) pending = void 0;
8373
+ else if (om?.kind === "om/observe-invalidate" && pending !== void 0 && om.data.pendingSeq === pending.seq) pending = void 0;
8284
8374
  }
8285
8375
  if (pending === void 0) return void 0;
8286
8376
  const { boundarySeq } = historySection(session);
8287
8377
  if (boundarySeq !== void 0 && boundarySeq > pending.seq) return void 0;
8288
8378
  return pending;
8289
8379
  }
8290
- /** 追加观察压缩待定标记(log-only):记录触发点完整消息 index,返回事件 seq。 */
8380
+ /** 追加观察压缩待定标记(log-only om 信封事件):记录触发点完整消息 index,返回事件 seq。 */
8291
8381
  function appendObservePending(session, triggerMessageIndex) {
8292
- return session.append("om/observe-pending", {
8293
- key: "observe",
8294
- triggerMessageIndex
8295
- }).seq;
8382
+ return appendOmEvent(session, "om/observe-pending", { triggerMessageIndex });
8296
8383
  }
8297
- /** 追加观察压缩待定失效标记(log-only):声明指定 pending 已失效,返回事件 seq。 */
8384
+ /** 追加观察压缩待定失效标记(log-only om 信封事件):声明指定 pending 已失效,返回事件 seq。 */
8298
8385
  function appendObserveInvalidate(session, pendingSeq) {
8299
- return session.append("om/observe-invalidate", {
8300
- key: "observe",
8301
- pendingSeq
8302
- }).seq;
8386
+ return appendOmEvent(session, "om/observe-invalidate", { pendingSeq });
8303
8387
  }
8304
8388
  /** 当前打开中的 turn 号(最近 turn/start 且未被 turn/end 关闭);无则 null。 */
8305
8389
  function openTurnOf(session) {
@@ -8346,7 +8430,7 @@ function appendCompactionSummary(session, data) {
8346
8430
  }
8347
8431
  /**
8348
8432
  * 追加 compaction/end(log-only,结束生命周期;error 记录失败原因,
8349
- * diagnosticSessionId 记录最终失败时的诊断子会话 id)。
8433
+ * diagnosticSessionId 记录最后一次摘要尝试(无论成功或失败)的诊断子会话 id)。
8350
8434
  */
8351
8435
  function appendCompactionEnd(session, lifecycle, error, diagnosticSessionId) {
8352
8436
  const payload = {
@@ -8378,9 +8462,9 @@ function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, comp
8378
8462
  });
8379
8463
  }
8380
8464
  /**
8381
- * 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要调用把整个块
8382
- * 区段合并替换为一条更紧凑的摘要。失败不产生部分替换;摘要尝试全部耗尽返回
8383
- * 失败结果(error = 最后一次尝试的实际报错/具体问题)。
8465
+ * 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,全部块内文拼合为
8466
+ * 单个 <history> 块送入摘要调用,整个块区段合并替换为一条更紧凑的摘要。失败不产生
8467
+ * 部分替换;摘要尝试全部耗尽返回失败结果(error = 最后一次尝试的实际报错/具体问题)。
8384
8468
  */
8385
8469
  async function reflectPass(ctx, agent, config, target, signal) {
8386
8470
  const session = agent.session;
@@ -8406,8 +8490,8 @@ async function reflectPass(ctx, agent, config, target, signal) {
8406
8490
  return { failed: false };
8407
8491
  }
8408
8492
  const blockSeqs = blocks.map((block) => block.seq);
8409
- const instruction = buildHistoryPrompt();
8410
- const contextText = blocks.map((block) => block.text).join("\n");
8493
+ const instruction = buildHistoryPrompt(config.compressSkipReasoning);
8494
+ const contextText = mergeHistoryBlocks(blocks);
8411
8495
  const expectedEnd = reflectExpectedEnd(contextText);
8412
8496
  const lifecycle = {
8413
8497
  compactionId: newCompactionId(),
@@ -8468,7 +8552,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
8468
8552
  end: last.seq
8469
8553
  }, lifecycle.compactionId);
8470
8554
  logger.step("反思提交:追加 compaction/end");
8471
- appendCompactionEnd(session, lifecycle);
8555
+ appendCompactionEnd(session, lifecycle, void 0, summaryResult.diagnosticSessionId);
8472
8556
  logger.info(`反思完成(摘要 ${tokens} tokens ≥ 阈值 ${threshold},合并 ${blocks.length} 个块为一条)`);
8473
8557
  return { failed: false };
8474
8558
  } catch (error) {
@@ -8484,7 +8568,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
8484
8568
  * 估算系统提示词 tokens:按 agent 作用域组装并渲染系统提示词,按长度/4 启发式计。
8485
8569
  * systemPrompt 服务经 ctx.get 容错读取(ctx 属性访问在服务未挂载时抛错);服务缺失
8486
8570
  * 或组装/渲染失败时按 0 计——只影响观察触发时机(偏早触发),不阻塞压缩。
8487
- * 服务缺失属挂载失败:console 外部 + om/warning 事件每会话报告一次;组装失败仅记日志。
8571
+ * 服务缺失属挂载失败:console 外部 + om 警告事件每会话报告一次;组装失败仅记日志。
8488
8572
  */
8489
8573
  async function estimateSystemPromptTokens(ctx, agent, logger, signal) {
8490
8574
  const systemPrompt = ctx.get("systemPrompt");
@@ -8524,7 +8608,7 @@ function estimateToolsTokens(session, logger) {
8524
8608
  }
8525
8609
  /**
8526
8610
  * 读取上下文压力 tokens(tokenMeter.measure 的 totalTokens)。tokenMeter 调用异常时
8527
- * 记日志并报告降级(console 外部 + om/warning 每会话一次),返回 undefined——本轮
8611
+ * 记日志并报告降级(console 外部 + om 警告事件每会话一次),返回 undefined——本轮
8528
8612
  * 跳过观察压缩(无压力数据不触发),不阻塞 turn。
8529
8613
  */
8530
8614
  function measurePressureTokens(ctx, session, logger) {
@@ -8631,8 +8715,8 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
8631
8715
  const startIndex = inRangeCms[0]?.index ?? 0;
8632
8716
  const endIndex = inRangeCms[inRangeCms.length - 1]?.index ?? startIndex;
8633
8717
  logger.step(`观察:保留旧块 ${blocks.length} 条,替换 [${replaceStart}..${range.end}](${replaceSeqs.length} 个表层节点,压缩至${triggerNote}),新消息 index ${startIndex}..${endIndex}`);
8634
- const instruction = buildHistoryPrompt();
8635
- const contextText = renderMessages(session, replaceSeqs);
8718
+ const instruction = buildHistoryPrompt(config.compressSkipReasoning);
8719
+ const contextText = renderMessages(session, replaceSeqs, config.compressSkipReasoning);
8636
8720
  const lifecycle = {
8637
8721
  compactionId: newCompactionId(),
8638
8722
  turn: openTurnOf(session)
@@ -8710,7 +8794,7 @@ async function observePass(ctx, agent, config, waitCount, target, signal) {
8710
8794
  end: range.end
8711
8795
  }, lifecycle.compactionId);
8712
8796
  logger.step("观察提交:追加 compaction/end");
8713
- appendCompactionEnd(session, lifecycle);
8797
+ appendCompactionEnd(session, lifecycle, void 0, summaryResult.diagnosticSessionId);
8714
8798
  clearPending();
8715
8799
  logger.info(`观察压缩完成(替换 ${replaceSeqs.length} 个表层节点,约 ${shadowedTokenCount} tokens,压缩至${triggerNote})`);
8716
8800
  return { failed: false };
@@ -8968,6 +9052,7 @@ const DEFAULT_CONFIG = Object.freeze({
8968
9052
  rateLimitWaitMs: 6e4,
8969
9053
  tailMessageCount: 5,
8970
9054
  compressRetryCount: 5,
9055
+ compressSkipReasoning: true,
8971
9056
  omEnabled: true,
8972
9057
  debug: false,
8973
9058
  recallEnabled: true,
@@ -9009,6 +9094,7 @@ function resolveConfig(raw) {
9009
9094
  if (integer && !Number.isInteger(value)) continue;
9010
9095
  config[key] = value;
9011
9096
  }
9097
+ config.compressSkipReasoning = resolveBoolean(input.compressSkipReasoning, true);
9012
9098
  config.omEnabled = resolveBoolean(input.omEnabled, true);
9013
9099
  config.debug = resolveBoolean(input.debug, process.env.NODE_ENV !== "production");
9014
9100
  config.recallEnabled = resolveBoolean(input.recallEnabled, true);