npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-archived

v0.4.1

Published

Archived-sessions page for DeepSeek Harness: per-row and batch deletion that clears the session log, the projection cache and the archive index together, with a 30-day recycle bin, subagent-aware refusals and a per-row disk footprint.

Readme

dsh-archived

给 DeepSeek Harness 的 设置 → 已归档 页补上删除:逐条删、勾选批量删、清空; 删除默认先进回收站(30 天内可恢复),并显示每行的磁盘占用。

DSH 至今没有官方「删除会话」的接口——归档只能取消归档,日志和投影缓存会一直留在磁盘上。 这个插件就是补上那一块。

它做了什么

DSH 0.2.0-rc.2 里,一个已归档会话牵涉五处状态,少清一处列表就会骗人:

| # | 状态 | 位置 | |---|---|---| | 1 | 会话日志目录 | $DSH_HOME/sessions/<编码工作区>/session-<id>/(session.v4.jsonl.zstd + session.lock) | | 2 | 投影缓存文档 | $DSH_HOME/storages/session_projcache/sessions/<id>.json | | 3 | 归档索引 | workspace registry 的 archivedSessionIds | | 4 | 工作区记账位 | workspace registry 里那个工作区的 sessionIds | | 5 | 回收站(本插件自建) | $DSH_HOME/.archived-sessions-quarantine/<id>/ |

删除有两条路径,界面上是两个分开的按钮:

  • 删除(默认):把 1、2 移动进 5,再清 3。面板顶部常显「回收站:N 个 · X MB」,可以逐条恢复或整体清空;30 天后自动清除。
  • 永久删除:只出现在二次确认里,直接 unlink,不进回收站;同时走官方 Workspace.detachSession 摘掉 4。

第 3 步走官方 ctx.workspaceRegistry.unarchiveSession()——它刻意不校验会话是否存在,正好能把「只剩索引」的残留一起清掉, 并借官方广播刷新所有已连接的客户端。

但它不是无条件立刻清。 归档只是把 id 放进 archivedSessionIds,而 DSH 的会话列表(session.list)是 「磁盘上的 header + 宿主持有的活会话」的并集——归档并不会让宿主里那个空闲 agent 忘记会话。所以只要宿主还记着它, 第 3 步就先留着(这正是唯一挡住幽灵行的那一处):插件把这条记进 $DSH_HOME/dsh-archived/deferred.json、面板不画它, 用户侧会话立刻消失;等宿主放手(重启必然放手)后自动清掉。永久删除还会额外广播 api-session/removed, 让已经打开的客户端当场收行,而不是等到下一次刷新。

安全边界

  • 只接受已归档的 id(not-archived 直接拒绝),不碰未归档会话;
  • agent.status === "running" 的会话拒绝删除(正在生成的回合不能丢日志),空闲但打开的会话可删;
  • 有正在运行的 subagent 后代时拒绝删除父会话(否则子会话的父会话会在它脚下消失);
  • 索引残留没有 payload 就不进回收站:一个只有索引、磁盘上什么都没有的 id,清掉即可,不该在回收站里留一个永远恢复不了的条目;
  • 路由仅限回环地址 + 同源 + 自定义标头:Origin 存在时必须与 Host 匹配;浏览器对同源 GET 不发 Origin, 该情形由 sec-fetch-site: same-origin 兜底,两个信号都没有则拒绝;
  • 宿主若失去 workspaceRegistry.unarchiveSession,破坏性路由直接拒绝(no-registry),不会「删了文件但留下索引」这种假成功。
  • 宿主还记着的会话,归档索引条目延迟清理:那一条正是唯一挡住幽灵行的东西——archivedSessionIds 一空,宿主内存里的活会话就会 重新出现在工作区里,直到重启才消失(0.4.0 的 bug)。永久删除另走官方 Workspace.detachSession 摘掉工作区记账位 (宿主没这个动词时只记日志、不拒绝删除——条目已经挡住幽灵行),并广播 api-session/removed; 回收站路径不摘记账位(恢复要回到原位),也不广播(客户端丢了摘要,恢复后要等下次刷新才画得出来)。
  • 陈旧清理只碰未归档会话,且只碰「日志超过 N 天没写」或「从没写过用户消息」的;已归档的仍归上面的归档流程管;
  • 24 小时硬保护:日志在 24 小时内写过的一律不列、不删(与天数设置无关);
  • 正在跑、或有运行中子代理后代的会话直接拒绝;
  • 从工作区列表移除走官方 Workspace.detachSession;宿主没有这个动词时拒绝删除(no-detach),不会删了文件却留下悬挂行;
  • 自动清理只进回收站,永不永久删除;默认关闭,开启后每周最多跑一次。

