dsh-session-bridge 0.2.1 → 0.3.0

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/lib/index.js CHANGED
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { appendFileSync, mkdirSync } from "node:fs";
6
6
  import { randomUUID } from "node:crypto";
7
- //#region node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-in_163d6a248f48546c43609789e3a6f2aa/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
7
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-in_7832b76c982cc1477c0eac59a4954baf/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
8
8
  /**
9
9
  * Shared filesystem path helpers for DeepSeek Harness user data.
10
10
  *
@@ -2200,7 +2200,7 @@ var Service = class Service {
2200
2200
  }
2201
2201
  };
2202
2202
  //#endregion
2203
- //#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.2-alpha.5_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
2203
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.2-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
2204
2204
  /** The one Remote failure class shared by owners, the Gateway, and consumers. */
2205
2205
  /**
2206
2206
  * One Remote call failure: a real Error carrying its stable code and typed
@@ -2345,7 +2345,7 @@ function validateName(subject, value) {
2345
2345
  if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
2346
2346
  }
2347
2347
  //#endregion
2348
- //#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_4c434a1a30063a3cb77c280072a5c101/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
2348
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_486cdbb63f2ab3a8eaad87d2b6e5a4b7/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
2349
2349
  /** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
2350
2350
  /**
2351
2351
  * Mark an unreachable closed-union branch.
@@ -2557,7 +2557,7 @@ function deepFreeze(value) {
2557
2557
  return value;
2558
2558
  }
2559
2559
  //#endregion
2560
- //#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_8594a45cf8ae488dc1880a22a6864aa8/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
2560
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_45952c52282327ee28c4189b1e05c80c/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
2561
2561
  /**
2562
2562
  * Random v4 UUID, minted from `crypto.getRandomValues`.
2563
2563
  * @returns the UUID string.
@@ -2570,7 +2570,7 @@ function randomUUID$1() {
2570
2570
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2571
2571
  }
2572
2572
  //#endregion
2573
- //#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_e2403996a25372848a6710513dd3725f/node_modules/@deepseek-ai/dsh-brand/lib/index.js
2573
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_0a6acdb1edc9f1b3caaaafba5a0cd464/node_modules/@deepseek-ai/dsh-brand/lib/index.js
2574
2574
  /**
2575
2575
  * Duplicate-install-safe nominal string helpers.
2576
2576
  *
@@ -3187,11 +3187,11 @@ defineMethod("transform", [
3187
3187
  "preserve"
3188
3188
  ], ({ inner }, isInner) => inner.toString(isInner));
3189
3189
  //#endregion
3190
- //#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.2-alpha.5_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
3190
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.2-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
3191
3191
  /** Largest delay Node schedules without clamping it to one millisecond. */
3192
3192
  const MAX_TIMER_DELAY_MS = 2147483647;
3193
3193
  //#endregion
3194
- //#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invariant_ba6560006b4459f1f9769001786a712b/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3194
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invariant_cc3ae850d309c6dee6d11afd096d0893/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3195
3195
  /**
3196
3196
  * Detach and deep-freeze a message whose identity already exists.
3197
3197
  * @param message - complete message, including its stable identity.
@@ -8966,7 +8966,7 @@ function superRefine(fn, params) {
8966
8966
  return /* @__PURE__ */ _superRefine(fn, params);
8967
8967
  }
8968
8968
  //#endregion
