dsh-session-bridge 0.2.1 → 0.3.1

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.5-alpha.2_@deepseek-ai+cordis@4.0.2/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.5-alpha.2_@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.5-alpha.2_@deepseek-ai+cordis@4.0.2/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.5-alpha.2_@deepseek-ai+cordis@4.0.2/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,16 +2570,16 @@ 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.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-brand/lib/index.js
2574
2574
  /**
2575
- * Duplicate-install-safe nominal string helpers.
2575
+ * Duplicate-install-safe nominal primitive helpers.
2576
2576
  *
2577
- * A brand makes structurally-identical strings non-interchangeable at the type
2578
- * level: a `SessionId` cannot be passed where a `ToolCallId` is expected, even
2579
- * though both are plain strings at runtime. Comparison, logging, and
2580
- * serialization all behave as ordinary strings.
2577
+ * A brand makes structurally identical strings or numbers non-interchangeable
2578
+ * at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
2579
+ * expected, and an event sequence cannot be passed as a log offset. Comparison,
2580
+ * logging, and serialization retain the underlying primitive behavior.
2581
2581
  *
2582
- * This package owns no concrete id and keeps no runtime identity or mutable
2582
+ * This package owns no concrete domain value and keeps no runtime identity or mutable
2583
2583
  * state, so independently installed copies produce interchangeable values.
2584
2584
  *
2585
2585
  * @module @deepseek-ai/dsh-brand
@@ -3187,11 +3187,19 @@ 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.5-alpha.2_@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.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3195
+ /**
3196
+ * Bound one `notice` summary to {@link CONTEXT_SUMMARY_MAX_CHARS}.
3197
+ * @param summary - the producer's one-line account, of any length.
3198
+ * @returns the account, ellipsized when it exceeds the bound.
3199
+ */
3200
+ function boundContextSummary(summary) {
3201
+ return summary.length <= 120 ? summary : `${summary.slice(0, 119)}…`;
3202
+ }
3195
3203
  /**
3196
3204
  * Detach and deep-freeze a message whose identity already exists.
3197
3205
  * @param message - complete message, including its stable identity.
@@ -3457,6 +3465,9 @@ function errorMessage$1(error) {
3457
3465
  function harnessErrorCode(error) {
3458
3466
  return error instanceof HarnessError ? error.code : "UNKNOWN";
3459
3467
  }
3468
+ function quoted(value) {
3469
+ return JSON.stringify(value);
3470
+ }
3460
3471
  /**
3461
3472
  * Stable text shown to a model that cannot accept one durable image reference.
3462
3473
  * @param ref - durable normalized attachment omitted from the request.
@@ -3476,6 +3487,76 @@ function textOnlyImageText(ref) {
3476
3487
  function contentHasImage(content) {
3477
3488
  return content.some((block) => block.type === "image" || block.type === "tool-result" && contentHasImage(block.content));
3478
3489
  }
3490
+ /**
3491
+ * True when typed model content contains a file block, walking nested
3492
+ * tool-result content on the same recursion every file policy shares.
3493
+ * Reads current content on every call without retaining scan results.
3494
+ * @param content - typed model content blocks.
3495
+ * @returns whether any nested block is a file.
3496
+ */
3497
+ function contentHasFile(content) {
3498
+ for (const block of content) if (block.type === "file" || block.type === "tool-result" && contentHasFile(block.content)) return true;
3499
+ return false;
3500
+ }
3501
+ /**
3502
+ * Stable model-facing handle for one durable file reference: the address of
3503
+ * the verbatim stored copy and the instruction to read it on demand. This is
3504
+ * the only representation a provider ever receives for a file.
3505
+ * @param ref - durable verbatim file reference.
3506
+ * @param readonlyPath - execution-world path of the stored copy, when resolvable.
3507
+ * @returns deterministic handle text naming the file, its size, and its address.
3508
+ */
3509
+ function fileHandleText(ref, readonlyPath) {
3510
+ const digest = String(ref.attachmentId).slice(7, 15);
3511
+ const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
3512
+ if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
3513
+ return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
3514
+ }
3515
+ /** Replace every file occurrence, including nested tool results, with handle text. */
3516
+ function replaceFilesWithHandles(blocks, resolvePath) {
3517
+ let next;
3518
+ for (const [index, block] of blocks.entries()) {
3519
+ if (block.type === "file") {
3520
+ next ??= blocks.slice(0, index);
3521
+ next.push({
3522
+ type: "text",
3523
+ text: fileHandleText(block.attachment, resolvePath(block.attachment))
3524
+ });
3525
+ continue;
3526
+ }
3527
+ if (block.type === "tool-result") {
3528
+ const content = replaceFilesWithHandles(block.content, resolvePath);
3529
+ if (content !== block.content) {
3530
+ next ??= blocks.slice(0, index);
3531
+ next.push({
3532
+ ...block,
3533
+ content
3534
+ });
3535
+ continue;
3536
+ }
3537
+ }
3538
+ next?.push(block);
3539
+ }
3540
+ return next ?? blocks;
3541
+ }
3542
+ /**
3543
+ * Project durable file history into deterministic handle text for every model
3544
+ * route. Unlike images, no provider receives file blocks natively, so this
3545
+ * projection is unconditional in request assembly.
3546
+ * @param messages - complete request history.
3547
+ * @param resolvePath - resolve one reference's current execution-world read path.
3548
+ * @returns the original list without files, otherwise shallow message copies with handle text.
3549
+ */
3550
+ function projectFilesToText(messages, resolvePath) {
3551
+ if (!messages.some((message) => contentHasFile(message.content))) return messages;
3552
+ return messages.map((message) => {
3553
+ const content = replaceFilesWithHandles(message.content, resolvePath);
3554
+ return content === message.content ? message : {
3555
+ ...message,
3556
+ content
3557
+ };
3558
+ });
3559
+ }
3479
3560
  /** Replace every image occurrence, including nested tool results, for a text-only model. */
