dsh-plugin-om 0.0.25 → 0.0.27

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,5 +1,7 @@
1
1
  import { scopeOf } from "@deepseek-ai/dsh-scope";
2
2
  import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
3
+ import { SessionId } from "@deepseek-ai/dsh-session";
4
+ import { SUBAGENT_DESCRIPTOR_VERSION } from "@deepseek-ai/dsh-subagent";
3
5
  import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
6
  import { homedir } from "node:os";
5
7
  import path from "node:path";
@@ -155,6 +157,11 @@ function routedTarget(session) {
155
157
  * assistant(模型输出文本)、toolcall(单个工具调用及其结果,result 按 callId 匹配并入)。
156
158
  * index 从 0 起、按日志顺序递增、只追加不重排(压缩后旧摘要条目引用的 index 仍然有效)。
157
159
  */
160
+ /** 查找 seq 在表层节点序列中的下标(不在则返回 -1)。 */
161
+ function surfaceIndexOf(nodes, seq) {
162
+ for (let i = 0; i < nodes.length; i += 1) if (nodes[i] === seq) return i;
163
+ return -1;
164
+ }
158
165
  /**
159
166
  * 完整消息索引:按日志顺序把消息事件折叠为完整消息序列(四类,见文件头)。
160
167
  * 工具调用结果按 source.callId 匹配其 tool-call 并入该条;未匹配的 result 独立成条(防御)。
@@ -7427,7 +7434,7 @@ var require_dom_parser = /* @__PURE__ */ __commonJSMin(((exports) => {
7427
7434
  exports.onWarningStopParsing = onWarningStopParsing;
7428
7435
  }));
7429
7436
  //#endregion
7430
- //#region src/rate-limit.ts
7437
+ //#region src/compaction-log.ts
7431
7438
  var import_lib = (/* @__PURE__ */ __commonJSMin(((exports) => {
7432
7439
  var conventions = require_conventions();
7433
7440
  exports.assign = conventions.assign;
@@ -7467,6 +7474,91 @@ var import_lib = (/* @__PURE__ */ __commonJSMin(((exports) => {
7467
7474
  exports.onErrorStopParsing = domParser.onErrorStopParsing;
7468
7475
  exports.onWarningStopParsing = domParser.onWarningStopParsing;
7469
7476
  })))();
7477
+ /** 诊断子会话的 descriptor provider(宿主子代理列表识别用)。 */
7478
+ const COMPACTION_LOG_PROVIDER = "om-compaction-log";
7479
+ /** 压缩 pass 的中文标签(诊断子会话 label 用;未知阶段回落「压缩」)。 */
7480
+ function phaseLabel(phase) {
7481
+ if (phase === "observe") return "观察";
7482
+ if (phase === "reflect") return "反思";
7483
+ return "压缩";
7484
+ }
7485
+ /** 诊断子会话 label:含压缩阶段与尝试序号。 */
7486
+ function compactionLogLabel(phase, attemptNo) {
7487
+ return `OM 压缩日志(${phaseLabel(phase)} · 第 ${attemptNo} 次尝试)`;
7488
+ }
7489
+ /** 追加一次尝试的「提示词 → 原始输出」消息组(surfaceOp append;id 为品牌类型,session.append 运行时校验)。 */
7490
+ function appendAttemptMessages(child, attempt, step, target) {
7491
+ const userMessage = {
7492
+ id: uuid(),
7493
+ role: "user",
7494
+ content: [{
7495
+ type: "text",
7496
+ text: attempt.prompt
7497
+ }],
7498
+ source: {
7499
+ kind: "plugin",
7500
+ plugin: PLUGIN_LABEL
7501
+ }
7502
+ };
7503
+ child.append("user/message", userMessage, { surfaceOp: "append" });
7504
+ const assistantMessage = {
7505
+ id: uuid(),
7506
+ role: "assistant",
7507
+ content: [{
7508
+ type: "text",
7509
+ text: attempt.rawOutput
7510
+ }],
7511
+ source: {
7512
+ kind: "model",
7513
+ provider: target.provider,
7514
+ model: target.model
7515
+ }
7516
+ };
7517
+ child.append("assistant/message", {
7518
+ turn: 0,
7519
+ step,
7520
+ message: assistantMessage
7521
+ }, { surfaceOp: "append" });
7522
+ }
7523
+ /**
7524
+ * 把一次摘要尝试落盘为诊断子会话:ctx.sessions.create 创建子会话(header origin
7525
+ * 'subagent'、parentSession 指向主会话、delegationDepth = 父 + 1、cwd 继承主会话),
7526
+ * 追加 one-shot descriptor(provider om-compaction-log,label 含压缩阶段与尝试序号),
7527
+ * 原样追加「提示词 + 原始输出」消息组,flush 持久化检查点,返回子会话 id。落盘自身
7528
+ * 绝不抛错:任何失败仅 logger.warn 并返回 undefined(不影响压缩流程)。
7529
+ */
7530
+ async function recordCompactionAttempt(ctx, parentSession, options) {
7531
+ const logger = makeLogger(ctx, options.debug);
7532
+ try {
7533
+ const header = parentSession.header;
7534
+ const child = ctx.sessions.create(SessionId(`om-compaction-log-${uuid()}`), { meta: {
7535
+ ...header.cwd === void 0 ? {} : { cwd: header.cwd },
7536
+ parentSession: parentSession.id,
7537
+ origin: "subagent",
7538
+ delegationDepth: (header.delegationDepth ?? 0) + 1
7539
+ } });
7540
+ child.append("subagent/descriptor", {
7541
+ version: SUBAGENT_DESCRIPTOR_VERSION,
7542
+ mode: "one-shot",
7543
+ provider: COMPACTION_LOG_PROVIDER,
7544
+ label: compactionLogLabel(options.phase, options.attemptNo)
7545
+ });
7546
+ appendAttemptMessages(child, options.attempt, 1, options.target);
7547
+ try {
7548
+ await ctx.sessions.flush(child);
7549
+ } catch (error) {
7550
+ const message = error instanceof Error ? error.message : String(error);
7551
+ logger.warn(`压缩日志子会话 flush 失败(子会话 ${child.id} 已创建): ${message}`);
7552
+ }
7553
+ return child.id;
7554
+ } catch (error) {
7555
+ const message = error instanceof Error ? error.message : String(error);
7556
+ logger.warn(`压缩日志子会话落盘失败(第 ${options.attemptNo} 次尝试): ${message}`);
7557
+ return;
7558
+ }
7559
+ }
7560
+ //#endregion
7561
+ //#region src/rate-limit.ts
7470
7562
  /**
7471
7563
  * 全局限流门(插件进程级共享状态):任一摘要请求遇 429 后进入冷却期,
7472
7564
  * 此后所有摘要请求发出前先等待到「最近一次 429 + rateLimitWaitMs」之后。
@@ -7524,8 +7616,10 @@ async function gateRateLimit(waitMs, signal) {
7524
7616
  /**
7525
7617
  * 共享压缩提示词(观察/反思同一套):定义 history 块(模型消息 + index 的表达形式)、
7526
7618
  * 完整消息定义、压缩要求、输出格式与数据源说明。
7619
+ * skipReasoning=true(默认,与 compressSkipReasoning 默认一致)时压缩输入不含
7620
+ * <reasoning> 参考条目,提示词相应省略 <reasoning> 的说明两行。
7527
7621
  */
7528
- function buildHistoryPrompt() {
7622
+ function buildHistoryPrompt(skipReasoning = true) {
7529
7623
  return [
7530
7624
  "压缩 <history> 消息记录。你应当输出**单个**合法的 <history> 块。",
7531
7625
  "",
@@ -7533,17 +7627,18 @@ function buildHistoryPrompt() {
7533
7627
  "- <history> 是历史消息的记录块。",
7534
7628
  "- <user_message index=\"N\">:用户消息条目。",
7535
7629
  "- <sys type=\"(kind)\" index=\"N\">:系统消息条目。",
7536
- "- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。",
7630
+ ...skipReasoning ? [] : ["- <reasoning>:模型的思考过程,仅作压缩参考,产物中不要出现。"],
7537
7631
  "- <assistant index=\"N\">:单条完整消息(模型输出文本,或 toolcall 及其 result)。",
7538
7632
  "- <assistant start=\"A\" end=\"B\">:多条连续完整消息聚合的模块(A/B 为模块首尾完整消息的 index)。",
7539
7633
  "",
7540
7634
  "【压缩要求】",
7541
7635
  "- <user_message> <sys> 条目从输入中逐条保留,不做任何处理。",
7542
- "- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。",
7543
- "- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\"> 块:块内描述模块的目的、行为与结果;涉及的具体文件保留在模块内容中,多个前缀相同的路径合并简写。",
7544
- "- 单条重要的完整消息以 <assistant index=\"\"> 单独呈现,内容不受限制。",
7545
- "- 加载的 skill 属于关键信息:应当产出独立块且不过多省略。",
7546
- "- 条目按 index 顺序覆盖本次压缩的全部完整消息:index/start/end 必须连续(区间内 index 连续、相邻条目相接),不跳号、不重叠、不遗漏。",
7636
+ ...skipReasoning ? [] : ["- <reasoning> 只作参考,输出产物中不包含 <reasoning> 块。"],
7637
+ "- 将具有关联性的 <assistant> 消息按内在逻辑连贯性划分为连续模块,聚合为 <assistant start=\"\" end=\"\"> ",
7638
+ "- 单条重要的完整消息以 <assistant index=\"\"> 单独呈现",
7639
+ "- 压缩后的 <assistant> 块内,应当描述**行为逻辑**,强调关键的**结论、产出和任务**;涉及到的具体文件保留完整路径",
7640
+ "- 加载的 skill 属于**关键信息**:应当产出独立块且不过多省略。",
7641
+ "- 压缩后的消息,区间边界与输入的消息必须完全相同,内部 index/start/end 必须连续,相邻区间的左右界必须相邻,",
7547
7642
  "",
7548
7643
  "【摘要粒度】",
7549
7644
  "- 越往后越细:靠近末尾(最近)的完整消息保留更多细节(关键文件、改动与结论),开头(较早)的完整消息可适当从简。",
@@ -7600,14 +7695,15 @@ function renderUserEntry(doc, session, cm) {
7600
7695
  }
7601
7696
  /**
7602
7697
  * 渲染完整消息记录(观察输入):输出一个合法的 <history> 块——
7603
- * user → <user_message>(文本原样、图片注释)、sys → <sys> 空块、assistant 的
7604
- * reasoning → <reasoning>(参考条目)、assistant/toolcall → <assistant>(原样文本)。
7698
+ * user → <user_message>(文本原样、图片注释)、sys → <sys> 空块、
7699
+ * assistant/toolcall → <assistant>(原样文本);skipReasoning=false 时另把
7700
+ * assistant 的 reasoning → <reasoning>(参考条目)。
7605
7701
  * 文本经 XML 序列化自动转义;仅渲染 seqs 全部落在给定集合内的完整消息。
7606
7702
  */
7607
- function renderMessages(session, seqs) {
7703
+ function renderMessages(session, seqs, skipReasoning = true) {
7608
7704
  const shadowed = new Set(seqs);
7609
7705
  const reasoningBySeq = /* @__PURE__ */ new Map();
7610
- for (const seq of seqs) {
7706
+ if (!skipReasoning) for (const seq of seqs) {
7611
7707
  const event = session.events[seq];
7612
7708
  if (event?.type !== "assistant/message") continue;
7613
7709
  const message = event.data.message;
@@ -7636,7 +7732,7 @@ function renderMessages(session, seqs) {
7636
7732
  } else {
7637
7733
  const callSeq = cm.seqs[0];
7638
7734
  const reasonings = callSeq === void 0 ? void 0 : reasoningBySeq.get(callSeq);
7639
- if (callSeq !== void 0 && reasonings !== void 0 && !emittedReasoning.has(callSeq)) {
7735
+ if (!skipReasoning && callSeq !== void 0 && reasonings !== void 0 && !emittedReasoning.has(callSeq)) {
7640
7736
  emittedReasoning.add(callSeq);
7641
7737
  for (const text of reasonings) {
7642
7738
  const re = doc.createElement("reasoning");
@@ -7718,6 +7814,14 @@ function buildSummaryOptions(session, instruction, contextText, maxTokens, targe
7718
7814
  }
7719
7815
  /** 产出日志后插入首个 <history> 后的格式说明(XML 注释,完整消息定义 + 条目标签语义)。 */
7720
7816
  const HISTORY_FORMAT_NOTE = `<!-- 完整消息:${COMPLETE_MESSAGE_DEFINITION} <TAG index="N">表示单条完整消息,<TAG start="A" end="B"> 表示连续模块,start/end 是首尾完整消息的 index;<sys type="KIND" index="N"> 表示被压缩的系统消息,块中为空 -->`;
7817
+ /**
7818
+ * 剥离 <history> 块内文块首的格式说明注释(HISTORY_FORMAT_NOTE 整体精确匹配,仅块首
7819
+ * 一处);正文条目内出现的同名注释串不动。非块首或不匹配时原样返回。
7820
+ */
7821
+ function stripLeadingFormatNote(inner) {
7822
+ if (!inner.startsWith(HISTORY_FORMAT_NOTE)) return inner;
7823
+ return inner.slice(HISTORY_FORMAT_NOTE.length).replace(/^\s+/, "");
7824
+ }
7721
7825
  /** 读取元素整数属性(非负整数;缺失 / 非数字返回 undefined)。 */
7722
7826
  function intAttr(el, name) {
7723
7827
  const raw = el.getAttribute(name);
@@ -7789,7 +7893,7 @@ function parseHistoryBlock(xml) {
7789
7893
  };
7790
7894
  }
7791
7895
  /**
7792
- * 解析文本中全部 <history> 块内的条目(反思输入为多个块拼接:逐块解析提取)。
7896
+ * 解析文本中全部 <history> 块内的条目(逐块解析提取,兼容多块拼接文本)。
7793
7897
  * 非法块跳过;仅提取不校验顺序(连续性由 historyContinuity 校验)。
7794
7898
  */
7795
7899
  function parseHistoryEntries(text) {
@@ -7934,9 +8038,11 @@ function extractSummaryDetailed(raw, expected) {
7934
8038
  }
7935
8039
  /**
7936
8040
  * 直连 LLM 执行一次摘要(观察或反思),返回文本与可选 token usage。
7937
- * 失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)均记录日志并重试,每次尝试的
7938
- * 结果或报错始终写入日志(成功 info / 失败 warn,不受 debug 影响);全部尝试耗尽
7939
- * 返回失败结果(携带最后一次尝试的实际报错/具体问题,不产生任何日志变更)。
8041
+ * 每次实际发出的 LLM 调用(无论成功、校验不通过、非 stop 结束还是异常)完成后,
8042
+ * 立即把该次尝试的完整提示词与模型原始输出原样落盘为诊断子会话(phase 标注观察
8043
+ * 或反思),子会话 id 写入该次尝试的主会话日志;请求发出前被中止的尝试(无实际
8044
+ * 调用)不落盘。失败(抛异常 / 空输出 / 非 stop 结束 / 校验不通过)记录日志并重试;
8045
+ * 全部尝试耗尽返回失败结果(携带最后一次尝试的实际报错/具体问题与其诊断子会话 id)。
7940
8046
  * signal 中止(含限流等待被中止)立即放弃并标记 aborted。每次请求发出前先过全局
7941
8047
  * 限流等待门。
7942
8048
  */
@@ -7951,7 +8057,8 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
7951
8057
  return {
7952
8058
  ok: false,
7953
8059
  error: COMPACTION_ABORTED_ERROR,
7954
- aborted: true
8060
+ aborted: true,
8061
+ ...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
7955
8062
  };
7956
8063
  }
7957
8064
  const rateLimitWaitMs = options?.rateLimitWaitMs ?? 6e4;
@@ -7960,43 +8067,70 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
7960
8067
  return {
7961
8068
  ok: false,
7962
8069
  error: COMPACTION_ABORTED_ERROR,
7963
- aborted: true
8070
+ aborted: true,
8071
+ ...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
7964
8072
  };
7965
8073
  }
7966
8074
  logger.step(`摘要调用开始(第 ${attempt}/${maxAttempts} 次,provider ${target.provider},model ${target.model},maxTokens ${maxTokens === void 0 ? "未设置" : String(maxTokens)})`);
8075
+ const prompt = `${instruction}\n\n${contextText ?? ""}`;
8076
+ const collector = new StreamCollector();
8077
+ let streamCompleted = false;
8078
+ /** 本次尝试的诊断子会话 id(调用完成后立即落盘;落盘失败时缺失)。 */
8079
+ const logAttempt = async () => recordCompactionAttempt(ctx, session, {
8080
+ phase: options?.phase,
8081
+ target,
8082
+ attempt: {
8083
+ prompt,
8084
+ rawOutput: collector.text
8085
+ },
8086
+ attemptNo: attempt,
8087
+ debug
8088
+ });
7967
8089
  try {
7968
8090
  const requestOptions = buildSummaryOptions(session, instruction, contextText, maxTokens, target, signal);
7969
- const collector = new StreamCollector();
7970
8091
  for await (const chunk of ctx.llm.stream(requestOptions)) collector.push(chunk);
8092
+ streamCompleted = true;
8093
+ const diagnosticSessionId = await logAttempt();
7971
8094
  const extracted = extractSummaryDetailed(collector.text, options?.expected);
7972
8095
  const finish = collector.finish;
7973
8096
  if (finish.kind !== "stop") {
7974
- lastFailure = { reason: `摘要流以 ${String(finish.kind)} 结束(非正常完成)` };
7975
- logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${lastFailure.reason})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8097
+ lastFailure = {
8098
+ reason: `摘要流以 ${String(finish.kind)} 结束(非正常完成)`,
8099
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8100
+ };
8101
+ logger.warn(`摘要未完成(第 ${attempt}/${maxAttempts} 次,${lastFailure.reason},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
7976
8102
  continue;
7977
8103
  }
7978
8104
  if ("error" in extracted) {
7979
- lastFailure = { reason: extracted.error };
7980
- logger.warn(`摘要输出未通过校验(第 ${attempt}/${maxAttempts} 次,${extracted.error})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8105
+ lastFailure = {
8106
+ reason: extracted.error,
8107
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8108
+ };
8109
+ logger.warn(`摘要输出未通过校验(第 ${attempt}/${maxAttempts} 次,${extracted.error},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
7981
8110
  continue;
7982
8111
  }
7983
8112
  const text = extracted.log;
7984
8113
  const usage = collector.usage;
7985
- logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + "");
8114
+ logger.info(`摘要调用成功(第 ${attempt}/${maxAttempts} 次,输出 ${text.length} 字符` + (usage === void 0 ? "" : `,input ${String(usage.inputTokens ?? "?")} / output ${String(usage.outputTokens ?? "?")} tokens`) + `,子会话 ${diagnosticSessionId ?? "未落盘"})`);
7986
8115
  return {
7987
8116
  ok: true,
7988
8117
  text,
7989
8118
  attemptCount: attempt,
7990
- ...usage === void 0 ? {} : { usage }
8119
+ ...usage === void 0 ? {} : { usage },
8120
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
7991
8121
  };
7992
8122
  } catch (error) {
8123
+ const diagnosticSessionId = streamCompleted ? void 0 : await logAttempt();
7993
8124
  const message = error instanceof Error ? error.message : String(error);
7994
8125
  if (isRateLimitError(message)) {
7995
8126
  noteRateLimit();
7996
8127
  logger.warn(`摘要调用触发限流(429,第 ${attempt}/${maxAttempts} 次),下一次请求前至少等待 ${rateLimitWaitMs}ms`);
7997
8128
  }
7998
- lastFailure = { error: message };
7999
- logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8129
+ lastFailure = {
8130
+ error: message,
8131
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8132
+ };
8133
+ logger.warn(`摘要调用失败(第 ${attempt}/${maxAttempts} 次,${message},子会话 ${diagnosticSessionId ?? "未落盘"})` + (attempt < maxAttempts ? ",将重试" : ",重试耗尽,放弃本次压缩"));
8000
8134
  }
8001
8135
  }
8002
8136
  const lastError = lastFailure.error ?? lastFailure.reason ?? "未知原因";
@@ -8004,7 +8138,8 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
8004
8138
  return {
8005
8139
  ok: false,
8006
8140
  error: lastError,
8007
- aborted: false
8141
+ aborted: false,
8142
+ ...lastFailure.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: lastFailure.diagnosticSessionId }
8008
8143
  };
8009
8144
  }
8010
8145
  //#endregion
@@ -8012,18 +8147,24 @@ async function runSummarySubagent(ctx, agent, instruction, contextText, maxToken
8012
8147
  /**
8013
8148
  * 两级自动压缩(观察/反思)与 compaction 生命周期提交。
8014
8149
  * 导出 estimateTextTokens / isPairBalancedAfter / computeCompressRange / historySection /
8015
- * reflectPass / observePass / maybeCompress。
8150
+ * findObservePending / reflectPass / observePass / maybeCompress。
8016
8151
  *
8017
- * - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要合并为一条
8018
- * - 观察:净压力(上下文压力 − 已压缩块 token 合计 − 系统提示词 token 估算 − 工具定义
8019
- * token 估算)≥ observeThresholdTokens 时,
8020
- * 摘要未压缩消息为新 <history> 块并精确替换被压缩区间(旧块保留)
8152
+ * - 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,块内文拼合为单个
8153
+ * <history> 块输入摘要,合并为一条
8154
+ * - 观察(触发 → 待定 → 延迟执行):净压力(上下文压力 − 已压缩块 token 合计 系统提示词
8155
+ * token 估算 − 工具定义 token 估算)首次 ≥ observeThresholdTokens 时记录待定标记
8156
+ * (触发点 = 当时的最后一条完整消息 index),本次不压缩;待定后新增完整消息数 ≥
8157
+ * tailMessageCount 时,把压缩边界至触发点的全部消息摘要为新 <history> 块并精确替换
8158
+ * 被压缩区间(旧块保留),等待期间的新消息成为下一轮未压缩尾部(延迟窗口内压力允许
8159
+ * 短暂超阈值);tailMessageCount=0 时触发当轮直接执行(不落待定标记)
8160
+ * - 待定标记以 log-only om/observe-pending / om/observe-invalidate 事件持久化在会话
8161
+ * 日志中(重启后从日志恢复);摘要失败保留待定,下个 pre-step 直接重试执行
8021
8162
  * - 两级在 pre-step 阻塞串行执行(先反思后观察);仅主会话生效;omEnabled=false 关闭
8022
8163
  * - 压缩边界:最后一个合法 <history> 块之后的消息视为未压缩,其前不重复压缩
8023
8164
  * - 摘要尝试全部耗尽时 pass 返回失败结果(携带最后一次尝试的实际报错),压缩流程
8024
8165
  * 向上传播,pre-step 据此拒绝本 step 中断当前 turn;signal 中止标记 aborted(不中断)
8025
8166
  * - 提交走宿主 compaction/* 生命周期事件(start 带 phase → summary → 替换消息 → end),
8026
- * 失败补 end(error,实际报错);替换消息 source 标记插件标识供 UI 认领
8167
+ * 失败补 end(error,实际报错 + 诊断子会话 sessionId);替换消息 source 标记插件标识供 UI 认领
8027
8168
  * - 挂载失败类问题(systemPrompt/tokenMeter 服务异常)始终 console 到外部进程,并追加
8028
8169
  * log-only om/warning 事件(客户端渲染功能降级警告行,每会话同一问题至多一次);
8029
8170
  * 辅助估算的普通运行时报错仅记日志。降级与报错都不阻塞压缩(tokenMeter 压力数据
@@ -8073,6 +8214,14 @@ function historyInnerText(text) {
8073
8214
  return text.slice(gt + 1, close).trim();
8074
8215
  }
8075
8216
  /**
8217
+ * 反思输入拼合:全部块内文(historyInnerText 去掉开标签属性,块首格式说明注释剥离)
8218
+ * 按序合并进单个 <history> 块。正文条目内出现的属性/注释同名串原样保留。
8219
+ */
8220
+ function mergeHistoryBlocks(blocks) {
8221
+ const inner = blocks.map((block) => stripLeadingFormatNote(historyInnerText(block.text))).join("\n");
8222
+ return `<${HISTORY_TAG}>\n${inner}\n</${HISTORY_TAG}>`;
8223
+ }
8224
+ /**
8076
8225
  * 判定表层节点 seq 之后的切点是否 tool-call/result 配对平衡:按表层顺序折叠未闭合的
8077
8226
  * 工具调用数,处理到 seq 后计数为 0 即平衡(防止把 tool-call 与其结果切到两侧)。
8078
8227
  */
@@ -8087,17 +8236,26 @@ function isPairBalancedAfter(session, seq) {
8087
8236
  return false;
8088
8237
  }
8089
8238
  /**
8090
- * 观察压缩区间:压缩边界后的首个表层节点 → 表层长度-1-tailCount(尾部保留 tailCount
8091
- * 条不压缩);区间终点回退到 tool-call/result 配对平衡点(不切段)。无可行区间返回
8092
- * undefined。返回区间起止表层 seq 与被遮蔽 seq 列表。
8239
+ * 观察压缩区间:压缩边界后的首个表层节点 → 触发点完整消息(endMessageIndex)对应的
8240
+ * 表层终点(取该完整消息最后一个事件 seq 在表层中的位置);区间终点回退到
8241
+ * tool-call/result 配对平衡点(不切段)。触发点完整消息不存在或其事件已不在表层
8242
+ * (被后续压缩遮蔽)时返回 undefined。返回区间起止表层 seq 与被遮蔽 seq 列表。
8093
8243
  */
8094
- function computeCompressRange(session, tailCount) {
8244
+ function computeCompressRange(session, endMessageIndex) {
8095
8245
  const surface = [...session.surface.nodes];
8096
8246
  if (surface.length === 0) return void 0;
8097
8247
  const { boundarySeq } = historySection(session);
8098
8248
  const startIdx = boundarySeq === void 0 ? 0 : surface.indexOf(boundarySeq) + 1;
8099
8249
  if (startIdx >= surface.length) return void 0;
8100
- let endIdx = surface.length - 1 - tailCount;
8250
+ const target = indexCompleteMessages(session).find((cm) => cm.index === endMessageIndex);
8251
+ if (target === void 0) return void 0;
8252
+ let endIdx = -1;
8253
+ for (let i = target.seqs.length - 1; i >= 0; i -= 1) {
8254
+ const seq = target.seqs[i];
8255
+ if (seq === void 0) continue;
8256
+ endIdx = surfaceIndexOf(surface, seq);
8257
+ if (endIdx !== -1) break;
8258
+ }
8101
8259
  if (endIdx < startIdx) return void 0;
8102
8260
  while (endIdx >= startIdx) {
8103
8261
  const node = surface[endIdx];
@@ -8137,6 +8295,42 @@ function historySection(session) {
8137
8295
  boundarySeq
8138
8296
  };
8139
8297
  }
8298
+ /**
8299
+ * 查找当前活跃的观察压缩待定标记:按日志顺序取最后一条 om/observe-pending,其后须无
8300
+ * 引用它的 om/observe-invalidate(已失效),且其后的压缩边界 seq 不大于标记 seq(边界
8301
+ * 后移说明标记期间已发生过压缩,标记过期——兜底「执行成功但失效标记未写出」的崩溃
8302
+ * 窗口)。无活跃标记返回 undefined。
8303
+ */
8304
+ function findObservePending(session) {
8305
+ let pending;
8306
+ for (let seq = 0; seq < session.events.length; seq += 1) {
8307
+ const event = session.events[seq];
8308
+ if (!event) continue;
8309
+ if (event.type === "om/observe-pending") pending = {
8310
+ seq,
8311
+ triggerMessageIndex: event.data.triggerMessageIndex
8312
+ };
8313
+ else if (event.type === "om/observe-invalidate" && pending !== void 0 && event.data.pendingSeq === pending.seq) pending = void 0;
8314
+ }
8315
+ if (pending === void 0) return void 0;
8316
+ const { boundarySeq } = historySection(session);
8317
+ if (boundarySeq !== void 0 && boundarySeq > pending.seq) return void 0;
8318
+ return pending;
8319
+ }
8320
+ /** 追加观察压缩待定标记(log-only):记录触发点完整消息 index,返回事件 seq。 */
8321
+ function appendObservePending(session, triggerMessageIndex) {
8322
+ return session.append("om/observe-pending", {
8323
+ key: "observe",
8324
+ triggerMessageIndex
8325
+ }).seq;
8326
+ }
8327
+ /** 追加观察压缩待定失效标记(log-only):声明指定 pending 已失效,返回事件 seq。 */
8328
+ function appendObserveInvalidate(session, pendingSeq) {
8329
+ return session.append("om/observe-invalidate", {
8330
+ key: "observe",
8331
+ pendingSeq
8332
+ }).seq;
8333
+ }
8140
8334
  /** 当前打开中的 turn 号(最近 turn/start 且未被 turn/end 关闭);无则 null。 */
8141
8335
  function openTurnOf(session) {
8142
8336
  let turn = null;
@@ -8180,13 +8374,18 @@ function appendCompactionSummary(session, data) {
8180
8374
  };
8181
8375
  return session.append("compaction/summary", payload).seq;
8182
8376
  }
8183
- /** 追加 compaction/end(log-only,结束生命周期;error 记录失败原因)。 */
8184
- function appendCompactionEnd(session, lifecycle, error) {
8185
- return session.append("compaction/end", {
8377
+ /**
8378
+ * 追加 compaction/end(log-only,结束生命周期;error 记录失败原因,
8379
+ * diagnosticSessionId 记录最后一次摘要尝试(无论成功或失败)的诊断子会话 id)。
8380
+ */
8381
+ function appendCompactionEnd(session, lifecycle, error, diagnosticSessionId) {
8382
+ const payload = {
8186
8383
  compactionId: lifecycle.compactionId,
8187
8384
  turn: lifecycle.turn,
8188
- ...error === void 0 ? {} : { error }
8189
- }).seq;
8385
+ ...error === void 0 ? {} : { error },
8386
+ ...diagnosticSessionId === void 0 ? {} : { diagnosticSessionId }
8387
+ };
8388
+ return session.append("compaction/end", payload).seq;
8190
8389
  }
8191
8390
  /** 追加 <history> 压缩日志消息(surfaceOp 替换遮蔽区间,source 标记插件自产 + compactionId)。 */
8192
8391
  function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, compactionId) {
@@ -8209,9 +8408,9 @@ function appendHistoryMessage(session, content, sourceEventSeqs, surfaceOp, comp
8209
8408
  });
8210
8409
  }
8211
8410
  /**
8212
- * 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,摘要调用把整个块
8213
- * 区段合并替换为一条更紧凑的摘要。失败不产生部分替换;摘要尝试全部耗尽返回
8214
- * 失败结果(error = 最后一次尝试的实际报错/具体问题)。
8411
+ * 反思:全部 <history> 块 token 合计 ≥ reflectThresholdTokens 时,全部块内文拼合为
8412
+ * 单个 <history> 块送入摘要调用,整个块区段合并替换为一条更紧凑的摘要。失败不产生
8413
+ * 部分替换;摘要尝试全部耗尽返回失败结果(error = 最后一次尝试的实际报错/具体问题)。
8215
8414
  */
8216
8415
  async function reflectPass(ctx, agent, config, target, signal) {
8217
8416
  const session = agent.session;
@@ -8237,8 +8436,8 @@ async function reflectPass(ctx, agent, config, target, signal) {
8237
8436
  return { failed: false };
8238
8437
  }
8239
8438
  const blockSeqs = blocks.map((block) => block.seq);
8240
- const instruction = buildHistoryPrompt();
8241
- const contextText = blocks.map((block) => block.text).join("\n");
8439
+ const instruction = buildHistoryPrompt(config.compressSkipReasoning);
8440
+ const contextText = mergeHistoryBlocks(blocks);
8242
8441
  const expectedEnd = reflectExpectedEnd(contextText);
8243
8442
  const lifecycle = {
8244
8443
  compactionId: newCompactionId(),
@@ -8258,17 +8457,19 @@ async function reflectPass(ctx, agent, config, target, signal) {
8258
8457
  start: 0,
8259
8458
  end: expectedEnd
8260
8459
  },
8261
- rateLimitWaitMs: config.rateLimitWaitMs
8460
+ rateLimitWaitMs: config.rateLimitWaitMs,
8461
+ phase: "reflect"
8262
8462
  });
8263
8463
  if (!summaryResult.ok) {
8264
- logger.warn(`反思:摘要调用失败(${summaryResult.error}),追加 compaction/end(error)`);
8464
+ logger.warn(`反思:摘要调用失败(${summaryResult.error}),诊断子会话 ${summaryResult.diagnosticSessionId ?? "未落盘"},追加 compaction/end(error)`);
8265
8465
  try {
8266
- appendCompactionEnd(session, lifecycle, summaryResult.error);
8466
+ appendCompactionEnd(session, lifecycle, summaryResult.error, summaryResult.diagnosticSessionId);
8267
8467
  } catch {}
8268
8468
  return {
8269
8469
  failed: true,
8270
8470
  error: summaryResult.error,
8271
- aborted: summaryResult.aborted
8471
+ aborted: summaryResult.aborted,
8472
+ ...summaryResult.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: summaryResult.diagnosticSessionId }
8272
8473
  };
8273
8474
  }
8274
8475
  const report = summaryResult.text;
@@ -8297,7 +8498,7 @@ async function reflectPass(ctx, agent, config, target, signal) {
8297
8498
  end: last.seq
8298
8499
  }, lifecycle.compactionId);
8299
8500
  logger.step("反思提交:追加 compaction/end");
8300
- appendCompactionEnd(session, lifecycle);
8501
+ appendCompactionEnd(session, lifecycle, void 0, summaryResult.diagnosticSessionId);
8301
8502
  logger.info(`反思完成(摘要 ${tokens} tokens ≥ 阈值 ${threshold},合并 ${blocks.length} 个块为一条)`);
8302
8503
  return { failed: false };
8303
8504
  } catch (error) {
@@ -8367,51 +8568,101 @@ function measurePressureTokens(ctx, session, logger) {
8367
8568
  }
8368
8569
  }
8369
8570
  /**
8370
- * 观察:净压力 tokens(上下文压力 已压缩 <history> token 合计 系统提示词
8371
- * token 估算 − 工具定义 token 估算)≥ observeThresholdTokens 时,摘要调用把未压缩
8372
- * 消息压缩为观察日志,追加到旧摘要并替换被压缩消息区间。失败不产生部分替换;
8373
- * 摘要尝试全部耗尽返回失败结果(error = 最后一次尝试的实际报错/具体问题)。
8571
+ * 观察(触发 待定 延迟执行):无活跃待定标记时测净压力 tokens(上下文压力已压缩
8572
+ * <history> 块 token 合计 − 系统提示词 token 估算 − 工具定义 token 估算),首次
8573
+ * observeThresholdTokens 时记录待定标记(触发点 = 当时的最后一条完整消息 index),本次
8574
+ * 不压缩(tailMessageCount=0 当轮直接执行,不落待定标记);已有待定标记时按新增完整
8575
+ * 消息数 ≥ tailMessageCount 决定执行,压缩区间截至触发点(新增消息成为新未压缩尾部,
8576
+ * 延迟窗口内压力允许短暂超阈值)。执行成功(或无可行区间)后写待定失效标记;摘要
8577
+ * 失败保留待定,下个 pre-step 直接重试执行。摘要尝试全部耗尽返回失败结果
8578
+ * (error = 最后一次尝试的实际报错/具体问题)。
8374
8579
  */
8375
- async function observePass(ctx, agent, config, tailCount, target, signal) {
8580
+ async function observePass(ctx, agent, config, waitCount, target, signal) {
8376
8581
  const session = agent.session;
8377
8582
  const logger = makeLogger(ctx, config.debug);
8378
- logger.step(`观察检查(观察阈值 ${config.observeThresholdTokens} tokens,尾部保留 ${tailCount} 条)`);
8379
- const threshold = config.observeThresholdTokens;
8380
- const pressureTokens = measurePressureTokens(ctx, session, logger);
8381
- if (pressureTokens === void 0) return { failed: false };
8583
+ logger.step(`观察检查(观察阈值 ${config.observeThresholdTokens} tokens,延迟等待 ${waitCount} 条完整消息)`);
8382
8584
  const { blocks } = historySection(session);
8383
- const historyTokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
8384
- const systemTokens = await estimateSystemPromptTokens(ctx, agent, logger, signal);
8385
- const toolsTokens = estimateToolsTokens(session, logger);
8386
- const netTokens = pressureTokens - historyTokens - systemTokens - toolsTokens;
8387
- if (netTokens < threshold) {
8388
- logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens} − 系统提示词 ${systemTokens} − 工具定义 ${toolsTokens})< 阈值 ${threshold},跳过`);
8389
- return { failed: false };
8585
+ const pending = findObservePending(session);
8586
+ let triggerMessageIndex;
8587
+ let pendingSeq;
8588
+ let triggerNote;
8589
+ if (pending === void 0) {
8590
+ const threshold = config.observeThresholdTokens;
8591
+ const pressureTokens = measurePressureTokens(ctx, session, logger);
8592
+ if (pressureTokens === void 0) return { failed: false };
8593
+ const historyTokens = blocks.reduce((total, block) => total + estimateTextTokens(block.text), 0);
8594
+ const systemTokens = await estimateSystemPromptTokens(ctx, agent, logger, signal);
8595
+ const toolsTokens = estimateToolsTokens(session, logger);
8596
+ const netTokens = pressureTokens - historyTokens - systemTokens - toolsTokens;
8597
+ if (netTokens < threshold) {
8598
+ logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens} − 系统提示词 ${systemTokens} − 工具定义 ${toolsTokens})< 阈值 ${threshold},跳过`);
8599
+ return { failed: false };
8600
+ }
8601
+ logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens} − 系统提示词 ${systemTokens} − 工具定义 ${toolsTokens})≥ 阈值 ${threshold},触发压缩`);
8602
+ const lastMessageIndex = indexCompleteMessages(session).length - 1;
8603
+ if (lastMessageIndex < 0) {
8604
+ logger.step("观察:会话尚无完整消息,跳过");
8605
+ return { failed: false };
8606
+ }
8607
+ triggerMessageIndex = lastMessageIndex;
8608
+ if (waitCount > 0) {
8609
+ try {
8610
+ pendingSeq = appendObservePending(session, triggerMessageIndex);
8611
+ } catch (error) {
8612
+ const message = error instanceof Error ? error.message : String(error);
8613
+ logger.warn(`观察:待定标记追加失败,本轮跳过(下轮重新评估): ${message}`);
8614
+ return { failed: false };
8615
+ }
8616
+ logger.step(`观察:记录待定标记(触发点完整消息 index ${triggerMessageIndex}),等待 ${waitCount} 条新完整消息后压缩`);
8617
+ return { failed: false };
8618
+ }
8619
+ triggerNote = `触发点完整消息 index ${triggerMessageIndex}`;
8620
+ } else {
8621
+ const arrived = indexCompleteMessages(session).length - 1 - pending.triggerMessageIndex;
8622
+ if (arrived < waitCount) {
8623
+ logger.step(`观察:待定标记等待中(触发点完整消息 index ${pending.triggerMessageIndex},新增 ${arrived}/${waitCount} 条),跳过`);
8624
+ return { failed: false };
8625
+ }
8626
+ logger.step(`观察:待定标记延迟到期(触发点完整消息 index ${pending.triggerMessageIndex},新增 ${arrived} ≥ ${waitCount} 条),执行压缩`);
8627
+ triggerMessageIndex = pending.triggerMessageIndex;
8628
+ pendingSeq = pending.seq;
8629
+ triggerNote = `待定标记触发点完整消息 index ${pending.triggerMessageIndex}`;
8390
8630
  }
8391
- logger.step(`观察:净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} 已压缩块 ${historyTokens} − 系统提示词 ${systemTokens} − 工具定义 ${toolsTokens})≥ 阈值 ${threshold},触发压缩`);
8392
- const range = computeCompressRange(session, tailCount);
8631
+ const clearPending = () => {
8632
+ if (pendingSeq === void 0) return;
8633
+ try {
8634
+ appendObserveInvalidate(session, pendingSeq);
8635
+ } catch (error) {
8636
+ const message = error instanceof Error ? error.message : String(error);
8637
+ logger.warn(`观察:待定失效标记追加失败: ${message}`);
8638
+ }
8639
+ };
8640
+ const range = computeCompressRange(session, triggerMessageIndex);
8393
8641
  if (!range) {
8394
- logger.step("观察:无可行压缩区间(边界后消息过短或配对无法平衡),跳过");
8642
+ logger.step("观察:无可行压缩区间(边界后无消息、触发点已被压缩或配对无法平衡),清除待定标记视为完成");
8643
+ clearPending();
8395
8644
  return { failed: false };
8396
8645
  }
8397
8646
  logger.step(`观察:压缩区间 [${range.start}..${range.end}],遮蔽 ${range.shadowedSeqs.length} 个表层节点`);
8398
8647
  const replaceSeqs = range.shadowedSeqs;
8399
8648
  const replaceStart = replaceSeqs[0];
8400
8649
  if (replaceStart === void 0) {
8401
- logger.step("观察:区间内无新消息(全部为压缩日志块),跳过");
8650
+ logger.step("观察:区间内无新消息(全部为压缩日志块),清除待定标记视为完成");
8651
+ clearPending();
8402
8652
  return { failed: false };
8403
8653
  }
8404
8654
  const shadowedSet = new Set(replaceSeqs);
8405
8655
  const inRangeCms = indexCompleteMessages(session).filter((cm) => cm.seqs.every((seq) => shadowedSet.has(seq)));
8406
8656
  if (inRangeCms.length === 0) {
8407
- logger.step("观察:区间内无完整消息,跳过");
8657
+ logger.step("观察:区间内无完整消息,清除待定标记视为完成");
8658
+ clearPending();
8408
8659
  return { failed: false };
8409
8660
  }
8410
8661
  const startIndex = inRangeCms[0]?.index ?? 0;
8411
8662
  const endIndex = inRangeCms[inRangeCms.length - 1]?.index ?? startIndex;
8412
- logger.step(`观察:保留旧块 ${blocks.length} 条,替换新消息 [${replaceSeqs[0]}..${range.end}](${replaceSeqs.length} 条),尾部保留 ${tailCount} 条(不压缩、不进日志),新消息 index ${startIndex}..${endIndex}`);
8413
- const instruction = buildHistoryPrompt();
8414
- const contextText = renderMessages(session, replaceSeqs);
8663
+ logger.step(`观察:保留旧块 ${blocks.length} 条,替换 [${replaceStart}..${range.end}](${replaceSeqs.length} 个表层节点,压缩至${triggerNote}),新消息 index ${startIndex}..${endIndex}`);
8664
+ const instruction = buildHistoryPrompt(config.compressSkipReasoning);
8665
+ const contextText = renderMessages(session, replaceSeqs, config.compressSkipReasoning);
8415
8666
  const lifecycle = {
8416
8667
  compactionId: newCompactionId(),
8417
8668
  turn: openTurnOf(session)
@@ -8430,17 +8681,19 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
8430
8681
  start: startIndex,
8431
8682
  end: endIndex
8432
8683
  },
8433
- rateLimitWaitMs: config.rateLimitWaitMs
8684
+ rateLimitWaitMs: config.rateLimitWaitMs,
8685
+ phase: "observe"
8434
8686
  });
8435
8687
  if (!summaryResult.ok) {
8436
- logger.warn(`观察:摘要调用失败(${summaryResult.error}),追加 compaction/end(error)`);
8688
+ logger.warn(`观察:摘要调用失败(${summaryResult.error}),诊断子会话 ${summaryResult.diagnosticSessionId ?? "未落盘"},追加 compaction/end(error)`);
8437
8689
  try {
8438
- appendCompactionEnd(session, lifecycle, summaryResult.error);
8690
+ appendCompactionEnd(session, lifecycle, summaryResult.error, summaryResult.diagnosticSessionId);
8439
8691
  } catch {}
8440
8692
  return {
8441
8693
  failed: true,
8442
8694
  error: summaryResult.error,
8443
- aborted: summaryResult.aborted
8695
+ aborted: summaryResult.aborted,
8696
+ ...summaryResult.diagnosticSessionId === void 0 ? {} : { diagnosticSessionId: summaryResult.diagnosticSessionId }
8444
8697
  };
8445
8698
  }
8446
8699
  const report = summaryResult.text;
@@ -8487,8 +8740,9 @@ async function observePass(ctx, agent, config, tailCount, target, signal) {
8487
8740
  end: range.end
8488
8741
  }, lifecycle.compactionId);
8489
8742
  logger.step("观察提交:追加 compaction/end");
8490
- appendCompactionEnd(session, lifecycle);
8491
- logger.info(`观察压缩完成(净压力 ${netTokens} tokens(上下文压力 ${pressureTokens} − 已压缩块 ${historyTokens})≥ 阈值 ${threshold},替换 ${replaceSeqs.length} 个表层节点,约 ${shadowedTokenCount} tokens)`);
8743
+ appendCompactionEnd(session, lifecycle, void 0, summaryResult.diagnosticSessionId);
8744
+ clearPending();
8745
+ logger.info(`观察压缩完成(替换 ${replaceSeqs.length} 个表层节点,约 ${shadowedTokenCount} tokens,压缩至${triggerNote})`);
8492
8746
  return { failed: false };
8493
8747
  } catch (error) {
8494
8748
  const message = error instanceof Error ? error.message : String(error);
@@ -8517,7 +8771,7 @@ async function maybeCompress(ctx, agent, config, signal) {
8517
8771
  return { failed: false };
8518
8772
  }
8519
8773
  logger.step(`会话路由:provider ${target.provider},model ${target.model}`);
8520
- const tailCount = config.tailMessageCount;
8774
+ const waitCount = config.tailMessageCount;
8521
8775
  logger.step("反思 pass 开始");
8522
8776
  const reflect = await reflectPass(ctx, agent, config, target, signal);
8523
8777
  if (reflect.failed) {
@@ -8525,7 +8779,7 @@ async function maybeCompress(ctx, agent, config, signal) {
8525
8779
  return reflect;
8526
8780
  }
8527
8781
  logger.step("反思 pass 结束,观察 pass 开始");
8528
- const observe = await observePass(ctx, agent, config, tailCount, target, signal);
8782
+ const observe = await observePass(ctx, agent, config, waitCount, target, signal);
8529
8783
  logger.step("观察 pass 结束,压缩流程完成");
8530
8784
  return observe;
8531
8785
  }
@@ -8744,6 +8998,7 @@ const DEFAULT_CONFIG = Object.freeze({
8744
8998
  rateLimitWaitMs: 6e4,
8745
8999
  tailMessageCount: 5,
8746
9000
  compressRetryCount: 5,
9001
+ compressSkipReasoning: true,
8747
9002
  omEnabled: true,
8748
9003
  debug: false,
8749
9004
  recallEnabled: true,
@@ -8785,6 +9040,7 @@ function resolveConfig(raw) {
8785
9040
  if (integer && !Number.isInteger(value)) continue;
8786
9041
  config[key] = value;
8787
9042
  }
9043
+ config.compressSkipReasoning = resolveBoolean(input.compressSkipReasoning, true);
8788
9044
  config.omEnabled = resolveBoolean(input.omEnabled, true);
8789
9045
  config.debug = resolveBoolean(input.debug, process.env.NODE_ENV !== "production");
8790
9046
  config.recallEnabled = resolveBoolean(input.recallEnabled, true);
@@ -9146,8 +9402,9 @@ function buildSemanticRecallTool(options) {
9146
9402
  * dsh-plugin-om 入口(tsdown 打包入口):导出 name / inject / apply。
9147
9403
  * apply 注册 recall / recall-semantic 工具,并接线 agent/pre-step 自动压缩
9148
9404
  * (先反思后观察,仅主会话生效)。压缩摘要尝试全部耗尽时拒绝本 step 中断当前
9149
- * turn(signal 中止除外)。压缩与检索的实现见 compress.ts / recall.ts /
9150
- * semantic-recall.ts
9405
+ * turn(signal 中止除外),主会话日志记录失败原因与诊断子会话 sessionId(每次
9406
+ * 尝试的完整提示词与模型原始输出由 compaction-log.ts 落盘为诊断子会话)。压缩与
9407
+ * 检索的实现见 compress.ts / compaction-log.ts / recall.ts / semantic-recall.ts。
9151
9408
  */
9152
9409
  /** 插件名(Loader 识别入口的稳定标识)。 */
9153
9410
  const name = "dsh-plugin-om";
@@ -9187,7 +9444,8 @@ function apply(ctx, config) {
9187
9444
  logger.warn(`pre-step 处理失败: ${error instanceof Error ? error.message : String(error)}`);
9188
9445
  }
9189
9446
  if (outcome.failed && !outcome.aborted) {
9190
- logger.warn(`上下文压缩失败,拒绝本 step 中断当前 turn:${outcome.error}`);
9447
+ const diagnostic = outcome.diagnosticSessionId === void 0 ? "" : `(诊断子会话 ${outcome.diagnosticSessionId})`;
9448
+ logger.warn(`上下文压缩失败,拒绝本 step 中断当前 turn:${outcome.error}${diagnostic}`);
9191
9449
  return { kind: "reject" };
9192
9450
  }
9193
9451
  return next();