dsh-session-bridge 0.3.0 → 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/dsh.plugin.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-session-bridge",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "main": "./lib/index.js",
5
5
  "description": "会话桥:创建主会话 / 向任意会话发消息 / 等待回复 / 读取消息 / 按名或 id 查找会话(支持跨工作区);另含监控/调度主任务与归档会话。",
6
6
  "engines": {
7
- "dsh": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0"
7
+ "dsh": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0"
8
8
  },
9
9
  "contributes": {
10
10
  "tools": [
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_7832b76c982cc1477c0eac59a4954baf/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-rc.1_@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_486cdbb63f2ab3a8eaad87d2b6e5a4b7/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_45952c52282327ee28c4189b1e05c80c/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_0a6acdb1edc9f1b3caaaafba5a0cd464/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-rc.1_@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_cc3ae850d309c6dee6d11afd096d0893/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-rc.1_@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-rc.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-i_9f44d404e0b20c2f6e8599234336f27b/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 {
@@ -9252,12 +9384,14 @@ function latestSegmentSince(events, sinceSeq = 0) {
9252
9384
  return segs.length === 0 ? null : segs[segs.length - 1] ?? null;
9253
9385
  }
9254
9386
  /**
9255
- * 对一个 live 会话的事件日志折叠出其"实时思维链"
9256
- * - 优先聚合流式 assistant/chunk 事件里的 reasoning-delta(turn 中途即可见、增量,
9257
- * 这正是监控思维链并按规则提前终止所需的粒度);
9258
- * - 同时聚合同窗口的 text-delta 增量;
9259
- * - 若没有进行中的 chunk(会话空闲、或该 provider 不流式推理/无推理),返回 null,
9260
- * 调用方应回落为 foldMessages 里的已定型 message.reasoning。
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。
9261
9395
  * 纯读、无副作用;事件形状不做任何假设,缺失/异常一律安全处理。
9262
9396
  */
9263
9397
  function liveReasoningSnapshot(events) {
@@ -9271,11 +9405,41 @@ function liveReasoningSnapshot(events) {
9271
9405
  let step = 0;
9272
9406
  let foundDelta = false;
9273
9407
  for (const event of list) {
9274
- if (event.type === "assistant/message") {
9275
- lastFinalizedSeq = event.seq;
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;
9276
9440
  continue;
9277
9441
  }
9278
- if (event.type !== "assistant/chunk") continue;
9442
+ if (type !== "assistant/chunk") continue;
9279
9443
  if (event.seq <= lastFinalizedSeq) continue;
9280
9444
  const data = event.data;
9281
9445
  if (data === null || data === void 0 || typeof data !== "object") continue;
@@ -9511,7 +9675,7 @@ function statusSnapshot(ctx, agent) {
9511
9675
  lastTurn,
9512
9676
  lastActivityAt,
9513
9677
  stalledMs: lastActivityAt === null ? null : Date.now() - lastActivityAt,
9514
- pendingWork: inbox?.hasPending ?? false,
9678
+ pendingWork: inbox?.hasPending === true || (inbox?.nextTurn?.length ?? 0) + (inbox?.nextStep?.length ?? 0) > 0,
9515
9679
  nextTurnCount: inbox?.nextTurn?.length ?? 0,
9516
9680
  nextStepCount: inbox?.nextStep?.length ?? 0,
9517
9681
  ...lastAssistantText === void 0 ? {} : { lastAssistantText },
@@ -9874,7 +10038,7 @@ var SessionMonitor = class {
9874
10038
  }
9875
10039
  };
9876
10040
  //#endregion
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
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
9878
10042
  /**
9879
10043
  * Shared insertion-ordered storage and effect ownership for scope-aware registries.
9880
10044
  *
@@ -10153,7 +10317,7 @@ function scopeTarget(base, key) {
10153
10317
  return carrier;
10154
10318
  }
10155
10319
  //#endregion
10156
- //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.2-alpha.2_96d3de6f0f5da790676b5bfb20e4a981/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
10157
10321
  /**
10158
10322
  * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
10159
10323
  * types, subagents, and workflows. The subset accepts any JSON root, an
@@ -11330,7 +11494,7 @@ function createRunCodeTool(registry, options) {
11330
11494
  if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
11331
11495
  const normalized = jsonNormalizeArgs(rawArgs);
11332
11496
  const n = ++dispatches;
11333
- const subCallId = brandString(`${String(exec.callId)}:code:${n}`);
11497
+ const subCallId = brandString(`${String(exec.callId)}:ptc:${n}`);
11334
11498
  const input = {
11335
11499
  callId: subCallId,
11336
11500
  rootCallId: exec.rootCallId,
@@ -11362,7 +11526,7 @@ function createRunCodeTool(registry, options) {
11362
11526
  isError: result.isError,
11363
11527
  content: result.content
11364
11528
  });
11365
- agent.session.append("tool/code-dispatch", {
11529
+ agent.session.append("tool/ptc-dispatch", {
11366
11530
  rootCallId: exec.rootCallId,
11367
11531
  parentCallId: exec.callId,
11368
11532
  subCallId,
@@ -11384,7 +11548,7 @@ function createRunCodeTool(registry, options) {
11384
11548
  reject(/* @__PURE__ */ new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
11385
11549
  },
11386
11550
  async start() {
11387
- exec.agent?.session.append("tool/code-dispatch-start", {
11551
+ exec.agent?.session.append("tool/ptc-dispatch-start", {
11388
11552
  rootCallId: exec.rootCallId,
11389
11553
  parentCallId: exec.callId,
11390
11554
  subCallId,
@@ -11418,7 +11582,7 @@ function createRunCodeTool(registry, options) {
11418
11582
  content: result.content,
11419
11583
  source: {
11420
11584
  kind: "plugin",
11421
- plugin: "tools-code-mode"
11585
+ plugin: "tools-ptc"
11422
11586
  }
11423
11587
  }));
11424
11588
  for (const context of result.additionalContexts ?? []) exec.deferContext(context);
@@ -12822,8 +12986,7 @@ var ToolRuntime = class extends Service {
12822
12986
  * reads return the same flavor — but a reload that swapped in a second
12823
12987
  * language between them would hand a program written against one SDK to the
12824
12988
  * other. Binding it is deferred until a second backend ships (the first
12825
- * point it is testable); rationale in the
12826
- * [language-dispatch note](../../../../.agents/notes/implemented/feature/2026-07-31-ptc-language-dispatch.md).
12989
+ * point it is testable).
12827
12990
  */
12828
12991
  requireCodeRuntime(mode) {
12829
12992
  const runtime = this.ctx.get("codeRuntime");
@@ -12909,9 +13072,9 @@ var ToolRuntime = class extends Service {
12909
13072
  * A restriction filters what a scope inherits — the global layer and every
12910
13073
  * ancestor layer on its chain — and never what its OWN layer registers.
12911
13074
  * That exemption is what a per-child capability filter has to keep intact:
12912
- * the delegation runtime registers a child's reporting and structured-output
12913
- * tools into the child's own layer, and a filter naming the capabilities the
12914
- * 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.
12915
13078
  *
12916
13079
  * Reading the exempt set as "the global layer" instead of "not mine" held
12917
13080
  * only while every model-facing tool sat in the host composition. Once
@@ -13029,7 +13192,7 @@ var ToolRuntime = class extends Service {
13029
13192
  }
13030
13193
  /**
13031
13194
  * Run the `tools/ptc-dispatch-log` waterfall over one settled sub-dispatch
13032
- * 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`.
13033
13196
  * Contained: when a listener throws, the method logs the original settled
13034
13197
  * content; that failure must not fail the dispatch or omit the settle event. Private:
13035
13198
  * the ONE consumer is the `run_code` bridge this registry constructs, which
@@ -13655,11 +13818,33 @@ function toolAbortedBeforeDispatchResult(prior) {
13655
13818
  };
13656
13819
  }
13657
13820
  //#endregion
13658
- //#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.2-alpha.2_82401c175458b799db42b9e8f7dd7700/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
13659
13822
  /**
13660
13823
  * Agent-scoped model selection shared by runtime entry points.
13661
13824
  * @module @deepseek-ai/dsh-agent/model-selection
13662
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
+ }
13663
13848
  /**
13664
13849
  * Couple one mutable selection to Agent-scoped prompt assembly and request routing.
13665
13850
  * Prompt assembly snapshots the selected model before delegating, then applies
@@ -13668,9 +13853,15 @@ function toolAbortedBeforeDispatchResult(prior) {
13668
13853
  * surfaces. An absent selected effort clears any inherited effort, restoring
13669
13854
  * the selected model's provider/default behavior.
13670
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
+ *
13671
13862
  * @param agentCtx - The selected Agent's scoped context.
13672
13863
  * @param selection - Mutable selection owned by the calling entry point.
13673
- * @returns Disposer for both scoped waterfall listeners.
13864
+ * @returns Disposer for all scoped waterfall listeners.
13674
13865
  */
13675
13866
  function installModelSelection(agentCtx, selection) {
13676
13867
  const disposeAssembly = agentCtx.on("system-prompt/assemble", async (_assembly, _context, next) => {
@@ -13699,9 +13890,22 @@ function installModelSelection(agentCtx, selection) {
13699
13890
  ...selected.reasoningEffort === void 0 ? {} : { reasoningEffort: selected.reasoningEffort }
13700
13891
  };
13701
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 });
13702
13905
  return () => {
13703
13906
  disposeAssembly();
13704
13907
  disposeRequest();
13908
+ disposeNotice();
13705
13909
  };
13706
13910
  }
13707
13911
  //#endregion
@@ -14022,7 +14226,11 @@ function registerResume(env) {
14022
14226
  }
14023
14227
  let headers = [];
14024
14228
  try {
14025
- 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
+ }));
14026
14234
  } catch (error) {
14027
14235
  throw new Error("session persistence unavailable: " + (error instanceof Error ? error.message : String(error)));
14028
14236
  }
@@ -14185,7 +14393,7 @@ function registerSegments(env) {
14185
14393
  events = sessionEvents(agent.session);
14186
14394
  live = true;
14187
14395
  } else try {
14188
- events = (await env.ctx.sessionPersistence.inspect(args.sessionId)).events;
14396
+ events = (await inspectPersistedSession(env.ctx, args.sessionId)).events;
14189
14397
  live = false;
14190
14398
  } catch (error) {
14191
14399
  throw new Error(String(error));
@@ -14279,7 +14487,7 @@ function registerRead(env) {
14279
14487
  } else {
14280
14488
  let inspection;
14281
14489
  try {
14282
- inspection = await env.ctx.sessionPersistence.inspect(args.sessionId);
14490
+ inspection = await inspectPersistedSession(env.ctx, args.sessionId);
14283
14491
  } catch (error) {
14284
14492
  throw new Error("cannot read session " + JSON.stringify(args.sessionId) + ": " + (error instanceof Error ? error.message : String(error)));
14285
14493
  }
@@ -14403,7 +14611,8 @@ function registerFind(env) {
14403
14611
  }
14404
14612
  if (args.liveOnly !== true) try {
14405
14613
  const persistence = env.ctx.sessionPersistence;
14406
- for (const header of await persistence.list()) {
14614
+ for (const rec of await persistence.list()) {
14615
+ const header = rec.header;
14407
14616
  if (items.some((item) => item.sessionId === header.id)) continue;
14408
14617
  items.push({
14409
14618
  sessionId: header.id,
@@ -14439,13 +14648,12 @@ function registerFind(env) {
14439
14648
  const wsFilter = typeof args.workspaceId === "string" ? args.workspaceId.trim() : "";
14440
14649
  const cwdFilter = typeof args.cwd === "string" ? args.cwd.trim().toLowerCase() : "";
14441
14650
  if (titleFilter !== "" || query !== "" && items.some((item) => !item.live)) {
14442
- const persistence = env.ctx.sessionPersistence;
14443
14651
  let inspected = 0;
14444
14652
  for (const item of items) {
14445
14653
  if (inspected >= 30) break;
14446
14654
  if (item.live || item.title !== void 0) continue;
14447
14655
  try {
14448
- item.title = titleOf((await persistence.inspect(item.sessionId)).events);
14656
+ item.title = titleOf((await inspectPersistedSession(env.ctx, item.sessionId)).events);
14449
14657
  inspected += 1;
14450
14658
  } catch {
14451
14659
  inspected += 1;
@@ -14950,7 +15158,6 @@ function registerArchive(env) {
14950
15158
  async execute(args) {
14951
15159
  const items = env.ctx.workspaceRegistry.archivedSessionIds.map(String).map((id) => ({ sessionId: id }));
14952
15160
  if (args.resolveTitles === true && items.length > 0) {
14953
- const persistence = env.ctx.sessionPersistence;
14954
15161
  const registryTitles = /* @__PURE__ */ new Map();
14955
15162
  try {
14956
15163
  const records = await env.registry.all();
@@ -14962,7 +15169,7 @@ function registerArchive(env) {
14962
15169
  continue;
14963
15170
  }
14964
15171
  try {
14965
- item.title = titleOf((await persistence.inspect(item.sessionId)).events);
15172
+ item.title = titleOf((await inspectPersistedSession(env.ctx, item.sessionId)).events);
14966
15173
  } catch {}
14967
15174
  }
14968
15175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-bridge",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "会话桥(dsh-session-bridge):通过提示词创建新的主会话(顶层 UI 会话)、向任意会话发送消息、等待会话的下一条回复、读取会话消息,并按会话名或 id 跨工作区查找会话;此外支持监控/调度主任务与归档会话。",
5
5
  "private": false,
6
6
  "type": "module",
@@ -52,28 +52,28 @@
52
52
  },
53
53
  "license": "MIT",
54
54
  "peerDependencies": {
55
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
56
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
57
- "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
58
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
59
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
60
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
61
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
62
- "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
55
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
56
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
57
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
58
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
59
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
60
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
61
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
62
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
63
63
  "cordis": "^4.0.0-rc.7"
64
64
  },
65
65
  "devDependencies": {
66
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
67
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
68
- "@deepseek-ai/dsh-brand": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
69
- "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
70
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
71
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
72
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
73
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
74
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
75
- "@deepseek-ai/dsh-util-values": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
76
- "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0",
66
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
67
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
68
+ "@deepseek-ai/dsh-brand": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
69
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
70
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
71
+ "@deepseek-ai/dsh-scope": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
72
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
73
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
74
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
75
+ "@deepseek-ai/dsh-util-values": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
76
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0",
77
77
  "@types/node": "^24.0.0",
78
78
  "cordis": "^4.0.0-rc.7",
79
79
  "tsdown": "^0.22.14",
package/src/core.ts CHANGED
@@ -21,7 +21,9 @@ export interface LiveAgentLike {
21
21
  whenIdle?(): Promise<void>
22
22
  readonly ctx: Context
23
23
  readonly inbox?: {
24
- hasPending: boolean
24
+ // dsh 0.1.2 及之前:`hasPending` getter;dsh 0.1.5 起已删除,仅有
25
+ // nextTurn / nextStep 只读数组(Inbox 成为 driver 持有的接口)。
26
+ hasPending?: boolean
25
27
  nextTurn?: readonly unknown[]
26
28
  nextStep?: readonly unknown[]
27
29
  }
@@ -86,6 +88,34 @@ export function asEventList(events: unknown): readonly SessionEvent[] {
86
88
  return Array.isArray(events) ? events : []
87
89
  }
88
90
 
91
+ /**
92
+ * 读取一个持久化(离线)会话的事件日志与头部元数据。兼容两代 persistence API:
93
+ * - 旧版:`sessionPersistence.inspect(id)` 直接返回 { events, meta };
94
+ * - dsh 0.1.5 之后:`inspect` 已删除,改用 `open(id, 'read')` → `handle.read()`
95
+ * (+ `handle.header`),读句柄不抢写所有权,读后必须 close()。
96
+ * 会话不存在时两个 API 都抛错,调用方按原语义处理。
97
+ */
98
+ export async function inspectPersistedSession(ctx: Context, id: string): Promise<{ events: readonly SessionEvent[]; meta: { cwd?: string } }> {
99
+ const persistence = ctx.sessionPersistence as unknown as {
100
+ inspect?: (sid: string) => Promise<{ events: readonly SessionEvent[]; meta: { cwd?: string } }>
101
+ open?: (sid: string, access: 'read') => Promise<{
102
+ header: { cwd?: string }
103
+ read(): Promise<{ events: readonly SessionEvent[] }>
104
+ close(): Promise<void>
105
+ }>
106
+ }
107
+ if (typeof persistence.inspect === 'function') {
108
+ return await persistence.inspect(id)
109
+ }
110
+ const handle = await persistence.open!(id, 'read')
111
+ try {
112
+ const { events } = await handle.read()
113
+ return { events, meta: handle.header }
114
+ } finally {
115
+ await handle.close()
116
+ }
117
+ }
118
+
89
119
  /** agent.followup / steer 接受的用户消息值。 */
90
120
  export function userMessage(text: string): unknown {
91
121
  return {
@@ -237,18 +267,20 @@ export interface CoTLiveSlice {
237
267
  }
238
268
 
239
269
  /**
240
- * 对一个 live 会话的事件日志折叠出其"实时思维链"
241
- * - 优先聚合流式 assistant/chunk 事件里的 reasoning-delta(turn 中途即可见、增量,
242
- * 这正是监控思维链并按规则提前终止所需的粒度);
243
- * - 同时聚合同窗口的 text-delta 增量;
244
- * - 若没有进行中的 chunk(会话空闲、或该 provider 不流式推理/无推理),返回 null,
245
- * 调用方应回落为 foldMessages 里的已定型 message.reasoning。
270
+ * 对一个 live 会话的事件日志折叠出其"最近推理/文本增量流"。兼容两代日志形状:
271
+ * - dsh 0.1.5 之后:turn 内不再发布独立 chunk 事件,流式增量内嵌在已定型
272
+ * assistant/message(或未定型 assistant/attempt)事件的 `stream` 记录里
273
+ * (reasoning-chunks / text-chunks 批量段 + 原始 chunk:reasoning-delta / text-delta);
274
+ * - 0.1.2-rc 之前:独立的 assistant/chunk 事件(turn 中途即可见、增量,
275
+ * 这正是旧版监控思维链并按规则提前终止所需的粒度)。
276
+ * 两者兼读、取最新一份;无任何增量(会话空闲、或该 provider 不流式推理/无推理)
277
+ * 返回 null,调用方应回落为 foldMessages 里已定型的 message.reasoning。
246
278
  * 纯读、无副作用;事件形状不做任何假设,缺失/异常一律安全处理。
247
279
  */
248
280
  export function liveReasoningSnapshot(events: readonly SessionEvent[]): CoTLiveSlice | null {
249
281
  const list = asEventList(events)
250
- // 只累积"最近一条已定型 assistant/message 之后"的流式增量(当前 in-flight 窗口),
251
- // 避免把历史每一轮的思维链都拼接进来。turn/step 记录窗口内最近的 chunk 归属。
282
+ // 旧格式只累积"最近一条已定型 assistant/message 之后"的流式增量(in-flight 窗口);
283
+ // 新格式的 stream 随定型事件落地,直接以最新携带 stream 的事件为准。
252
284
  let lastFinalizedSeq = -1
253
285
  let reasoning = ''
254
286
  let text = ''
@@ -258,11 +290,42 @@ export function liveReasoningSnapshot(events: readonly SessionEvent[]): CoTLiveS
258
290
  let step = 0
259
291
  let foundDelta = false
260
292
  for (const event of list) {
261
- if (event.type === 'assistant/message') {
262
- lastFinalizedSeq = event.seq
293
+ const type = event.type as string
294
+ if (type === 'assistant/message' || type === 'assistant/attempt') {
295
+ const data = event.data as { turn?: unknown; step?: unknown; stream?: unknown } | null | undefined
296
+ if (data !== null && data !== undefined && typeof data === 'object' && Array.isArray(data.stream)) {
297
+ let r = ''
298
+ let t = ''
299
+ for (const raw of data.stream as unknown[]) {
300
+ const record = raw as { type?: unknown; texts?: unknown; text?: unknown; chunk?: unknown } | null
301
+ if (record === null || typeof record !== 'object') continue
302
+ if (record.type === 'reasoning-chunks' && Array.isArray(record.texts)) {
303
+ for (const s of record.texts as unknown[]) if (typeof s === 'string') r += s
304
+ } else if (record.type === 'text-chunks' && Array.isArray(record.texts)) {
305
+ for (const s of record.texts as unknown[]) if (typeof s === 'string') t += s
306
+ } else if (record.type === 'chunk') {
307
+ const c = record.chunk as { type?: unknown; text?: unknown } | null
308
+ if (c !== null && typeof c === 'object' && typeof c.type === 'string' && typeof c.text === 'string') {
309
+ if (c.type === 'reasoning-delta') r += c.text
310
+ else if (c.type === 'text-delta') t += c.text
311
+ }
312
+ }
313
+ }
314
+ if (r !== '' || t !== '') {
315
+ if (typeof data.turn === 'number') turn = data.turn
316
+ if (typeof data.step === 'number') step = data.step
317
+ reasoning = r
318
+ text = t
319
+ seq = event.seq
320
+ time = event.time
321
+ foundDelta = true
322
+ }
323
+ }
324
+ if (type === 'assistant/message') lastFinalizedSeq = event.seq
263
325
  continue
264
326
  }
265
- if (event.type !== 'assistant/chunk') continue
327
+ // 旧格式:独立 assistant/chunk 事件。
328
+ if (type !== 'assistant/chunk') continue
266
329
  if (event.seq <= lastFinalizedSeq) continue
267
330
  const data = event.data as { turn?: unknown; step?: unknown; chunk?: unknown } | null | undefined
268
331
  if (data === null || data === undefined || typeof data !== 'object') continue
@@ -559,7 +622,8 @@ export function statusSnapshot(ctx: Context, agent: LiveAgentLike): BridgeStatus
559
622
  lastTurn,
560
623
  lastActivityAt,
561
624
  stalledMs: lastActivityAt === null ? null : Date.now() - lastActivityAt,
562
- pendingWork: inbox?.hasPending ?? false,
625
+ // 双兼容:0.1.2 用 hasPending getter;0.1.5 起只有 nextTurn/nextStep 数组。
626
+ pendingWork: inbox?.hasPending === true || (inbox?.nextTurn?.length ?? 0) + (inbox?.nextStep?.length ?? 0) > 0,
563
627
  nextTurnCount: inbox?.nextTurn?.length ?? 0,
564
628
  nextStepCount: inbox?.nextStep?.length ?? 0,
565
629
  ...(lastAssistantText === undefined ? {} : { lastAssistantText }),
package/src/tools.ts CHANGED
@@ -7,7 +7,7 @@ import type { Context } from '@deepseek-ai/cordis'
7
7
  import { defineTool } from '@deepseek-ai/dsh-tools'
8
8
  import type { JsonValue } from '@deepseek-ai/dsh-util-values'
9
9
  import type {} from '@deepseek-ai/dsh-agent-presets'
10
- import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
10
+ import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
11
11
  import * as agentApi from '@deepseek-ai/dsh-agent'
12
12
  import type {
13
13
  BridgeFindItem,
@@ -15,11 +15,11 @@ import type {
15
15
  BridgeStatusSnapshot,
16
16
  BridgeWaitResult,
17
17
  LiveAgentLike,
18
- WaitForReplyOptions,
19
18
  } from './core.ts'
20
19
  import {
21
20
  attachSessionToWorkspace,
22
21
  foldMessages,
22
+ inspectPersistedSession,
23
23
  maxSeq,
24
24
  resolveTargetCwd,
25
25
  segmentsSince,
@@ -102,10 +102,10 @@ interface WaitArgs {
102
102
  timeoutMs?: number
103
103
  }
104
104
 
105
- async function maybeWait(env: BridgeEnv, session: { events: readonly SessionEvent[] }, baselineSeq: number, args: WaitArgs, signal: AbortSignal): Promise<ReturnType<typeof renderWait> | undefined> {
105
+ async function maybeWait(env: BridgeEnv, session: Session, baselineSeq: number, args: WaitArgs, signal: AbortSignal): Promise<ReturnType<typeof renderWait> | undefined> {
106
106
  if (args.waitForReply !== true) return undefined
107
107
  const result = await waitForReply({
108
- session: session as WaitForReplyOptions['session'],
108
+ session,
109
109
  baselineSeq,
110
110
  timeoutMs: clampTimeout(args.timeoutMs),
111
111
  signal,
@@ -354,8 +354,13 @@ function registerResume(env: BridgeEnv): void {
354
354
  }
355
355
  let headers: readonly { id: string; cwd?: string; createdAt: number }[] = []
356
356
  try {
357
- const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { id: string; cwd?: string; createdAt: number }[]> }
358
- headers = await persistence.list()
357
+ // dsh >= 0.1.5-alpha.1:list() 返回 { header, ... } 快照;映射回平铺字段。
358
+ const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { header: { id: string; cwd?: string; createdAt: number } }[]> }
359
+ headers = (await persistence.list()).map((rec) => ({
360
+ id: rec.header.id,
361
+ ...(rec.header.cwd === undefined ? {} : { cwd: rec.header.cwd }),
362
+ createdAt: rec.header.createdAt,
363
+ }))
359
364
  } catch (error) {
360
365
  throw new Error('session persistence unavailable: ' + (error instanceof Error ? error.message : String(error)))
361
366
  }
@@ -502,8 +507,7 @@ function registerSegments(env: BridgeEnv): void {
502
507
  live = true
503
508
  } else {
504
509
  try {
505
- const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
506
- const inspection = await persistence.inspect(args.sessionId)
510
+ const inspection = await inspectPersistedSession(env.ctx, args.sessionId)
507
511
  events = inspection.events
508
512
  live = false
509
513
  } catch (error) {
@@ -580,8 +584,7 @@ function registerRead(env: BridgeEnv): void {
580
584
  } else {
581
585
  let inspection: { events: readonly SessionEvent[]; meta: { cwd?: string } }
582
586
  try {
583
- const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[]; meta: { cwd?: string } }> }
584
- inspection = await persistence.inspect(args.sessionId)
587
+ inspection = await inspectPersistedSession(env.ctx, args.sessionId)
585
588
  } catch (error) {
586
589
  throw new Error('cannot read session ' + JSON.stringify(args.sessionId) + ': ' + (error instanceof Error ? error.message : String(error)))
587
590
  }
@@ -685,8 +688,11 @@ function registerFind(env: BridgeEnv): void {
685
688
  }
686
689
  if (args.liveOnly !== true) {
687
690
  try {
688
- const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { id: string; cwd?: string; parentSession?: string; origin?: 'subagent'; createdAt: number; agentPreset?: string }[]> }
689
- for (const header of await persistence.list()) {
691
+ // dsh >= 0.1.5-alpha.1:list() 返回 { header: SessionHeader, revision, ... } 快照,
692
+ // 不再平铺 id/cwd;旧版直接返回 header 平铺字段。两者都按新形状读,向下兼容。
693
+ const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { header: { id: string; cwd?: string; parentSession?: string; origin?: 'subagent'; createdAt: number; agentPreset?: string } }[]> }
694
+ for (const rec of await persistence.list()) {
695
+ const header = rec.header
690
696
  if (items.some((item) => item.sessionId === header.id)) continue
691
697
  items.push({
692
698
  sessionId: header.id,
@@ -720,13 +726,12 @@ function registerFind(env: BridgeEnv): void {
720
726
  const cwdFilter = typeof args.cwd === 'string' ? args.cwd.trim().toLowerCase() : ''
721
727
  const needsOfflineTitle = titleFilter !== '' || (query !== '' && items.some((item) => !item.live))
722
728
  if (needsOfflineTitle) {
723
- const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
724
729
  let inspected = 0
725
730
  for (const item of items) {
726
731
  if (inspected >= 30) break
727
732
  if (item.live || item.title !== undefined) continue
728
733
  try {
729
- const inspection = await persistence.inspect(item.sessionId)
734
+ const inspection = await inspectPersistedSession(env.ctx, item.sessionId)
730
735
  item.title = titleOf(inspection.events)
731
736
  inspected += 1
732
737
  } catch {
@@ -1118,7 +1123,6 @@ function registerArchive(env: BridgeEnv): void {
1118
1123
  const archived = env.ctx.workspaceRegistry.archivedSessionIds.map(String)
1119
1124
  const items: Array<{ sessionId: string; title?: string }> = archived.map((id) => ({ sessionId: id }))
1120
1125
  if (args.resolveTitles === true && items.length > 0) {
1121
- const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
1122
1126
  const registryTitles = new Map<string, string>()
1123
1127
  try {
1124
1128
  const records = await env.registry.all()
@@ -1130,7 +1134,7 @@ function registerArchive(env: BridgeEnv): void {
1130
1134
  continue
1131
1135
  }
1132
1136
  try {
1133
- const inspection = await persistence.inspect(item.sessionId)
1137
+ const inspection = await inspectPersistedSession(env.ctx, item.sessionId)
1134
1138
  item.title = titleOf(inspection.events)
1135
1139
  } catch { /* offline title unavailable */ }
1136
1140
  }