dsh-session-bridge 0.2.0 → 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
@@ -9098,6 +9098,29 @@ Service.init;
9098
9098
  * dsh-session-bridge core: 会话创建 / 发消息 / 等待回复 / 读取 / 查找。
9099
9099
  * 所有逻辑跑在插件宿主上下文(rootCtx),操作的是 DSH 真实的会话与 agent 注册表。
9100
9100
  */
9101
+ /**
9102
+ * 兼容读取不同 dsh-session 版本上的会话事件:
9103
+ * - 旧版 `Session` 暴露 `get events(): readonly SessionEvent[]`;
9104
+ * - 新版(如宿主实际运行的 0.1.2-rc.x)将 `get events` 改为
9105
+ * `snapshotEvents(fromSeq?, toSeqExclusive?)` 方法——`.events` 直接读取会
9106
+ * 得到 `undefined`,对 for...of 迭代即抛 `events is not iterable`。
9107
+ * 两者都读不到(或 session 不存在)时回退为空数组,绝不抛迭代错误。
9108
+ */
9109
+ function sessionEvents(session) {
9110
+ const s = session;
9111
+ if (s === null || s === void 0) return [];
9112
+ if (Array.isArray(s.events)) return s.events;
9113
+ if (typeof s.snapshotEvents === "function") try {
9114
+ return s.snapshotEvents();
9115
+ } catch {
9116
+ return [];
9117
+ }
9118
+ return [];
9119
+ }
9120
+ /** 规约任意"事件列表"值为数组(非数组 → []),避免 events is not iterable。 */
9121
+ function asEventList(events) {
9122
+ return Array.isArray(events) ? events : [];
9123
+ }
9101
9124
  /** agent.followup / steer 接受的用户消息值。 */