3480
3561
  function replaceImagesForTextModel(blocks) {
3481
3562
  let next;
@@ -3916,6 +3997,15 @@ var LlmError = class extends HarnessError {
3916
3997
  imageRequestPricing(provider, model) {
3917
3998
  return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
3918
3999
  }
4000
+ /**
4001
+ * Resolve the exact text one durable file occurrence contributes to every
4002
+ * provider request in the current execution environment.
4003
+ * @param ref - durable verbatim file reference from model history.
4004
+ * @returns the same deterministic handle text used at adapter dispatch.
4005
+ */
4006
+ fileRequestText(ref) {
4007
+ return fileHandleText(ref, this.fileReadPath(ref));
4008
+ }
3919
4009
  /** Detach typed adapter-owned modality metadata. */
3920
4010
  detachedModalities(modalities) {
3921
4011
  return modalities === void 0 ? void 0 : [...modalities];
@@ -3965,6 +4055,8 @@ var LlmError = class extends HarnessError {
3965
4055
  const context = resolved.context;
3966
4056
  if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
3967
4057
  const inputModalities = this.detachedModalities(resolved.inputModalities);
4058
+ const systemPromptUpdate = resolved.systemPromptUpdate;
4059
+ if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
3968
4060
  const defaultMaxTokens = resolved.defaultMaxTokens;
3969
4061
  if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
3970
4062
  const info = {
@@ -3974,7 +4066,8 @@ var LlmError = class extends HarnessError {
3974
4066
  ...resolved.description === void 0 ? {} : { description: resolved.description },
3975
4067
  ...inputModalities === void 0 ? {} : { inputModalities },
3976
4068
  ...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
3977
- ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens }
4069
+ ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
4070
+ ...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
3978
4071
  };
3979
4072
  const reasoning = resolved.reasoning;
3980
4073
  if (reasoning === void 0) return info;
@@ -4068,6 +4161,7 @@ var LlmError = class extends HarnessError {
4068
4161
  adapterDefaults,
4069
4162
  ...context === void 0 ? {} : { context },
4070
4163
  ...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
4164
+ ...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
4071
4165
  stream: (options) => {
4072
4166
  if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
4073
4167
  if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
@@ -4109,6 +4203,20 @@ var LlmError = class extends HarnessError {
4109
4203
  return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
4110
4204
  }
4111
4205
  /**
4206
+ * Resolve the current execution-world read path of one durable file
4207
+ * reference through the mounted attachment and filesystem providers.
4208
+ */
4209
+ fileReadPath(ref) {
4210
+ let hostPath;
4211
+ try {
4212
+ hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
4213
+ } catch {
4214
+ return;
4215
+ }
4216
+ if (hostPath === void 0) return void 0;
4217
+ return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
4218
+ }
4219
+ /**
4112
4220
  * Final adapter boundary. Adapter selection, dispatch, iterator construction,
4113
4221
  * and iteration failures become one terminal failure chunk. Middleware and
4114
4222
  * downstream consumer failures remain thrown plugin or consumer errors.
@@ -4139,13 +4247,16 @@ var LlmError = class extends HarnessError {
4139
4247
  ...options,
4140
4248
  ...resolvedConfig
4141
4249
  };
4142
- const projectedOptions = modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && resolvedOptions.messages.some((message) => contentHasImage(message.content)) ? Object.isFrozen(resolvedOptions) ? deepFreeze({
4250
+ let projectedMessages = resolvedOptions.messages;
4251
+ if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
4252
+ if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
4253
+ const projectedOptions = projectedMessages === resolvedOptions.messages ? resolvedOptions : Object.isFrozen(resolvedOptions) ? deepFreeze({
4143
4254
  ...resolvedOptions,
4144
- messages: projectImagesForTextModel(resolvedOptions.messages)
4255
+ messages: projectedMessages
4145
4256
  }) : {
4146
4257
  ...resolvedOptions,
4147
- messages: projectImagesForTextModel(resolvedOptions.messages)
4148
- } : resolvedOptions;
4258
+ messages: projectedMessages
4259
+ };
4149
4260
  iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
4150
4261
  } catch (error) {
4151
4262
  yield adapterFailureChunk(error, options.signal);
@@ -8966,7 +9077,7 @@ function superRefine(fn, params) {
8966
9077
  return /* @__PURE__ */ _superRefine(fn, params);
8967
9078
  }
8968
9079
  //#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
9080
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
8970
9081
  /**
8971
9082
  * Backend-facing vocabulary of the storage hub: a backend owns one medium
8972
9083
  * (a file-tree root, a database file) and exposes operation groups over it.
@@ -8977,7 +9088,7 @@ function superRefine(fn, params) {
8977
9088
  /** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
8978
9089
  const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/;
8979
9090
  //#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
9091
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+ds_8f094eabec231701d08ef59df76cca5d/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
8981
9092
  /**
8982
9093
  * Domain declaration vocabulary. A spec object is the single source of a
8983
9094
  * domain's identity, layout, and record schemas: the owning package defines
@@ -9121,6 +9232,27 @@ function sessionEvents(session) {
9121
9232
  function asEventList(events) {
9122
9233
  return Array.isArray(events) ? events : [];
9123
9234
  }
9235
+ /**
9236
+ * 读取一个持久化(离线)会话的事件日志与头部元数据。兼容两代 persistence API:
9237
+ * - 旧版:`sessionPersistence.inspect(id)` 直接返回 { events, meta };
9238
+ * - dsh 0.1.5 之后:`inspect` 已删除,改用 `open(id, 'read')` → `handle.read()`
9239
+ * (+ `handle.header`),读句柄不抢写所有权,读后必须 close()。
9240
+ * 会话不存在时两个 API 都抛错,调用方按原语义处理。
9241
+ */
9242
+ async function inspectPersistedSession(ctx, id) {
9243
+ const persistence = ctx.sessionPersistence;
9244
+ if (typeof persistence.inspect === "function") return await persistence.inspect(id);
9245
+ const handle = await persistence.open(id, "read");
9246
+ try {
9247
+ const { events } = await handle.read();
9248
+ return {
9249
+ events,
9250
+ meta: handle.header
9251
+ };
9252
+ } finally {
9253
+ await handle.close();
9254
+ }
9255
+ }
9124
9256
  /** agent.followup / steer 接受的用户消息值。 */
9125
9257
  function userMessage(text) {
9126
9258
  return {
@@ -9144,6 +9276,18 @@ function blockText(content) {
9144
9276
  }
9145
9277
  return parts.length === 0 ? void 0 : parts.join("\n");
9146
9278
  }
9279
+ /** 提取指定类型的 content block 文本(如 reasoning),用于从消息 content 里读出思维链。 */
9280
+ function blockTextByType(content, wantedType) {
9281
+ if (!Array.isArray(content)) return void 0;
9282
+ const parts = [];
9283
+ for (const raw of content) {
9284
+ const block = raw;
9285
+ if (block !== null && typeof block === "object" && block.type === wantedType && typeof block.text === "string") {
9286
+ if (block.text !== "") parts.push(block.text);
9287
+ }
9288
+ }
9289
+ return parts.length === 0 ? void 0 : parts.join("\n");
9290
+ }
9147
9291
  function countImages(content) {
9148
9292
  if (!Array.isArray(content)) return 0;
9149
9293
  let n = 0;
@@ -9181,7 +9325,7 @@ function foldMessages(events) {
9181
9325
  } else if (event.type === "assistant/message") {
9182
9326
  const message = event.data.message;
9183
9327
  const text = message === null ? void 0 : blockText(message.content);
9184
- const reasoning = message === null ? void 0 : blockText(message.reasoning);
9328
+ const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
9185
9329
  const images = message === null ? 0 : countImages(message.content);
9186
9330
  const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
9187
9331
  if (text === void 0 && reasoning === void 0 && toolCalls.length === 0) continue;
@@ -9198,6 +9342,143 @@ function foldMessages(events) {
9198
9342
  }
9199
9343
  return rows;
9200
9344
  }
9345
+ /** 按已完成输出段落折叠事件日志:每个 assistant/message 割一段,返回 sinceSeq 后的所有段落(事件序)。 */
9346
+ function segmentsSince(events, sinceSeq = 0) {
9347
+ const list = asEventList(events);
9348
+ const out = [];
9349
+ let openTurn = false;
9350
+ for (const event of list) {
9351
+ if (event.type === "turn/start") {
9352
+ openTurn = true;
9353
+ continue;
9354
+ }
9355
+ if (event.type === "turn/end") {
9356
+ openTurn = false;
9357
+ continue;
9358
+ }
9359
+ if (event.type !== "assistant/message") continue;
9360
+ if (event.seq <= sinceSeq) continue;
9361
+ const message = event.data.message;
9362
+ const text = message === null ? void 0 : blockText(message.content);
9363
+ const reasoning = message === null ? void 0 : blockTextByType(message.content, "reasoning") ?? blockText(message.reasoning);
9364
+ const toolCalls = message === null ? [] : toolNamesOf(message.toolCalls);
9365
+ const turn = event.data?.turn;
9366
+ const step = event.data?.step;
9367
+ const seg = {
9368
+ seq: event.seq,
9369
+ time: event.time,
9370
+ turn: typeof turn === "number" ? turn : 0,
9371
+ step: typeof step === "number" ? step : 0,
9372
+ toolCalls,
9373
+ openTurn
9374
+ };
9375
+ if (text !== void 0) seg.text = text;
9376
+ if (reasoning !== void 0) seg.reasoning = reasoning;
9377
+ out.push(seg);
9378
+ }
9379
+ return out;
9380
+ }
9381
+ /** 取自 sinceSeq 之后最新一个已完成输出段落(无则 null)。 */
9382
+ function latestSegmentSince(events, sinceSeq = 0) {
9383
+ const segs = segmentsSince(events, sinceSeq);
9384
+ return segs.length === 0 ? null : segs[segs.length - 1] ?? null;
9385
+ }
9386
+ /**
9387
+ * 对一个 live 会话的事件日志折叠出其"最近推理/文本增量流"。兼容两代日志形状:
9388
+ * - dsh 0.1.5 之后:turn 内不再发布独立 chunk 事件,流式增量内嵌在已定型
9389
+ * assistant/message(或未定型 assistant/attempt)事件的 `stream` 记录里
9390
+ * (reasoning-chunks / text-chunks 批量段 + 原始 chunk:reasoning-delta / text-delta);
9391
+ * - 0.1.2-rc 之前:独立的 assistant/chunk 事件(turn 中途即可见、增量,
9392
+ * 这正是旧版监控思维链并按规则提前终止所需的粒度)。
9393
+ * 两者兼读、取最新一份;无任何增量(会话空闲、或该 provider 不流式推理/无推理)
9394
+ * 返回 null,调用方应回落为 foldMessages 里已定型的 message.reasoning。
9395
+ * 纯读、无副作用;事件形状不做任何假设,缺失/异常一律安全处理。
9396
+ */
9397
+ function liveReasoningSnapshot(events) {
9398
+ const list = asEventList(events);
9399
+ let lastFinalizedSeq = -1;
9400
+ let reasoning = "";
9401
+ let text = "";
9402
+ let seq = -1;
9403
+ let time = 0;
9404
+ let turn = 0;
9405
+ let step = 0;
9406
+ let foundDelta = false;
9407
+ for (const event of list) {
9408
+ const type = event.type;
9409
+ if (type === "assistant/message" || type === "assistant/attempt") {
9410
+ const data = event.data;
9411
+ if (data !== null && data !== void 0 && typeof data === "object" && Array.isArray(data.stream)) {
9412
+ let r = "";
9413
+ let t = "";
9414
+ for (const raw of data.stream) {
9415
+ const record = raw;
9416
+ if (record === null || typeof record !== "object") continue;
9417
+ if (record.type === "reasoning-chunks" && Array.isArray(record.texts)) {
9418
+ for (const s of record.texts) if (typeof s === "string") r += s;
9419
+ } else if (record.type === "text-chunks" && Array.isArray(record.texts)) {
9420
+ for (const s of record.texts) if (typeof s === "string") t += s;
9421
+ } else if (record.type === "chunk") {
9422
+ const c = record.chunk;
9423
+ if (c !== null && typeof c === "object" && typeof c.type === "string" && typeof c.text === "string") {
9424
+ if (c.type === "reasoning-delta") r += c.text;
9425
+ else if (c.type === "text-delta") t += c.text;
9426
+ }
9427
+ }
9428
+ }
9429
+ if (r !== "" || t !== "") {
9430
+ if (typeof data.turn === "number") turn = data.turn;
9431
+ if (typeof data.step === "number") step = data.step;
9432
+ reasoning = r;
9433
+ text = t;
9434
+ seq = event.seq;
9435
+ time = event.time;
9436
+ foundDelta = true;
9437
+ }
9438
+ }
9439
+ if (type === "assistant/message") lastFinalizedSeq = event.seq;
9440
+ continue;
9441
+ }
9442
+ if (type !== "assistant/chunk") continue;
9443
+ if (event.seq <= lastFinalizedSeq) continue;
9444
+ const data = event.data;
9445
+ if (data === null || data === void 0 || typeof data !== "object") continue;
9446
+ const c = data.chunk;
9447
+ if (c === null || typeof c !== "object" || typeof c.type !== "string") continue;
9448
+ if (c.type === "reasoning-delta" || c.type === "text-delta") {
9449
+ if (typeof c.text !== "string") continue;
9450
+ if (c.type === "reasoning-delta") reasoning += c.text;
9451
+ else text += c.text;
9452
+ if (typeof data.turn === "number") turn = data.turn;
9453
+ if (typeof data.step === "number") step = data.step;
9454
+ foundDelta = true;
9455
+ if (event.seq > seq) seq = event.seq;
9456
+ if (event.time > time) time = event.time;
9457
+ }
9458
+ }
9459
+ if (!foundDelta) return null;
9460
+ return {
9461
+ turn,
9462
+ step,
9463
+ reasoning,
9464
+ text,
9465
+ seq,
9466
+ time
9467
+ };
9468
+ }
9469
+ /** 思维链尾巴默认截断上限(字符)。 */
9470
+ const REASONING_TAIL_LIMIT = 4e3;
9471
+ /**
9472
+ * 组装一条受字符上限约束的"思维链尾巴"用于展示/摘要:
9473
+ * 优先进行中的 liveReasoning,否则最近已定型的 lastReasoning;
9474
+ * 超出上限时截断并附省略标记。两者皆无返回 undefined。
9475
+ */
9476
+ function reasoningTailOf(liveReasoning, lastReasoning, limit = REASONING_TAIL_LIMIT) {
9477
+ const source = liveReasoning ?? lastReasoning;
9478
+ if (source === void 0 || source === "") return void 0;
9479
+ if (source.length <= limit) return source;
9480
+ return source.slice(0, limit) + "…";
9481
+ }
9201
9482
  /**
9202
9483
  * 会话标题:优先 session/title 事件(last-wins,与 UI 一致);
9203
9484
  * 无标题事件时回落为第一条用户消息(截断 80 字符)。
@@ -9249,8 +9530,10 @@ async function waitForReply(opts) {
9249
9530
  const started = Date.now();
9250
9531
  const deadline = started + opts.timeoutMs;
9251
9532
  const requireTurnEnd = opts.requireTurnEnd ?? false;
9533
+ const waitForSegment = opts.waitForSegment ?? false;
9252
9534
  let latest = null;
9253
9535
  let textReply = null;
9536
+ let segment = null;
9254
9537
  let turnEnded = false;
9255
9538
  for (;;) {
9256
9539
  if (opts.signal !== void 0 && opts.signal.aborted) break;
@@ -9260,22 +9543,38 @@ async function waitForReply(opts) {
9260
9543
  if (latest === null || row.seq > latest.seq) latest = row;
9261
9544
  if (row.text !== void 0 && (textReply === null || row.seq > textReply.seq)) textReply = row;
9262
9545
  }
9546
+ if (waitForSegment) {
9547
+ const seg = latestSegmentSince(events, opts.baselineSeq);
9548
+ if (seg !== null && (segment === null || seg.seq > segment.seq)) segment = seg;
9549
+ }
9263
9550
  if (requireTurnEnd && latest !== null) {
9264
9551
  for (const event of events) if (event.seq > latest.seq && event.type === "turn/end") {
9265
9552
  turnEnded = true;
9266
9553
  break;
9267
9554
  }
9268
9555
  }
9269
- if (requireTurnEnd ? latest !== null && turnEnded : textReply !== null) break;
9556
+ let done;
9557
+ if (requireTurnEnd) done = latest !== null && turnEnded;
9558
+ else if (waitForSegment) done = segment !== null;
9559
+ else done = textReply !== null;
9560
+ if (done) break;
9270
9561
  if (Date.now() >= deadline) break;
9271
9562
  await sleep(100);
9272
9563
  }
9273
- const message = textReply ?? latest;
9564
+ let message = waitForSegment && segment !== null ? {
9565
+ seq: segment.seq,
9566
+ time: segment.time,
9567
+ role: "assistant",
9568
+ images: 0,
9569
+ ...segment.text !== void 0 ? { text: segment.text } : {},
9570
+ ...segment.reasoning !== void 0 ? { reasoning: segment.reasoning } : {},
9571
+ ...segment.toolCalls.length > 0 ? { toolCalls: segment.toolCalls } : {}
9572
+ } : textReply ?? latest;
9274
9573
  return {
9275
9574
  message,
9276
9575
  seq: message === null ? opts.baselineSeq : message.seq,
9277
9576
  turnEnded,
9278
- timedOut: requireTurnEnd ? latest !== null && !turnEnded : textReply === null,
9577
+ timedOut: requireTurnEnd ? latest !== null && !turnEnded : waitForSegment ? segment === null : textReply === null,
9279
9578
  aborted: opts.signal !== void 0 && opts.signal.aborted,
9280
9579
  waitedMs: Date.now() - started
9281
9580
  };
@@ -9351,13 +9650,18 @@ function statusSnapshot(ctx, agent) {
9351
9650
  }
9352
9651
  const recent = rows.slice(-8);
9353
9652
  let lastAssistantText;
9653
+ let lastReasoning;
9354
9654
  for (let i = rows.length - 1; i >= 0; i -= 1) {
9355
9655
  const row = rows[i];
9356
- if (row !== void 0 && row.role === "assistant" && row.text !== void 0) {
9357
- lastAssistantText = row.text;
9358
- break;
9656
+ if (row !== void 0 && row.role === "assistant") {
9657
+ if (lastAssistantText === void 0 && row.text !== void 0) lastAssistantText = row.text;
9658
+ if (lastReasoning === void 0 && row.reasoning !== void 0) lastReasoning = row.reasoning;
9659
+ if (lastAssistantText !== void 0 && lastReasoning !== void 0) break;
9359
9660
  }
9360
9661
  }
9662
+ const live = liveReasoningSnapshot(events);
9663
+ const liveReasoning = live !== null && live.reasoning !== "" ? live.reasoning : void 0;
9664
+ const reasoningTail = reasoningTailOf(liveReasoning, lastReasoning);
9361
9665
  const inbox = agent.inbox;
9362
9666
  const cwd = agent.session.header.cwd;
9363
9667
  return {
@@ -9371,10 +9675,13 @@ function statusSnapshot(ctx, agent) {
9371
9675
  lastTurn,
9372
9676
  lastActivityAt,
9373
9677
  stalledMs: lastActivityAt === null ? null : Date.now() - lastActivityAt,
9374
- pendingWork: inbox?.hasPending ?? false,
9678
+ pendingWork: inbox?.hasPending === true || (inbox?.nextTurn?.length ?? 0) + (inbox?.nextStep?.length ?? 0) > 0,
9375
9679
  nextTurnCount: inbox?.nextTurn?.length ?? 0,
9376
9680
  nextStepCount: inbox?.nextStep?.length ?? 0,
9377
9681
  ...lastAssistantText === void 0 ? {} : { lastAssistantText },
9682
+ ...lastReasoning === void 0 ? {} : { lastReasoning },
9683
+ ...liveReasoning === void 0 ? {} : { liveReasoning },
9684
+ ...reasoningTail === void 0 ? {} : { reasoningTail },
9378
9685
  messageCount: rows.length,
9379
9686
  recent
9380
9687
  };
@@ -9429,6 +9736,16 @@ var SessionMonitor = class {
9429
9736
  start(config) {
9430
9737
  const existing = this.entries.get(config.sessionId);
9431
9738
  const now = Date.now();
9739
+ const cotInit = { rules: (config.coTRules ?? []).reduce((acc, rule, index) => {
9740
+ const key = String(index) + ":" + rule.match + ":" + rule.field + ":" + rule.value;
9741
+ acc[key] = {
9742
+ hits: 0,
9743
+ lastFiredAt: null,
9744
+ lastMatch: false,
9745
+ lastSubject: ""
9746
+ };
9747
+ return acc;
9748
+ }, {}) };
9432
9749
  const entry = existing !== void 0 ? {
9433
9750
  ...existing,
9434
9751
  config,
@@ -9442,8 +9759,10 @@ var SessionMonitor = class {
9442
9759
  lastActionAt: null,
9443
9760
  lastNote: "监控已启动",
9444
9761
  done: false,
9445
- cycles: 0
9762
+ cycles: 0,
9763
+ cot: cotInit
9446
9764
  };
9765
+ if (entry.cot === void 0) entry.cot = cotInit;
9447
9766
  this.entries.set(config.sessionId, entry);
9448
9767
  this.ensureTimer();
9449
9768
  this.log(entry, "monitor start session=" + config.sessionId + " interval=" + config.intervalMs + " stalled=" + String(config.stalledMs ?? 6e4) + " maxStuck=" + String(config.maxStuckCycles ?? 3));
@@ -9569,6 +9888,7 @@ var SessionMonitor = class {
9569
9888
  this.log(entry, "STALL(#" + entry.stuckCount + ") → steer: " + steer);
9570
9889
  return;
9571
9890
  }
9891
+ if (this.evaluateCoTRules(entry, snapshot, sessionId)) return;
9572
9892
  if (entry.stuckCount !== 0) entry.stuckCount = 0;
9573
9893
  entry.lastAction = "steady";
9574
9894
  entry.lastActionAt = Date.now();
@@ -9593,6 +9913,76 @@ var SessionMonitor = class {
9593
9913
  if (!text) return false;
9594
9914
  return keywords.some((kw) => kw !== "" && text.includes(kw));
9595
9915
  }
9916
+ /**
9917
+ * 评估思维链(CoT)规则。规则只对 live 会话有意义:running 时用进行中的
9918
+ * reasoning-delta 流,空闲时回落到已定型 lastReasoning。not-contains 规则作用在
9919
+ * reasoning 字段时,若该会话本就没有推理(非推理模型/尚未产出),不判定为命中,
9920
+ * 避免误 cancel 一个根本不产生思维链的会话。
9921
+ * @returns true 表示本 tick 已被某条规则触发并处理(steer/cancel),调用方应 return。
9922
+ */
9923
+ evaluateCoTRules(entry, snapshot, sessionId) {
9924
+ const rules = entry.config.coTRules ?? [];
9925
+ if (rules.length === 0) return false;
9926
+ const minHits = entry.config.cotMinHits ?? 1;
9927
+ const hasLive = (snapshot.liveReasoning ?? "") !== "";
9928
+ const subject = {
9929
+ reasoning: snapshot.liveReasoning ?? snapshot.lastReasoning ?? "",
9930
+ text: snapshot.lastAssistantText ?? "",
9931
+ hasReasoning: hasLive || (snapshot.lastReasoning ?? "") !== ""
9932
+ };
9933
+ const now = Date.now();
9934
+ const rulesState = entry.cot;
9935
+ let engaged = false;
9936
+ rules.forEach((rule, index) => {
9937
+ if (engaged) return;
9938
+ const key = String(index) + ":" + rule.match + ":" + rule.field + ":" + rule.value;
9939
+ const st = rulesState.rules[key] ?? {
9940
+ hits: 0,
9941
+ lastFiredAt: null,
9942
+ lastMatch: false
9943
+ };
9944
+ if (rule.field === "reasoning" && rule.match === "not-contains" && !subject.hasReasoning) {
9945
+ st.lastMatch = false;
9946
+ st.hits = 0;
9947
+ return;
9948
+ }
9949
+ let hit;
9950
+ if (rule.field === "reasoning") hit = rule.match === "contains" ? subject.reasoning.includes(rule.value) : !subject.reasoning.includes(rule.value);
9951
+ else if (rule.field === "text") hit = rule.match === "contains" ? subject.text.includes(rule.value) : !subject.text.includes(rule.value);
9952
+ else {
9953
+ const reasonHas = subject.reasoning.includes(rule.value);
9954
+ const textHas = subject.text.includes(rule.value);
9955
+ hit = rule.match === "contains" ? reasonHas || textHas : !reasonHas && !textHas;
9956
+ }
9957
+ st.lastMatch = hit;
9958
+ st.hits = hit ? st.hits + 1 : 0;
9959
+ this.log(entry, "CoT rule " + key + " hit=" + String(hit) + " hits=" + String(st.hits) + "/" + String(minHits));
9960
+ if (!(hit && st.hits >= minHits)) return;
9961
+ const coolMs = Math.max(entry.config.intervalMs, 5e3) * 2;
9962
+ if (st.lastFiredAt !== null && now - st.lastFiredAt < coolMs) return;
9963
+ st.lastFiredAt = now;
9964
+ if (rule.action === "cancel") {
9965
+ const reason = "reasoning-rule:" + rule.value;
9966
+ cancelLiveSession(this.ctx, sessionId, false, reason);
9967
+ entry.lastAction = "cot-cancel";
9968
+ entry.lastActionAt = Date.now();
9969
+ entry.lastNote = "思维链规则触发 cancel: " + rule.value;
9970
+ entry.stuckCount = 0;
9971
+ this.log(entry, "COT-CANCEL session=" + sessionId + " rule=" + key + " value=" + rule.value);
9972
+ } else {
9973
+ const steer = rule.message !== void 0 && rule.message.trim() !== "" ? rule.message.trim() : "监控判定:思维链/回复匹配规则「" + rule.value + "」,请据此调整方向并继续推进任务。";
9974
+ sendLiveMessage(this.ctx, sessionId, steer, "steer");
9975
+ entry.lastAction = "cot-steer";
9976
+ entry.lastActionAt = Date.now();
9977
+ entry.lastNote = "思维链规则触发 steer: " + rule.value;
9978
+ entry.stuckCount = 0;
9979
+ this.log(entry, "COT-STEER session=" + sessionId + " rule=" + key + " steer=" + steer);
9980
+ }
9981
+ engaged = true;
9982
+ st.hits = 0;
9983
+ });
9984
+ return engaged;
9985
+ }
9596
9986
  hasLlm() {
9597
9987
  return this.ctx.llm !== void 0;
9598
9988
  }
@@ -9648,7 +10038,7 @@ var SessionMonitor = class {
9648
10038
  }
9649
10039
  };
9650
10040
  //#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
10041
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.5-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_a35c65afc1061b4908fad5a531841731/node_modules/@deepseek-ai/dsh-scope/lib/index.js
9652
10042
  /**
9653
10043
  * Shared insertion-ordered storage and effect ownership for scope-aware registries.
9654
10044
  *
@@ -9927,7 +10317,7 @@ function scopeTarget(base, key) {
9927
10317
  return carrier;
9928
10318
  }
9929
10319
  //#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
10320
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.5-alpha.2_2b754afda902b21956a20e8a701ed6ef/node_modules/@deepseek-ai/dsh-tools/lib/index.js
9931
10321
  /**
9932
10322
  * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
9933
10323
  * types, subagents, and workflows. The subset accepts any JSON root, an
@@ -11104,7 +11494,7 @@ function createRunCodeTool(registry, options) {
11104
11494
  if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
11105
11495
  const normalized = jsonNormalizeArgs(rawArgs);
11106
11496
  const n = ++dispatches;
11107
- const subCallId = brandString(`${String(exec.callId)}:code:${n}`);
11497
+ const subCallId = brandString(`${String(exec.callId)}:ptc:${n}`);
11108
11498
  const input = {
11109
11499
  callId: subCallId,
11110
11500
  rootCallId: exec.rootCallId,
@@ -11136,7 +11526,7 @@ function createRunCodeTool(registry, options) {
11136
11526
  isError: result.isError,
11137
11527
  content: result.content
11138
11528
  });
11139
- agent.session.append("tool/code-dispatch", {
11529
+ agent.session.append("tool/ptc-dispatch", {
11140
11530
  rootCallId: exec.rootCallId,
11141
11531
  parentCallId: exec.callId,
11142
11532
  subCallId,
@@ -11158,7 +11548,7 @@ function createRunCodeTool(registry, options) {
11158
11548
  reject(/* @__PURE__ */ new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
11159
11549
  },
11160
11550
  async start() {
11161
- exec.agent?.session.append("tool/code-dispatch-start", {
11551
+ exec.agent?.session.append("tool/ptc-dispatch-start", {
11162
11552
  rootCallId: exec.rootCallId,
11163
11553
  parentCallId: exec.callId,
11164
11554
  subCallId,
@@ -11192,7 +11582,7 @@ function createRunCodeTool(registry, options) {
11192
11582
  content: result.content,
11193
11583
  source: {
11194
11584
  kind: "plugin",
11195
- plugin: "tools-code-mode"
11585
+ plugin: "tools-ptc"
11196
11586
  }
11197
11587
  }));
11198
11588
  for (const context of result.additionalContexts ?? []) exec.deferContext(context);
@@ -12596,8 +12986,7 @@ var ToolRuntime = class extends Service {
12596
12986
  * reads return the same flavor — but a reload that swapped in a second
12597
12987
  * language between them would hand a program written against one SDK to the
12598
12988
  * other. Binding it is deferred until a second backend ships (the first
12599
- * point it is testable); rationale in the
12600
- * [language-dispatch note](../../../../.agents/notes/implemented/feature/2026-07-31-ptc-language-dispatch.md).
12989
+ * point it is testable).
12601
12990
  */
12602
12991
  requireCodeRuntime(mode) {
12603
12992
  const runtime = this.ctx.get("codeRuntime");
@@ -12683,9 +13072,9 @@ var ToolRuntime = class extends Service {
12683
13072
  * A restriction filters what a scope inherits — the global layer and every
12684
13073
  * ancestor layer on its chain — and never what its OWN layer registers.
12685
13074
  * That exemption is what a per-child capability filter has to keep intact:
12686
- * the delegation runtime registers a child's reporting and structured-output
12687
- * tools into the child's own layer, and a filter naming the capabilities the
12688
- * child may use must not strip the machinery it answers through.
13075
+ * the delegation runtime registers a child's structured-output tool into the
13076
+ * child's own layer, and a filter naming the capabilities the child may use
13077
+ * must not strip the machinery it answers through.
12689
13078
  *
12690
13079
  * Reading the exempt set as "the global layer" instead of "not mine" held
12691
13080
  * only while every model-facing tool sat in the host composition. Once
@@ -12803,7 +13192,7 @@ var ToolRuntime = class extends Service {
12803
13192
  }
12804
13193
  /**
12805
13194
  * Run the `tools/ptc-dispatch-log` waterfall over one settled sub-dispatch
12806
- * and return the content the bridge should log on `tool/code-dispatch`.
13195
+ * and return the content the bridge should log on `tool/ptc-dispatch`.
12807
13196
  * Contained: when a listener throws, the method logs the original settled
12808
13197
  * content; that failure must not fail the dispatch or omit the settle event. Private:
12809
13198
  * the ONE consumer is the `run_code` bridge this registry constructs, which
@@ -13429,11 +13818,33 @@ function toolAbortedBeforeDispatchResult(prior) {
13429
13818
  };
13430
13819
  }
13431
13820
  //#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
13821
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.5-alpha.2_cfc1690176e5c008b05d52ad11161b75/node_modules/@deepseek-ai/dsh-agent/lib/index.js
13433
13822
  /**
13434
13823
  * Agent-scoped model selection shared by runtime entry points.
13435
13824
  * @module @deepseek-ai/dsh-agent/model-selection
13436
13825
  */
13826
+ function sameRoute(left, right) {
13827
+ return left.provider === right.provider && left.model === right.model;
13828
+ }
13829
+ function routeLabel(route, other) {
13830
+ return route.provider === other.provider ? route.model : `${route.provider}/${route.model}`;
13831
+ }
13832
+ function modelSwitchNotice(previous, selected) {
13833
+ const from = routeLabel(previous, selected);
13834
+ const to = routeLabel(selected, previous);
13835
+ return createUserMessage({
13836
+ content: [{
13837
+ type: "text",
13838
+ text: `[model changed: assistant turns above this point were generated by ${from}; the session continues with ${to}]`
13839
+ }],
13840
+ source: {
13841
+ kind: "plugin",
13842
+ plugin: "model-selection",
13843
+ form: "notice",
13844
+ summary: boundContextSummary(`${from} → ${to}`)
13845
+ }
13846
+ });
13847
+ }
13437
13848
  /**
13438
13849
  * Couple one mutable selection to Agent-scoped prompt assembly and request routing.
13439
13850
  * Prompt assembly snapshots the selected model before delegating, then applies
@@ -13442,9 +13853,15 @@ function toolAbortedBeforeDispatchResult(prior) {
13442
13853
  * surfaces. An absent selected effort clears any inherited effort, restoring
13443
13854
  * the selected model's provider/default behavior.
13444
13855
  *
13856
+ * A provider/model change appends a durable user-role notice to the next
13857
+ * admitted request. It compares the assembled selection with the latest
13858
+ * request header; effort-only changes and empty no-request decisions add no
13859
+ * notice. Failure before header persistence repeats the notice on the next
13860
+ * request.
13861
+ *
13445
13862
  * @param agentCtx - The selected Agent's scoped context.
13446
13863
  * @param selection - Mutable selection owned by the calling entry point.
13447
- * @returns Disposer for both scoped waterfall listeners.
13864
+ * @returns Disposer for all scoped waterfall listeners.
13448
13865
  */
13449
13866
  function installModelSelection(agentCtx, selection) {
13450
13867
  const disposeAssembly = agentCtx.on("system-prompt/assemble", async (_assembly, _context, next) => {
@@ -13473,9 +13890,22 @@ function installModelSelection(agentCtx, selection) {
13473
13890
  ...selected.reasoningEffort === void 0 ? {} : { reasoningEffort: selected.reasoningEffort }
13474
13891
  };
13475
13892
  });
13893
+ const disposeNotice = agentCtx.on("agent/pre-step", async ({ agent, messages, signal, step }, next) => {
13894
+ const decision = await next();
13895
+ if (decision.kind === "reject" || signal.aborted) return decision;
13896
+ if (decision.messages.length === 0 && (step === 1 || messages.length > 0)) return decision;
13897
+ const selected = selection.assembled;
13898
+ const previous = agent.session.requestHeader()?.config;
13899
+ if (selected === void 0 || previous === void 0 || sameRoute(selected, previous)) return decision;
13900
+ return {
13901
+ ...decision,
13902
+ messages: [...decision.messages, modelSwitchNotice(previous, selected)]
13903
+ };
13904
+ }, { prepend: true });
13476
13905
  return () => {
13477
13906
  disposeAssembly();
13478
13907
  disposeRequest();
13908
+ disposeNotice();
13479
13909
  };
13480
13910
  }
13481
13911
  //#endregion
@@ -13796,7 +14226,11 @@ function registerResume(env) {
13796
14226
  }
13797
14227
  let headers = [];
13798
14228
  try {
13799
- headers = await env.ctx.sessionPersistence.list();
14229
+ headers = (await env.ctx.sessionPersistence.list()).map((rec) => ({
14230
+ id: rec.header.id,
14231
+ ...rec.header.cwd === void 0 ? {} : { cwd: rec.header.cwd },
14232
+ createdAt: rec.header.createdAt
14233
+ }));
13800
14234
  } catch (error) {
13801
14235
  throw new Error("session persistence unavailable: " + (error instanceof Error ? error.message : String(error)));
13802
14236
  }
@@ -13831,7 +14265,7 @@ function registerResume(env) {
13831
14265
  function registerWait(env) {
13832
14266
  env.ctx.tools.register(defineTool({
13833
14267
  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).",
14268
+ 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
14269
  parameters: {
13836
14270
  sessionId: {
13837
14271
  type: "string",
@@ -13849,6 +14283,11 @@ function registerWait(env) {
13849
14283
  requireTurnEnd: {
13850
14284
  type: "boolean",
13851
14285
  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)."
14286
+ },
14287
+ waitFor: {
14288
+ type: "string",
14289
+ enum: ["reply", "segment"],
14290
+ 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
14291
  }
13853
14292
  },
13854
14293
  output: {
@@ -13877,9 +14316,13 @@ function registerWait(env) {
13877
14316
  if (typeof args.sessionId !== "string" || args.sessionId.trim() === "") throw new Error("invalid sessionId: expected a non-empty string");
13878
14317
  const agent = liveAgent(env, args.sessionId);
13879
14318
  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)");
14319
+ const waitSegment = args.waitFor === "segment";
13880
14320
  let baseline;
13881
14321
  if (typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) baseline = args.sinceSeq;
13882
- else {
14322
+ else if (waitSegment) {
14323
+ const segs = segmentsSince(sessionEvents(agent.session));
14324
+ baseline = segs.length === 0 ? -1 : segs[segs.length - 1]?.seq ?? -1;
14325
+ } else {
13883
14326
  let lastText = -1;
13884
14327
  for (const row of foldMessages(sessionEvents(agent.session))) if (row.text !== void 0) lastText = row.seq;
13885
14328
  baseline = lastText;
@@ -13889,7 +14332,8 @@ function registerWait(env) {
13889
14332
  baselineSeq: baseline,
13890
14333
  timeoutMs: clampTimeout(args.timeoutMs),
13891
14334
  signal: exec.signal,
13892
- requireTurnEnd: args.requireTurnEnd === true
14335
+ requireTurnEnd: args.requireTurnEnd === true,
14336
+ ...waitSegment ? { waitForSegment: true } : {}
13893
14337
  });
13894
14338
  env.registry.touch(args.sessionId);
13895
14339
  return asJson({
@@ -13900,6 +14344,83 @@ function registerWait(env) {
13900
14344
  }
13901
14345
  }));
13902
14346
  }
14347
+ function registerSegments(env) {
14348
+ env.ctx.tools.register(defineTool({
14349
+ name: "session_bridge_segments",
14350
+ 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.",
14351
+ parameters: {
14352
+ sessionId: {
14353
+ type: "string",
14354
+ required: true,
14355
+ description: "Session id to read segments from (live or offline)."
14356
+ },
14357
+ sinceSeq: {
14358
+ type: "number",
14359
+ description: "Only return completed segments after this event seq (paging cursor)."
14360
+ },
14361
+ limit: {
14362
+ type: "number",
14363
+ description: "Maximum number of segments to return (default 10, max 50)."
14364
+ }
14365
+ },
14366
+ output: {
14367
+ schema: {
14368
+ type: "object",
14369
+ additionalProperties: true
14370
+ },
14371
+ render: (_args, value) => {
14372
+ const segs = value.segments ?? [];
14373
+ if (segs.length === 0) return [{
14374
+ type: "text",
14375
+ text: "(no completed segments)"
14376
+ }];
14377
+ return [{
14378
+ type: "text",
14379
+ text: segs.map((sg) => {
14380
+ const head = "seg #" + String(sg.seq) + " (turn " + String(sg.turn) + " step " + String(sg.step) + ")";
14381
+ const text = typeof sg.text === "string" ? sg.text : "";
14382
+ const reason = typeof sg.reasoning === "string" && sg.reasoning !== "" ? " [reasoning " + sg.reasoning.length + " chars]" : "";
14383
+ return head + (Array.isArray(sg.toolCalls) && sg.toolCalls.length > 0 ? " [tools: " + sg.toolCalls.join(",") + "]" : "") + reason + ": " + text.slice(0, 160);
14384
+ }).join("\n")
14385
+ }];
14386
+ }
14387
+ },
14388
+ async execute(args) {
14389
+ const agent = liveAgent(env, args.sessionId);
14390
+ let events;
14391
+ let live;
14392
+ if (agent !== void 0) {
14393
+ events = sessionEvents(agent.session);
14394
+ live = true;
14395
+ } else try {
14396
+ events = (await inspectPersistedSession(env.ctx, args.sessionId)).events;
14397
+ live = false;
14398
+ } catch (error) {
14399
+ throw new Error(String(error));
14400
+ }
14401
+ const sinceSeq = typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0 ? args.sinceSeq : 0;
14402
+ clampLimit(args.limit, 10, 50);
14403
+ const segs = segmentsSince(events, sinceSeq);
14404
+ env.registry.touch(args.sessionId);
14405
+ const nextCursorSeq = segs.length === 0 ? sinceSeq : segs[segs.length - 1]?.seq ?? sinceSeq;
14406
+ return asJson({
14407
+ sessionId: args.sessionId,
14408
+ live,
14409
+ nextCursorSeq,
14410
+ segments: segs.map((sg) => ({
14411
+ seq: sg.seq,
14412
+ time: sg.time,
14413
+ turn: sg.turn,
14414
+ step: sg.step,
14415
+ openTurn: sg.openTurn,
14416
+ ...sg.text === void 0 ? {} : { text: sg.text },
14417
+ ...sg.reasoning === void 0 ? {} : { reasoning: sg.reasoning },
14418
+ ...sg.toolCalls.length > 0 ? { toolCalls: sg.toolCalls } : {}
14419
+ }))
14420
+ });
14421
+ }
14422
+ }));
14423
+ }
13903
14424
  function registerRead(env) {
13904
14425
  env.ctx.tools.register(defineTool({
13905
14426
  name: "session_bridge_read",
@@ -13966,7 +14487,7 @@ function registerRead(env) {
13966
14487
  } else {
13967
14488
  let inspection;
13968
14489
  try {
13969
- inspection = await env.ctx.sessionPersistence.inspect(args.sessionId);
14490
+ inspection = await inspectPersistedSession(env.ctx, args.sessionId);
13970
14491
  } catch (error) {
13971
14492
  throw new Error("cannot read session " + JSON.stringify(args.sessionId) + ": " + (error instanceof Error ? error.message : String(error)));
13972
14493
  }
@@ -14090,7 +14611,8 @@ function registerFind(env) {
14090
14611
  }
14091
14612
  if (args.liveOnly !== true) try {
14092
14613
  const persistence = env.ctx.sessionPersistence;
14093
- for (const header of await persistence.list()) {
14614
+ for (const rec of await persistence.list()) {
14615
+ const header = rec.header;
14094
14616
  if (items.some((item) => item.sessionId === header.id)) continue;
14095
14617
  items.push({
14096
14618
  sessionId: header.id,
@@ -14126,13 +14648,12 @@ function registerFind(env) {
14126
14648
  const wsFilter = typeof args.workspaceId === "string" ? args.workspaceId.trim() : "";
14127
14649
  const cwdFilter = typeof args.cwd === "string" ? args.cwd.trim().toLowerCase() : "";
14128
14650
  if (titleFilter !== "" || query !== "" && items.some((item) => !item.live)) {
14129
- const persistence = env.ctx.sessionPersistence;
14130
14651
  let inspected = 0;
14131
14652
  for (const item of items) {
14132
14653
  if (inspected >= 30) break;
14133
14654
  if (item.live || item.title !== void 0) continue;
14134
14655
  try {
14135
- item.title = titleOf((await persistence.inspect(item.sessionId)).events);
14656
+ item.title = titleOf((await inspectPersistedSession(env.ctx, item.sessionId)).events);
14136
14657
  inspected += 1;
14137
14658
  } catch {
14138
14659
  inspected += 1;
@@ -14180,6 +14701,7 @@ function registerBridgeTools(env) {
14180
14701
  registerSend(env);
14181
14702
  registerResume(env);
14182
14703
  registerWait(env);
14704
+ registerSegments(env);
14183
14705
  registerRead(env);
14184
14706
  registerFind(env);
14185
14707
  registerStatus(env);
@@ -14187,11 +14709,33 @@ function registerBridgeTools(env) {
14187
14709
  registerMonitor(env);
14188
14710
  registerArchive(env);
14189
14711
  }
14712
+ /** 校验并规范化 coRules 参数为 CoTRule[];非法项抛清晰错误。 */
14713
+ function parseCoRules(raw) {
14714
+ return raw.map((rule, index) => {
14715
+ return {
14716
+ match: rule.match === "contains" || rule.match === "not-contains" ? rule.match : (() => {
14717
+ throw new Error("coRules[" + index + "].match must be \"contains\" or \"not-contains\"");
14718
+ })(),
14719
+ field: rule.field === "reasoning" || rule.field === "text" || rule.field === "both" ? rule.field : (() => {
14720
+ throw new Error("coRules[" + index + "].field must be \"reasoning\", \"text\" or \"both\"");
14721
+ })(),
14722
+ action: rule.action === "steer" || rule.action === "cancel" ? rule.action : (() => {
14723
+ throw new Error("coRules[" + index + "].action must be \"steer\" or \"cancel\"");
14724
+ })(),
14725
+ value: typeof rule.value === "string" && rule.value.trim() !== "" ? rule.value.trim() : (() => {
14726
+ throw new Error("coRules[" + index + "].value must be a non-empty string");
14727
+ })(),
14728
+ ...typeof rule.message === "string" && rule.message.trim() !== "" ? { message: rule.message.trim() } : {}
14729
+ };
14730
+ });
14731
+ }
14190
14732
  function renderMonitorState(entry) {
14191
14733
  const lines = [];
14192
14734
  lines.push(`monitoring ${entry.config.sessionId}${entry.config.label === void 0 ? "" : " (\"" + entry.config.label + "\")"}`);
14193
14735
  lines.push(`interval: ${entry.config.intervalMs}ms | stalled: ${String(entry.config.stalledMs ?? 6e4)}ms | maxStuck: ${String(entry.config.maxStuckCycles ?? 3)}`);
14194
14736
  lines.push(`cycles: ${entry.cycles} | stuck: ${entry.stuckCount} | lastAction: ${entry.lastAction}`);
14737
+ const cotCount = (entry.config.coTRules ?? []).length;
14738
+ if (cotCount > 0) lines.push(`coRules: ${cotCount} rule(s) (minHits ${String(entry.config.cotMinHits ?? 1)})`);
14195
14739
  if (entry.lastNote !== "") lines.push(`note: ${entry.lastNote}`);
14196
14740
  if (entry.done) lines.push("done: yes");
14197
14741
  return lines;
@@ -14199,7 +14743,7 @@ function renderMonitorState(entry) {
14199
14743
  function registerMonitor(env) {
14200
14744
  env.ctx.tools.register(defineTool({
14201
14745
  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.",
14746
+ 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
14747
  parameters: {
14204
14748
  sessionId: {
14205
14749
  type: "string",
@@ -14238,6 +14782,51 @@ function registerMonitor(env) {
14238
14782
  label: {
14239
14783
  type: "string",
14240
14784
  description: "Optional human label for logs/display."
14785
+ },
14786
+ coRules: {
14787
+ type: "array",
14788
+ items: {
14789
+ type: "object",
14790
+ additionalProperties: false,
14791
+ properties: {
14792
+ match: {
14793
+ type: "string",
14794
+ required: true,
14795
+ enum: ["contains", "not-contains"],
14796
+ description: "contains = must include value in the chosen field; not-contains = must NOT include it."
14797
+ },
14798
+ field: {
14799
+ type: "string",
14800
+ required: true,
14801
+ enum: [
14802
+ "reasoning",
14803
+ "text",
14804
+ "both"
14805
+ ],
14806
+ description: "Match on reasoning (chain-of-thought), text (reply), or both (either)."
14807
+ },
14808
+ value: {
14809
+ type: "string",
14810
+ required: true,
14811
+ description: "The substring to match (non-empty)."
14812
+ },
14813
+ action: {
14814
+ type: "string",
14815
+ required: true,
14816
+ enum: ["steer", "cancel"],
14817
+ description: "steer injects a guiding message; cancel terminates the session."
14818
+ },
14819
+ message: {
14820
+ type: "string",
14821
+ description: "Custom steer text (default is a guidance prompt)."
14822
+ }
14823
+ }
14824
+ },
14825
+ 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."
14826
+ },
14827
+ cotMinHits: {
14828
+ type: "number",
14829
+ description: "How many consecutive matched polls before a CoT rule fires (default 1 = immediately)."
14241
14830
  }
14242
14831
  },
14243
14832
  output: {
@@ -14268,7 +14857,9 @@ function registerMonitor(env) {
14268
14857
  ...args.useLlm === true ? { useLlm: true } : {},
14269
14858
  ...typeof args.onStallSteer === "string" && args.onStallSteer.trim() !== "" ? { onStallSteer: args.onStallSteer.trim() } : {},
14270
14859
  ...typeof args.onOffTrackSteer === "string" && args.onOffTrackSteer.trim() !== "" ? { onOffTrackSteer: args.onOffTrackSteer.trim() } : {},
14271
- ...typeof args.label === "string" && args.label.trim() !== "" ? { label: args.label.trim() } : {}
14860
+ ...typeof args.label === "string" && args.label.trim() !== "" ? { label: args.label.trim() } : {},
14861
+ ...Array.isArray(args.coRules) && args.coRules.length > 0 ? { coTRules: parseCoRules(args.coRules) } : {},
14862
+ ...typeof args.cotMinHits === "number" && Number.isInteger(args.cotMinHits) && args.cotMinHits >= 1 ? { cotMinHits: Math.floor(args.cotMinHits) } : {}
14272
14863
  };
14273
14864
  const entry = env.monitor.start(config);
14274
14865
  env.registry.touch(args.sessionId);
@@ -14340,7 +14931,19 @@ function registerMonitor(env) {
14340
14931
  }
14341
14932
  }));
14342
14933
  }
14343
- /** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复。 */
14934
+ /** reasoning 选项裁剪快照里的思维链字段(避免入 token 时有 diff 语义差)。 */
14935
+ function pruneReasoning(snapshot, mode) {
14936
+ if (mode === "tail") return snapshot;
14937
+ const rest = { ...snapshot };
14938
+ delete rest.reasoningTail;
14939
+ if (mode === "none") {
14940
+ delete rest.lastReasoning;
14941
+ delete rest.liveReasoning;
14942
+ } else if (mode === "last") delete rest.liveReasoning;
14943
+ else delete rest.lastReasoning;
14944
+ return rest;
14945
+ }
14946
+ /** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复(+ 思维链预览)。 */
14344
14947
  function renderStatus(snapshot, stalledMsThreshold) {
14345
14948
  const lines = [];
14346
14949
  const runLabel = snapshot.running === "running" ? "running" : "idle";
@@ -14353,12 +14956,16 @@ function renderStatus(snapshot, stalledMsThreshold) {
14353
14956
  }
14354
14957
  if (snapshot.pendingWork) lines.push(`pendingWork: ${snapshot.nextTurnCount} turn + ${snapshot.nextStepCount} step`);
14355
14958
  if (snapshot.lastAssistantText !== void 0) lines.push(`lastReply: ${snapshot.lastAssistantText}`);
14959
+ if (snapshot.reasoningTail !== void 0 && snapshot.reasoningTail !== "") {
14960
+ const preview = snapshot.reasoningTail.length > 160 ? snapshot.reasoningTail.slice(0, 160) + "…" : snapshot.reasoningTail;
14961
+ lines.push(`reasoning: ${preview}`);
14962
+ }
14356
14963
  return lines;
14357
14964
  }
14358
14965
  function registerStatus(env) {
14359
14966
  env.ctx.tools.register(defineTool({
14360
14967
  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.",
14968
+ 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
14969
  parameters: {
14363
14970
  sessionId: {
14364
14971
  type: "string",
@@ -14372,6 +14979,16 @@ function registerStatus(env) {
14372
14979
  recent: {
14373
14980
  type: "number",
14374
14981
  description: "Number of recent messages to include in the snapshot (default 8, max 20)."
14982
+ },
14983
+ reasoning: {
14984
+ type: "string",
14985
+ enum: [
14986
+ "none",
14987
+ "last",
14988
+ "live",
14989
+ "tail"
14990
+ ],
14991
+ 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
14992
  }
14376
14993
  },
14377
14994
  output: {
@@ -14397,7 +15014,8 @@ function registerStatus(env) {
14397
15014
  const agent = liveAgent(env, args.sessionId);
14398
15015
  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
15016
  const threshold = typeof args.stalledMsThreshold === "number" && Number.isFinite(args.stalledMsThreshold) && args.stalledMsThreshold >= 0 ? args.stalledMsThreshold : 6e4;
14400
- const snapshot = statusSnapshot(env.ctx, agent);
15017
+ const reasoningMode = args.reasoning === "none" || args.reasoning === "last" || args.reasoning === "live" ? args.reasoning : "tail";
15018
+ const snapshot = pruneReasoning(statusSnapshot(env.ctx, agent), reasoningMode);
14401
15019
  const shown = typeof args.recent === "number" && Number.isInteger(args.recent) && args.recent >= 0 ? Math.min(args.recent, 20) : 8;
14402
15020
  const envCwd = agent.session.header.cwd;
14403
15021
  env.registry.touch(args.sessionId, {
@@ -14540,7 +15158,6 @@ function registerArchive(env) {
14540
15158
  async execute(args) {
14541
15159
  const items = env.ctx.workspaceRegistry.archivedSessionIds.map(String).map((id) => ({ sessionId: id }));
14542
15160
  if (args.resolveTitles === true && items.length > 0) {
14543
- const persistence = env.ctx.sessionPersistence;
14544
15161
  const registryTitles = /* @__PURE__ */ new Map();
14545
15162
  try {
14546
15163
  const records = await env.registry.all();
@@ -14552,7 +15169,7 @@ function registerArchive(env) {
14552
15169
  continue;
14553
15170
  }
14554
15171
  try {
14555
- item.title = titleOf((await persistence.inspect(item.sessionId)).events);
15172
+ item.title = titleOf((await inspectPersistedSession(env.ctx, item.sessionId)).events);
14556
15173
  } catch {}
14557
15174
  }
14558
15175
  }