dsh-session-bridge 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-session-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "main": "./lib/index.js",
5
5
  "description": "会话桥:创建主会话 / 向任意会话发消息 / 等待回复 / 读取消息 / 按名或 id 查找会话(支持跨工作区);另含监控/调度主任务与归档会话。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -9098,6 +9098,29 @@ Service.init;
9098
9098
  * dsh-session-bridge core: 会话创建 / 发消息 / 等待回复 / 读取 / 查找。
9099
9099
  * 所有逻辑跑在插件宿主上下文(rootCtx),操作的是 DSH 真实的会话与 agent 注册表。
9100
9100
  */
9101
+ /**
9102
+ * 兼容读取不同 dsh-session 版本上的会话事件:
9103
+ * - 旧版 `Session` 暴露 `get events(): readonly SessionEvent[]`;
9104
+ * - 新版(如宿主实际运行的 0.1.2-rc.x)将 `get events` 改为
9105
+ * `snapshotEvents(fromSeq?, toSeqExclusive?)` 方法——`.events` 直接读取会
9106
+ * 得到 `undefined`,对 for...of 迭代即抛 `events is not iterable`。
9107
+ * 两者都读不到(或 session 不存在)时回退为空数组,绝不抛迭代错误。
9108
+ */
9109
+ function sessionEvents(session) {
9110
+ const s = session;
9111
+ if (s === null || s === void 0) return [];
9112
+ if (Array.isArray(s.events)) return s.events;
9113
+ if (typeof s.snapshotEvents === "function") try {
9114
+ return s.snapshotEvents();
9115
+ } catch {
9116
+ return [];
9117
+ }
9118
+ return [];
9119
+ }
9120
+ /** 规约任意"事件列表"值为数组(非数组 → []),避免 events is not iterable。 */
9121
+ function asEventList(events) {
9122
+ return Array.isArray(events) ? events : [];
9123
+ }
9101
9124
  /** agent.followup / steer 接受的用户消息值。 */