9102
9125
  function userMessage(text) {
9103
9126
  return {
@@ -9121,6 +9144,18 @@ function blockText(content) {
9121
9144
  }
9122
9145
  return parts.length === 0 ? void 0 : parts.join("\n");
9123
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
+ }
9124
9159
  function countImages(content) {
9125
9160
  if (!Array.isArray(content)) return 0;
9126
9161
  let n = 0;
@@ -9141,8 +9176,9 @@ function toolNamesOf(toolCalls) {
9141
9176
  }
9142
9177
  /** 折叠会话事件日志为可读消息行(跳过非 user 来源与空消息)。 */
9143
9178
  function foldMessages(events) {
9179
+ const list = asEventList(events);
9144
9180
  const rows = [];
9145
- for (const event of events) if (event.type === "user/message") {
9181
+ for (const event of list) if (event.type === "user/message") {
9146
9182
  const source = event.data.source;
9147
9183
  if (source !== void 0 && source.kind !== "user") continue;
9148
9184
  const text = blockText(event.data.content);
@@ -9157,7 +9193,7 @@ function foldMessages(events) {
9157
9193
  } else if (event.type === "assistant/message") {
9158
9194
  const message = event.data.message;
9159
9195
  const text = message === null ? void 0 : blockText(message.content);
9160
- const reasoning = message === null ? void 0 : blockText(message.reasoning);
9196
+ const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
9161
9197
  const images = message === null ? 0 : countImages(message.content);
9162
9198
  const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
9163
9199
  if (text === void 0 && reasoning === void 0 && toolCalls.length === 0) continue;
@@ -9174,13 +9210,119 @@ function foldMessages(events) {
9174
9210
  }
9175
9211
  return rows;
9176
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
+ }
9177
9318
  /**
9178
9319
  * 会话标题:优先 session/title 事件(last-wins,与 UI 一致);
9179
9320
  * 无标题事件时回落为第一条用户消息(截断 80 字符)。
9180
9321
  */
9181
9322
  function titleOf(events) {
9323
+ const list = asEventList(events);
9182
9324
  let title;
9183
- for (const event of events) {
9325
+ for (const event of list) {
9184
9326
  const raw = event;
9185
9327
  if (raw.type === "session/title") {
9186
9328
  const t = raw.data.title;
@@ -9188,7 +9330,7 @@ function titleOf(events) {
9188
9330
  }
9189
9331
  }
9190
9332
  if (title !== void 0) return title;
9191
- for (const event of events) if (event.type === "user/message") {
9333
+ for (const event of list) if (event.type === "user/message") {
9192
9334
  const source = event.data.source;
9193
9335
  if (source !== void 0 && source.kind !== "user") continue;
9194
9336
  const text = blockText(event.data.content);
@@ -9200,8 +9342,9 @@ function titleOf(events) {
9200
9342
  }
9201
9343
  /** 日志最大事件序号。 */
9202
9344
  function maxSeq(events) {
9345
+ const list = asEventList(events);
9203
9346
  let m = -1;
9204
- for (const event of events) if (event.seq > m) m = event.seq;
9347
+ for (const event of list) if (event.seq > m) m = event.seq;
9205
9348
  return m;
9206
9349
  }
9207
9350
  function sleep(ms) {
@@ -9223,33 +9366,51 @@ async function waitForReply(opts) {
9223
9366
  const started = Date.now();
9224
9367
  const deadline = started + opts.timeoutMs;
9225
9368
  const requireTurnEnd = opts.requireTurnEnd ?? false;
9369
+ const waitForSegment = opts.waitForSegment ?? false;
9226
9370
  let latest = null;
9227
9371
  let textReply = null;
9372
+ let segment = null;
9228
9373
  let turnEnded = false;
9229
9374
  for (;;) {
9230
9375
  if (opts.signal !== void 0 && opts.signal.aborted) break;
9231
- const events = opts.session.events;
9376
+ const events = sessionEvents(opts.session);
9232
9377
  for (const row of foldMessages(events)) {
9233
9378
  if (row.seq <= opts.baselineSeq || row.role !== "assistant") continue;
9234
9379
  if (latest === null || row.seq > latest.seq) latest = row;
9235
9380
  if (row.text !== void 0 && (textReply === null || row.seq > textReply.seq)) textReply = row;
9236
9381
  }
9382
+ if (waitForSegment) {
9383
+ const seg = latestSegmentSince(events, opts.baselineSeq);
9384
+ if (seg !== null && (segment === null || seg.seq > segment.seq)) segment = seg;
9385
+ }
9237
9386
  if (requireTurnEnd && latest !== null) {
9238
9387
  for (const event of events) if (event.seq > latest.seq && event.type === "turn/end") {
9239
9388
  turnEnded = true;
9240
9389
  break;
9241
9390
  }
9242
9391
  }
9243
- 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;
9244
9397
  if (Date.now() >= deadline) break;
9245
9398
  await sleep(100);
9246
9399
  }
9247
- 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;
9248
9409
  return {
9249
9410
  message,
9250
9411
  seq: message === null ? opts.baselineSeq : message.seq,
9251
9412
  turnEnded,
9252
- timedOut: requireTurnEnd ? latest !== null && !turnEnded : textReply === null,
9413
+ timedOut: requireTurnEnd ? latest !== null && !turnEnded : waitForSegment ? segment === null : textReply === null,
9253
9414
  aborted: opts.signal !== void 0 && opts.signal.aborted,
9254
9415
  waitedMs: Date.now() - started
9255
9416
  };
@@ -9309,7 +9470,7 @@ async function attachSessionToWorkspace(ctx, sessionId, cwd) {
9309
9470
  * 纯读,无副作用。用于 `session_bridge_status`。
9310
9471
  */
9311
9472
  function statusSnapshot(ctx, agent) {
9312
- const events = agent.session.events;
9473
+ const events = sessionEvents(agent.session);
9313
9474
  const rows = foldMessages(events);
9314
9475
  let lastActivityAt = null;
9315
9476
  let lastTurn = 0;
@@ -9325,13 +9486,18 @@ function statusSnapshot(ctx, agent) {
9325
9486
  }
9326
9487
  const recent = rows.slice(-8);
9327
9488
  let lastAssistantText;
9489
+ let lastReasoning;
9328
9490
  for (let i = rows.length - 1; i >= 0; i -= 1) {
9329
9491
  const row = rows[i];
9330
- if (row !== void 0 && row.role === "assistant" && row.text !== void 0) {
9331
- lastAssistantText = row.text;
9332
- 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;
9333
9496
  }
9334
9497
  }
9498
+ const live = liveReasoningSnapshot(events);
9499
+ const liveReasoning = live !== null && live.reasoning !== "" ? live.reasoning : void 0;
9500
+ const reasoningTail = reasoningTailOf(liveReasoning, lastReasoning);
9335
9501
  const inbox = agent.inbox;
9336
9502
  const cwd = agent.session.header.cwd;
9337
9503
  return {
@@ -9349,6 +9515,9 @@ function statusSnapshot(ctx, agent) {
9349
9515
  nextTurnCount: inbox?.nextTurn?.length ?? 0,
9350
9516
  nextStepCount: inbox?.nextStep?.length ?? 0,
9351
9517
  ...lastAssistantText === void 0 ? {} : { lastAssistantText },
9518
+ ...lastReasoning === void 0 ? {} : { lastReasoning },
9519
+ ...liveReasoning === void 0 ? {} : { liveReasoning },
9520
+ ...reasoningTail === void 0 ? {} : { reasoningTail },
9352
9521
  messageCount: rows.length,
9353
9522
  recent
9354
9523
  };
@@ -9403,6 +9572,16 @@ var SessionMonitor = class {
9403
9572
  start(config) {
9404
9573
  const existing = this.entries.get(config.sessionId);
9405
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
+ }, {}) };
9406
9585
  const entry = existing !== void 0 ? {
9407
9586
  ...existing,
9408
9587
  config,
@@ -9416,8 +9595,10 @@ var SessionMonitor = class {
9416
9595
  lastActionAt: null,
9417
9596
  lastNote: "监控已启动",
9418
9597
  done: false,
9419
- cycles: 0
9598
+ cycles: 0,
9599
+ cot: cotInit
9420
9600
  };
9601
+ if (entry.cot === void 0) entry.cot = cotInit;
9421
9602
  this.entries.set(config.sessionId, entry);
9422
9603
  this.ensureTimer();
9423
9604
  this.log(entry, "monitor start session=" + config.sessionId + " interval=" + config.intervalMs + " stalled=" + String(config.stalledMs ?? 6e4) + " maxStuck=" + String(config.maxStuckCycles ?? 3));
@@ -9543,6 +9724,7 @@ var SessionMonitor = class {
9543
9724
  this.log(entry, "STALL(#" + entry.stuckCount + ") → steer: " + steer);
9544
9725
  return;
9545
9726
  }
9727
+ if (this.evaluateCoTRules(entry, snapshot, sessionId)) return;
9546
9728
  if (entry.stuckCount !== 0) entry.stuckCount = 0;
9547
9729
  entry.lastAction = "steady";
9548
9730
  entry.lastActionAt = Date.now();
@@ -9567,6 +9749,76 @@ var SessionMonitor = class {
9567
9749
  if (!text) return false;
9568
9750
  return keywords.some((kw) => kw !== "" && text.includes(kw));
9569
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
+ }
9570
9822
  hasLlm() {
9571
9823
  return this.ctx.llm !== void 0;
9572
9824
  }
@@ -9622,7 +9874,7 @@ var SessionMonitor = class {
9622
9874
  }
9623
9875
  };
9624
9876
  //#endregion
9625
- //#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
9626
9878
  /**
9627
9879
  * Shared insertion-ordered storage and effect ownership for scope-aware registries.
9628
9880
  *
@@ -9901,7 +10153,7 @@ function scopeTarget(base, key) {
9901
10153
  return carrier;
9902
10154
  }
9903
10155
  //#endregion
9904
- //#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
9905
10157
  /**
9906
10158
  * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
9907
10159
  * types, subagents, and workflows. The subset accepts any JSON root, an
@@ -13403,7 +13655,7 @@ function toolAbortedBeforeDispatchResult(prior) {
13403
13655
  };
13404
13656
  }
13405
13657
  //#endregion
13406
- //#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
13407
13659
  /**
13408
13660
  * Agent-scoped model selection shared by runtime entry points.
13409
13661
  * @module @deepseek-ai/dsh-agent/model-selection
@@ -13633,7 +13885,7 @@ function registerCreate(env) {
13633
13885
  });
13634
13886
  let reply;
13635
13887
  if (typeof args.prompt === "string" && args.prompt.trim() !== "") {
13636
- const baseline = maxSeq(agent.session.events);
13888
+ const baseline = maxSeq(sessionEvents(agent.session));
13637
13889
  agent.followup(userMessage(args.prompt.trim()));
13638
13890
  env.registry.touch(sessionId);
13639
13891
  if (args.waitForReply === true) reply = await maybeWait(env, agent.session, baseline, args, exec.signal);
@@ -13701,7 +13953,7 @@ function registerSend(env) {
13701
13953
  if (typeof args.message !== "string" || args.message.trim() === "") throw new Error("invalid message: expected a non-empty string");
13702
13954
  const agent = liveAgent(env, args.sessionId);
13703
13955
  if (agent === void 0) throw new Error("session " + JSON.stringify(args.sessionId) + " is not live — call session_bridge_resume first to bring it online (or session_bridge_find to locate it)");
13704
- const baseline = maxSeq(agent.session.events);
13956
+ const baseline = maxSeq(sessionEvents(agent.session));
13705
13957
  if (args.mode === "steer") agent.steer(userMessage(args.message.trim()));
13706
13958
  else agent.followup(userMessage(args.message.trim()));
13707
13959
  const headerCwd = agent.session.header.cwd;
@@ -13764,7 +14016,7 @@ function registerResume(env) {
13764
14016
  sessionId: args.sessionId,
13765
14017
  alreadyLive: true,
13766
14018
  ...existing.session.header.cwd === void 0 ? {} : { cwd: existing.session.header.cwd },
13767
- title: titleOf(existing.session.events) ?? null,
14019
+ title: titleOf(sessionEvents(existing.session)) ?? null,
13768
14020
  running: existing.status === "running"
13769
14021
  });
13770
14022
  }
@@ -13797,7 +14049,7 @@ function registerResume(env) {
13797
14049
  sessionId: args.sessionId,
13798
14050
  resumed: true,
13799
14051
  ...headerCwd === void 0 ? {} : { cwd: headerCwd },
13800
- title: titleOf(handle.agent.session.events) ?? null
14052
+ title: titleOf(sessionEvents(handle.agent.session)) ?? null
13801
14053
  });
13802
14054
  }
13803
14055
  }));
@@ -13805,7 +14057,7 @@ function registerResume(env) {
13805
14057
  function registerWait(env) {
13806
14058
  env.ctx.tools.register(defineTool({
13807
14059
  name: "session_bridge_wait",
13808
- 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).",
13809
14061
  parameters: {
13810
14062
  sessionId: {
13811
14063
  type: "string",
@@ -13823,6 +14075,11 @@ function registerWait(env) {
13823
14075
  requireTurnEnd: {
13824
14076
  type: "boolean",
13825
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."
13826
14083
  }
13827
14084
  },
13828
14085
  output: {
@@ -13851,11 +14108,15 @@ function registerWait(env) {
13851
14108
  if (typeof args.sessionId !== "string" || args.sessionId.trim() === "") throw new Error("invalid sessionId: expected a non-empty string");
13852
14109
  const agent = liveAgent(env, args.sessionId);
13853
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";
13854
14112
  let baseline;
13855
14113
  if (typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) baseline = args.sinceSeq;
13856
- 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 {
13857
14118
  let lastText = -1;
13858
- for (const row of foldMessages(agent.session.events)) if (row.text !== void 0) lastText = row.seq;
14119
+ for (const row of foldMessages(sessionEvents(agent.session))) if (row.text !== void 0) lastText = row.seq;
13859
14120
  baseline = lastText;
13860
14121
  }
13861
14122
  const result = await waitForReply({
@@ -13863,7 +14124,8 @@ function registerWait(env) {
13863
14124
  baselineSeq: baseline,
13864
14125
  timeoutMs: clampTimeout(args.timeoutMs),
13865
14126
  signal: exec.signal,
13866
- requireTurnEnd: args.requireTurnEnd === true
14127
+ requireTurnEnd: args.requireTurnEnd === true,
14128
+ ...waitSegment ? { waitForSegment: true } : {}
13867
14129
  });
13868
14130
  env.registry.touch(args.sessionId);
13869
14131
  return asJson({
@@ -13874,6 +14136,83 @@ function registerWait(env) {
13874
14136
  }
13875
14137
  }));
13876
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
+ }
13877
14216
  function registerRead(env) {
13878
14217
  env.ctx.tools.register(defineTool({
13879
14218
  name: "session_bridge_read",
@@ -13934,7 +14273,7 @@ function registerRead(env) {
13934
14273
  let live;
13935
14274
  let cwd;
13936
14275
  if (agent !== void 0) {
13937
- events = agent.session.events;
14276
+ events = sessionEvents(agent.session);
13938
14277
  live = true;
13939
14278
  cwd = agent.session.header.cwd;
13940
14279
  } else {
@@ -14051,7 +14390,7 @@ function registerFind(env) {
14051
14390
  const wsId = wsById.get(agent.id) ?? workspaceByPath(env.ctx, headerCwd);
14052
14391
  items.push({
14053
14392
  sessionId: agent.id,
14054
- title: titleOf(agent.session.events),
14393
+ title: titleOf(sessionEvents(agent.session)),
14055
14394
  ...headerCwd === void 0 ? {} : { cwd: headerCwd },
14056
14395
  ...wsId === void 0 ? {} : { workspaceId: wsId },
14057
14396
  live: true,
@@ -14154,6 +14493,7 @@ function registerBridgeTools(env) {
14154
14493
  registerSend(env);
14155
14494
  registerResume(env);
14156
14495
  registerWait(env);
14496
+ registerSegments(env);
14157
14497
  registerRead(env);
14158
14498
  registerFind(env);
14159
14499
  registerStatus(env);
@@ -14161,11 +14501,33 @@ function registerBridgeTools(env) {
14161
14501
  registerMonitor(env);
14162
14502
  registerArchive(env);
14163
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
+ }
14164
14524
  function renderMonitorState(entry) {
14165
14525
  const lines = [];
14166
14526
  lines.push(`monitoring ${entry.config.sessionId}${entry.config.label === void 0 ? "" : " (\"" + entry.config.label + "\")"}`);
14167
14527
  lines.push(`interval: ${entry.config.intervalMs}ms | stalled: ${String(entry.config.stalledMs ?? 6e4)}ms | maxStuck: ${String(entry.config.maxStuckCycles ?? 3)}`);
14168
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)})`);
14169
14531
  if (entry.lastNote !== "") lines.push(`note: ${entry.lastNote}`);
14170
14532
  if (entry.done) lines.push("done: yes");
14171
14533
  return lines;
@@ -14173,7 +14535,7 @@ function renderMonitorState(entry) {
14173
14535
  function registerMonitor(env) {
14174
14536
  env.ctx.tools.register(defineTool({
14175
14537
  name: "session_bridge_monitor_start",
14176
- 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.",
14177
14539
  parameters: {
14178
14540
  sessionId: {
14179
14541
  type: "string",
@@ -14212,6 +14574,51 @@ function registerMonitor(env) {
14212
14574
  label: {
14213
14575
  type: "string",
14214
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)."
14215
14622
  }
14216
14623
  },
14217
14624
  output: {
@@ -14242,7 +14649,9 @@ function registerMonitor(env) {
14242
14649
  ...args.useLlm === true ? { useLlm: true } : {},
14243
14650
  ...typeof args.onStallSteer === "string" && args.onStallSteer.trim() !== "" ? { onStallSteer: args.onStallSteer.trim() } : {},
14244
14651
  ...typeof args.onOffTrackSteer === "string" && args.onOffTrackSteer.trim() !== "" ? { onOffTrackSteer: args.onOffTrackSteer.trim() } : {},
14245
- ...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) } : {}
14246
14655
  };
14247
14656
  const entry = env.monitor.start(config);
14248
14657
  env.registry.touch(args.sessionId);
@@ -14314,7 +14723,19 @@ function registerMonitor(env) {
14314
14723
  }
14315
14724
  }));
14316
14725
  }
14317
- /** 渲染监控快照为一行摘要:运行态 + 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 + 卡住/待处理 + 最新回复(+ 思维链预览)。 */
14318
14739
  function renderStatus(snapshot, stalledMsThreshold) {
14319
14740
  const lines = [];
14320
14741
  const runLabel = snapshot.running === "running" ? "running" : "idle";
@@ -14327,12 +14748,16 @@ function renderStatus(snapshot, stalledMsThreshold) {
14327
14748
  }
14328
14749
  if (snapshot.pendingWork) lines.push(`pendingWork: ${snapshot.nextTurnCount} turn + ${snapshot.nextStepCount} step`);
14329
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
+ }
14330
14755
  return lines;
14331
14756
  }
14332
14757
  function registerStatus(env) {
14333
14758
  env.ctx.tools.register(defineTool({
14334
14759
  name: "session_bridge_status",
14335
- 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.",
14336
14761
  parameters: {
14337
14762
  sessionId: {
14338
14763
  type: "string",
@@ -14346,6 +14771,16 @@ function registerStatus(env) {
14346
14771
  recent: {
14347
14772
  type: "number",
14348
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."
14349
14784
  }
14350
14785
  },
14351
14786
  output: {
@@ -14371,7 +14806,8 @@ function registerStatus(env) {
14371
14806
  const agent = liveAgent(env, args.sessionId);
14372
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)");
14373
14808
  const threshold = typeof args.stalledMsThreshold === "number" && Number.isFinite(args.stalledMsThreshold) && args.stalledMsThreshold >= 0 ? args.stalledMsThreshold : 6e4;
14374
- 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);
14375
14811
  const shown = typeof args.recent === "number" && Number.isInteger(args.recent) && args.recent >= 0 ? Math.min(args.recent, 20) : 8;
14376
14812
  const envCwd = agent.session.header.cwd;
14377
14813
  env.registry.touch(args.sessionId, {