dsh-session-bridge 0.3.2-alpha.1 → 0.3.2-alpha.3

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.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js
7
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-home-paths@0.1.6-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.6-alpha.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.6-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.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
2348
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.6-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.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
2560
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.6-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,7 +2570,7 @@ function randomUUID$1() {
2570
2570
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2571
2571
  }
2572
2572
  //#endregion
2573
- //#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-brand/lib/index.js
2573
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.6-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-brand/lib/index.js
2574
2574
  /**
2575
2575
  * Duplicate-install-safe nominal primitive helpers.
2576
2576
  *
@@ -3187,11 +3187,11 @@ defineMethod("transform", [
3187
3187
  "preserve"
3188
3188
  ], ({ inner }, isInner) => inner.toString(isInner));
3189
3189
  //#endregion
3190
- //#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.6-alpha.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.6-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.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3194
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.6-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3195
3195
  /**
3196
3196
  * Bound one `notice` summary to {@link CONTEXT_SUMMARY_MAX_CHARS}.
3197
3197
  * @param summary - the producer's one-line account, of any length.
@@ -3957,7 +3957,8 @@ var LlmError = class extends HarnessError {
3957
3957
  id: model.id,
3958
3958
  ...model.name === void 0 ? {} : { name: model.name },
3959
3959
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
3960
- ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
3960
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
3961
+ ...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
3961
3962
  });
3962
3963
  }
3963
3964
  return models;
@@ -9080,7 +9081,7 @@ function superRefine(fn, params) {
9080
9081
  return /* @__PURE__ */ _superRefine(fn, params);
9081
9082
  }
9082
9083
  //#endregion
9083
- //#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
9084
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-storage@0.1.6-alpha.2_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-storage/lib/index.js
9084
9085
  /**
9085
9086
  * Backend-facing vocabulary of the storage hub: a backend owns one medium
9086
9087
  * (a file-tree root, a database file) and exposes operation groups over it.
@@ -9091,7 +9092,7 @@ function superRefine(fn, params) {
9091
9092
  /** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
9092
9093
  const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/;
9093
9094
  //#endregion
9094
- //#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+ds_404bb44176e435660710548cd6a4e0aa/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
9095
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-storage-domain@0.1.6-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+ds_5fa439225de576bdb2338dd42cd57e82/node_modules/@deepseek-ai/dsh-storage-domain/lib/index.js
9095
9096
  /**
9096
9097
  * Domain declaration vocabulary. A spec object is the single source of a
9097
9098
  * domain's identity, layout, and record schemas: the owning package defines
@@ -9214,11 +9215,19 @@ Service.init;
9214
9215
  */
9215
9216
  /**
9216
9217
  * 兼容读取不同 dsh-session 版本上的会话事件:
9217
- * - 旧版 `Session` 暴露 `get events(): readonly SessionEvent[]`;
9218
- * - 新版(如宿主实际运行的 0.1.2-rc.x)将 `get events` 改为
9219
- * `snapshotEvents(fromSeq?, toSeqExclusive?)` 方法——`.events` 直接读取会
9220
- * 得到 `undefined`,对 for...of 迭代即抛 `events is not iterable`。
9218
+ * - 旧版 \`Session\` 暴露 \`get events(): readonly SessionEvent[]\`;
9219
+ * - 新版将 \`get events\` 改为 \`snapshotEvents(fromSeq?, toSeqExclusive?)\` 方法——
9220
+ * \`.events\` 直接读取会得到 \`undefined\`,对 for...of 迭代即抛
9221
+ * \`events is not iterable\`。
9221
9222
  * 两者都读不到(或 session 不存在)时回退为空数组,绝不抛迭代错误。
9223
+ *
9224
+ * 注意:dsh 0.1.6-alpha.1 起 \`snapshotEvents()\`(连同 \`eventAt()\` /
9225
+ * \`ownEvents()\`)已标记 @deprecated —— 官方策略是"现有逻辑可暂不迁移,但禁止
9226
+ * 新增调用";其替代不是同步读,而是(a)恢复后读取 Session 投影/派生状态,或
9227
+ * (b)按需异步分页读取历史窗口(见 dsh 决策
9228
+ * 2026-09-09-deprecate-synchronous-session-event-reads)。本函数的调用方
9229
+ * (foldMessages / segmentsSince / wait 基线)仍依赖完整同步快照,故按该决定暂缓
9230
+ * 迁移;将来官方补齐分页读后,只需改造这一个入口。
9222
9231
  */
