dsh-session-bridge 0.3.0 → 0.3.2-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-bridge",
3
- "version": "0.3.0",
3
+ "version": "0.3.2-alpha.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 || ^0.1.6-0",
56
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
57
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
58
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
59
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
60
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
61
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
62
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-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 || ^0.1.6-0",
67
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
68
+ "@deepseek-ai/dsh-brand": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
69
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
70
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
71
+ "@deepseek-ai/dsh-scope": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
72
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
73
+ "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
74
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
75
+ "@deepseek-ai/dsh-util-values": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
76
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-0 || ^0.1.6-0",
77
77
  "@types/node": "^24.0.0",
78
78
  "cordis": "^4.0.0-rc.7",
79
79
  "tsdown": "^0.22.14",
@@ -82,6 +82,7 @@
82
82
  "scripts": {
83
83
  "build": "bash scripts/build.sh",
84
84
  "build:client": "tsdown",
85
+ "check:compat": "node scripts/check-dsh-compat.mjs",
85
86
  "typecheck": "tsc -p tsconfig.json --noEmit"
86
87
  }
87
88
  }
package/scripts/build.sh CHANGED
@@ -14,6 +14,24 @@ if [ -z "$CHECKOUT" ] || [ ! -d "$CHECKOUT/packages" ]; then
14
14
  exit 1
15
15
  fi
16
16
 
17
+ # The type-check below runs against $DSH_CHECKOUT, which commonly lags the dsh
18
+ # that actually loads this plugin — a stale checkout type-checks green while the
19
+ # running harness has already moved on. Warn loudly rather than implying support.
20
+ read_pkg_version() {
21
+ node -e "try{process.stdout.write(String(require(process.argv[1]).version))}catch(e){process.stdout.write('unknown')}" "$1" 2>/dev/null || echo unknown
22
+ }
23
+ CHECKOUT_VERSION="$(read_pkg_version "$CHECKOUT/package.json")"
24
+ INSTALLED_VERSION="unknown"
25
+ GLOBAL_ROOT="$(npm root -g 2>/dev/null | head -n1 || true)"
26
+ if [ -n "$GLOBAL_ROOT" ] && [ -f "$GLOBAL_ROOT/@deepseek-ai/dsh/package.json" ]; then
27
+ INSTALLED_VERSION="$(read_pkg_version "$GLOBAL_ROOT/@deepseek-ai/dsh/package.json")"
28
+ fi
29
+ if [ "$INSTALLED_VERSION" != "unknown" ] && [ "$CHECKOUT_VERSION" != "$INSTALLED_VERSION" ]; then
30
+ echo "build: WARNING — checkout is $CHECKOUT_VERSION but the installed dsh is $INSTALLED_VERSION." >&2
31
+ echo "build: the type-check below therefore does NOT cover the running harness;" >&2
32
+ echo "build: run 'npm run check:compat' to type-check against the installed dsh." >&2
33
+ fi
34
+
17
35
  TSC="$CHECKOUT/node_modules/.bin/tsc"
18
36
  if [ ! -x "$TSC" ]; then
