dsh-session-superagent 0.8.27 → 0.8.30

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/CHANGELOG.md CHANGED
@@ -2,6 +2,74 @@
2
2
 
3
3
  格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.8.30] - 2026-11-02 —— 0.8.28 没修好:宿主重载是「重新 import 一份模块」
6
+
7
+ ### 真机判定实验(20:00 重载后,routes 6 / failed [])
8
+ 面板(HTTP)建的 watcher **仍然报**:
9
+
10
+ ```
11
+ lastError: cannot get required service "agents" in inactive context
12
+ consecutiveFailures: 0 / nextAt: None
13
+ ```
14
+
15
+ 说明 0.8.28 的模块级 `activeRoutes` 没有生效 —— 因为**宿主重载是重新 import 一份模块**,
16
+ 模块级变量全新(数组是空的),新实例根本不知道旧实例注册过什么。
17
+
18
+ ### 修复
19
+ 把三个必须跨实例共享的结构挂到 `globalThis` 上(用 `Symbol.for` 做键):
20
+
21
+ | 结构 | 为什么必须共享 |
22
+ |---|---|
23
+ | `activeRoutes`(路由 disposer) | 新实例要能摘掉旧实例的路由,否则旧 ctx 继续吃 HTTP |
24
+ | `timers`(定时器句柄) | 旧实例的 tick 还持有已 dispose 的 ctx,必须清掉 |
25
+ | `inFlight`(并发闸) | 旧实例没跑完的那轮,新实例不能同时再跑一遍 |
26
+
27
+ ### 守门
28
+ 新增 `test/cross-instance.test.mjs`:用 `import('../lib/index.js?instance=1')` 与 `?instance=2`
29
+ 模拟两次 import,断言第二个实例会摘掉第一个实例注册的 6 条路由。
30
+
31
+ ### 测试
32
+ - 合计 **92 例**。
33
+
34
+ ## [0.8.29] - 2026-11-02 —— `watch_doctor` 现在会告诉你「面板接口死没死」
35
+
36
+ 这一轮的真机事故(重复路由 → 六条接口全灭)有个共同特征:**工具还是好的,只有面板挂了**。
37
+ `watch_status` 看不出异常,用户只会觉得"面板空白"。而 `state.server` 里其实一直记着挂载结果。
38
+
39
+ ### 新增
40
+ `watch_doctor` 第一行现在是挂载状态:
41
+
42
+ ```
43
+ OK 面板接口挂载:6 条,无失败
44
+ FAIL 面板接口挂载:webserver: duplicate exact route "/session-superagent/state"(工具仍可用,但面板会空白;重载一次即可)
45
+ WARN 面板接口部分失败(5/6 成功):…
46
+ ```
47
+
48
+ ### 测试
49
+ - 新增用例:doctor 输出里必须有挂载状态行。合计 **91 例**。
50
+ (写这个用例时踩到一个小坑:`cleanup` 用例会删掉临时状态目录,doctor 用例必须排在它前面 —— 否则看到的是"没有挂载记录"。)
51
+
52
+ ## [0.8.28] - 2026-11-02 —— 旧实例还在吃 HTTP 请求(面板建的 watcher 一开就废)
53
+
54
+ ### 真机复验发现
55
+ 宿主在 19:49 挂载成功(`routes: 6, failed: []`),但**通过 HTTP(面板)建的 watcher 仍然报**:
56
+
57
+ ```
58
+ lastError: cannot get required service "agents" in inactive context
59
+ consecutiveFailures: 0 / nextAt: None
60
+ ```
61
+
62
+ 对照实验:用**工具**(`watch_start`)建的 watcher 同一时刻轮询正常、无错误。
63
+ 差别在于请求被谁处理 —— 路由最初由 19:32 那个实例注册,重载后若没走 dispose,
64
+ 它继续吃 HTTP 请求,而它的 `ctx` 早已 dispose;于是那些 watcher 的定时器跑在死上下文里。
65
+
66
+ ### 修复
67
+ 模块级 `activeRoutes`:每次 `apply()` 注册路由前,**先摘掉上一次注册的**(接管而不是并存)。
68
+ 配合 0.8.25 的单条容错与 0.8.26 的 teardown,重载后的三条路径都覆盖了。
69
+
70
+ ### 测试
71
+ - 新增用例:同一模块 `apply` 两次时,第二次会先 unregister 第一批(6 条)再注册新的 6 条。合计 **90 例**。
72
+
5
73
  ## [0.8.27] - 2026-11-02 —— 文案核对 + 把 0.8.21–0.8.27 一次发到 npm