9102
9125
  function userMessage(text) {
9103
9126
  return {
@@ -9141,8 +9164,9 @@ function toolNamesOf(toolCalls) {
9141
9164
  }
9142
9165
  /** 折叠会话事件日志为可读消息行(跳过非 user 来源与空消息)。 */
9143
9166
  function foldMessages(events) {
9167
+ const list = asEventList(events);
9144
9168
  const rows = [];
9145
- for (const event of events) if (event.type === "user/message") {
9169
+ for (const event of list) if (event.type === "user/message") {
9146
9170
  const source = event.data.source;
9147
9171
  if (source !== void 0 && source.kind !== "user") continue;
9148
9172
  const text = blockText(event.data.content);
@@ -9179,8 +9203,9 @@ function foldMessages(events) {
9179
9203
  * 无标题事件时回落为第一条用户消息(截断 80 字符)。
9180
9204
  */
9181
9205
  function titleOf(events) {
9206
+ const list = asEventList(events);
9182
9207
  let title;
9183
- for (const event of events) {
9208
+ for (const event of list) {
9184
9209
  const raw = event;
9185
9210
  if (raw.type === "session/title") {
9186
9211
  const t = raw.data.title;
@@ -9188,7 +9213,7 @@ function titleOf(events) {
9188
9213
  }
9189
9214
  }
9190
9215
  if (title !== void 0) return title;
9191
- for (const event of events) if (event.type === "user/message") {
9216
+ for (const event of list) if (event.type === "user/message") {
9192
9217
  const source = event.data.source;
9193
9218
  if (source !== void 0 && source.kind !== "user") continue;
9194
9219
  const text = blockText(event.data.content);
@@ -9200,8 +9225,9 @@ function titleOf(events) {
9200
9225
  }
9201
9226
  /** 日志最大事件序号。 */
9202
9227
  function maxSeq(events) {
9228
+ const list = asEventList(events);
9203
9229
  let m = -1;
9204
- for (const event of events) if (event.seq > m) m = event.seq;
9230
+ for (const event of list) if (event.seq > m) m = event.seq;
9205
9231
  return m;
9206
9232
  }
9207
9233
  function sleep(ms) {
@@ -9228,7 +9254,7 @@ async function waitForReply(opts) {
9228
9254
  let turnEnded = false;
9229
9255
  for (;;) {
9230
9256
  if (opts.signal !== void 0 && opts.signal.aborted) break;
9231
- const events = opts.session.events;
9257
+ const events = sessionEvents(opts.session);
9232
9258
  for (const row of foldMessages(events)) {
9233
9259
  if (row.seq <= opts.baselineSeq || row.role !== "assistant") continue;
9234
9260
  if (latest === null || row.seq > latest.seq) latest = row;
@@ -9309,7 +9335,7 @@ async function attachSessionToWorkspace(ctx, sessionId, cwd) {
9309
9335
  * 纯读,无副作用。用于 `session_bridge_status`。
9310
9336
  */
9311
9337
  function statusSnapshot(ctx, agent) {
9312
- const events = agent.session.events;
9338
+ const events = sessionEvents(agent.session);
9313
9339
  const rows = foldMessages(events);
9314
9340
  let lastActivityAt = null;
9315
9341
  let lastTurn = 0;
@@ -13633,7 +13659,7 @@ function registerCreate(env) {
13633
13659
  });
13634
13660
  let reply;
13635
13661
  if (typeof args.prompt === "string" && args.prompt.trim() !== "") {
13636
- const baseline = maxSeq(agent.session.events);
13662
+ const baseline = maxSeq(sessionEvents(agent.session));
13637
13663
  agent.followup(userMessage(args.prompt.trim()));
13638
13664
  env.registry.touch(sessionId);
13639
13665
  if (args.waitForReply === true) reply = await maybeWait(env, agent.session, baseline, args, exec.signal);
@@ -13701,7 +13727,7 @@ function registerSend(env) {
13701
13727
  if (typeof args.message !== "string" || args.message.trim() === "") throw new Error("invalid message: expected a non-empty string");
13702
13728
  const agent = liveAgent(env, args.sessionId);
13703
13729
  if (agent === void 0) throw new Error("session " + JSON.stringify(args.sessionId) + " is not live — call session_bridge_resume first to bring it online (or session_bridge_find to locate it)");
13704
- const baseline = maxSeq(agent.session.events);
13730
+ const baseline = maxSeq(sessionEvents(agent.session));
13705
13731
  if (args.mode === "steer") agent.steer(userMessage(args.message.trim()));
13706
13732
  else agent.followup(userMessage(args.message.trim()));
13707
13733
  const headerCwd = agent.session.header.cwd;
@@ -13764,7 +13790,7 @@ function registerResume(env) {
13764
13790
  sessionId: args.sessionId,
13765
13791
  alreadyLive: true,
13766
13792
  ...existing.session.header.cwd === void 0 ? {} : { cwd: existing.session.header.cwd },
13767
- title: titleOf(existing.session.events) ?? null,
13793
+ title: titleOf(sessionEvents(existing.session)) ?? null,
13768
13794
  running: existing.status === "running"
13769
13795
  });
13770
13796
  }
@@ -13797,7 +13823,7 @@ function registerResume(env) {
13797
13823
  sessionId: args.sessionId,
13798
13824
  resumed: true,
13799
13825
  ...headerCwd === void 0 ? {} : { cwd: headerCwd },
13800
- title: titleOf(handle.agent.session.events) ?? null
13826
+ title: titleOf(sessionEvents(handle.agent.session)) ?? null
13801
13827
  });
13802
13828
  }
13803
13829
  }));
@@ -13855,7 +13881,7 @@ function registerWait(env) {
13855
13881
  if (typeof args.sinceSeq === "number" && Number.isInteger(args.sinceSeq) && args.sinceSeq >= 0) baseline = args.sinceSeq;
13856
13882
  else {
13857
13883
  let lastText = -1;
13858
- for (const row of foldMessages(agent.session.events)) if (row.text !== void 0) lastText = row.seq;
13884
+ for (const row of foldMessages(sessionEvents(agent.session))) if (row.text !== void 0) lastText = row.seq;
13859
13885
  baseline = lastText;
13860
13886
  }
13861
13887
  const result = await waitForReply({
@@ -13934,7 +13960,7 @@ function registerRead(env) {
13934
13960
  let live;
13935
13961
  let cwd;
13936
13962
  if (agent !== void 0) {
13937
- events = agent.session.events;
13963
+ events = sessionEvents(agent.session);
13938
13964
  live = true;
13939
13965
  cwd = agent.session.header.cwd;
13940
13966
  } else {
@@ -14051,7 +14077,7 @@ function registerFind(env) {
14051
14077
  const wsId = wsById.get(agent.id) ?? workspaceByPath(env.ctx, headerCwd);
14052
14078
  items.push({
14053
14079
  sessionId: agent.id,
14054
- title: titleOf(agent.session.events),
14080
+ title: titleOf(sessionEvents(agent.session)),
14055
14081
  ...headerCwd === void 0 ? {} : { cwd: headerCwd },
14056
14082
  ...wsId === void 0 ? {} : { workspaceId: wsId },
14057
14083
  live: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "会话桥(dsh-session-bridge):通过提示词创建新的主会话(顶层 UI 会话)、向任意会话发送消息、等待会话的下一条回复、读取会话消息,并按会话名或 id 跨工作区查找会话;此外支持监控/调度主任务与归档会话。",
5
5
  "private": false,
6
6
  "type": "module",
package/src/core.ts CHANGED
@@ -60,6 +60,32 @@ export interface BridgeFindItem {
60
60
  agentPreset?: string
61
61
  }
62
62
 
63
+ /**
64
+ * 兼容读取不同 dsh-session 版本上的会话事件:
65
+ * - 旧版 `Session` 暴露 `get events(): readonly SessionEvent[]`;
66
+ * - 新版(如宿主实际运行的 0.1.2-rc.x)将 `get events` 改为
67
+ * `snapshotEvents(fromSeq?, toSeqExclusive?)` 方法——`.events` 直接读取会
68
+ * 得到 `undefined`,对 for...of 迭代即抛 `events is not iterable`。
69
+ * 两者都读不到(或 session 不存在)时回退为空数组,绝不抛迭代错误。
70
+ */
71
+ export function sessionEvents(session: unknown): readonly SessionEvent[] {
72
+ const s = session as {
73
+ events?: readonly SessionEvent[]
74
+ snapshotEvents?: (fromSeq?: number, toSeqExclusive?: number) => readonly SessionEvent[]
75
+ } | null | undefined
76
+ if (s === null || s === undefined) return []
77
+ if (Array.isArray(s.events)) return s.events
78
+ if (typeof s.snapshotEvents === 'function') {
79
+ try { return s.snapshotEvents() } catch { return [] }
80
+ }
81
+ return []
82
+ }
83
+
84
+ /** 规约任意"事件列表"值为数组(非数组 → []),避免 events is not iterable。 */
85
+ export function asEventList(events: unknown): readonly SessionEvent[] {
86
+ return Array.isArray(events) ? events : []
87
+ }
88
+
63
89
  /** agent.followup / steer 接受的用户消息值。 */
64
90
  export function userMessage(text: string): unknown {
65
91
  return {
@@ -104,8 +130,9 @@ function toolNamesOf(toolCalls: unknown): string[] {
104
130
 
105
131
  /** 折叠会话事件日志为可读消息行(跳过非 user 来源与空消息)。 */
106
132
  export function foldMessages(events: readonly SessionEvent[]): BridgeMessageRow[] {
133
+ const list = asEventList(events)
107
134
  const rows: BridgeMessageRow[] = []
108
- for (const event of events) {
135
+ for (const event of list) {
109
136
  if (event.type === 'user/message') {
110
137
  const source = (event.data as { source?: { kind?: string } }).source
111
138
  if (source !== undefined && source.kind !== 'user') continue
@@ -134,8 +161,9 @@ export function foldMessages(events: readonly SessionEvent[]): BridgeMessageRow[
134
161
  * 无标题事件时回落为第一条用户消息(截断 80 字符)。
135
162
  */
136
163
  export function titleOf(events: readonly SessionEvent[]): string | undefined {
164
+ const list = asEventList(events)
137
165
  let title: string | undefined
138
- for (const event of events) {
166
+ for (const event of list) {
139
167
  const raw = event as unknown as { type: string; data: { title?: unknown } }
140
168
  if (raw.type === 'session/title') {
141
169
  const t = raw.data.title
@@ -143,7 +171,7 @@ export function titleOf(events: readonly SessionEvent[]): string | undefined {
143
171
  }
144
172
  }
145
173
  if (title !== undefined) return title
146
- for (const event of events) {
174
+ for (const event of list) {
147
175
  if (event.type === 'user/message') {
148
176
  const source = (event.data as { source?: { kind?: string } }).source
149
177
  if (source !== undefined && source.kind !== 'user') continue
@@ -159,8 +187,9 @@ export function titleOf(events: readonly SessionEvent[]): string | undefined {
159
187
 
160
188
  /** 日志最大事件序号。 */
161
189
  export function maxSeq(events: readonly SessionEvent[]): number {
190
+ const list = asEventList(events)
162
191
  let m = -1
163
- for (const event of events) if (event.seq > m) m = event.seq
192
+ for (const event of list) if (event.seq > m) m = event.seq
164
193
  return m
165
194
  }
166
195
 
@@ -197,7 +226,7 @@ export async function waitForReply(opts: WaitForReplyOptions): Promise<BridgeWai
197
226
  let turnEnded = false
198
227
  for (;;) {
199
228
  if (opts.signal !== undefined && opts.signal.aborted) break
200
- const events = opts.session.events
229
+ const events = sessionEvents(opts.session)
201
230
  for (const row of foldMessages(events)) {
202
231
  if (row.seq <= opts.baselineSeq || row.role !== 'assistant') continue
203
232
  if (latest === null || row.seq > latest.seq) latest = row
@@ -324,7 +353,7 @@ export interface BridgeStatusSnapshot {
324
353
  * 纯读,无副作用。用于 `session_bridge_status`。
325
354
  */
326
355
  export function statusSnapshot(ctx: Context, agent: LiveAgentLike): BridgeStatusSnapshot {
327
- const events = agent.session.events
356
+ const events = sessionEvents(agent.session)
328
357
  const rows = foldMessages(events)
329
358
  let lastActivityAt: number | null = null
330
359
  let lastTurn = 0
package/src/tools.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  foldMessages,
23
23
  maxSeq,
24
24
  resolveTargetCwd,
25
+ sessionEvents,
25
26
  statusSnapshot,
26
27
  titleOf,
27
28
  userMessage,
@@ -236,7 +237,7 @@ function registerCreate(env: BridgeEnv): void {
236
237
 
237
238
  let reply: ReturnType<typeof renderWait> | undefined
238
239
  if (typeof args.prompt === 'string' && args.prompt.trim() !== '') {
239
- const baseline = maxSeq(agent.session.events)
240
+ const baseline = maxSeq(sessionEvents(agent.session))
240
241
  agent.followup(userMessage(args.prompt.trim()))
241
242
  env.registry.touch(sessionId)
242
243
  if (args.waitForReply === true) {
@@ -294,7 +295,7 @@ function registerSend(env: BridgeEnv): void {
294
295
  if (agent === undefined) {
295
296
  throw new Error('session ' + JSON.stringify(args.sessionId) + ' is not live — call session_bridge_resume first to bring it online (or session_bridge_find to locate it)')
296
297
  }
297
- const baseline = maxSeq(agent.session.events)
298
+ const baseline = maxSeq(sessionEvents(agent.session))
298
299
  if (args.mode === 'steer') agent.steer(userMessage(args.message.trim()))
299
300
  else agent.followup(userMessage(args.message.trim()))
300
301
  const headerCwd = agent.session.header.cwd
@@ -346,7 +347,7 @@ function registerResume(env: BridgeEnv): void {
346
347
  sessionId: args.sessionId,
347
348
  alreadyLive: true,
348
349
  ...(existing.session.header.cwd === undefined ? {} : { cwd: existing.session.header.cwd }),
349
- title: titleOf(existing.session.events) ?? null,
350
+ title: titleOf(sessionEvents(existing.session)) ?? null,
350
351
  running: existing.status === 'running',
351
352
  })
352
353
  }
@@ -381,7 +382,7 @@ function registerResume(env: BridgeEnv): void {
381
382
  sessionId: args.sessionId,
382
383
  resumed: true,
383
384
  ...(headerCwd === undefined ? {} : { cwd: headerCwd }),
384
- title: titleOf(handle.agent.session.events) ?? null,
385
+ title: titleOf(sessionEvents(handle.agent.session)) ?? null,
385
386
  })
386
387
  },
387
388
  }))
@@ -432,7 +433,7 @@ function registerWait(env: BridgeEnv): void {
432
433
  baseline = args.sinceSeq
433
434
  } else {
434
435
  let lastText = -1
435
- for (const row of foldMessages(agent.session.events)) {
436
+ for (const row of foldMessages(sessionEvents(agent.session))) {
436
437
  if (row.text !== undefined) lastText = row.seq
437
438
  }
438
439
  baseline = lastText
@@ -494,7 +495,7 @@ function registerRead(env: BridgeEnv): void {
494
495
  let live: boolean
495
496
  let cwd: string | undefined
496
497
  if (agent !== undefined) {
497
- events = agent.session.events
498
+ events = sessionEvents(agent.session)
498
499
  live = true
499
500
  cwd = agent.session.header.cwd
500
501
  } else {
@@ -592,7 +593,7 @@ function registerFind(env: BridgeEnv): void {
592
593
  const wsId = wsById.get(agent.id) ?? workspaceByPath(env.ctx, headerCwd)
593
594
  items.push({
594
595
  sessionId: agent.id,
595
- title: titleOf(agent.session.events),
596
+ title: titleOf(sessionEvents(agent.session)),
596
597
  ...(headerCwd === undefined ? {} : { cwd: headerCwd }),
597
598
  ...(wsId === undefined ? {} : { workspaceId: wsId }),
598
599
  live: true,