dsh-session-superagent 0.8.13 → 0.8.15

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,63 @@
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.15] - 2026-11-02 —— sync-profile 支持修复断链(事故 1 的第二种形态)
6
+
7
+ ### 修什么
8
+ `sync-profile.mjs` 之前只会「补缺失文件」,遇到**已存在但 inode 不同**(断链)只打印一句“注意”。
9
+ 断链的来源很日常:任何用「写临时文件再 rename」保存的工具(含某些编辑器的原子写)都会让
10
+ profile 里那份留在旧 inode 上 —— 于是**宿主跑旧代码,仓库里看着是新代码**。
11
+
12
+ 现在三种模式:
13
+
14
+ ```bash
15
+ pnpm run sync:profile # 补齐缺失(不动断链,只警告)
16
+ pnpm run sync:profile:repair # 断链重建硬链接 + 清理 profile 里的陈旧文件
17
+ pnpm run sync:profile:check # 只检查:有缺失/断链/陈旧就 exit 1(CI/发布前用)
18
+ ```
19
+
20
+ 实测:profile 里 `README.md` / `CHANGELOG.md` 当时确实是断链,`--repair` 重建后
21
+ `--check` 从 exit 1 变 exit 0。另外故意把 README 换成新 inode,验证 `--check` 会红(exit 1)。
22
+
23
+ ### 守门
24
+ - 新增用例:`sync-profile` 的 `TARGETS` 必须覆盖 `lib/` 下所有 js 文件 ——
25
+ 防止「新增 lib 文件忘了登记」再次导致插件不挂载。
26
+
27
+ ### 测试
28
+ - 合计 **81 例**。
29
+
30
+ ## [0.8.14] - 2026-11-02 —— 第四轮审核:1 条文档 + 3 条代码建议
31
+
32
+ 审核范围 0.8.9→0.8.13(上一轮门禁只覆盖到 0.8.8)。结论:代码侧无阻塞,文档有 1 处必须修。
33
+
34
+ ### 修复
35
+ - **幽灵会话不退避**(真问题):绑到「已删除会话」的 watcher 每次轮询都打一次 GitHub 且永不休止
36
+ (0.8.11 的分支直接 `return`,没走 `bumpFailure`)。现在会正常进入 30 分钟退避。
37
+ - **删掉 `/client-hello` 路由**:客户端只在那里发过一次 fire-and-forget fetch,而 `note('apply')`
38
+ 已经覆盖同一用途。路由 7 → 6,并加用例锁死路由集合(`/config`、`/client-hello` 都不该回来)。
39
+
40
+ ### 文档
41
+ - HANDOFF 版本号 0.8.11 → 0.8.14;README / MAINTENANCE / HANDOFF 的测试数 77 → 80。
42
+
43
+ ### 未采纳(附理由)
44
+ - **goal 文本里的 `【主理鱼】` 内联**:已压平换行(实测伪造抬头不会独占一行),
45
+ 且 goal 只能由面板或本地 POST 写入,风险低;强行改写用户文本得不偿失。
46
+ - **`sync-profile --repair`**:断链(inode 不同)目前只警告。已在 HANDOFF 写明「新增文件后必须跑」,
47
+ 留作后续(改起来简单,但此刻没有证据表明它造成过事故)。
48
+
49
+ ### 发布
50
+ - 已发 `dsh-session-superagent@0.8.14`(registry `latest = 0.8.14`)。
51
+ - **从 tarball 真装验证**:`npm i https://registry.npmjs.org/dsh-session-superagent/-/dsh-session-superagent-0.8.14.tgz`
52
+ → `apply()` 成功、**路由 6 条**(`/client-hello` 确实没了)、`client.js` 语法通过、`lib/` 四个文件齐全。
53
+ 注:刚发布时 `npm i pkg@x.y.z` 可能因客户端元数据缓存报 ETARGET,用 tarball URL 或 `--prefer-online` 可绕过。
54
+
55
+ ### 其他
56
+ - `scripts/smoke-observe.mjs` 加 `gh auth status` 预检:没登录时明确 SKIP 并退出 0,
57
+ 不再把「凭证/网络问题」混报成逻辑失败(第四轮审核的建议)。
58
+
59
+ ### 测试
60
+ - 合计 **80 例**(新增路由集合用例)。
61
+
5
62
  ## [0.8.13] - 2026-11-02 —— 真机跑「只观察」闭环,抓出两个真 bug