8969
- //#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.2-alpha.5_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
8969
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.2-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
8970
8970
  /**
8971
8971
  * Backend-facing vocabulary of the storage hub: a backend owns one medium
8972
8972
  * (a file-tree root, a database file) and exposes operation groups over it.
@@ -8977,7 +8977,7 @@ function superRefine(fn, params) {
8977
8977
  /** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
8978
8978
  const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/;
8979
8979
  //#endregion
8980
- //#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.2-alpha.5_@deepseek-ai+cordis@4.0.2_@deepseek-ai+ds_045e358f2bf6c5fc406379121d884012/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
8980
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.2-rc.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_9f44d404e0b20c2f6e8599234336f27b/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
8981
8981
  /**
8982
8982
  * Domain declaration vocabulary. A spec object is the single source of a
8983
8983
  * domain's identity, layout, and record schemas: the owning package defines
@@ -9144,6 +9144,18 @@ function blockText(content) {
9144
9144
  }
9145
9145
  return parts.length === 0 ? void 0 : parts.join("\n");
9146
9146
  }
9147
+ /** 提取指定类型的 content block 文本(如 reasoning),用于从消息 content 里读出思维链。 */
9148
+ function blockTextByType(content, wantedType) {
9149
+ if (!Array.isArray(content)) return void 0;
9150
+ const parts = [];
9151
+ for (const raw of content) {
9152
+ const block = raw;
9153
+ if (block !== null && typeof block === "object" && block.type === wantedType && typeof block.text === "string") {
9154
+ if (block.text !== "") parts.push(block.text);
9155
+ }
9156
+ }
9157
+ return parts.length === 0 ? void 0 : parts.join("\n");
9158
+ }
9147
9159
  function countImages(content) {
9148
9160
  if (!Array.isArray(content)) return 0;
9149
9161
  let n = 0;
@@ -9181,7 +9193,7 @@ function foldMessages(events) {
9181
9193
  } else if (event.type === "assistant/message") {
9182
9194
  const message = event.data.message;
9183
9195
  const text = message === null ? void 0 : blockText(message.content);
9184
- const reasoning = message === null ? void 0 : blockText(message.reasoning);
9196
+ const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
9185
9197
  const images = message === null ? 0 : countImages(message.content);
9186
9198
  const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
9187
9199
  if (text === void 0 && reasoning === void 0 && toolCalls.length === 0) continue;
@@ -9198,6 +9210,111 @@ function foldMessages(events) {
9198
9210
  }
9199
9211
  return rows;
9200
9212
  }
9213
+ /** 按已完成输出段落折叠事件日志:每个 assistant/message 割一段,返回 sinceSeq 后的所有段落(事件序)。 */
9214
+ function segmentsSince(events, sinceSeq = 0) {
9215
+ const list = asEventList(events);
9216
+ const out = [];
9217
+ let openTurn = false;
9218
+ for (const event of list) {
9219
+ if (event.type === "turn/start") {
9220
+ openTurn = true;
9221
+ continue;
9222
+ }
9223
+ if (event.type === "turn/end") {
9224
+ openTurn = false;
9225
+ continue;
9226
+ }
9227
+ if (event.type !== "assistant/message") continue;
9228
+ if (event.seq <= sinceSeq) continue;
9229
+ const message = event.data.message;
9230
+ const text = message === null ? void 0 : blockText(message.content);
9231
+ const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
9232
+ const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
9233
+ const turn = event.data?.turn;
9234
+ const step = event.data?.step;
9235
+ const seg = {
9236
+ seq: event.seq,
9237
+ time: event.time,
9238
+ turn: typeof turn === "number" ? turn : 0,
9239
+ step: typeof step === "number" ? step : 0,
9240
+ toolCalls,
9241
+ openTurn
9242
+ };
9243
+ if (text !== void 0) seg.text = text;
9244
+ if (reasoning !== void 0) seg.reasoning = reasoning;
9245
+ out.push(seg);
9246
+ }
9247
+ return out;
9248
+ }
9249
+ /** 取自 sinceSeq 之后最新一个已完成输出段落(无则 null)。 */
9250
+ function latestSegmentSince(events, sinceSeq = 0) {
9251
+ const segs = segmentsSince(events, sinceSeq);
9252
+ return segs.length === 0 ? null : segs[segs.length - 1] ?? null;
9253
+ }
9254
+ /**
9255
+ * 对一个 live 会话的事件日志折叠出其"实时思维链":
9256
+ * - 优先聚合流式 assistant/chunk 事件里的 reasoning-delta(turn 中途即可见、增量,
9257
+ * 这正是监控思维链并按规则提前终止所需的粒度);
9258
+ * - 同时聚合同窗口的 text-delta 增量;
9259
+ * - 若没有进行中的 chunk(会话空闲、或该 provider 不流式推理/无推理),返回 null,
9260
+ * 调用方应回落为 foldMessages 里的已定型 message.reasoning。
9261
+ * 纯读、无副作用;事件形状不做任何假设,缺失/异常一律安全处理。
9262
+ */
9263
+ function liveReasoningSnapshot(events) {
9264
+ const list = asEventList(events);
9265
+ let lastFinalizedSeq = -1;
9266
+ let reasoning = "";
9267
+ let text = "";
9268
+ let seq = -1;
9269
+ let time = 0;
9270
+ let turn = 0;
9271
+ let step = 0;
9272
+ let foundDelta = false;
9273
+ for (const event of list) {
9274
+ if (event.type === "assistant/message") {
9275
+ lastFinalizedSeq = event.seq;
9276
+ continue;
9277
+ }
9278
+ if (event.type !== "assistant/chunk") continue;
9279
+ if (event.seq <= lastFinalizedSeq) continue;
9280
+ const data = event.data;
9281
+ if (data === null || data === void 0 || typeof data !== "object") continue;
9282
+ const c = data.chunk;
9283
+ if (c === null || typeof c !== "object" || typeof c.type !== "string") continue;
9284
+ if (c.type === "reasoning-delta" || c.type === "text-delta") {
9285
+ if (typeof c.text !== "string") continue;
9286
+ if (c.type === "reasoning-delta") reasoning += c.text;
9287
+ else text += c.text;
9288
+ if (typeof data.turn === "number") turn = data.turn;
9289
+ if (typeof data.step === "number") step = data.step;
9290
+ foundDelta = true;
9291
+ if (event.seq > seq) seq = event.seq;
9292
+ if (event.time > time) time = event.time;
9293
+ }
9294
+ }
9295
+ if (!foundDelta) return null;
9296
+ return {
9297
+ turn,
9298
+ step,
9299
+ reasoning,
9300
+ text,
9301
+ seq,
9302
+ time
9303
+ };
9304
+ }
9305
+ /** 思维链尾巴默认截断上限(字符)。 */
9306
+ const REASONING_TAIL_LIMIT = 4e3;
9307
+ /**
9308
+ * 组装一条受字符上限约束的"思维链尾巴"用于展示/摘要:
9309
+ * 优先进行中的 liveReasoning,否则最近已定型的 lastReasoning;
9310
+ * 超出上限时截断并附省略标记。两者皆无返回 undefined。
9311
+ */
9312
+ function reasoningTailOf(liveReasoning, lastReasoning, limit = REASONING_TAIL_LIMIT) {
9313
+ const source = liveReasoning ?? lastReasoning;
9314
+ if (source === void 0 || source === "") return void 0;
9315
+ if (source.length <= limit) return source;
9316
+ return source.slice(0, limit) + "…";
9317
+ }
9201
9318
  /**
9202
9319
  * 会话标题:优先 session/title 事件(last-wins,与 UI 一致);
9203
9320
  * 无标题事件时回落为第一条用户消息(截断 80 字符)。
@@ -9249,8 +9366,10 @@ async function waitForReply(opts) {
9249
9366
  const started = Date.now();
9250
9367
  const deadline = started + opts.timeoutMs;
9251
9368
  const requireTurnEnd = opts.requireTurnEnd ?? false;
9369
+ const waitForSegment = opts.waitForSegment ?? false;
9252
9370
  let latest = null;
9253
9371
  let textReply = null;
9372
+ let segment = null;
9254
9373
  let turnEnded = false;
9255
9374
  for (;;) {
9256
9375
  if (opts.signal !== void 0 && opts.signal.aborted) break;
@@ -9260,22 +9379,38 @@ async function waitForReply(opts) {
9260
9379
  if (latest === null || row.seq > latest.seq) latest = row;
9261
9380
  if (row.text !== void 0 && (textReply === null || row.seq > textReply.seq)) textReply = row;
9262
9381
  }
9382
+ if (waitForSegment) {
9383
+ const seg = latestSegmentSince(events, opts.baselineSeq);
9384
+ if (seg !== null && (segment === null || seg.seq > segment.seq)) segment = seg;
9385
+ }
9263
9386
  if (requireTurnEnd && latest !== null) {
9264
9387
  for (const event of events) if (event.seq > latest.seq && event.type === "turn/end") {
9265
9388
  turnEnded = true;
9266
9389
  break;
9267
9390
  }
9268
9391
  }
9269
- if (requireTurnEnd ? latest !== null && turnEnded : textReply !== null) break;
9392
+ let done;
9393
+ if (requireTurnEnd) done = latest !== null && turnEnded;
9394
+ else if (waitForSegment) done = segment !== null;
9395
+ else done = textReply !== null;
9396
+ if (done) break;
9270
9397
  if (Date.now() >= deadline) break;
9271
9398
  await sleep(100);
9272
9399
  }
9273
- const message = textReply ?? latest;
9400
+ let message = waitForSegment && segment !== null ? {
9401
+ seq: segment.seq,
9402
+ time: segment.time,
9403
+ role: "assistant",
9404
+ images: 0,
9405
+ ...segment.text !== void 0 ? { text: segment.text } : {},
9406
+ ...segment.reasoning !== void 0 ? { reasoning: segment.reasoning } : {},
9407
+ ...segment.toolCalls.length > 0 ? { toolCalls: segment.toolCalls } : {}
9408
+ } : textReply ?? latest;
9274
9409
  return {
9275
9410
  message,
9276
9411
  seq: message === null ? opts.baselineSeq : message.seq,
9277
9412
  turnEnded,
9278
- timedOut: requireTurnEnd ? latest !== null && !turnEnded : textReply === null,
9413
+ timedOut: requireTurnEnd ? latest !== null && !turnEnded : waitForSegment ? segment === null : textReply === null,
9279
9414
  aborted: opts.signal !== void 0 && opts.signal.aborted,
9280
9415
  waitedMs: Date.now() - started
9281
9416
  };
@@ -9351,13 +9486,18 @@ function statusSnapshot(ctx, agent) {
9351
9486
  }
9352
9487
  const recent = rows.slice(-8);
9353
9488
  let lastAssistantText;
9489
+ let lastReasoning;
9354
9490
  for (let i = rows.length - 1; i >= 0; i -= 1) {
9355
9491
  const row = rows[i];
9356
- if (row !== void 0 && row.role === "assistant" && row.text !== void 0) {
9357
- lastAssistantText = row.text;
9358
- break;
9492
+ if (row !== void 0 && row.role === "assistant") {
9493
+ if (lastAssistantText === void 0 && row.text !== void 0) lastAssistantText = row.text;
9494
+ if (lastReasoning === void 0 && row.reasoning !== void 0) lastReasoning = row.reasoning;
9495
+ if (lastAssistantText !== void 0 && lastReasoning !== void 0) break;
9359
9496
  }
9360
9497
  }
9498
+ const live = liveReasoningSnapshot(events);
9499
+ const liveReasoning = live !== null && live.reasoning !== "" ? live.reasoning : void 0;
9500
+ const reasoningTail = reasoningTailOf(liveReasoning, lastReasoning);
9361
9501
  const inbox = agent.inbox;
9362
9502
  const cwd = agent.session.header.cwd;
9363
9503
  return {
@@ -9375,6 +9515,9 @@ function statusSnapshot(ctx, agent) {
9375
9515
  nextTurnCount: inbox?.nextTurn?.length ?? 0,
9376
9516
  nextStepCount: inbox?.nextStep?.length ?? 0,
9377
9517
  ...lastAssistantText === void 0 ? {} : { lastAssistantText },
9518
+ ...lastReasoning === void 0 ? {} : { lastReasoning },
9519
+ ...liveReasoning === void 0 ? {} : { liveReasoning },
9520
+ ...reasoningTail === void 0 ? {} : { reasoningTail },
9378
9521
  messageCount: rows.length,
9379
9522
  recent
9380
9523
  };
@@ -9429,6 +9572,16 @@ var SessionMonitor = class {
9429
9572
  start(config) {
9430
9573
  const existing = this.entries.get(config.sessionId);
9431
9574
  const now = Date.now();
9575
+ const cotInit = { rules: (config.coTRules ?? []).reduce((acc, rule, index) => {
9576
+ const key = String(index) + ":" + rule.match + ":" + rule.field + ":" + rule.value;
9577
+ acc[key] = {
9578
+ hits: 0,
9579
+ lastFiredAt: null,
9580
+ lastMatch: false,
9581
+ lastSubject: ""
9582
+ };
9583
+ return acc;
9584
+ }, {}) };
9432
9585
  const entry = existing !== void 0 ? {
9433
9586
  ...existing,
9434
9587
  config,
@@ -9442,8 +9595,10 @@ var SessionMonitor = class {
9442
9595
  lastActionAt: null,
9443
9596
  lastNote: "监控已启动",
9444
9597
  done: false,
9445
- cycles: 0
9598
+ cycles: 0,
9599
+ cot: cotInit
9446
9600
  };
9601
+ if (entry.cot === void 0) entry.cot = cotInit;
9447
9602
  this.entries.set(config.sessionId, entry);
9448
9603
  this.ensureTimer();
9449
9604
  this.log(entry, "monitor start session=" + config.sessionId + " interval=" + config.intervalMs + " stalled=" + String(config.stalledMs ?? 6e4) + " maxStuck=" + String(config.maxStuckCycles ?? 3));
@@ -9569,6 +9724,7 @@ var SessionMonitor = class {
9569
9724
  this.log(entry, "STALL(#" + entry.stuckCount + ") → steer: " + steer);
9570
9725
  return;
9571
9726
  }
9727
+ if (this.evaluateCoTRules(entry, snapshot, sessionId)) return;
9572
9728
  if (entry.stuckCount !== 0) entry.stuckCount = 0;
9573
9729
  entry.lastAction = "steady";
9574
9730
  entry.lastActionAt = Date.now();
@@ -9593,6 +9749,76 @@ var SessionMonitor = class {
9593
9749
  if (!text) return false;
9594
9750
  return keywords.some((kw) => kw !== "" && text.includes(kw));
9595
9751
  }
9752
+ /**
9753
+ * 评估思维链(CoT)规则。规则只对 live 会话有意义:running 时用进行中的
9754
+ * reasoning-delta 流,空闲时回落到已定型 lastReasoning。not-contains 规则作用在
9755
+ * reasoning 字段时,若该会话本就没有推理(非推理模型/尚未产出),不判定为命中,
9756
+ * 避免误 cancel 一个根本不产生思维链的会话。
9757
+ * @returns true 表示本 tick 已被某条规则触发并处理(steer/cancel),调用方应 return。
9758
+ */
9759
+ evaluateCoTRules(entry, snapshot, sessionId) {
9760
+ const rules = entry.config.coTRules ?? [];
9761
+ if (rules.length === 0) return false;
9762
+ const minHits = entry.config.cotMinHits ?? 1;
9763
+ const hasLive = (snapshot.liveReasoning ?? "") !== "";
9764
+ const subject = {
9765
+ reasoning: snapshot.liveReasoning ?? snapshot.lastReasoning ?? "",
9766
+ text: snapshot.lastAssistantText ?? "",
9767
+ hasReasoning: hasLive || (snapshot.lastReasoning ?? "") !== ""
9768
+ };
9769
+ const now = Date.now();
9770
+ const rulesState = entry.cot;
9771
+ let engaged = false;
9772
+ rules.forEach((rule, index) => {
9773
+ if (engaged) return;
9774
+ const key = String(index) + ":" + rule.match + ":" + rule.field + ":" + rule.value;
9775
+ const st = rulesState.rules[key] ?? {
9776
+ hits: 0,
9777
+ lastFiredAt: null,
9778
+ lastMatch: false
9779
+ };
9780
+ if (rule.field === "reasoning" && rule.match === "not-contains" && !subject.hasReasoning) {
9781
+ st.lastMatch = false;
9782
+ st.hits = 0;
9783
+ return;
9784
+ }
9785
+ let hit;
9786
+ if (rule.field === "reasoning") hit = rule.match === "contains" ? subject.reasoning.includes(rule.value) : !subject.reasoning.includes(rule.value);
9787
+ else if (rule.field === "text") hit = rule.match === "contains" ? subject.text.includes(rule.value) : !subject.text.includes(rule.value);
9788
+ else {
9789
+ const reasonHas = subject.reasoning.includes(rule.value);
9790
+ const textHas = subject.text.includes(rule.value);
9791
+ hit = rule.match === "contains" ? reasonHas || textHas : !reasonHas && !textHas;
9792
+ }
9793
+ st.lastMatch = hit;
9794
+ st.hits = hit ? st.hits + 1 : 0;
9795
+ this.log(entry, "CoT rule " + key + " hit=" + String(hit) + " hits=" + String(st.hits) + "/" + String(minHits));
9796
+ if (!(hit && st.hits >= minHits)) return;
9797
+ const coolMs = Math.max(entry.config.intervalMs, 5e3) * 2;
9798
+ if (st.lastFiredAt !== null && now - st.lastFiredAt < coolMs) return;
9799
+ st.lastFiredAt = now;
9800
+ if (rule.action === "cancel") {
9801
+ const reason = "reasoning-rule:" + rule.value;
9802
+ cancelLiveSession(this.ctx, sessionId, false, reason);
9803
+ entry.lastAction = "cot-cancel";
9804
+ entry.lastActionAt = Date.now();
9805
+ entry.lastNote = "思维链规则触发 cancel: " + rule.value;
9806
+ entry.stuckCount = 0;
9807
+ this.log(entry, "COT-CANCEL session=" + sessionId + " rule=" + key + " value=" + rule.value);
9808
+ } else {
9809
+ const steer = rule.message !== void 0 && rule.message.trim() !== "" ? rule.message.trim() : "监控判定:思维链/回复匹配规则「" + rule.value + "」,请据此调整方向并继续推进任务。";
9810
+ sendLiveMessage(this.ctx, sessionId, steer, "steer");
9811
+ entry.lastAction = "cot-steer";
9812
+ entry.lastActionAt = Date.now();
9813
+ entry.lastNote = "思维链规则触发 steer: " + rule.value;
9814
+ entry.stuckCount = 0;
9815
+ this.log(entry, "COT-STEER session=" + sessionId + " rule=" + key + " steer=" + steer);
9816
+ }
9817
+ engaged = true;
9818
+ st.hits = 0;
9819
+ });
9820
+ return engaged;
9821
+ }
9596
9822
  hasLlm() {
9597
9823
  return this.ctx.llm !== void 0;
9598
9824
  }
@@ -9648,7 +9874,7 @@ var SessionMonitor = class {
9648
9874
  }
9649
9875
  };