删除后的残留清理

会话被归档时如果它的 agent 还开着(空闲未运行),DSH 的投影服务可能在删除后几秒把 <id>.json 缓存重新写回 (日志目录不会复活)。因此每次成功删除都会排三个定时清扫(+3s / +15s / +60s),再次清掉这类残留。 清扫只删缓存文档,且要求该会话既没回到归档集合、也没在跑、磁盘上也没有会话目录——目录回来了说明会话被恢复了,绝不碰。 (例外:延迟清理的 id 仍在归档集合里,那正是挡住幽灵行的那一条,所以它继续参与清扫。) 定时器在插件卸载时统一取消。

延迟条目在插件加载时、每次读 GET /state 时、以及宿主自己的每小时 tick 上复查:宿主一旦不再持有该会话 (ctx.sessions.get() 查不到),条目就走官方 unarchiveSession() 清掉,顺带再扫一次缓存残留。 重启后的第一次加载必然把它清干净——所以「重启才消失」的那部分,现在变成了「重启顺手收尾」。

界面

行的事实源是宿主(GET /state:归档集合 + 磁盘 + 投影缓存文档),浏览器的会话摘要只用来把标题和时间显示得更准。 这一点是有意的:索引里还留着、但浏览器没加载过的会话,必须照样成行显示,否则用户根本无从清理。

页面提供:搜索(标题 / 会话 ID / 工作区)、按工作区分组、逐行勾选与全选、每行展开详情 (占用空间、创建时间、缓存有无、父/子会话)、「打开文件夹」、行内二次确认、结果就地反馈, 以及顶部常显的回收站(恢复最近一个 / 清空)。

导航行与页面标题都是 Archived(英文)/ 已归档(中文)。

页面顶部还有一节**「清理陈旧会话 / Clean up stale sessions」:陈旧标准下拉(7 / 14 / 30 天)+「扫描」按钮; 候选行显示标题、原因(陈旧 / 空会话 / 两者)和「N 天未动 · 占用」,勾选后二次确认移入回收站; 底部是「每周自动清理」开关与上次运行结果。这一节只列未归档**会话,扫描本身是只读的。

宿主路由

| 方法 | 路径 | 说明 | |---|---|---| | GET | /api/dsh-archived/state | 归档行 + 磁盘占用 + 血缘 + 回收站 + 旧聚合探测 | | POST | /api/dsh-archived/delete | { sessionId, mode: "quarantine" \| "forever" } | | POST | /api/dsh-archived/delete-all | { mode, ids? };ids 缺省表示清空全部 | | POST | /api/dsh-archived/restore | { sessionId } 从回收站恢复并重新归档 | | POST | /api/dsh-archived/empty-quarantine | 彻底清空回收站 | | POST | /api/dsh-archived/reveal | { sessionId } 在系统文件管理器里打开该会话目录 | | POST | /api/dsh-archived/sweep/scan | { days? } 陈旧/空会话候选(只读,不写盘) | | POST | /api/dsh-archived/sweep/delete | { ids, mode } 逐个进回收站 + 官方 detach | | POST | /api/dsh-archived/sweep/settings | { enabled?, days? } 每周自动清理的开关与天数 | | POST | /api/dsh-archived/sweep/run | 立刻跑一次每周清理(同一套保护) |

拒绝码:invalid-session-id、not-archived、session-running、subagent-running、no-registry、 archived-session、too-recent、no-detach、 busy、not-quarantined、no-artifact、forbidden、internal。

安装

# 装进 profile(包自带的 bundle patch 会把它挂上)
dsh plugin --profile web add dsh-archived

# 重启 dsh web,然后 设置 → 已归档

不需要改 profile patch。 DSH 0.2.0-rc.2 已经没有官方的归档设置页,settings.section 的 archived-sessions id 是空的,本插件是它唯一的占用者。

从源码(开发用)

# 1) 让 profile 能解析到这个包(link: 指向本仓库根目录)
ln -s /path/to/repo ~/.dsh/profiles/web/node_modules/dsh-archived
#    ~/.dsh/profiles/web/package.json
#      "dependencies": { "dsh-archived": "link:/path/to/repo" }
#      "dsh": { "profile": { "bundles": [ ..., "dsh-archived" ] } }

# 2) 开发时热重载宿主代码(默认 root: [] 只监听 profile 清单与 patch 文件)
#    ~/.dsh/profiles/web/cordis.patch.yml
#      - id: hmr
#        config:
#          root:
#            - "/path/to/repo"

