dsh-plugin-rollout-scout 1.4.0 → 1.5.0

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.en.md CHANGED
@@ -3,8 +3,9 @@
3
3
  English | [简体中文](README.md)
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/dsh-plugin-rollout-scout?color=cb3837&logo=npm)](https://www.npmjs.com/package/dsh-plugin-rollout-scout)
6
+ [![CI](https://github.com/SpookySandwich/dsh-plugin-rollout-scout/actions/workflows/ci.yml/badge.svg)](https://github.com/SpookySandwich/dsh-plugin-rollout-scout/actions/workflows/ci.yml)
6
7
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
- [![dsh](https://img.shields.io/badge/dsh-0.1.1--rc.2-4b8dff)](https://github.com/deepseek-ai/deepseek-harness)
8
+ [![dsh](https://img.shields.io/badge/dsh-0.1.5--rc.2-4b8dff)](https://github.com/deepseek-ai/deepseek-harness)
8
9
  [![stars](https://img.shields.io/github/stars/SpookySandwich/dsh-plugin-rollout-scout?style=flat&label=stars)](https://github.com/SpookySandwich/dsh-plugin-rollout-scout/stargazers)
9
10
 
10
11
  Providers sometimes roll a new conversation model out gradually, so which one you get is luck of the draw. Rollout Scout opens throwaway conversations on your own account, reads each one's chain-of-thought **as it streams**, and scores how the reasoning is written — cancelling the ones that read like the model you already have, and keeping the ones that don't.
@@ -134,6 +135,15 @@ Implementation notes live in [`docs/`](docs/) — [architecture](docs/architectu
134
135
 
135
136
  ## Compatibility
136
137
 
138
+ Version `1.5.0`: Consume transient agent/assistant-stream frames on DSH 0.1.5, retain the legacy feed without duplicate counting, and unwrap persistence snapshot headers for cleanup.
139
+
140
+ The declared host range is `>=0.1.5-rc.2 <0.1.6-0`; the official `0.1.5-rc.2` runtime was verified. DSH `0.1.6` alpha is not claimed compatible. Keep the previous plugin release on older DSH. [Validation record](.github/reviews/dsh-0.1.5.md).
141
+
142
+ Download the archive from the [GitHub Release](https://github.com/SpookySandwich/dsh-plugin-rollout-scout/releases/tag/v1.5.0), then run `dsh plugin --profile desktop add ./dsh-plugin-rollout-scout-1.5.0.tgz`.
143
+
144
+ This release targets DSH `0.1.5-rc.2`. Run `npm ci`, `npm test`, and `npm run check:package` to verify the build and package. Restart DSH after updating.
145
+
146
+
137
147
  The launcher takes a `sidebar.footer.action` seat (a list slot, so it sits beside any other footer action rather than displacing one) and the console renders on the frame-wide `shell.overlay` layer. Neither is per-session, so it conflicts with no session plugin. Requires a DSH whose sidebar declares that seat; without it the console has no way in. Part of the same family as [dsh-plugin-smooth-stream](https://github.com/SpookySandwich/dsh-plugin-smooth-stream), [dsh-plugin-no-workspace](https://github.com/SpookySandwich/dsh-plugin-no-workspace) and [dsh-plugin-message-edit](https://github.com/SpookySandwich/dsh-plugin-message-edit).
138
148
 
139
149
  ## A note on cost
package/README.md CHANGED
@@ -3,8 +3,9 @@
3
3
  [English](README.en.md) | 简体中文
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/dsh-plugin-rollout-scout?color=cb3837&logo=npm)](https://www.npmjs.com/package/dsh-plugin-rollout-scout)
6
+ [![CI](https://github.com/SpookySandwich/dsh-plugin-rollout-scout/actions/workflows/ci.yml/badge.svg)](https://github.com/SpookySandwich/dsh-plugin-rollout-scout/actions/workflows/ci.yml)
6
7
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
- [![dsh](https://img.shields.io/badge/dsh-0.1.1--rc.2-4b8dff)](https://github.com/deepseek-ai/deepseek-harness)
8
+ [![dsh](https://img.shields.io/badge/dsh-0.1.5--rc.2-4b8dff)](https://github.com/deepseek-ai/deepseek-harness)
8
9
  [![stars](https://img.shields.io/github/stars/SpookySandwich/dsh-plugin-rollout-scout?style=flat&label=stars)](https://github.com/SpookySandwich/dsh-plugin-rollout-scout/stargazers)
9
10
 
10
11
  服务商有时会灰度发布新的对话模型,你分到哪一个全看运气。灰度侦察会用你自己的账号开启一批临时会话,**在思维链流式输出的同时**读取它,并按「推理是怎么写的」打分——读起来像你手上这个旧模型的立刻中止,不像的留下来。
@@ -132,6 +133,15 @@ dsh plugin --profile web add github:SpookySandwich/dsh-plugin-rollout-scout
132
133
 
133
134
  ## 兼容性
134
135
 
136
+ 版本 `1.5.0`:适配 DSH 0.1.5 的 agent/assistant-stream 瞬态流式事件,保留旧事件支持并防止重复计数,兼容持久化列表的新快照结构。
137
+
138
+ 声明兼容范围为 `>=0.1.5-rc.2 <0.1.6-0`;已验证官方 `0.1.5-rc.2`,不声明兼容 `0.1.6` alpha。旧版 DSH 请保留上一插件版本。[验证记录](.github/reviews/dsh-0.1.5.md)。
139
+
140
+ 可从 [GitHub Release](https://github.com/SpookySandwich/dsh-plugin-rollout-scout/releases/tag/v1.5.0) 下载发布包,然后执行 `dsh plugin --profile desktop add ./dsh-plugin-rollout-scout-1.5.0.tgz`。
141
+
142
+ 本次兼容目标为 DSH `0.1.5-rc.2`。运行 `npm ci`、`npm test` 和 `npm run check:package` 可验证构建及发布包。更新后请重启 DSH。
143
+
144
+
135
145
  入口占用 `sidebar.footer.action` 座位(list 类型,会与其它底部操作并排,而不是把谁挤掉),控制台本体渲染在全局 `shell.overlay` 层。两者都不属于会话作用域,因此不与任何会话内插件冲突。需要侧边栏声明了该座位的 DSH 版本;否则控制台将没有入口。与 [dsh-plugin-smooth-stream](https://github.com/SpookySandwich/dsh-plugin-smooth-stream)、[dsh-plugin-no-workspace](https://github.com/SpookySandwich/dsh-plugin-no-workspace)、[dsh-plugin-message-edit](https://github.com/SpookySandwich/dsh-plugin-message-edit) 同族。
136
146
 
137
147
  ## 关于额度
package/_wrap-client.mjs CHANGED
@@ -29,4 +29,4 @@ const out = [
29
29
  "",
30
30
  ].join('\n');
31
31
  fs.writeFileSync(new URL('./lib/client.js', import.meta.url), out);
32
- console.log('wrote lib/client.js', out.length);
32
+ console.error('wrote lib/client.js', out.length);
package/lib/client.js CHANGED
@@ -489,13 +489,14 @@ const CSS = [
489
489
  ].join('');
490
490
 
491
491
  return {
492
+ inject: ['slots', 'sessions', 'locale'],
492
493
  apply(ctx) {
493
494
  const slots = ctx.get('slots');
494
- if (slots === undefined) return;
495
+ if (slots === undefined) throw new Error('[dsh-plugin-rollout-scout] Missing DSH slots service; check client dependencies.');
495
496
  ctx.effect(function () { return styles.insert(CSS); });
496
497
 
497
- let sessions = null;
498
- try { sessions = ctx.get('sessions'); } catch (e) {}
498
+ const sessions = ctx.get('sessions');
499
+ if (!sessions || typeof sessions.open !== 'function') throw new Error('[dsh-plugin-rollout-scout] Missing DSH session navigation service.');
499
500
 
500
501
  const I18N_NS = 'dsh-plugin-rollout-scout';
501
502
  const I18N = {
package/lib/index.js CHANGED
@@ -1192,20 +1192,7 @@ function pump(ctx) {
1192
1192
  state.attempts.unshift(attempt);
1193
1193
  trimHistory();
1194
1194
  const task = launch(ctx, attempt).catch((error) => {
1195
- const message = error instanceof Error ? error.message : String(error);
1196
- if (attempt.stopReason === null) attempt.error = message;
1197
- // Only a launch that never reached 'streaming' counts towards the
1198
- // breaker: a failure after the turn started is the probe's problem,
1199
- // not a sign that launching itself is broken.
1200
- if (!attempt.streamed && attempt.stopReason === null) {
1201
- state.launchFailures += 1;
1202
- if (state.launchFailures >= LAUNCH_FAILURE_LIMIT) {
1203
- state.running = false;
1204
- state.paused = true;
1205
- state.note = 'launch-failed';
1206
- state.lastError = message;
1207
- }
1208
- }
1195
+ if (attempt.stopReason === null) recordProbeFailure(attempt, error);
1209
1196
  finish(ctx, attempt);
1210
1197
  }).finally(() => {
1211
1198
  attempt.launching = false;
@@ -1314,7 +1301,7 @@ async function launch(ctx, attempt) {
1314
1301
  const handle = await ctx.agents.create({
1315
1302
  sessionId,
1316
1303
  meta: { cwd: workspace.path },
1317
- agentOptions: { provider: selection.provider, model: selection.model },
1304
+ agentOptions: selection,
1318
1305
  signal: attempt.launchController.signal,
1319
1306
  setup: (agentCtx) => {
1320
1307
  if (installModelSelection) {
@@ -1322,6 +1309,11 @@ async function launch(ctx, attempt) {
1322
1309
  }
1323
1310
  // Scoped: only this agent's events reach this listener.
1324
1311
  agentCtx.on('session/event', (session, event) => onSessionEvent(ctx, attempt, event));
1312
+ // DSH 0.1.5 keeps chunks transient instead of persisting assistant/chunk.
1313
+ // Subscribe before publication so even a fast first chunk is observed.
1314
+ agentCtx.on('agent/assistant-stream', ({ frame }) => {
1315
+ if (frame?.type === 'chunk') onAssistantChunk(ctx, attempt, frame.chunk, 'agent');
1316
+ });
1325
1317
  },
1326
1318
  });
1327
1319
  attempt.handle = handle;
@@ -1340,51 +1332,85 @@ async function launch(ctx, attempt) {
1340
1332
  }
1341
1333
 
1342
1334
  attempt.execution = 'running';
1343
- attempt.streamed = true;
1344
- // One probe that got as far as its first turn clears the breaker.
1345
- state.launchFailures = 0;
1346
1335
  titleAttempt(ctx, attempt, 'probe');
1347
1336
  attempt.promptSentAt = Date.now();
1348
1337
  armWatchdog(ctx, attempt);
1349
1338
  handle.agent.followup(probeMessage(config.prompt));
1350
1339
  }
1351
1340
 
1352
- function onSessionEvent(ctx, attempt, event) {
1341
+ function onAssistantChunk(ctx, attempt, chunk, source) {
1353
1342
  if (isEnded(attempt)) return;
1354
- if (event.type === 'assistant/chunk') {
1355
- const chunk = event.data.chunk;
1356
- if (chunk.type === 'reasoning-delta') {
1357
- const now = Date.now();
1358
- const text = chunk.text || '';
1359
- if (attempt.firstChunkAt === null) {
1360
- attempt.firstChunkAt = now;
1361
- if (attempt.promptSentAt !== null) {
1362
- attempt.ttft = Math.round((now - attempt.promptSentAt) / 100) / 10;
1363
- }
1364
- }
1365
- attempt.chunkCount = (attempt.chunkCount || 0) + 1;
1366
- const elapsedSec = (now - attempt.firstChunkAt) / 1000;
1367
- if (attempt.chunkCount >= 8 && elapsedSec >= 0.4) {
1368
- attempt.tps = Math.round((attempt.chunkCount / elapsedSec) * 10) / 10;
1369
- }
1370
- if (attempt.lastChunkAt !== null && text.length > 0
1371
- && now - attempt.lastChunkAt >= PAUSE_MS
1372
- && attempt.burstChars >= BURST_MIN_CHARS) {
1373
- attempt.pauses += 1;
1374
- attempt.burstChars = 0;
1343
+ // Some transitional hosts may expose both feeds. Consume exactly one so
1344
+ // scores, timing and paragraph counts cannot double-count the same text.
1345
+ if (attempt.chunkSource !== undefined && attempt.chunkSource !== source) return;
1346
+ attempt.chunkSource = source;
1347
+ if (chunk.type === 'reasoning-delta') {
1348
+ const now = Date.now();
1349
+ const text = chunk.text || '';
1350
+ if (attempt.firstChunkAt === null) {
1351
+ attempt.firstChunkAt = now;
1352
+ if (attempt.promptSentAt !== null) {
1353
+ attempt.ttft = Math.round((now - attempt.promptSentAt) / 100) / 10;
1375
1354
  }
1376
- attempt.lastChunkAt = now;
1377
- attempt.burstChars += text.length;
1378
- attempt.reasoning += text;
1379
- evaluate(ctx, attempt);
1380
1355
  }
1356
+ attempt.chunkCount = (attempt.chunkCount || 0) + 1;
1357
+ const elapsedSec = (now - attempt.firstChunkAt) / 1000;
1358
+ if (attempt.chunkCount >= 8 && elapsedSec >= 0.4) {
1359
+ attempt.tps = Math.round((attempt.chunkCount / elapsedSec) * 10) / 10;
1360
+ }
1361
+ if (attempt.lastChunkAt !== null && text.length > 0
1362
+ && now - attempt.lastChunkAt >= PAUSE_MS
1363
+ && attempt.burstChars >= BURST_MIN_CHARS) {
1364
+ attempt.pauses += 1;
1365
+ attempt.burstChars = 0;
1366
+ }
1367
+ attempt.lastChunkAt = now;
1368
+ attempt.burstChars += text.length;
1369
+ attempt.reasoning += text;
1370
+ evaluate(ctx, attempt);
1371
+ }
1372
+ }
1373
+
1374
+ function onSessionEvent(ctx, attempt, event) {
1375
+ if (isEnded(attempt)) return;
1376
+ if (event.type === 'assistant/chunk') {
1377
+ onAssistantChunk(ctx, attempt, event.data.chunk, 'session');
1381
1378
  return;
1382
1379
  }
1383
1380
  if (event.type === 'turn/end') {
1381
+ // Current DSH reports request/model failures as terminal session events,
1382
+ // after create() and followup() have succeeded. Treating those as normal
1383
+ // empty replies misclassifies them and can launch replacements forever.
1384
+ if (attempt.stopReason === null) {
1385
+ if (event.data?.reason?.kind === 'error') {
1386
+ recordProbeFailure(attempt, event.data.reason.error);
1387
+ } else if (!attempt.error) {
1388
+ state.launchFailures = 0;
1389
+ }
1390
+ }
1384
1391
  finish(ctx, attempt);
1385
1392
  }
1386
1393
  }
1387
1394
 
1395
+ function recordProbeFailure(attempt, error) {
1396
+ attempt.error = error?.message ?? String(error);
1397
+ if (!retained(attempt)) {
1398
+ attempt.verdict = 'unknown';
1399
+ attempt.reason = 'error';
1400
+ attempt.discard = 'none';
1401
+ resetFadeGrace(attempt);
1402
+ }
1403
+ if (attempt.failureCounted) return;
1404
+ attempt.failureCounted = true;
1405
+ state.launchFailures += 1;
1406
+ if (state.launchFailures >= LAUNCH_FAILURE_LIMIT) {
1407
+ state.running = false;
1408
+ state.paused = true;
1409
+ state.note = 'launch-failed';
1410
+ state.lastError = attempt.error;
1411
+ }
1412
+ }
1413
+
1388
1414
  /**
1389
1415
  * The fade grace has one owner and three derived phases:
1390
1416
  * idle — no timer, deadline or saved remainder
@@ -2115,7 +2141,7 @@ function finish(ctx, attempt) {
2115
2141
  attempt.endedAt = Date.now();
2116
2142
  clearWatchdog(attempt);
2117
2143
  clearCancelTimer(attempt);
2118
- if (attempt.stopReason === null) {
2144
+ if (attempt.stopReason === null && !attempt.error) {
2119
2145
  evaluate(ctx, attempt, true);
2120
2146
  }
2121
2147
  if (attempt.verdict === null) {
@@ -2169,7 +2195,7 @@ async function removeSessionLog(ctx, sessionId, headers, cwd) {
2169
2195
  // computation over `{ id, cwd }`. Depending on it alone left behind the log
2170
2196
  // of any session it had not caught up with — and a log whose workspace slot
2171
2197
  // is gone is an ungrouped sidebar row with no Delete in its menu.
2172
- const header = headers.find((h) => h.id === sessionId)
2198
+ const header = headers.map((snapshot) => snapshot.header ?? snapshot).find((h) => h.id === sessionId)
2173
2199
  ?? (cwd === undefined ? undefined : { id: sessionId, cwd });
2174
2200
  if (header === undefined) throw new Error(`找不到会话记录 ${sessionId} / session record not found`);
2175
2201
  let location;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-rollout-scout",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Detect which conversation model your DeepSeek Harness account is served: score probe conversations' live chain-of-thought, cancel old-model probes in seconds, keep confident catches.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -40,7 +40,13 @@
40
40
  },
41
41
  "client": {
42
42
  "platform": "web",
43
- "inject": []
43
+ "inject": [
44
+ "@deepseek-ai/dsh-api-session-controller",
45
+ "@deepseek-ai/dsh-client-locale",
46
+ "@deepseek-ai/dsh-client-ui-renderer",
47
+ "@deepseek-ai/dsh-client-ui-layout",
48
+ "@deepseek-ai/dsh-client-modules"
49
+ ]
44
50
  }
45
51
  },
46
52
  "scripts": {
@@ -48,7 +54,17 @@
48
54
  "prepare": "node _wrap-client.mjs",
49
55
  "prepack": "node _wrap-client.mjs",
50
56
  "pretest": "node _wrap-client.mjs",
51
- "test": "node test/classifier.test.mjs && node test/route.test.mjs && node test/pump.test.mjs && node test/delete.test.mjs && node test/lifecycle.test.mjs && node test/ownership.test.mjs && node test/manifest.test.mjs && node test/manifest-concurrency.test.mjs && node test/manifest-process.test.mjs && node test/history.test.mjs && node test/state-machine.test.mjs && node test/client-review.test.mjs && node test/client-anchor.test.mjs && node test/client-css.test.mjs",
52
- "check:client": "node _wrap-client.mjs && git diff --exit-code -- lib/client.js"
57
+ "test": "node test/classifier.test.mjs && node test/route.test.mjs && node test/pump.test.mjs && node test/delete.test.mjs && node test/lifecycle.test.mjs && node test/ownership.test.mjs && node test/manifest.test.mjs && node test/manifest-concurrency.test.mjs && node test/manifest-process.test.mjs && node test/history.test.mjs && node test/state-machine.test.mjs && node test/client-review.test.mjs && node test/client-anchor.test.mjs && node test/client-css.test.mjs && node --test test/client-loading.test.mjs && node --test test/probe-errors.test.mjs && node --test test/stream-events.test.mjs",
58
+ "check:client": "node _wrap-client.mjs && git diff --exit-code -- lib/client.js",
59
+ "check:package": "node scripts/check-package.mjs"
60
+ },
61
+ "devDependencies": {
62
+ "@deepseek-ai/cordis": "^4.0.2",
63
+ "jsdom": "^29.1.1",
64
+ "react": "^18.3.1",
65
+ "react-dom": "^18.3.1"
66
+ },
67
+ "engines": {
68
+ "dsh": ">=0.1.5-rc.2 <0.1.6-0"
53
69
  }
54
70
  }
package/plugin.client.js CHANGED
@@ -469,13 +469,14 @@ const CSS = [
469
469
  ].join('');
470
470
 
471
471
  return {
472
+ inject: ['slots', 'sessions', 'locale'],
472
473
  apply(ctx) {
473
474
  const slots = ctx.get('slots');
474
- if (slots === undefined) return;
475
+ if (slots === undefined) throw new Error('[dsh-plugin-rollout-scout] Missing DSH slots service; check client dependencies.');
475
476
  ctx.effect(function () { return styles.insert(CSS); });
476
477
 
477
- let sessions = null;
478
- try { sessions = ctx.get('sessions'); } catch (e) {}
478
+ const sessions = ctx.get('sessions');
479
+ if (!sessions || typeof sessions.open !== 'function') throw new Error('[dsh-plugin-rollout-scout] Missing DSH session navigation service.');
479
480
 
480
481
  const I18N_NS = 'dsh-plugin-rollout-scout';
481
482
  const I18N = {