6
63
 
7
64
  拿真仓库(MYCF711/dsh-GOkifu-compact,3 个真 issue)跑 observe 模式验证,抓到:
@@ -21,9 +78,46 @@ history: [{"numbers":[4,2,3,1],"ok":false,"reason":"observe"}]
21
78
  filteredLog: [] ← 改成 labels:["ai-fix"] 后仍为空(3 个 issue 都没这个标签)
22
79
  ```
23
80
 
81
+ ### 发布与真机验证
82
+ - 已发 `dsh-session-superagent@0.8.13`。
83
+ - **`watch_ack` 第一次在真仓库上验证**:给 `MYCF711/dsh-GOkifu-compact#2` 打上 `主理鱼:已处理`——
84
+ GitHub **接受中文 + 冒号**的标签名(此前审核把它列为"未验证");重复 ack 幂等不报错;
85
+ 验证完已把 issue 标签与仓库标签定义都撤掉,不留痕迹。
86
+
24
87
  ### 测试
25
88
  - 合计 **79 例**。
26
89
 
90
+ ### 进程内冒烟(补证:这两个修复到底生效没有)
91
+ `pnpm run smoke:observe`(`scripts/smoke-observe.mjs`)—— 独立进程、独立状态目录、
92
+ 捕获 `setInterval` 手动驱动,会真的读 GitHub 只读 API:
93
+
94
+ ```
95
+ OK 观模式仍未唤醒 (followups = 0)
96
+ OK filteredLog 已落盘 (4 条)
97
+ OK 过滤原因可读 ("不在标签白名单: ai-fix")
98
+ OK /state 能读到过滤记录
99
+ OK 空轮询清掉了过期的 lastDeferred
100
+
101
+ == 场景 B:真唤醒 + 隔离 worktree ==
102
+ OK 唤醒了一次 (followups = 1)
103
+ OK 消息带【主理鱼】抬头
104
+ OK 消息带条目编号 (#2 #4 #3 #1,真 GitHub 数据)
105
+ OK 消息带本条目主理目标 (「把 widget 的 issue 都处理完」)
106
+ OK 消息带隔离路径
107
+ OK worktree 真的建了 (.dsh-zhuli/MYCF711-dsh-GOkifu-compact-smoke-se-…)
108
+ OK .dsh-zhuli 下正好一个副本
109
+ ```
110
+
111
+ 消息实样(真数据):
112
+
113
+ ```
114
+ 【主理鱼】MYCF711/dsh-GOkifu-compact 有 4 条新动态:
115
+ #2 [open] docs/bench:给「351 tokens」和「失败保留 8/8」补一条可复跑的复现命令 — @MYCF711 (issue) …
116
+ #4 [closed] 【联调】v0.1.0 唤醒链路验证 — @MYCF711 (issue) …
117
+ 【隔离工作区】…\.dsh-zhuli\MYCF711-dsh-GOkifu-compact-smoke-se-…
118
+ 本条目主理目标:把 widget 的 issue 都处理完
119
+ ```
120
+
27
121
  ## [0.8.12] - 2026-11-02 —— 体验打磨四条
28
122
 