19
37
  echo "build: tsc not found at $TSC" >&2
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Type-check src/ against the DSH packages that are ACTUALLY INSTALLED,
4
+ * rather than against $DSH_CHECKOUT.
5
+ *
6
+ * Why this exists: scripts/build.sh type-checks against a DSH *source* checkout.
7
+ * That checkout routinely lags the harness the plugin is loaded into (e.g. the
8
+ * checkout sat at 0.1.5-alpha.2 while the running harness was 0.1.6-alpha.1), so
9
+ * a green `build.sh` silently proves nothing about the running version. This
10
+ * script reads the `lib/types/*.d.ts` that ship inside the installed DSH npm
11
+ * package, i.e. the exact API surface the plugin loads against — so an upstream
12
+ * break shows up here even when the checkout is stale.
13
+ *
14
+ * Usage:
15
+ * node scripts/check-dsh-compat.mjs [--dsh <path>]
16
+ *
17
+ * <path> may be either the DSH package directory or the `@deepseek-ai` scope
18
+ * directory that holds dsh-session, dsh-agent, ... . When omitted, the installed
19
+ * location is probed (DSH_INSTALLED_MODULES, then `npm root -g`).
20
+ *
21
+ * Exits 0 when the plugin's src/ type-checks against those types, 1 otherwise.
22
+ */
23
+ import { execFileSync, execSync } from 'node:child_process'
24
+ import { existsSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
25
+ import { dirname, join, resolve } from 'node:path'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
29
+
30
+ /** Read a JSON file, or return undefined when it is absent/unreadable. */
31
+ function readJson(file) {
32
+ try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return undefined }
33
+ }
34
+
35
+ /** True when `dir` looks like the `@deepseek-ai` scope directory. */
36
+ function isScopeDir(dir) {
37
+ return dir !== undefined && existsSync(join(dir, 'dsh-session', 'package.json'))
38
+ }
39
+
40
+ /**
41
+ * Resolve the `@deepseek-ai` scope directory holding the installed DSH packages.
42
+ * Accepts an explicit path (package dir or scope dir) and otherwise probes.
43
+ */
44
+ function resolveScopeDir(argv) {
45
+ const flagAt = argv.indexOf('--dsh')
46
+ const explicit = flagAt !== -1 ? argv[flagAt + 1] : (process.env.DSH_INSTALLED_MODULES ?? undefined)
47
+ const candidates = []
48
+ if (explicit !== undefined && explicit !== '') {
49
+ const base = resolve(explicit)
50
+ // Accept the scope dir itself, the dsh package dir, or a node_modules tree.
51
+ candidates.push(base, join(base, 'node_modules', '@deepseek-ai'), join(base, '@deepseek-ai'))
52
+ }
53
+
54
+ // Anchor 1: the global install root. execSync (string command) rather than
55
+ // execFileSync, because npm is a .cmd shim on Windows that cannot be spawned
56
+ // without a shell.
57
+ let globalRoot
58
+ try {
59
+ globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
60
+ } catch { globalRoot = undefined }
61
+ if (globalRoot !== undefined && globalRoot !== '') {
62
+ // npm layout, then the common "dsh nests its own deps" layout.
63
+ candidates.push(join(globalRoot, '@deepseek-ai'), join(globalRoot, '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai'))
64
+ }
65
+
66
+ // Anchor 2: walk up from the `dsh` executable itself. Its path is usually a
67
+ // symlink (scoop/volta), so resolve it first or the walk misses the real tree.
68
+ try {
69
+ const found = execSync(process.platform === 'win32' ? 'where dsh' : 'which dsh', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
70
+ .split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== '')
71
+ for (const line of found) {
72
+ let dir
73
+ try { dir = dirname(realpathSync(line)) } catch { dir = dirname(line) }
74
+ for (let hop = 0; hop < 4 && dir !== dirname(dir); hop++) {
75
+ candidates.push(join(dir, 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai'))
76
+ candidates.push(join(dir, 'node_modules', '@deepseek-ai'))
77
+ dir = dirname(dir)
78
+ }
79
+ }
80
+ } catch { /* dsh not on PATH; the other anchors decide */ }
81
+
82
+ for (const candidate of candidates) if (isScopeDir(candidate)) return candidate
83
+ return undefined
84
+ }
85
+
86
+ /** Resolve a package's types entry point inside the scope directory. */
87
+ function typesEntry(scope, pkg) {
88
+ const dir = join(scope, pkg)
89
+ const manifest = readJson(join(dir, 'package.json'))
90
+ if (manifest === undefined) return undefined
91
+ const declared = typeof manifest.types === 'string' ? manifest.types : undefined
92
+ const candidates = [
93
+ ...(declared !== undefined ? [join(dir, declared)] : []),
94
+ join(dir, 'lib', 'types', 'index.d.ts'),
95
+ join(dir, 'index.d.ts'),
96
+ ]
97
+ return candidates.find((c) => existsSync(c))
98
+ }
99
+
100
+ function fail(message) {
101
+ console.error('check-dsh-compat: ' + message)
102
+ process.exit(1)
103
+ }
104
+
105
+ const scope = resolveScopeDir(process.argv.slice(2))
106
+ if (scope === undefined) {
107
+ fail('could not locate the installed DSH packages. Pass --dsh <path> (the DSH package dir or its @deepseek-ai scope dir) or set DSH_INSTALLED_MODULES.')
108
+ }
109
+
110
+ /** Read the version of the `dsh` package that owns this scope directory. */
111
+ function dshVersionOf(scopeDir) {
112
+ // Nested layout puts dsh two levels above its own scope dir; a flat global
113
+ // install puts it beside the other packages.
114
+ for (const candidate of [join(scopeDir, 'dsh', 'package.json'), join(scopeDir, '..', '..', 'package.json')]) {
115
+ const manifest = readJson(candidate)
116
+ if (manifest?.name === '@deepseek-ai/dsh' && typeof manifest.version === 'string') return manifest.version
117
+ }
118
+ return undefined
119
+ }
120
+
121
+ const dshVersion = dshVersionOf(scope) ?? '(unknown)'
122
+
123
+ // Every DSH package src/ imports, plus the plain `cordis` specifier that
124
+ // package.json declares as a peer.
125
+ const manifest = readJson(join(ROOT, 'package.json')) ?? {}
126
+ const declared = [...Object.keys(manifest.peerDependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]
127
+ const wanted = new Set(declared.filter((name) => name.startsWith('@deepseek-ai/')))
128
+ wanted.add('@deepseek-ai/cordis')
129
+ wanted.add('@deepseek-ai/schemastery')
130
+
131
+ const paths = {}
132
+ const missing = []
133
+ for (const name of [...wanted].sort()) {
134
+ const entry = typesEntry(scope, name.slice('@deepseek-ai/'.length))
135
+ if (entry === undefined) { missing.push(name); continue }
136
+ paths[name] = [entry]
137
+ }
138
+ // tsconfig.json maps the bare `cordis` specifier; keep that working under the
139
+ // installed layout too, where cordis is published scoped.
140
+ if (paths['@deepseek-ai/cordis'] !== undefined) paths['cordis'] = paths['@deepseek-ai/cordis']
141
+
142
+ if (missing.length > 0) {
143
+ fail('installed DSH is missing the packages this plugin imports: ' + missing.join(', ') + '\n (looked in ' + scope + ')')
144
+ }
145
+
146
+ // The generated config must live in the repo root: tsconfig.json's `include`
147
+ // is relative to the config that declares it, so a config placed elsewhere
148
+ // would look for src/ in the wrong directory. Every mapped path is absolute,
149
+ // so only `include`/`extends` depend on the location. Removed again below.
150
+ const configPath = join(ROOT, '.dsh-compat.tsconfig.json')
151
+ writeFileSync(configPath, JSON.stringify({ extends: './tsconfig.json', compilerOptions: { paths } }, null, 2))
152
+
153
+ // Run tsc's JS entry through the current node binary. Spawning the .bin shell
154
+ // shim instead would need a shell on Windows (npm .cmd/.bat shims cannot be
155
+ // spawned directly), and this way the exit code and output stay unambiguous.
156
+ const tsc = join(ROOT, 'node_modules', 'typescript', 'bin', 'tsc')
157
+ if (!existsSync(tsc)) { rmSync(configPath, { force: true }); fail('local typescript not found at ' + tsc + ' (run the package manager install first)') }
158
+
159
+ console.log('check-dsh-compat: type-checking src/ against installed dsh ' + dshVersion)
160
+ console.log('check-dsh-compat: scope ' + scope)
161
+ let failure
162
+ try {
163
+ execFileSync(process.execPath, [tsc, '-p', configPath, '--noEmit'], { cwd: ROOT, stdio: 'inherit' })
164
+ } catch (error) {
165
+ failure = error
166
+ } finally {
167
+ rmSync(configPath, { force: true })
168
+ }
169
+ if (failure !== undefined) {
170
+ const status = typeof failure.status === 'number' ? ' (tsc exit ' + failure.status + ')' : ''
171
+ fail('src/ does NOT type-check against installed dsh ' + dshVersion + status + ' — this is a real incompatibility with the running harness.')
172
+ }
173
+ console.log('check-dsh-compat: OK — src/ type-checks against installed dsh ' + dshVersion)
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,
@@ -194,8 +194,16 @@ function registerCreate(env: BridgeEnv): void {
194
194
  ...(model !== '' ? { model } : {}),
195
195
  ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
196
196
  },
197
- setup: (agentCtx: Context) => {
198
- if (caller !== undefined) {
197
+ setup: async (agentCtx: Context) => {
198
+ const requestedPreset = typeof args.agentPreset === 'string' && args.agentPreset.trim() !== '' ? args.agentPreset.trim() : undefined
199
+ if (requestedPreset !== undefined) {
200
+ // An explicit agentPreset composes THAT preset. Falling through to the
201
+ // caller-join below would silently ignore the request: the id would
202
+ // still reach the session header, while the agent kept running on the
203
+ // caller's standing composition and its tools.
204
+ const presets = env.ctx.agentPresets as unknown as { mount(agentCtx: Context, id: string): Promise<unknown> }
205
+ await presets.mount(agentCtx, requestedPreset)
206
+ } else if (caller !== undefined) {
199
207
  const presets = env.ctx.agentPresets as unknown as { composeFrom(agentCtx: Context, parentCtx: Context): string | undefined }
200
208
  presets.composeFrom(agentCtx, caller.ctx)
201
209
  }
@@ -354,8 +362,13 @@ function registerResume(env: BridgeEnv): void {
354
362
  }
355
363
  let headers: readonly { id: string; cwd?: string; createdAt: number }[] = []
356
364
  try {
357
- const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { id: string; cwd?: string; createdAt: number }[]> }
358
- headers = await persistence.list()
365
+ // dsh >= 0.1.5-alpha.1:list() 返回 { header, ... } 快照;映射回平铺字段。
366
+ const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { header: { id: string; cwd?: string; createdAt: number } }[]> }
367
+ headers = (await persistence.list()).map((rec) => ({
368
+ id: rec.header.id,
369
+ ...(rec.header.cwd === undefined ? {} : { cwd: rec.header.cwd }),
370
+ createdAt: rec.header.createdAt,
371
+ }))
359
372
  } catch (error) {
360
373
  throw new Error('session persistence unavailable: ' + (error instanceof Error ? error.message : String(error)))
361
374
  }
@@ -502,8 +515,7 @@ function registerSegments(env: BridgeEnv): void {
502
515
  live = true
503
516
  } else {
504
517
  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)
518
+ const inspection = await inspectPersistedSession(env.ctx, args.sessionId)
507
519
  events = inspection.events
508
520
  live = false
509
521
  } catch (error) {
@@ -580,8 +592,7 @@ function registerRead(env: BridgeEnv): void {
580
592
  } else {
581
593
  let inspection: { events: readonly SessionEvent[]; meta: { cwd?: string } }
582
594
  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)
595
+ inspection = await inspectPersistedSession(env.ctx, args.sessionId)
585
596
  } catch (error) {
586
597
  throw new Error('cannot read session ' + JSON.stringify(args.sessionId) + ': ' + (error instanceof Error ? error.message : String(error)))
587
598
  }
@@ -685,8 +696,11 @@ function registerFind(env: BridgeEnv): void {
685
696
  }
686
697
  if (args.liveOnly !== true) {
687
698
  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()) {
699
+ // dsh >= 0.1.5-alpha.1:list() 返回 { header: SessionHeader, revision, ... } 快照,
700
+ // 不再平铺 id/cwd;旧版直接返回 header 平铺字段。两者都按新形状读,向下兼容。
701
+ const persistence = env.ctx.sessionPersistence as unknown as { list(): Promise<readonly { header: { id: string; cwd?: string; parentSession?: string; origin?: 'subagent'; createdAt: number; agentPreset?: string } }[]> }
702
+ for (const rec of await persistence.list()) {
703
+ const header = rec.header
690
704
  if (items.some((item) => item.sessionId === header.id)) continue
691
705
  items.push({
692
706
  sessionId: header.id,
@@ -720,13 +734,12 @@ function registerFind(env: BridgeEnv): void {
720
734
  const cwdFilter = typeof args.cwd === 'string' ? args.cwd.trim().toLowerCase() : ''
721
735
  const needsOfflineTitle = titleFilter !== '' || (query !== '' && items.some((item) => !item.live))
722
736
  if (needsOfflineTitle) {
723
- const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
724
737
  let inspected = 0
725
738
  for (const item of items) {
726
739
  if (inspected >= 30) break
727
740
  if (item.live || item.title !== undefined) continue
728
741
  try {
729
- const inspection = await persistence.inspect(item.sessionId)
742
+ const inspection = await inspectPersistedSession(env.ctx, item.sessionId)
730
743
  item.title = titleOf(inspection.events)
731
744
  inspected += 1
732
745
  } catch {
@@ -1118,7 +1131,6 @@ function registerArchive(env: BridgeEnv): void {
1118
1131
  const archived = env.ctx.workspaceRegistry.archivedSessionIds.map(String)
1119
1132
  const items: Array<{ sessionId: string; title?: string }> = archived.map((id) => ({ sessionId: id }))
1120
1133
  if (args.resolveTitles === true && items.length > 0) {
1121
- const persistence = env.ctx.sessionPersistence as unknown as { inspect(id: string): Promise<{ events: readonly SessionEvent[] }> }
1122
1134
  const registryTitles = new Map<string, string>()
1123
1135
  try {
1124
1136
  const records = await env.registry.all()
@@ -1130,7 +1142,7 @@ function registerArchive(env: BridgeEnv): void {
1130
1142
  continue
1131
1143
  }
1132
1144
  try {
1133
- const inspection = await persistence.inspect(item.sessionId)
1145
+ const inspection = await inspectPersistedSession(env.ctx, item.sessionId)
1134
1146
  item.title = titleOf(inspection.events)
1135
1147
  } catch { /* offline title unavailable */ }
1136
1148
  }