dsh-hmr 只监听 profile 清单与 patch 文件;改了 lib/index.js 想立即生效,需要把本仓库根目录加进 hmr.config.root。 客户端半边改动刷新浏览器即可。

兼容性

  • 目标宿主:DSH 0.2.0-rc.2(peerDependencies 收敛到 ^0.2.0-rc.1)。
  • 不支持 0.1.x:那时官方还有一个归档设置页,需要手动 disabled: true;0.2.0-rc.2 已经没有它了。 代码里的 priority: -1 只是对「还带官方页的宿主」的防御。
  • DSH 升级后先跑契约检查,它直接读本机 app.asar,逐条确认插件依赖的接口还在: node tools/compat-check.mjs。

测试

npm test                        # 宿主离线 + 客户端冒烟 + 宿主契约,都不需要 DSH
node tools/host-smoke.mjs       # 宿主半边:临时 $DSH_HOME + 假 ctx,跑完删除/恢复/回收站全流程
node tools/client-smoke.mjs     # 客户端半边:桩 React + 桩 fetch
node tools/compat-check.mjs     # 宿主契约(读 app.asar)
python3 tools/e2e.py            # 真宿主端到端;需要 dsh web 在跑且插件已加载
python3 tools/browser-acceptance.py   # 真浏览器;先起无头 Chrome --remote-debugging-port=9333

tools/e2e.py 会建临时会话 → 归档 → 删除(进回收站)→ 恢复 → 永久删除,并核对 「其他归档会话与磁盘上的其他会话数量都没变」。

tools/browser-acceptance.py 是纯标准库的 CDP 客户端(自己实现 WebSocket 握手与帧):用本机密钥签出浏览器会话 cookie 注入无头 Chrome,打开 设置 → 已归档,断言导航里只有一个「已归档」入口,然后真的点删除 → 确认 → 恢复最近一个。

tools/rpc.py 是本地 RPC 小工具:用 ~/.dsh/.credentials.yaml 里的浏览器会话密钥现场签 cookie, 从终端调用官方 RPC(session/create、workspace/archiveSession 等),测试因此不需要浏览器,也不会碰真实会话。

回滚

  1. 从 ~/.dsh/profiles/web/package.json 的 bundles 与 dependencies 里删掉 dsh-archived; 或直接 dsh plugin --profile web remove dsh-archived;
  2. 删掉 ~/.dsh/profiles/web/node_modules/dsh-archived 软链(源码安装时);
  3. ~/.dsh/.archived-sessions-quarantine/ 里可能还有待恢复的会话——确认不需要后手动删除即可。

插件删掉的会话不会回来(forever),或者还躺在回收站里等 30 天过期(默认路径)。

故障排查

dsh plugin add 报 ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION(Lockfile failed supply-chain policy check)。

这不是本插件的问题:pnpm 11 默认给「24 小时内发布的版本」设了闸门,而 profile 的 pnpm-workspace.yaml 里 minimumReleaseAgeExclude 是按「包名@版本」逐条累加的——同一个包名出现两条时只有其中一条生效 (实测是列表里靠前的那条),于是新发布的那个版本永远过不了闸门。报错点名的包可能根本不是你要装的那个。

修法:让每个包名只留一条,把已接受的确切版本用 || 连起来:

minimumReleaseAgeExclude:
  - [email protected] || 2.0.0   # ✅ 一条顶两条
minimumReleaseAgeExclude:
  - [email protected]            # ❌ 这两条同时存在时,
  - [email protected]            #    2.0.0 那条不生效

⚠️ 只接受确切版本:写成 dsh-notifications@* 或 @^2.0.0 会被直接拒绝 (ERR_PNPM_INVALID_MINIMUM_RELEASE_AGE_EXCLUDE: Use exact versions only.)。

pnpm 每次成功安装后都会往这个列表追加一条「包名@版本」,所以升级几次之后又会出现同名两条—— 发现同名时把它们合并成上面那种 || 形式即可。删掉重复行后重跑 dsh plugin --profile <name> add <包名> 就通了。

已知边界

  • $DSH_HOME/storages/schedule.json 与 message_feedback.json 里可能残留指向已删会话的引用。 它们是宿主自己的 domain store,外部改写会被内存状态覆盖,本插件不动它们(见 docs/decisions.md)。
  • $DSH_HOME/storages/session_projcache.json 是 per-record 布局之前的旧聚合,实测早已停止写入 (mtime 远早于 session_projcache/sessions/)。插件只探测并在详情里标注,不写入。
  • 无头宿主没有文件管理器:设 DSH_ARCHIVED_OPENER=none,「打开文件夹」只回路径不启动程序。