29
123
  ### 改了什么
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-session-superagent
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
- [![Version](https://img.shields.io/badge/version-0.8.13-green.svg)](https://github.com/MYCF711/dsh-session-superagent/releases)
4
+ [![Version](https://img.shields.io/badge/version-0.8.15-green.svg)](https://github.com/MYCF711/dsh-session-superagent/releases)
5
5
  [![npm](https://img.shields.io/npm/v/dsh-session-superagent.svg)](https://www.npmjs.com/package/dsh-session-superagent)
6
6
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-plugin-orange.svg)](https://github.com/topics/dsh-plugin)
7
7
 
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.13'
17
+ const CLIENT_VERSION = '0.8.15'
18
18
  const BASE = '/session-superagent'
19
19
  const ENDPOINT = BASE + '/state'
20
20
  const POLL_MS = 5000
@@ -974,7 +974,6 @@ window.__ModuleLoader__.load({
974
974
  apply(ctx) {
975
975
  try { noteOnce('client-version', CLIENT_VERSION) } catch {}
976
976
  try { note('apply', 'start') } catch {}
977
- try { fetch(BASE + '/client-hello').catch(() => {}) } catch {}
978
977
 
979
978
  // ① the 主理鱼 tab in the centre-column view ring
980
979
  ctx.effect(() => {
package/lib/index.js CHANGED
@@ -470,8 +470,10 @@ async function pollOnce(ctx, entry, baseMs) {
470
470
  // 会话忙就别开隔离区了:白建一个没人用的 worktree
471
471
  const liveAgent = ctx.agents && typeof ctx.agents.get === 'function' ? ctx.agents.get(entry.sessionId) : undefined
472
472
  if (!liveAgent) {
473
- // 会话已经不在了(被删/换机器):别开隔离区,也别假装在盯
473
+ // 会话已经不在了(被删/换机器):别开隔离区,也别假装在盯。
474
+ // 必须走 bumpFailure —— 否则这条会每 intervalSec 打一次 GitHub,永不退避(第四轮审核指出)。
474
475
  entry.lastError = '会话已不在:请删除或重建这条主理鱼'
476
+ bumpFailure(entry, baseMs)
475
477
  return
476
478
  }
477
479
  const gate = decideWake(liveAgent && liveAgent.status)
@@ -626,11 +628,6 @@ export function apply(ctx) {
626
628
  }))
627
629
  },
628
630
  }))
629
- routes.push(httpServer.register({
630
- kind: 'exact',
631
- path: '/session-superagent/client-hello',
632
- handler: (req, res) => { record('hello', String(req.url)); res.statusCode = 204; res.end() },
633
- }))
634
631
  routes.push(httpServer.register({
635
632
  kind: 'exact',
636
633
  path: '/session-superagent/client-note',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-superagent",
3
- "version": "0.8.13",
3
+ "version": "0.8.15",
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",
@@ -30,7 +30,10 @@
30
30
  "test": "node --test",
31
31
  "check": "node --test",
32
32
  "preview:orbit": "node scripts/preview-orbit.mjs",
33
- "sync:profile": "node scripts/sync-profile.mjs"
33
+ "sync:profile": "node scripts/sync-profile.mjs",
34
+ "smoke:observe": "node scripts/smoke-observe.mjs",
35
+ "sync:profile:check": "node scripts/sync-profile.mjs --check",
36
+ "sync:profile:repair": "node scripts/sync-profile.mjs --repair"
34
37
  },
35
38
  "keywords": [
36
39
  "dsh",
@@ -1,17 +1,28 @@
1
1
  // 把插件源码目录同步到 profile 的安装副本。
2
2
  //
3
3
  // 为什么需要:profile 里是 `file:` 安装 —— pnpm 为**已存在的文件**建了硬链接,
4
- // 所以改内容会自动生效;但**新增文件不会**自动出现。
5
- // 真实事故(2026-11-01 23:59 重启):新增的 lib/body.js 没进 profile,
6
- // 宿主 import './body.js' 失败 → 整个插件不挂载 → 工具和 HTTP 路由全消失,
7
- // 而客户端 bundle 照常加载,表面看起来"插件还在"。
4
+ // 改内容会自动生效;但**新增文件不会**自动出现,而**被原子写入替换的文件会断链**。
8
5
  //
9
- // 用法: node scripts/sync-profile.mjs [profile 安装目录]
10
- import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'
6
+ // 真实事故 1(2026-11-01 23:59 重启):新增的 lib/body.js 没进 profile,
7
+ // 宿主 import './body.js' 失败 → 整个插件不挂载 → 工具和 HTTP 路由全消失,
8
+ // 客户端 bundle 却照常加载,表面看起来"插件还在"。
9
+ // 真实事故 2(潜伏):某些工具用「写临时文件再 rename」保存,profile 里那份仍是旧 inode,
10
+ // 于是宿主跑的是旧代码,而仓库里看着已经是新代码。
11
+ //
12
+ // 用法:
13
+ // node scripts/sync-profile.mjs 补齐缺失文件(不动断链,只警告)
14
+ // node scripts/sync-profile.mjs --repair 断链也重建硬链接,并清理 profile 里的陈旧文件
15
+ // node scripts/sync-profile.mjs --check 只检查:有缺失/断链/陈旧就 exit 1(给 CI 用)
16
+ // node scripts/sync-profile.mjs --dir <path> 指定 profile 安装目录
17
+ import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs'
11
18
  import { dirname, join } from 'node:path'
12
19
  import { fileURLToPath } from 'node:url'
13
20
 
14
21
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
22
+ const args = process.argv.slice(2)
23
+ const REPAIR = args.includes('--repair')
24
+ const CHECK = args.includes('--check')
25
+ const dirFlag = args.indexOf('--dir')
15
26
  const DEFAULT_PROFILE = join(
16
27
  process.env.DSH_HOME || join(process.env.USERPROFILE || '', '.dsh'),
17
28
  'profiles',
@@ -19,7 +30,7 @@ const DEFAULT_PROFILE = join(
19
30
  'node_modules',
20
31
  'dsh-session-superagent',
21
32
  )
22
- const DEST = process.argv[2] || DEFAULT_PROFILE
33
+ const DEST = dirFlag >= 0 ? args[dirFlag + 1] : DEFAULT_PROFILE
23
34
 
24
35
  /** 运行时要用的文件(不含 docs / test / scripts 这些开发件)。 */
25
36
  const TARGETS = [
@@ -32,45 +43,87 @@ if (!existsSync(DEST)) {
32
43
  process.exit(1)
33
44
  }
34
45
 
46
+ const drift = { missing: [], broken: [], stale: [] }
35
47
  let linked = 0
36
- let skipped = 0
48
+
37
49
  for (const rel of TARGETS) {
38
50
  const src = join(ROOT, rel)
39
51
  const dst = join(DEST, rel)
40
52
  if (!existsSync(src)) continue
41
- if (existsSync(dst)) {
42
- const same = statSync(src).ino === statSync(dst).ino
43
- console.log((same ? 'ok ' : '注意 ') + rel + (same ? '' : '(inode 不同,内容改动不会同步)'))
44
- skipped += 1
53
+ if (!existsSync(dst)) {
54
+ drift.missing.push(rel)
55
+ if (!CHECK) {
56
+ mkdirSync(dirname(dst), { recursive: true })
57
+ try { linkSync(src, dst); linked += 1; console.log('linked ' + rel) } catch (error) {
58
+ console.log('link 失败:' + rel + ' — ' + (error && error.message))
59
+ }
60
+ }
45
61
  continue
46
62
  }
47
- mkdirSync(dirname(dst), { recursive: true })
48
- try {
49
- linkSync(src, dst)
50
- linked += 1
51
- console.log('linked ' + rel)
52
- } catch (error) {
53
- console.log('link 失败:' + rel + ' — ' + (error && error.message))
63
+ const same = statSync(src).ino === statSync(dst).ino
64
+ if (same) continue
65
+ drift.broken.push(rel)
66
+ if (REPAIR && !CHECK) {
67
+ try {
68
+ unlinkSync(dst)
69
+ linkSync(src, dst)
70
+ linked += 1
71
+ console.log('relinked ' + rel + '(原来断了)')
72
+ } catch (error) {
73
+ console.log('重建失败:' + rel + ' — ' + (error && error.message))
74
+ }
75
+ } else {
76
+ console.log('注意 ' + rel + '(inode 不同:内容改动不会同步到 profile,用 --repair 重建)')
77
+ }
78
+ }
79
+
80
+ // 陈旧文件:profile 里有、仓库里已经没有(例如删掉的模块)
81
+ for (const name of readdirSync(join(DEST, 'lib')).filter((f) => f.endsWith('.js'))) {
82
+ if (!existsSync(join(ROOT, 'lib', name))) {
83
+ drift.stale.push('lib/' + name)
84
+ if (REPAIR && !CHECK) {
85
+ try { unlinkSync(join(DEST, 'lib', name)); console.log('pruned lib/' + name) } catch {}
86
+ } else {
87
+ console.log('陈旧 lib/' + name + '(仓库已删除,profile 里还在)')
88
+ }
54
89
  }
55
90
  }
56
91
 
57
- // lib/ 里每个相对 import 都必须能解析到真实文件(这次事故的直接原因)
92
+ // lib/ 里每个相对 import 都必须能解析(这正是事故 1 的直接原因)
58
93
  const libDir = join(ROOT, 'lib')
59
- const missing = []
94
+ const missingImports = []
60
95
  for (const name of readdirSync(libDir)) {
61
96
  if (!name.endsWith('.js')) continue
62
- const file = join(libDir, name)
63
- const code = readFileSync(file, 'utf8')
64
- for (const m of code.matchAll(/from '(\.[^']+)'/g)) {
65
- if (!existsSync(join(dirname(file), m[1]))) missing.push(name + ' → ' + m[1])
97
+ const code = readFileSync(join(libDir, name), 'utf8')
98
+ for (const line of code.split(String.fromCharCode(10))) {
99
+ const trimmed = line.trim()
100
+ if (!trimmed.startsWith('import ')) continue
101
+ const at = trimmed.lastIndexOf("from '")
102
+ let spec = null
103
+ if (at >= 0) {
104
+ spec = trimmed.slice(at + 6).split("'")[0]
105
+ } else if (trimmed.slice('import '.length).startsWith("'")) {
106
+ spec = trimmed.slice('import '.length + 1).split("'")[0]
107
+ }
108
+ if (spec && spec.startsWith('.') && !existsSync(join(libDir, spec))) {
109
+ missingImports.push(name + ' -> ' + spec)
110
+ }
66
111
  }
67
112
  }
68
113
 
114
+ const drifted = drift.missing.length + drift.broken.length + drift.stale.length
69
115
  console.log('---')
70
- console.log(`新增链接 ${linked} 个,跳过 ${skipped} 个`)
71
116
  console.log(`目标:${DEST}`)
72
- if (missing.length > 0) {
73
- console.error('相对 import 解析失败:' + missing.join(', '))
117
+ console.log(`缺失 ${drift.missing.length} / 断链 ${drift.broken.length} / 陈旧 ${drift.stale.length}` +
118
+ (linked ? `,本次处理 ${linked} 个` : ''))
119
+
120
+ if (missingImports.length > 0) {
121
+ console.error('相对 import 解析失败:' + missingImports.join(', '))
74
122
  process.exit(2)
75
123
  }
76
124
  console.log('相对 import 全部可解析 ✓')
125
+
126
+ if (CHECK && drifted > 0) {
127
+ console.error('profile 与仓库不一致(跑 --repair 修)')
128
+ process.exit(1)
129
+ }