9650
9876
  //#endregion
9651
- //#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_cc61ed22356bd3509f8221ef1e37b1c1/node_modules/@deepseek-ai/dsh-scope/lib/index.js
9877
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.2-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_51e6fefef6ddce4f991b3d02344a1a72/node_modules/@deepseek-ai/dsh-scope/lib/index.js
9652
9878
  /**
9653
9879
  * Shared insertion-ordered storage and effect ownership for scope-aware registries.
9654
9880
  *
@@ -9927,7 +10153,7 @@ function scopeTarget(base, key) {
9927
10153
  return carrier;
9928
10154
  }
9929
10155
  //#endregion
9930
- //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.2-alpha.2_3813614b220b461dfd055a6c3c46e93f/node_modules/@deepseek-ai/dsh-tools/lib/index.js
10156
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.2-alpha.2_96d3de6f0f5da790676b5bfb20e4a981/node_modules/@deepseek-ai/dsh-tools/lib/index.js
9931
10157
  /**
9932
10158
  * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
9933
10159
  * types, subagents, and workflows. The subset accepts any JSON root, an
@@ -13429,7 +13655,7 @@ function toolAbortedBeforeDispatchResult(prior) {
13429
13655
  };
13430
13656
  }
13431
13657
  //#endregion
13432
- //#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.2-alpha.2_58555bdbcb744cce566f3f4d16801116/node_modules/@deepseek-ai/dsh-agent/lib/index.js
13658
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.2-alpha.2_82401c175458b799db42b9e8f7dd7700/node_modules/@deepseek-ai/dsh-agent/lib/index.js
13433
13659
  /**
13434
13660
  * Agent-scoped model selection shared by runtime entry points.
13435
13661
  * @module @deepseek-ai/dsh-agent/model-selection
@@ -13831,7 +14057,7 @@ function registerResume(env) {
13831
14057
  function registerWait(env) {
13832
14058
  env.ctx.tools.register(defineTool({
13833
14059
  name: "session_bridge_wait",
13834
- description: "Wait for a session next assistant reply: blocks (polling the session log) until a NEW assistant text reply appears after sinceSeq (default: the latest seq at call time). Returns the reply summary, or timedOut/aborted when the deadline or caller cancellation ends the wait. Use it to consume a reply produced asynchronously by another session (e.g. a session you sent a message to, or one working on its own).",
14060
+ description: "Wait for a session next assistant output: blocks (polling the session log) until a NEW assistant output appears after sinceSeq (default: the latest seq at call time). waitFor=reply returns as soon as a new assistant TEXT reply is readable; waitFor=segment returns as soon as any new COMPLETED output segment appears (an assistant/message step — text, reasoning, or tool-call turn), i.e. it does NOT wait for the whole turn, so you can observe the chain-of-thought/output paragraph by paragraph as it is produced. Returns the output summary, or timedOut/aborted when the deadline or caller cancellation ends the wait. Use it to consume output produced asynchronously by another session (e.g. a session you sent a message to, or one working on its own).",
13835
14061
  parameters: {
13836
14062
  sessionId: {
13837
14063
  type: "string",
@@ -13849,6 +14075,11 @@ function registerWait(env) {
13849
14075
  requireTurnEnd: {
13850
14076
  type: "boolean",
13851
14077
  description: "When true, wait for the reply turn/end to settle before returning (default false; false returns as soon as the reply text is readable)."
14078
+ },
14079
+ waitFor: {
14080
+ type: "string",
14081
+ enum: ["reply", "segment"],
14082
+ description: "reply (default) waits for a new assistant TEXT reply; segment waits for any new completed output segment (an assistant/message step, incl. reasoning/tool turns) and returns it immediately, without waiting for the whole turn."
13852
14083
  }
13853
14084
  },
13854
14085
  output: {
@@ -13877,9 +14108,13 @@ function registerWait(env) {
13877
14108
  if (typeof args.sessionId !== "string" || args.sessionId.trim() === "") throw new Error("invalid sessionId: expected a non-empty string");
13878
14109
  const agent = liveAgent(env, args.sessionId);
13879
14110
  if (agent === void 0) throw new Error("session " + JSON.stringify(args.sessionId) + " is not live — call session_bridge_resume first (waiting requires a live session)");
14111
+ const waitSegment = args.waitFor === "segment";
13880
14112
  let baseline;
13881
14113
  if (typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) baseline = args.sinceSeq;
13882
- else {
14114
+ else if (waitSegment) {
14115
+ const segs = segmentsSince(sessionEvents(agent.session));
14116
+ baseline = segs.length === 0 ? -1 : segs[segs.length - 1]?.seq ?? -1;
14117
+ } else {
13883
14118
  let lastText = -1;
13884
14119
  for (const row of foldMessages(sessionEvents(agent.session))) if (row.text !== void 0) lastText = row.seq;
13885
14120
  baseline = lastText;
@@ -13889,7 +14124,8 @@ function registerWait(env) {
13889
14124
  baselineSeq: baseline,
13890
14125
  timeoutMs: clampTimeout(args.timeoutMs),
13891
14126
  signal: exec.signal,
13892
- requireTurnEnd: args.requireTurnEnd === true
14127
+ requireTurnEnd: args.requireTurnEnd === true,
14128
+ ...waitSegment ? { waitForSegment: true } : {}
13893
14129
  });
13894
14130
  env.registry.touch(args.sessionId);
13895
14131
  return asJson({
@@ -13900,6 +14136,83 @@ function registerWait(env) {
13900
14136
  }
13901
14137
  }));
13902
14138
  }
14139
+ function registerSegments(env) {
14140
+ env.ctx.tools.register(defineTool({
14141
+ name: "session_bridge_segments",
14142
+ description: "Read the completed output segments of a session incrementally - each finished assistant step (one assistant/message paragraph), without waiting for the whole turn. Pass sinceSeq to page forward. Use this to watch a session produce its chain-of-thought / output paragraph by paragraph as each step finishes.",
14143
+ parameters: {
14144
+ sessionId: {
14145
+ type: "string",
14146
+ required: true,
14147
+ description: "Session id to read segments from (live or offline)."
14148
+ },
14149
+ sinceSeq: {
14150
+ type: "number",
14151
+ description: "Only return completed segments after this event seq (paging cursor)."
14152
+ },
14153
+ limit: {
14154
+ type: "number",
14155
+ description: "Maximum number of segments to return (default 10, max 50)."
14156
+ }
14157
+ },
14158
+ output: {
14159
+ schema: {
14160
+ type: "object",
14161
+ additionalProperties: true
14162
+ },
14163
+ render: (_args, value) => {
14164
+ const segs = value.segments ?? [];
14165
+ if (segs.length === 0) return [{
14166
+ type: "text",
14167
+ text: "(no completed segments)"
14168
+ }];
14169
+ return [{
14170
+ type: "text",
14171
+ text: segs.map((sg) => {
14172
+ const head = "seg #" + String(sg.seq) + " (turn " + String(sg.turn) + " step " + String(sg.step) + ")";
14173
+ const text = typeof sg.text === "string" ? sg.text : "";
14174
+ const reason = typeof sg.reasoning === "string" && sg.reasoning !== "" ? " [reasoning " + sg.reasoning.length + " chars]" : "";
14175
+ return head + (Array.isArray(sg.toolCalls) && sg.toolCalls.length > 0 ? " [tools: " + sg.toolCalls.join(",") + "]" : "") + reason + ": " + text.slice(0, 160);
14176
+ }).join("\n")
14177
+ }];
14178
+ }
14179
+ },
14180
+ async execute(args) {
14181
+ const agent = liveAgent(env, args.sessionId);
14182
+ let events;
14183
+ let live;
14184
+ if (agent !== void 0) {
14185
+ events = sessionEvents(agent.session);
14186
+ live = true;
14187
+ } else try {
14188
+ events = (await env.ctx.sessionPersistence.inspect(args.sessionId)).events;
14189
+ live = false;
14190
+ } catch (error) {
14191
+ throw new Error(String(error));
14192
+ }
14193
+ const sinceSeq = typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0 ? args.sinceSeq : 0;
14194
+ clampLimit(args.limit, 10, 50);
14195
+ const segs = segmentsSince(events, sinceSeq);
14196
+ env.registry.touch(args.sessionId);
14197
+ const nextCursorSeq = segs.length === 0 ? sinceSeq : segs[segs.length - 1]?.seq ?? sinceSeq;
14198
+ return asJson({
14199
+ sessionId: args.sessionId,
14200
+ live,
14201
+ nextCursorSeq,
14202
+ segments: segs.map((sg) => ({
14203
+ seq: sg.seq,
14204
+ time: sg.time,
14205
+ turn: sg.turn,
14206
+ step: sg.step,
14207
+ openTurn: sg.openTurn,
14208
+ ...sg.text === void 0 ? {} : { text: sg.text },
14209
+ ...sg.reasoning === void 0 ? {} : { reasoning: sg.reasoning },
14210
+ ...sg.toolCalls.length > 0 ? { toolCalls: sg.toolCalls } : {}
14211
+ }))
14212
+ });
14213
+ }
14214
+ }));
14215
+ }
13903
14216
  function registerRead(env) {
13904
14217
  env.ctx.tools.register(defineTool({
13905
14218
  name: "session_bridge_read",
@@ -14180,6 +14493,7 @@ function registerBridgeTools(env) {
14180
14493
  registerSend(env);
14181
14494
  registerResume(env);
14182
14495
  registerWait(env);
14496
+ registerSegments(env);
14183
14497
  registerRead(env);
14184
14498
  registerFind(env);
14185
14499
  registerStatus(env);
@@ -14187,11 +14501,33 @@ function registerBridgeTools(env) {
14187
14501
  registerMonitor(env);
14188
14502
  registerArchive(env);
14189
14503
  }
14504
+ /** 校验并规范化 coRules 参数为 CoTRule[];非法项抛清晰错误。 */
14505
+ function parseCoRules(raw) {
14506
+ return raw.map((rule, index) => {
14507
+ return {
14508
+ match: rule.match === "contains" || rule.match === "not-contains" ? rule.match : (() => {
14509
+ throw new Error("coRules[" + index + "].match must be \"contains\" or \"not-contains\"");
14510
+ })(),
14511
+ field: rule.field === "reasoning" || rule.field === "text" || rule.field === "both" ? rule.field : (() => {
14512
+ throw new Error("coRules[" + index + "].field must be \"reasoning\", \"text\" or \"both\"");
14513
+ })(),
14514
+ action: rule.action === "steer" || rule.action === "cancel" ? rule.action : (() => {
14515
+ throw new Error("coRules[" + index + "].action must be \"steer\" or \"cancel\"");
14516
+ })(),
14517
+ value: typeof rule.value === "string" && rule.value.trim() !== "" ? rule.value.trim() : (() => {
14518
+ throw new Error("coRules[" + index + "].value must be a non-empty string");
14519
+ })(),
14520
+ ...typeof rule.message === "string" && rule.message.trim() !== "" ? { message: rule.message.trim() } : {}
14521
+ };
14522
+ });
14523
+ }
14190
14524
  function renderMonitorState(entry) {
14191
14525
  const lines = [];
14192
14526
  lines.push(`monitoring ${entry.config.sessionId}${entry.config.label === void 0 ? "" : " (\"" + entry.config.label + "\")"}`);
14193
14527
  lines.push(`interval: ${entry.config.intervalMs}ms | stalled: ${String(entry.config.stalledMs ?? 6e4)}ms | maxStuck: ${String(entry.config.maxStuckCycles ?? 3)}`);
14194
14528
  lines.push(`cycles: ${entry.cycles} | stuck: ${entry.stuckCount} | lastAction: ${entry.lastAction}`);
14529
+ const cotCount = (entry.config.coTRules ?? []).length;
14530
+ if (cotCount > 0) lines.push(`coRules: ${cotCount} rule(s) (minHits ${String(entry.config.cotMinHits ?? 1)})`);
14195
14531
  if (entry.lastNote !== "") lines.push(`note: ${entry.lastNote}`);
14196
14532
  if (entry.done) lines.push("done: yes");
14197
14533
  return lines;
@@ -14199,7 +14535,7 @@ function renderMonitorState(entry) {
14199
14535
  function registerMonitor(env) {
14200
14536
  env.ctx.tools.register(defineTool({
14201
14537
  name: "session_bridge_monitor_start",
14202
- description: "Start a background watchdog on a main session: poll its progress at an interval, and automatically schedule — steer the session when it stalls (or, with useLlm, when it drifts off-track), cancel it when it stays stuck past maxStuckCycles, and stop when a done keyword appears while idle. This is the \"monitor worker\" that watches a main task thread and corrects/stops it. Uses session_bridge_status-style facts; pass sessionId of a live session. Returns the watchdog state.",
14538
+ description: "Start a background watchdog on a main session: poll its progress at an interval, and automatically schedule — steer the session when it stalls (or, with useLlm, when it drifts off-track), cancel it when it stays stuck past maxStuckCycles, and stop when a done keyword appears while idle. It can also enforce chain-of-thought rules via coRules: e.g. coRules=[{match:\"not-contains\",field:\"reasoning\",value:\"I'm\",action:\"cancel\"}] stops the session the moment its live reasoning stops containing I'm (evaluated on each poll while running). This is the \"monitor worker\" that watches a main task thread and corrects/stops it. Uses session_bridge_status-style facts; pass sessionId of a live session. Returns the watchdog state.",
14203
14539
  parameters: {
14204
14540
  sessionId: {
14205
14541
  type: "string",
@@ -14238,6 +14574,51 @@ function registerMonitor(env) {
14238
14574
  label: {
14239
14575
  type: "string",
14240
14576
  description: "Optional human label for logs/display."
14577
+ },
14578
+ coRules: {
14579
+ type: "array",
14580
+ items: {
14581
+ type: "object",
14582
+ additionalProperties: false,
14583
+ properties: {
14584
+ match: {
14585
+ type: "string",
14586
+ required: true,
14587
+ enum: ["contains", "not-contains"],
14588
+ description: "contains = must include value in the chosen field; not-contains = must NOT include it."
14589
+ },
14590
+ field: {
14591
+ type: "string",
14592
+ required: true,
14593
+ enum: [
14594
+ "reasoning",
14595
+ "text",
14596
+ "both"
14597
+ ],
14598
+ description: "Match on reasoning (chain-of-thought), text (reply), or both (either)."
14599
+ },
14600
+ value: {
14601
+ type: "string",
14602
+ required: true,
14603
+ description: "The substring to match (non-empty)."
14604
+ },
14605
+ action: {
14606
+ type: "string",
14607
+ required: true,
14608
+ enum: ["steer", "cancel"],
14609
+ description: "steer injects a guiding message; cancel terminates the session."
14610
+ },
14611
+ message: {
14612
+ type: "string",
14613
+ description: "Custom steer text (default is a guidance prompt)."
14614
+ }
14615
+ }
14616
+ },
14617
+ description: "Chain-of-thought rules: when a rule stays matched for cotMinHits consecutive polls (default 1), trigger its action. Example: [{match:\"not-contains\",field:\"reasoning\",value:\"I'm\",action:\"cancel\"}] stops the session the moment its live reasoning no longer contains I'm."
14618
+ },
14619
+ cotMinHits: {
14620
+ type: "number",
14621
+ description: "How many consecutive matched polls before a CoT rule fires (default 1 = immediately)."
14241
14622
  }
14242
14623
  },
14243
14624
  output: {
@@ -14268,7 +14649,9 @@ function registerMonitor(env) {
14268
14649
  ...args.useLlm === true ? { useLlm: true } : {},
14269
14650
  ...typeof args.onStallSteer === "string" && args.onStallSteer.trim() !== "" ? { onStallSteer: args.onStallSteer.trim() } : {},
14270
14651
  ...typeof args.onOffTrackSteer === "string" && args.onOffTrackSteer.trim() !== "" ? { onOffTrackSteer: args.onOffTrackSteer.trim() } : {},
14271
- ...typeof args.label === "string" && args.label.trim() !== "" ? { label: args.label.trim() } : {}
14652
+ ...typeof args.label === "string" && args.label.trim() !== "" ? { label: args.label.trim() } : {},
14653
+ ...Array.isArray(args.coRules) && args.coRules.length > 0 ? { coTRules: parseCoRules(args.coRules) } : {},
14654
+ ...typeof args.cotMinHits === "number" && Number.isInteger(args.cotMinHits) && args.cotMinHits >= 1 ? { cotMinHits: Math.floor(args.cotMinHits) } : {}
14272
14655
  };
14273
14656
  const entry = env.monitor.start(config);
14274
14657
  env.registry.touch(args.sessionId);
@@ -14340,7 +14723,19 @@ function registerMonitor(env) {
14340
14723
  }
14341
14724
  }));
14342
14725
  }
14343
- /** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复。 */
14726
+ /** reasoning 选项裁剪快照里的思维链字段(避免入 token 时有 diff 语义差)。 */
14727
+ function pruneReasoning(snapshot, mode) {
14728
+ if (mode === "tail") return snapshot;
14729
+ const rest = { ...snapshot };
14730
+ delete rest.reasoningTail;
14731
+ if (mode === "none") {
14732
+ delete rest.lastReasoning;
14733
+ delete rest.liveReasoning;
14734
+ } else if (mode === "last") delete rest.liveReasoning;
14735
+ else delete rest.lastReasoning;
14736
+ return rest;
14737
+ }
14738
+ /** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复(+ 思维链预览)。 */
14344
14739
  function renderStatus(snapshot, stalledMsThreshold) {
14345
14740
  const lines = [];
14346
14741
  const runLabel = snapshot.running === "running" ? "running" : "idle";
@@ -14353,12 +14748,16 @@ function renderStatus(snapshot, stalledMsThreshold) {
14353
14748
  }
14354
14749
  if (snapshot.pendingWork) lines.push(`pendingWork: ${snapshot.nextTurnCount} turn + ${snapshot.nextStepCount} step`);
14355
14750
  if (snapshot.lastAssistantText !== void 0) lines.push(`lastReply: ${snapshot.lastAssistantText}`);
14751
+ if (snapshot.reasoningTail !== void 0 && snapshot.reasoningTail !== "") {
14752
+ const preview = snapshot.reasoningTail.length > 160 ? snapshot.reasoningTail.slice(0, 160) + "…" : snapshot.reasoningTail;
14753
+ lines.push(`reasoning: ${preview}`);
14754
+ }
14356
14755
  return lines;
14357
14756
  }
14358
14757
  function registerStatus(env) {
14359
14758
  env.ctx.tools.register(defineTool({
14360
14759
  name: "session_bridge_status",
14361
- description: "Inspect a session's live progress for monitoring/scheduling. Returns running/idle, whether a turn is open, last turn number, time since the last event (for stall detection), pending queued work, and the latest text reply. When stalledMsThreshold is given, marks the session as stalled when the time since the last event exceeds it. Pass sessionId of a live session (use session_bridge_find to locate; session_bridge_resume to bring an offline one online). Use this as the \"observe\" step of a monitor→decide→steer/cancel loop.",
14760
+ description: "Inspect a session's live progress for monitoring/scheduling. Returns running/idle, whether a turn is open, last turn number, time since the last event (for stall detection), pending queued work, and the latest text reply. It also surfaces the session's chain-of-thought: lastReasoning is the most recent finalized reasoning block, liveReasoning is the in-flight reasoning streamed for the current handled turn (reasoning-delta), and reasoningTail is a compact merged preview. reasoning=none drops all three to keep tokens small. When stalledMsThreshold is given, marks the session as stalled when the time since the last event exceeds it. Pass sessionId of a live session (use session_bridge_find to locate; session_bridge_resume to bring an offline one online). Use this as the \"observe\" step of a monitor→decide→steer/cancel loop.",
14362
14761
  parameters: {
14363
14762
  sessionId: {
14364
14763
  type: "string",
@@ -14372,6 +14771,16 @@ function registerStatus(env) {
14372
14771
  recent: {
14373
14772
  type: "number",
14374
14773
  description: "Number of recent messages to include in the snapshot (default 8, max 20)."
14774
+ },
14775
+ reasoning: {
14776
+ type: "string",
14777
+ enum: [
14778
+ "none",
14779
+ "last",
14780
+ "live",
14781
+ "tail"
14782
+ ],
14783
+ description: "Which chain-of-thought fields to include: tail (default) returns lastReasoning/liveReasoning/reasoningTail; last only the finalized reasoning; live only the in-flight reasoning; none drops all reasoning fields."
14375
14784
  }
14376
14785
  },
14377
14786
  output: {
@@ -14397,7 +14806,8 @@ function registerStatus(env) {
14397
14806
  const agent = liveAgent(env, args.sessionId);
14398
14807
  if (agent === void 0) throw new Error("session " + JSON.stringify(args.sessionId) + " is not live — call session_bridge_resume first (status requires a live session)");
14399
14808
  const threshold = typeof args.stalledMsThreshold === "number" && Number.isFinite(args.stalledMsThreshold) && args.stalledMsThreshold >= 0 ? args.stalledMsThreshold : 6e4;
14400
- const snapshot = statusSnapshot(env.ctx, agent);
14809
+ const reasoningMode = args.reasoning === "none" || args.reasoning === "last" || args.reasoning === "live" ? args.reasoning : "tail";
14810
+ const snapshot = pruneReasoning(statusSnapshot(env.ctx, agent), reasoningMode);
14401
14811
  const shown = typeof args.recent === "number" && Number.isInteger(args.recent) && args.recent >= 0 ? Math.min(args.recent, 20) : 8;
14402
14812
  const envCwd = agent.session.header.cwd;
14403
14813
  env.registry.touch(args.sessionId, {