9223
9232
  function sessionEvents(session) {
9224
9233
  const s = session;
@@ -9526,8 +9535,11 @@ function sleep(ms) {
9526
9535
  * 因此默认不再用 turn/end 作门控;需要完整收尾语义时用 requireTurnEnd:true 显式开启,
9527
9536
  * 此时才会等到其后出现 turn/end。
9528
9537
  *
9529
- * 返回时 message 为"最新的带文本 assistant 行",否则为最新 assistant 行(可能无文本,
9530
- * 如纯工具调用中间态);超时/中止返回已收集内容。
9538
+ * 返回的 message:默认是 baseline 之后最新的一条**带文本** assistant 行;整个等待
9539
+ * 窗口内没有等到新文本回复时,回落为 baseline 及之前最新的一条带文本回复并置
9540
+ * stale:true(绝不把更晚的无文本中间行当回复返回 —— 那正是渲染层打印 "(no text)"
9541
+ * 的来源)。segment 模式同理:新段落优先,否则回落既有段落。
9542
+ * 超时/中止返回已收集内容;timedOut 表示"本次等待要求的输出未在预算内出现"。
9531
9543
  */
9532
9544
  async function waitForReply(opts) {
9533
9545
  const started = Date.now();
@@ -9564,7 +9576,25 @@ async function waitForReply(opts) {
9564
9576
  if (Date.now() >= deadline) break;
9565
9577
  await sleep(100);
9566
9578
  }
9567
- let message = waitForSegment && segment !== null ? {
9579
+ const observedNew = waitForSegment ? segment !== null : textReply !== null;
9580
+ const sawNewRow = latest !== null;
9581
+ let stale = false;
9582
+ if (waitForSegment) {
9583
+ if (segment === null) {
9584
+ const prev = lastSegmentUpTo(sessionEvents(opts.session), opts.baselineSeq);
9585
+ if (prev !== null) {
9586
+ segment = prev;
9587
+ stale = true;
9588
+ }
9589
+ }
9590
+ } else if (textReply === null) {
9591
+ const prev = lastTextRowUpTo(foldMessages(sessionEvents(opts.session)), opts.baselineSeq);
9592
+ if (prev !== null) {
9593
+ textReply = prev;
9594
+ stale = true;
9595
+ }
9596
+ }
9597
+ const message = waitForSegment && segment !== null ? {
9568
9598
  seq: segment.seq,
9569
9599
  time: segment.time,
9570
9600
  role: "assistant",
@@ -9572,16 +9602,34 @@ async function waitForReply(opts) {
9572
9602
  ...segment.text !== void 0 ? { text: segment.text } : {},
9573
9603
  ...segment.reasoning !== void 0 ? { reasoning: segment.reasoning } : {},
9574
9604
  ...segment.toolCalls.length > 0 ? { toolCalls: segment.toolCalls } : {}
9575
- } : textReply ?? latest;
9605
+ } : textReply;
9576
9606
  return {
9577
9607
  message,
9578
9608
  seq: message === null ? opts.baselineSeq : message.seq,
9579
9609
  turnEnded,
9580
- timedOut: requireTurnEnd ? latest !== null && !turnEnded : waitForSegment ? segment === null : textReply === null,
9610
+ timedOut: requireTurnEnd ? !(sawNewRow && turnEnded) : !observedNew,
9611
+ stale,
9581
9612
  aborted: opts.signal !== void 0 && opts.signal.aborted,
9582
9613
  waitedMs: Date.now() - started
9583
9614
  };
9584
9615
  }
9616
+ /** seq 及之前最新一个已完成输出段落(wait 零新输出时回落既有段落)。 */
9617
+ function lastSegmentUpTo(events, seq) {
9618
+ const segs = segmentsSince(events);
9619
+ for (let i = segs.length - 1; i >= 0; i -= 1) {
9620
+ const seg = segs[i];
9621
+ if (seg !== void 0 && seg.seq <= seq) return seg;
9622
+ }
9623
+ return null;
9624
+ }
9625
+ /** seq 及之前最新一条带文本的 assistant 行(wait 零新文本回复时回落既有回复)。 */
9626
+ function lastTextRowUpTo(rows, seq) {
9627
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
9628
+ const row = rows[i];
9629
+ if (row !== void 0 && row.role === "assistant" && row.text !== void 0 && row.seq <= seq) return row;
9630
+ }
9631
+ return null;
9632
+ }
9585
9633
  /**
9586
9634
  * 解析目标工作目录:workspaceId(注册表)> cwd(绝对路径/任意拼写)> 调用者会话 cwd。
9587
9635
  * 跨工作区由 workspaceId / cwd 任意指定实现。
@@ -9633,6 +9681,14 @@ async function attachSessionToWorkspace(ctx, sessionId, cwd) {
9633
9681
  return workspace.id;
9634
9682
  }
9635
9683
  /**
9684
+ * 卡住判定(唯一真源,status 渲染与监控 watchdog 共用):只有 **running** 会话才可能
9685
+ * "卡住" —— 空闲/已收尾的会话没有进展是正常状态,不能报 stall。阈值语义为"超过"
9686
+ * (严格大于),与工具描述/README 的 "more than stalledMs" 一致。
9687
+ */
9688
+ function isStalled(running, stalledMs, thresholdMs) {
9689
+ return running === "running" && stalledMs !== null && stalledMs > thresholdMs;
9690
+ }
9691
+ /**
9636
9692
  * 计算一个 live 会话的监控快照(agent 状态 + 事件折叠出的进度/卡住事实)。
9637
9693
  * 纯读,无副作用。用于 `session_bridge_status`。
9638
9694
  */
@@ -9852,7 +9908,7 @@ var SessionMonitor = class {
9852
9908
  this.stopWithLog(sessionId);
9853
9909
  return;
9854
9910
  }
9855
- if (snapshot.running === "running" && snapshot.stalledMs !== null && snapshot.stalledMs > (entry.config.stalledMs ?? 6e4)) {
9911
+ if (isStalled(snapshot.running, snapshot.stalledMs, entry.config.stalledMs ?? 6e4)) {
9856
9912
  entry.stuckCount += 1;
9857
9913
  entry.lastActionAt = Date.now();
9858
9914
  if (this.shouldCancel(entry)) {
@@ -10041,7 +10097,7 @@ var SessionMonitor = class {
10041
10097
  }
10042
10098
  };
10043
10099
  //#endregion
10044
- //#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_eaf7046112a9b60866fe7f69bc8f6f64/node_modules/@deepseek-ai/dsh-scope/lib/index.js
10100
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1.6-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-invaria_8802cbeca2a66ef97a5b4e663664f4cd/node_modules/@deepseek-ai/dsh-scope/lib/index.js
10045
10101
  /**
10046
10102
  * Shared insertion-ordered storage and effect ownership for scope-aware registries.
10047
10103
  *
@@ -10320,7 +10376,7 @@ function scopeTarget(base, key) {
10320
10376
  return carrier;
10321
10377
  }
10322
10378
  //#endregion
10323
- //#region node_modules/.pnpm/@deepseek-ai+dsh-sandbox@0.1.6-alpha.1_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-llm@0_1d3a97f96d2766469566ca2b01247bfa/node_modules/@deepseek-ai/dsh-sandbox/lib/index.js
10379
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-sandbox@0.1.6-alpha.2_@deepseek-ai+cordis@4.0.2_@deepseek-ai+dsh-llm@0_5328c6913d63e98a7878ca36cab93ca2/node_modules/@deepseek-ai/dsh-sandbox/lib/index.js
10324
10380
  /**
10325
10381
  * The escalation vocabulary and choreography shared by every sandbox-enforcing
10326
10382
  * tool family (`@deepseek-ai/dsh-tool-bash`, `@deepseek-ai/dsh-tool-fs`): the
@@ -10371,21 +10427,18 @@ function validateEscalationArgs(sandboxPermissions, justification) {
10371
10427
  if (justification !== void 0 && justification.trim().length === 0) throw new Error("invalid justification: expected a non-empty sentence");
10372
10428
  }
10373
10429
  /**
10374
- * Resolve a sandbox-escalation request BEFORE anything executes: check strict
10375
- * widening against the call's effective mode, then resolve the approval
10376
- * channel, then map every outcome — the ordered fail-closed sequence both
10377
- * enforcing families share. Returns the granted mode to stamp onto exactly
10378
- * this call; throws the distinct verbatim text for every other path (a
10379
- * non-widening request, a missing approval service, an agent-less execution,
10380
- * a rejection, a cancellation, an unanswerable ask) — the tool registry turns
10381
- * the throw into the call's isError result, and nothing has run. A
10382
- * non-widening request never prompts a human.
10430
+ * Resolve a sandbox permission request before execution. Repeating the call's
10431
+ * effective mode returns it without approval. A strictly wider mode requires
10432
+ * approval and applies only to this call. Narrower or unsupported targets,
10433
+ * missing approval services or agents for widening, and non-grant outcomes
10434
+ * throw before execution.
10383
10435
  * @param request - the escalation to judge (see {@link EscalationRequest}).
10384
10436
  * @param approval - the approval ingredients the tool holds (see {@link EscalationApproval}).
10385
10437
  * @returns the granted mode, consumed by the one call that asked.
10386
10438
  */
10387
10439
  async function approveEscalation(request, approval) {
10388
10440
  const { requestedMode: mode, effectiveMode, justification, subject } = request;
10441
+ if (mode === effectiveMode) return effectiveMode;
10389
10442
  if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode)) throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`);
10390
10443
  if (approval.approver === void 0) throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval service is composed`);
10391
10444
  if (approval.agent === void 0) throw new Error(`sandbox escalation to "${mode}" requires approval, but the call has no agent to route it through`);
@@ -10405,7 +10458,7 @@ async function approveEscalation(request, approval) {
10405
10458
  }
10406
10459
  }
10407
10460
  //#endregion
10408
- //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.6-alpha.1_e89197212305e1b19bd6df05221bc44b/node_modules/@deepseek-ai/dsh-tools/lib/index.js
10461
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1.6-alpha.2_e8d664b8446198a3dac4043bd11091d7/node_modules/@deepseek-ai/dsh-tools/lib/index.js
10409
10462
  /**
10410
10463
  * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
10411
10464
  * types, subagents, and workflows. The subset accepts any JSON root, an
@@ -14018,7 +14071,7 @@ function toolAbortedBeforeDispatchResult(prior) {
14018
14071
  };
14019
14072
  }
14020
14073
  //#endregion
14021
- //#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.6-alpha.1_ce40d13c69e7e1ce48818baba37665e1/node_modules/@deepseek-ai/dsh-agent/lib/index.js
14074
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-agent@0.1.6-alpha.2_44110910133b3cc261ea9dfdcaed39ee/node_modules/@deepseek-ai/dsh-agent/lib/index.js
14022
14075
  /**
14023
14076
  * Agent-scoped model selection shared by runtime entry points.
14024
14077
  * @module @deepseek-ai/dsh-agent/model-selection
@@ -14135,6 +14188,14 @@ function liveAgent(env, sessionId) {
14135
14188
  function liveAgents(env) {
14136
14189
  return env.ctx.agents.list();
14137
14190
  }
14191
+ /** 等待结果里的状态附注行(wait / send / create 三处渲染共用)。 */
14192
+ function waitNotes(reply) {
14193
+ const notes = [];
14194
+ if (reply.timedOut === true) notes.push("[wait timed out]");
14195
+ if (reply.stale === true) notes.push("[stale] pre-existing reply — nothing new arrived during the wait");
14196
+ if (reply.aborted === true) notes.push("[wait aborted]");
14197
+ return notes;
14198
+ }
14138
14199
  /** 渲染等待结果为 JSON 友好值。 */
14139
14200
  function renderWait(wait) {
14140
14201
  const row = wait.message;
@@ -14150,6 +14211,7 @@ function renderWait(wait) {
14150
14211
  seq: wait.seq,
14151
14212
  turnEnded: wait.turnEnded,
14152
14213
  timedOut: wait.timedOut,
14214
+ ...wait.stale === true ? { stale: true } : {},
14153
14215
  aborted: wait.aborted,
14154
14216
  waitedMs: wait.waitedMs
14155
14217
  };
@@ -14221,9 +14283,10 @@ function registerCreate(env) {
14221
14283
  if (typeof v.cwd === "string") lines.push(`cwd: ${v.cwd}`);
14222
14284
  if (typeof v.title === "string") lines.push(`title: ${v.title}`);
14223
14285
  const reply = v.reply;
14286
+ if (typeof v.sinceSeq === "number") lines.push(`sinceSeq: ${String(v.sinceSeq)} (pass to session_bridge_wait)`);
14224
14287
  if (reply !== void 0) {
14225
14288
  lines.push(`reply: ${String(reply.message?.text ?? "(no text)")}`);
14226
- if (reply.timedOut === true) lines.push("[wait timed out]");
14289
+ lines.push(...waitNotes(reply));
14227
14290
  }
14228
14291
  return [{
14229
14292
  type: "text",
@@ -14290,8 +14353,10 @@ function registerCreate(env) {
14290
14353
  source: "create"
14291
14354
  });
14292
14355
  let reply;
14356
+ let createBaseline;
14293
14357
  if (typeof args.prompt === "string" && args.prompt.trim() !== "") {
14294
14358
  const baseline = maxSeq(sessionEvents(agent.session));
14359
+ createBaseline = baseline;
14295
14360
  agent.followup(userMessage(args.prompt.trim()));
14296
14361
  env.registry.touch(sessionId);
14297
14362
  if (args.waitForReply === true) reply = await maybeWait(env, agent.session, baseline, args, exec.signal);
@@ -14301,6 +14366,7 @@ function registerCreate(env) {
14301
14366
  cwd: targetCwd,
14302
14367
  ...workspaceId === void 0 ? {} : { workspaceId },
14303
14368
  ...typeof args.title === "string" && args.title.trim() !== "" ? { title: args.title.trim() } : {},
14369
+ ...reply === void 0 && createBaseline !== void 0 ? { sinceSeq: createBaseline } : {},
14304
14370
  ...reply === void 0 ? {} : { reply }
14305
14371
  });
14306
14372
  }
@@ -14344,9 +14410,10 @@ function registerSend(env) {
14344
14410
  const v = value;
14345
14411
  const reply = v.reply;
14346
14412
  const lines = ["sent to " + String(v.sessionId)];
14413
+ if (typeof v.sinceSeq === "number") lines.push("sinceSeq: " + String(v.sinceSeq) + " (pass to session_bridge_wait)");
14347
14414
  if (reply !== null && reply !== void 0) {
14348
14415
  lines.push("reply: " + String(reply.message?.text ?? "(no text)"));
14349
- if (reply.timedOut === true) lines.push("[wait timed out]");
14416
+ lines.push(...waitNotes(reply));
14350
14417
  }
14351
14418
  return [{
14352
14419
  type: "text",
@@ -14372,7 +14439,7 @@ function registerSend(env) {
14372
14439
  return asJson({
14373
14440
  accepted: true,
14374
14441
  sessionId: args.sessionId,
14375
- ...reply === void 0 ? {} : { reply }
14442
+ ...reply === void 0 ? { sinceSeq: baseline } : { reply }
14376
14443
  });
14377
14444
  }
14378
14445
  }));
@@ -14467,7 +14534,7 @@ function registerResume(env) {
14467
14534
  function registerWait(env) {
14468
14535
  env.ctx.tools.register(defineTool({
14469
14536
  name: "session_bridge_wait",
14470
- 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).",
14537
+ 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. If no new output arrives within the budget, the latest PRE-EXISTING reply/segment is returned with stale=true (so an already-landed reply is never lost as \"(no text)\"). 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); to read a reply that may already exist, pass the sinceSeq anchor returned by send/create.",
14471
14538
  parameters: {
14472
14539
  sessionId: {
14473
14540
  type: "string",
@@ -14476,7 +14543,7 @@ function registerWait(env) {
14476
14543
  },
14477
14544
  sinceSeq: {
14478
14545
  type: "number",
14479
- description: "Only replies after this event seq count (default: latest seq at call time)."
14546
+ description: "Only replies after this event seq count (default: latest seq at call time; -1 = count every event, the anchor returned by create for a brand-new session)."
14480
14547
  },
14481
14548
  timeoutMs: {
14482
14549
  type: "number",
@@ -14504,10 +14571,9 @@ function registerWait(env) {
14504
14571
  text: "no reply observed"
14505
14572
  }];
14506
14573
  const message = reply.message;
14507
- const lines = ["reply seq " + String(reply.seq) + ": " + String(message === null ? "(no text)" : message.text ?? "(no text)")];
14574
+ const lines = [message === null ? "no new reply within " + String(reply.waitedMs ?? "?") + "ms (no assistant text in this session)" : "reply seq " + String(reply.seq) + ": " + String(message.text ?? "(no text)")];
14508
14575
  if (typeof reply.turnEnded === "boolean") lines.push("turnEnded: " + String(reply.turnEnded));
14509
- if (reply.timedOut === true) lines.push("[wait timed out]");
14510
- if (reply.aborted === true) lines.push("[wait aborted]");
14576
+ lines.push(...waitNotes(reply));
14511
14577
  return [{
14512
14578
  type: "text",
14513
14579
  text: lines.join("\n")
@@ -14518,28 +14584,19 @@ function registerWait(env) {
14518
14584
  if (typeof args.sessionId !== "string" || args.sessionId.trim() === "") throw new Error("invalid sessionId: expected a non-empty string");
14519
14585
  const agent = liveAgent(env, args.sessionId);
14520
14586
  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)");
14521
- const waitSegment = args.waitFor === "segment";
14522
- let baseline;
14523
- if (typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) baseline = args.sinceSeq;
14524
- else if (waitSegment) {
14525
- const segs = segmentsSince(sessionEvents(agent.session));
14526
- baseline = segs.length === 0 ? -1 : segs[segs.length - 1]?.seq ?? -1;
14527
- } else {
14528
- let lastText = -1;
14529
- for (const row of foldMessages(sessionEvents(agent.session))) if (row.text !== void 0) lastText = row.seq;
14530
- baseline = lastText;
14531
- }
14587
+ const baseline = typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= -1 ? args.sinceSeq : maxSeq(sessionEvents(agent.session));
14532
14588
  const result = await waitForReply({
14533
14589
  session: agent.session,
14534
14590
  baselineSeq: baseline,
14535
14591
  timeoutMs: clampTimeout(args.timeoutMs),
14536
14592
  signal: exec.signal,
14537
14593
  requireTurnEnd: args.requireTurnEnd === true,
14538
- ...waitSegment ? { waitForSegment: true } : {}
14594
+ ...args.waitFor === "segment" ? { waitForSegment: true } : {}
14539
14595
  });
14540
14596
  env.registry.touch(args.sessionId);
14541
14597
  return asJson({
14542
14598
  sessionId: args.sessionId,
14599
+ sinceSeq: baseline,
14543
14600
  reply: renderWait(result),
14544
14601
  running: agent.status === "running"
14545
14602
  });
@@ -15145,7 +15202,11 @@ function pruneReasoning(snapshot, mode) {
15145
15202
  else delete rest.lastReasoning;
15146
15203
  return rest;
15147
15204
  }
15148
- /** 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复(+ 思维链预览)。 */
15205
+ /**
15206
+ * 渲染监控快照为一行摘要:运行态 + openTurn + 卡住/待处理 + 最新回复(+ 思维链预览)。
15207
+ * 卡住判定走 core.isStalled(唯一真源):只有 running 会话才可能被标 [STALLED] ——
15208
+ * 空闲/已收尾的会话"很久没事件"是正常状态,不是卡住(与监控 watchdog 一致)。
15209
+ */
15149
15210
  function renderStatus(snapshot, stalledMsThreshold) {
15150
15211
  const lines = [];
15151
15212
  const runLabel = snapshot.running === "running" ? "running" : "idle";
@@ -15153,7 +15214,7 @@ function renderStatus(snapshot, stalledMsThreshold) {
15153
15214
  if (snapshot.openTurn) lines.push(`openTurn: yes (turn #${snapshot.lastTurn})`);
15154
15215
  else lines.push(`openTurn: no (last turn #${snapshot.lastTurn})`);
15155
15216
  if (snapshot.stalledMs !== null) {
15156
- const stalled = snapshot.stalledMs >= stalledMsThreshold;
15217
+ const stalled = isStalled(snapshot.running, snapshot.stalledMs, stalledMsThreshold);
15157
15218
  lines.push(`lastActivity: now-${snapshot.stalledMs}ms${stalled ? " [STALLED]" : ""}`);
15158
15219
  }
15159
15220
  if (snapshot.pendingWork) lines.push(`pendingWork: ${snapshot.nextTurnCount} turn + ${snapshot.nextStepCount} step`);
@@ -15167,7 +15228,7 @@ function renderStatus(snapshot, stalledMsThreshold) {
15167
15228
  function registerStatus(env) {
15168
15229
  env.ctx.tools.register(defineTool({
15169
15230
  name: "session_bridge_status",
15170
- 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.",
15231
+ 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 a RUNNING session as stalled once the time since the last event exceeds it (idle sessions are never flagged — a quiet finished session is not stuck). 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.",
15171
15232
  parameters: {
15172
15233
  sessionId: {
15173
15234
  type: "string",
@@ -15176,7 +15237,7 @@ function registerStatus(env) {
15176
15237
  },
15177
15238
  stalledMsThreshold: {
15178
15239
  type: "number",
15179
- description: "Mark the session STALLED when time since the last event exceeds this many ms (default 60000)."
15240
+ description: "Mark a RUNNING session STALLED when time since the last event exceeds this many ms (default 60000); idle sessions are never marked."
15180
15241
  },
15181
15242
  recent: {
15182
15243
  type: "number",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-bridge",
3
- "version": "0.3.2-alpha.1",
3
+ "version": "0.3.2-alpha.3",
4
4
  "description": "会话桥(dsh-session-bridge):通过提示词创建新的主会话(顶层 UI 会话)、向任意会话发送消息、等待会话的下一条回复、读取会话消息,并按会话名或 id 跨工作区查找会话;此外支持监控/调度主任务与归档会话。",
5
5
  "private": false,
6
6
  "type": "module",
@@ -83,6 +83,7 @@
83
83
  "build": "bash scripts/build.sh",
84
84
  "build:client": "tsdown",
85
85
  "check:compat": "node scripts/check-dsh-compat.mjs",
86
+ "test": "node --experimental-strip-types scripts/test-bridge-core.mjs",
86
87
  "typecheck": "tsc -p tsconfig.json --noEmit"
87
88
  }
88
89
  }
@@ -79,6 +79,13 @@ function resolveScopeDir(argv) {
79
79
  }
80
80
  } catch { /* dsh not on PATH; the other anchors decide */ }
81
81
 
82
+ // Anchor 3 (last resort): this repository's own install. CI pins the
83
+ // @deepseek-ai packages through pnpm overrides and installs no DSH app, so
84
+ // the scope only exists here; probing it keeps check:compat runnable in CI.
85
+ // A developer machine resolves one of the anchors above first, so the
86
+ // *running* harness keeps priority there.
87
+ candidates.push(join(ROOT, 'node_modules', '@deepseek-ai'))
88
+
82
89
  for (const candidate of candidates) if (isScopeDir(candidate)) return candidate
83
90
  return undefined
84
91
  }
@@ -115,9 +122,57 @@ function dshVersionOf(scopeDir) {
115
122
  const manifest = readJson(candidate)
116
123
  if (manifest?.name === '@deepseek-ai/dsh' && typeof manifest.version === 'string') return manifest.version
117
124
  }
125
+ // Repo-local install (CI overrides): there is no `dsh` app package, but the
126
+ // release versions every dsh-* package together, so any of them names the
127
+ // harness version the types and the bundle were resolved from.
128
+ for (const pkg of ['dsh-tools', 'dsh-session', 'dsh-agent']) {
129
+ const manifest = readJson(join(scopeDir, pkg, 'package.json'))
130
+ if (typeof manifest?.version === 'string') return manifest.version
131
+ }
118
132
  return undefined
119
133
  }
120
134
 
135
+ /**
136
+ * Inspect the built tsdown bundle's source map for the DSH version it INLINED.
137
+ *
138
+ * `check:compat` type-checks src/ against the installed harness, but src/ is not
139
+ * what loads: lib/index.js is a self-contained bundle that inlines every
140
+ * @deepseek-ai/* import. A stale checkout therefore type-checks green while the
141
+ * artifact ships old DSH code (observed: checkout 0.1.5-alpha.2 / host
142
+ * 0.1.6-alpha.2). This reads the map's sources — a checkout path names the root
143
+ * whose package.json carries the version; a .pnpm path encodes it.
144
+ *
145
+ * @returns { versions: Set<string>, roots: Set<string> } or undefined when no map exists.
146
+ */
147
+ function artifactProvenance() {
148
+ const mapPath = join(ROOT, 'lib', 'index.js.map')
149
+ if (!existsSync(mapPath)) return undefined
150
+ let map
151
+ try { map = JSON.parse(readFileSync(mapPath, 'utf8')) } catch { return undefined }
152
+ const versions = new Set()
153
+ const roots = new Set()
154
+ for (const source of Array.isArray(map.sources) ? map.sources : []) {
155
+ if (typeof source !== 'string') continue
156
+ // Source checkout: <root>/packages/... or <root>/vendor/... (relative to the map in lib/).
157
+ const checkout = source.match(/^(.*?)[/\\](?:packages|vendor)[/\\]/)
158
+ if (checkout !== null) { roots.add(resolve(ROOT, 'lib', checkout[1])); continue }
159
+ // Registry install: .../.pnpm/@deepseek-ai+dsh-<name>@<version>[_hash]/node_modules/...
160
+ // Only dsh-* packages name the harness release; sibling @deepseek-ai
161
+ // packages (cordis, schemastery) are versioned independently, so counting
162
+ // them would make every CI build look like a provenance mismatch.
163
+ const registry = source.match(/[\\/]\.pnpm[\\/]@deepseek-ai\+dsh-[a-z0-9-]+@([^\\/]+)[\\/]node_modules/)
164
+ if (registry !== null) {
165
+ const version = registry[1].replace(/_.*$/, '')
166
+ if (/^\d/.test(version)) versions.add(version)
167
+ }
168
+ }
169
+ for (const root of roots) {
170
+ const manifest = readJson(join(root, 'package.json'))
171
+ if (typeof manifest?.version === 'string') versions.add(manifest.version)
172
+ }
173
+ return { versions, roots }
174
+ }
175
+
121
176
  const dshVersion = dshVersionOf(scope) ?? '(unknown)'
122
177
 
123
178
  // Every DSH package src/ imports, plus the plain `cordis` specifier that
@@ -170,4 +225,18 @@ if (failure !== undefined) {
170
225
  const status = typeof failure.status === 'number' ? ' (tsc exit ' + failure.status + ')' : ''
171
226
  fail('src/ does NOT type-check against installed dsh ' + dshVersion + status + ' — this is a real incompatibility with the running harness.')
172
227
  }
173
- console.log('check-dsh-compat: OK — src/ type-checks against installed dsh ' + dshVersion)
228
+ console.log('check-dsh-compat: OK — src/ type-checks against installed dsh ' + dshVersion)
229
+
230
+ const provenance = artifactProvenance()
231
+ if (provenance === undefined) {
232
+ console.log('check-dsh-compat: no lib/index.js.map — skipping bundle provenance check (run pnpm build:client to enable it)')
233
+ } else if (provenance.versions.size === 0) {
234
+ console.log('check-dsh-compat: bundle provenance not resolvable from the source map — skipped')
235
+ } else {
236
+ const inlined = [...provenance.versions].sort().join(', ')
237
+ console.log('check-dsh-compat: lib/index.js inlined DSH ' + inlined + ' (from ' + provenance.roots.size + ' checkout(s) / registry entries)')
238
+ if (dshVersion !== '(unknown)' && !(provenance.versions.size === 1 && provenance.versions.has(dshVersion))) {
239
+ fail('the built lib/index.js inlines DSH ' + inlined + ' but the installed harness is ' + dshVersion
240
+ + ' — rebuild the bundle against the running harness (a stale checkout type-checks green while shipping old DSH code).')
241
+ }
242
+ }