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/README.md +50 -18
- package/README.zh.md +36 -14
- package/dsh.plugin.json +1 -1
- package/lib/index.js +118 -57
- package/package.json +2 -1
- package/scripts/check-dsh-compat.mjs +70 -1
- package/scripts/test-bridge-core.mjs +154 -0
- package/src/core.ts +72 -9
- package/src/monitor.ts +3 -3
- package/src/tools.ts +46 -30
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
* - 旧版
|
|
9218
|
-
* -
|
|
9219
|
-
*
|
|
9220
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
9605
|
+
} : textReply;
|
|
9576
9606
|
return {
|
|
9577
9607
|
message,
|
|
9578
9608
|
seq: message === null ? opts.baselineSeq : message.seq,
|
|
9579
9609
|
turnEnded,
|
|
9580
|
-
timedOut: requireTurnEnd ?
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
10375
|
-
*
|
|
10376
|
-
*
|
|
10377
|
-
*
|
|
10378
|
-
*
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
}
|