6
74
 
7
75
  ### 文案
@@ -9,7 +77,8 @@
9
77
  改成「已添加,首轮立即开始轮询」。
10
78
 
11
79
  ### 发布
12
- - 一次把 0.8.21–0.8.27 发出去(npm 上原来停在 0.8.20)。这一批里有三个真机事故修复:
80
+ - 一次把 0.8.21–0.8.27 发出去(npm 上原来停在 0.8.20)。已确认 `latest = 0.8.27`,
81
+ tarball 直装验证:`version 0.8.27`、`routes 6`、`client.js` 语法通过、新文案在位。这一批里有三个真机事故修复:
13
82
  `inactive context`(唤醒会失败)、未预期异常不退避、热重载后重复路由导致面板全灭。
14
83
 
15
84
  ### 测试
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # dsh-session-superagent
2
2
 
3
+ <img src="assets/icon-180.png" width="88" height="88" alt="">
4
+
3
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
- [![Version](https://img.shields.io/badge/version-0.8.27-green.svg)](https://github.com/MYCF711/dsh-session-superagent/releases)
6
+ [![Version](https://img.shields.io/badge/version-0.8.30-green.svg)](https://github.com/MYCF711/dsh-session-superagent/releases)
5
7
  [![npm](https://img.shields.io/npm/v/dsh-session-superagent.svg)](https://www.npmjs.com/package/dsh-session-superagent)
6
8
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-plugin-orange.svg)](https://github.com/topics/dsh-plugin)
7
9
 
package/client.js CHANGED
@@ -14,7 +14,7 @@ window.__ModuleLoader__.load({
14
14
  const { useCallback, useEffect, useRef, useState } = React
15
15
 
16
16
  // 客户端版本探针:每次模块加载写一次,用来确认浏览器里跑的是哪一版
17
- const CLIENT_VERSION = '0.8.27'
17
+ const CLIENT_VERSION = '0.8.30'
18
18
  const BASE = '/session-superagent'
19
19
  const ENDPOINT = BASE + '/state'
20
20
  const POLL_MS = 5000
package/lib/index.js CHANGED
@@ -41,6 +41,27 @@ const TRIGGER_FILE = join(STATE_DIR, 'trigger.json')
41
41
  const CLIENT_NOTE_KEYS = new Set(['client-version', 'apply', 'desk-registered', 'desk-fail', 'orbit-registered', 'orbit-fail', 'orbit-mount', 'badge-registered', 'badge-mount', 'badge-state', 'error'])
42
42
  const DEFAULT_COOLDOWN_MS = 180_000
43
43
 
44
+ /**
45
+ * 当前挂在这个进程上的路由 disposer。
46
+ *
47
+ * 宿主重载时若没走 dispose,旧实例会继续吃 HTTP 请求,而它的 ctx 可能已经 dispose ——
48
+ * 真机表现:面板建的 watcher 第一轮就报
49
+ * `cannot get required service "agents" in inactive context`(2026-11-02 复验)。
50
+ *
51
+ * 关键:宿主重载常常是**重新 import 一份模块**(模块级变量全新),所以光用模块级数组
52
+ * 接不过去 —— 必须挂在 `globalThis` 上(用 Symbol.for 做键,多个模块实例共享同一个)。
53
+ */
54
+ // 这三个结构必须跨模块实例共享:宿主重载常常是重新 import 一份模块,
55
+ // 模块级变量全新,但进程只有一个。新实例要能清掉旧实例的路由与定时器,
56
+ // 也要能看见旧实例「正在跑的那轮」(否则并发闸形同虚设)。
57
+ const TIMERS_KEY = Symbol.for('dsh-session-superagent.timers')
58
+ const INFLIGHT_KEY = Symbol.for('dsh-session-superagent.inFlight')
59
+ const sharedTimers = (globalThis[TIMERS_KEY] ||= new Map())
60
+ const sharedInFlight = (globalThis[INFLIGHT_KEY] ||= new Set())
61
+
62
+ const ROUTES_KEY = Symbol.for('dsh-session-superagent.activeRoutes')
63
+ const activeRoutes = (globalThis[ROUTES_KEY] ||= [])
64
+
44
65
  function readState() {
45
66
  try { return JSON.parse(readFileSync(STATE_FILE, 'utf8')) } catch { return { watchers: {} } }
46
67
  }
@@ -537,10 +558,11 @@ async function pollOnce(ctx, entry, baseMs) {
537
558
  }
538
559
 
539
560
  export function apply(ctx) {
540
- const timers = new Map()
561
+ const timers = sharedTimers
541
562
  // 并发闸的唯一事实源:tick 和 HTTP「立刻」拿到的是两个不同的 entry 对象,
542
563
  // 靠 entry.polling 标记永远对不上(审核专员 probe-c 实证:同一条被唤醒 3 次)。
543
- const inFlight = new Set()
564
+ // 跨模块实例共享:旧实例还没跑完的那轮,新实例不能同时再跑一遍。
565
+ const inFlight = sharedInFlight
544
566
 
545
567
  const startWatcher = (entry) => {
546
568
  if (timers.has(entry.id)) return
@@ -589,6 +611,10 @@ export function apply(ctx) {
589
611
  // does not wait for it.
590
612
  const registerRoutes = (httpServer) => {
591
613
  if (!httpServer || typeof httpServer.register !== 'function') return
614
+ for (const dispose of activeRoutes) { try { dispose() } catch {} }
615
+ activeRoutes.length = 0
616
+ for (const handle of timers.values()) { try { clearInterval(handle) } catch {} }
617
+ timers.clear()
592
618
  const routes = []
593
619
  // 宿主热重载时旧实例的路由可能还没 dispose;一条重复会让整个 try 崩掉,
594
620
  // 面板和所有接口一起 404(2026-11-02 真机踩到 webserver: duplicate exact route)。
@@ -596,7 +622,9 @@ export function apply(ctx) {
596
622
  const failedRoutes = []
597
623
  const addRoute = (options) => {
598
624
  try {
599
- routes.push(httpServer['register'](options))
625
+ const dispose = httpServer['register'](options)
626
+ routes.push(dispose)
627
+ activeRoutes.push(dispose)
600
628
  } catch (error) {
601
629
  failedRoutes.push(String((error && error.message) || error).slice(0, 120))
602
630
  }
@@ -857,6 +885,7 @@ export function apply(ctx) {
857
885
  const teardown = () => {
858
886
  for (const dispose of routes) { try { dispose() } catch {} }
859
887
  routes.length = 0
888
+ activeRoutes.length = 0
860
889
  // 插件卸载/热重载后旧定时器不能继续跑:它们还持有已 dispose 的 ctx
861
890
  for (const handle of timers.values()) { try { clearInterval(handle) } catch {} }
862
891
  timers.clear()
@@ -972,6 +1001,22 @@ export function apply(ctx) {
972
1001
  const all = Object.values(state.watchers || {})
973
1002
  const wanted = args.repo ? all.filter((w) => w && w.repo === String(args.repo)) : all
974
1003
 
1004
+ // 0. 挂载状态:面板/HTTP 靠路由,路由注册失败时工具仍然可用 —— 这种「半死」状态
1005
+ // 光看工具是发现不了的(2026-11-02 真机踩过:duplicate exact route 让六条接口全灭)
1006
+ if (state.server) {
1007
+ const mounted = Number(state.server.routes) || 0
1008
+ const failed = Array.isArray(state.server.failed) ? state.server.failed : []
1009
+ if (state.server.error) {
1010
+ lines.push(`FAIL 面板接口挂载:${state.server.error}(工具仍可用,但面板会空白;重载一次即可)`)
1011
+ } else if (failed.length > 0) {
1012
+ lines.push(`WARN 面板接口部分失败(${mounted}/6 成功):${failed.join(';')}`)
1013
+ } else {
1014
+ lines.push(`OK 面板接口挂载:${mounted} 条,无失败`)
1015
+ }
1016
+ } else {
1017
+ lines.push('WARN 还没有挂载记录(刚装上或从未加载过路由)')
1018
+ }
1019
+
975
1020
  // 1. gh 是否登录
976
1021
  const auth = await run('gh', ['auth', 'status'], { env: ghEnv() })
977
1022
  const authed = auth.code === 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-superagent",
3
- "version": "0.8.27",
3
+ "version": "0.8.30",
4
4
  "description": "Session-level superagent for DeepSeek Harness: watch a GitHub repository and wake a session when something needs doing. Credentials never reach the model.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",