dsh-plugin-bridge 0.2.10 → 0.2.11

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 CHANGED
@@ -5,6 +5,7 @@
5
5
  </p>
6
6
 
7
7
  [![dsh-plugin](https://img.shields.io/badge/dsh-plugin-blue)](https://github.com/deepseek-ai/deepseek-harness)
8
+ [![npm](https://img.shields.io/npm/v/dsh-plugin-bridge?color=cb3837)](https://www.npmjs.com/package/dsh-plugin-bridge)
8
9
  [![ci](https://github.com/Totoro-qaq/dsh-plugin-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/Totoro-qaq/dsh-plugin-bridge/actions/workflows/ci.yml)
9
10
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
10
11
  [![node ≥22](https://img.shields.io/badge/node-%E2%89%A522-339933)](package.json)
package/README.zh.md CHANGED
@@ -5,6 +5,7 @@
5
5
  </p>
6
6
 
7
7
  [![dsh-plugin](https://img.shields.io/badge/dsh-plugin-blue)](https://github.com/deepseek-ai/deepseek-harness)
8
+ [![npm](https://img.shields.io/npm/v/dsh-plugin-bridge?color=cb3837)](https://www.npmjs.com/package/dsh-plugin-bridge)
8
9
  [![ci](https://github.com/Totoro-qaq/dsh-plugin-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/Totoro-qaq/dsh-plugin-bridge/actions/workflows/ci.yml)
9
10
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
10
11
  [![node ≥22](https://img.shields.io/badge/node-%E2%89%A522-339933)](package.json)
package/lib/cli.js CHANGED
@@ -279,6 +279,7 @@ async function main(argv) {
279
279
  const source = await findSession(rpc, sessionId).catch(() => undefined);
280
280
  const result = await executeMigration(rpc, {
281
281
  sessionId,
282
+ sourceSession: source,
282
283
  to,
283
284
  summary,
284
285
  goalRounds: num(args, 'goal-rounds'),
@@ -322,9 +323,9 @@ async function main(argv) {
322
323
  ...(num(args, 'worker-timeout') === undefined ? {} : { workerTimeoutMs: num(args, 'worker-timeout') }),
323
324
  onProgress: progress,
324
325
  });
325
- const source = await findSession(rpc, sessionId).catch(() => undefined);
326
326
  const result = await executeMigration(rpc, {
327
327
  sessionId,
328
+ sourceSession: preview.sourceSession,
328
329
  to,
329
330
  summary: preview.summary,
330
331
  lang: preview.lang,
@@ -332,7 +333,7 @@ async function main(argv) {
332
333
  inject: injectOf(args),
333
334
  kickoff: !bool(args, 'no-kickoff'),
334
335
  autoContinue: bool(args, 'continue'),
335
- title: str(args, 'title') ?? migratedTitle(titleOf(source), to),
336
+ title: str(args, 'title') ?? migratedTitle(titleOf(preview.sourceSession), to),
336
337
  onProgress: progress,
337
338
  });
338
339
  out({ ...result, summary: preview.summary, source: preview.source }, () => `已迁移到 ${result.agentPreset}:${result.sessionId}\n\n${preview.summary}\n`);
package/lib/command.js CHANGED
@@ -171,10 +171,12 @@ export function createBridgeCommand(deps) {
171
171
  const rpc = deps.rpcFor(invocation.signal);
172
172
  const config = deps.config;
173
173
  let presets;
174
+ let sourceSession;
174
175
  let current;
175
176
  try {
176
177
  presets = await listPresets(rpc);
177
- current = (await findSession(rpc, sessionId))?.agentPreset;
178
+ sourceSession = await findSession(rpc, sessionId);
179
+ current = sourceSession?.agentPreset;
178
180
  }
179
181
  catch (error) {
180
182
  return { kind: 'error', text: describe(error) };
@@ -258,11 +260,11 @@ export function createBridgeCommand(deps) {
258
260
  };
259
261
  }
260
262
  try {
261
- const row = await findSession(rpc, sessionId).catch(() => undefined);
262
- const sourceTitle = titleOf(row);
263
+ const sourceTitle = titleOf(sourceSession);
263
264
  const targetTitle = sourceTitle ? migratedTitle(sourceTitle, target) : (runLang === 'en' ? `Migrated to ${target}` : migratedTitle(sourceTitle, target));
264
265
  const result = await executeMigration(rpc, {
265
266
  sessionId,
267
+ sourceSession,
266
268
  to: target,
267
269
  summary,
268
270
  goalRounds: parsed.goalRounds ?? config.goalRounds,
@@ -311,6 +313,7 @@ export function createBridgeCommand(deps) {
311
313
  try {
312
314
  const preview = await previewMigration(rpc, {
313
315
  sessionId,
316
+ sourceSession,
314
317
  tier: parsed.tier ?? config.modelTier,
315
318
  sourceCharBudget: config.sourceCharBudget,
316
319
  summaryCharBudget: config.summaryCharBudget,
package/lib/migrate.d.ts CHANGED
@@ -69,16 +69,18 @@ export declare function resolveWorkerModel(rpc: Rpc, sessionId: string, tier: Mo
69
69
  export declare function resolveWorkerPreset(rpc: Rpc): Promise<string | undefined>;
70
70
  export interface WaitOptions {
71
71
  timeoutMs?: number;
72
- /** 等「开始跑」的宽限期;超过还没 running 就当它已经跑完了。 */
72
+ /** 等待新 `turn/start` 的宽限期;超过仍未出现就按未启动处理。 */
73
73
  startGraceMs?: number;
74
74
  pollMs?: number;
75
+ /** 只观察这个事件序号之后的新一轮;worker 新建后通常为 0。 */
76
+ afterSeq?: number;
75
77
  }
76
78
  /**
77
- * 等一个会话回到空闲。
79
+ * 等一个会话的新一轮写入 `turn/end`。
78
80
  *
79
- * 旧实现是「先 sleep 2s,再看 running 是不是 false」——host 排队稍慢一点,
80
- * 第一次轮询就会把「还没开始」误判成「已经跑完」,取到的是上一轮的回答。
81
- * 这里先等它真的 running 起来(有宽限期),再等它落回 false。
81
+ * `session.list` 是全局列表,拿它每两秒轮询一个 worker 会把会话总量放大成
82
+ * O(会话数 × 轮询次数)。`session.history` 则只读目标会话;用 prompt 前的事件
83
+ * 水位隔开旧轮次后,`turn/start` / `turn/end` 也比易过期的 running 快照更可靠。
82
84
  */
83
85
  export declare function waitIdle(rpc: Rpc, sessionId: string, options?: WaitOptions): Promise<{
84
86
  idle: boolean;
@@ -93,6 +95,8 @@ export declare function foldedHistory(rpc: Rpc, sessionId: string, options?: {
93
95
  export declare function lastAssistantText(rpc: Rpc, sessionId: string): Promise<string>;
94
96
  export interface PreviewOptions {
95
97
  sessionId: string;
98
+ /** 同一命令已经读取过的源会话行,避免重复扫描全局列表。 */
99
+ sourceSession?: SessionRow;
96
100
  tier?: ModelTier;
97
101
  provider?: string;
98
102
  model?: string;
@@ -125,6 +129,8 @@ export interface PreviewResult {
125
129
  export declare function previewMigration(rpc: Rpc, options: PreviewOptions): Promise<PreviewResult>;
126
130
  export interface MigrateOptions {
127
131
  sessionId: string;
132
+ /** 同一流程已经读取过的源会话行,避免重复扫描全局列表。 */
133
+ sourceSession?: SessionRow;
128
134
  to: string;
129
135
  summary: string;
130
136
  lang?: 'zh' | 'en';
package/lib/migrate.js CHANGED
@@ -95,31 +95,40 @@ export async function resolveWorkerPreset(rpc) {
95
95
  return presets.find((preset) => preset.isDefault)?.id;
96
96
  }
97
97
  /**
98
- * 等一个会话回到空闲。
98
+ * 等一个会话的新一轮写入 `turn/end`。
99
99
  *
100
- * 旧实现是「先 sleep 2s,再看 running 是不是 false」——host 排队稍慢一点,
101
- * 第一次轮询就会把「还没开始」误判成「已经跑完」,取到的是上一轮的回答。
102
- * 这里先等它真的 running 起来(有宽限期),再等它落回 false。
100
+ * `session.list` 是全局列表,拿它每两秒轮询一个 worker 会把会话总量放大成
101
+ * O(会话数 × 轮询次数)。`session.history` 则只读目标会话;用 prompt 前的事件
102
+ * 水位隔开旧轮次后,`turn/start` / `turn/end` 也比易过期的 running 快照更可靠。
103
103
  */
104
104
  export async function waitIdle(rpc, sessionId, options = {}) {
105
105
  const pollMs = options.pollMs ?? 2000;
106
106
  const deadline = Date.now() + (options.timeoutMs ?? 360_000);
107
107
  const startBy = Date.now() + (options.startGraceMs ?? 25_000);
108
+ const afterSeq = options.afterSeq ?? 0;
108
109
  let started = false;
109
110
  while (Date.now() < deadline) {
110
111
  await sleep(pollMs);
111
- const row = await findSession(rpc, sessionId).catch(() => undefined);
112
- if (row?.running) {
112
+ const events = await tailSessionEvents(rpc, sessionId).catch(() => []);
113
+ const fresh = events.filter((event) => typeof event.seq === 'number' && event.seq > afterSeq);
114
+ if (fresh.some((event) => event.type === 'turn/start'))
113
115
  started = true;
114
- continue;
115
- }
116
- if (started)
116
+ if (fresh.some((event) => event.type === 'turn/end'))
117
117
  return { idle: true, started: true };
118
- if (Date.now() > startBy)
118
+ if (!started && Date.now() > startBy)
119
119
  return { idle: true, started: false };
120
120
  }
121
121
  return { idle: false, started };
122
122
  }
123
+ /** 读取单个会话的尾页;不触发全局 session.list 扫描。 */
124
+ async function tailSessionEvents(rpc, sessionId) {
125
+ const res = await rpc('session.history', { sessionId, maxMessages: 2 }, 60_000);
126
+ return (res.events ?? []).map((entry) => entry.event);
127
+ }
128
+ async function latestSessionSeq(rpc, sessionId) {
129
+ const events = await tailSessionEvents(rpc, sessionId);
130
+ return events.reduce((max, event) => (typeof event.seq === 'number' && event.seq > max ? event.seq : max), 0);
131
+ }
123
132
  /** 拉取并折叠会话历史(按需翻页)。 */
124
133
  export async function foldedHistory(rpc, sessionId, options = {}) {
125
134
  const pageMessages = options.pageMessages ?? HISTORY_PAGE_MESSAGES;
@@ -149,7 +158,7 @@ export async function lastAssistantText(rpc, sessionId) {
149
158
  /** 生成交接摘要:取材 → 起临时工人 → 收摘要 → 归档工人。 */
150
159
  export async function previewMigration(rpc, options) {
151
160
  const progress = options.onProgress ?? (() => { });
152
- const sourceSession = await findSession(rpc, options.sessionId);
161
+ const sourceSession = options.sourceSession ?? await findSession(rpc, options.sessionId);
153
162
  progress('拉取并折叠会话历史…');
154
163
  const messages = await foldedHistory(rpc, options.sessionId);
155
164
  const source = buildBridgeSource(messages, { sourceCharBudget: options.sourceCharBudget });
@@ -179,6 +188,7 @@ export async function previewMigration(rpc, options) {
179
188
  });
180
189
  }
181
190
  const instruction = buildBridgeInstruction(lang, { summaryCharBudget: options.summaryCharBudget });
191
+ const workerBaselineSeq = await latestSessionSeq(rpc, worker.sessionId).catch(() => 0);
182
192
  await rpc('session.prompt', {
183
193
  sessionId: worker.sessionId,
184
194
  mode: 'queue',
@@ -187,6 +197,7 @@ export async function previewMigration(rpc, options) {
187
197
  progress('等待摘要…');
188
198
  const settled = await waitIdle(rpc, worker.sessionId, {
189
199
  timeoutMs: options.workerTimeoutMs ?? 360_000,
200
+ afterSeq: workerBaselineSeq,
190
201
  ...(options.pollMs === undefined ? {} : { pollMs: options.pollMs, startGraceMs: options.pollMs * 6 }),
191
202
  });
192
203
  if (!settled.idle) {
@@ -280,7 +291,7 @@ export async function executeMigration(rpc, options) {
280
291
  const summary = options.summary.trim();
281
292
  if (!summary)
282
293
  throw new RpcError('bridge.migrate', 'empty-summary', '摘要为空,拒绝迁移。');
283
- const sourceSession = await findSession(rpc, options.sessionId);
294
+ const sourceSession = options.sourceSession ?? await findSession(rpc, options.sessionId);
284
295
  const where = await placement(rpc, sourceSession, options.sessionId);
285
296
  let sourceModel;
286
297
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-bridge",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Previewable cross-preset session migration for DeepSeek Harness with bounded, fixed-schema handoffs",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",