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

@mcd0luo/celes-worker-spawn

v0.3.4

Published

Celes 调度中心 DSH 插件:纯 JS 的 worker 会话拉起 + registry 巡检自动重派(去 shell 化替代 spawn-worker.sh / watcher.sh / respawn-w100.sh)

Readme

@mcd0luo/celes-worker-spawn

Celes 调度中心 DSH 插件:worker 会话拉起 + registry 巡检自动重派(纯 JS 去 shell 化)。 仅面向 DSH web profile 宿主使用(注入 sessions / agents / sessionPersistence / webServer / tools / settings,经 ctx.get("sessionController") 走进程内官方 RPC + workspaceRegistry + agentPresets;0.1.1 宿主回退 ctx.get("apiProxy") 走调度中心内部 RPC(W248 适配)。

替代现役 bash 三件套(/server-center/worker-ops,git 真源):

| 现役 bash | 本插件对应 | |---|---| | spawn-worker.sh(bridge RPC 拉起,五步) | spawnWorker() / POST {prefix}/spawn / agent 工具 spawn_worker | | 手工 rm -rf 会话目录(调度中心硬删) | agent 工具 delete_worker(文件级真删,插件内无 shell) | | cat registry.tsv 看自己派的 worker | agent 工具 list_workers(domain = 发起者本会话;all:true 列全部) | | watcher.sh(30s 巡检 + 失败重派 + DONE/FAILED 收口) | watch 巡检(config.watch.enabled 时 setInterval 驱动) | | respawn-w100.sh(重启后重派) | 语义并入 watch(registry 持久化在盘,陈旧 RUNNING 行重启后判 gone → 自动重派);registry 行缺失时外部 POST {prefix}/spawn 复现 |

能力

1. 拉起(spawn-worker.sh 五步移植)

spawnWorker(wid, briefText, {title, workspace, provider, model, effort, agentPreset, reportTo, autoDelete}):

  1. 有 workspace → workspace.create 拿 workspaceId(分组);否则跳过;
  2. sessions.create(payload = {workspaceId} 或 {cwd: workspace || workerBase},可选并入 agentPreset 预设字段)→ sessionId;
  3. sessions.rename → 标题 <wid>·<短名>(短名自动剥掉 <wid>· 前缀,防双前缀);
  4. provider+model 齐 → sessions.selectModel(reasoningEffort 缺省 = 配置默认);
  5. sessions.prompt(简报文本,mode: "queue")。

返回 {ok:true, sessionId, title, workspaceId, agentPreset?, autoDelete};任一步失败返回 {ok:false, step, error}(step 为该 RPC 步骤名)。agentPreset 为 session.create 响应回显的实际 生效预设 id(透传验证用);autoDelete 回显本次是否请求汇报后自动归档。强制三规则落地: 主动命名(标题)、工作区/分组、非裸无头会话。

2. agent 工具 spawn_worker(delete_worker / list_workers 见 §3.9/§3.10)

任意会话内 AI 直接调用:{wid, brief, title?, workspace?, provider?, model?, reasoningEffort?, agentPreset?, reportTo?, autoDelete?}, brief 为自包含简报文本。返回同 spawnWorker 结果,并附 sourceSession(调用方会话 id)标来源。 autoDelete/deleteAfterReport 为布尔可选项(deleteAfterReport 是 autoDelete 的兼容别名)。

3. HTTP API(挂在 dsh web webServer,默认 127.0.0.1 回环)

| 方法 | 路径 | 用途 | |---|---|---| | GET | {prefix}/health | 健康检查(无鉴权,供重启探活) | | POST | {prefix}/spawn | 拉起 worker(含可选 autoDelete) | | GET | {prefix}/usage | 查询会话上下文/token 占用(?sessionId= 或 ?wid=) | | POST | {prefix}/archive | 归档会话({sessionId} 或 {wid};DSH 无 session.delete,归档=隐藏+日志保留) | | GET | {prefix}/presets | 枚举 DSH agent 预设(agentPreset.list) | | POST | {prefix}/preset | 切换已存在会话的 agent 预设({sessionId, agentPreset};仅 blank 会话可切) |

POST /spawn body:{wid, brief?, briefFile?, title?, workspace?, provider?, model?, reasoningEffort?, agentPreset?, reportTo?, autoDelete?} ——reportTo/autoDelete 语义见 §3.6/§3.8;brief 文本优先;缺则读 briefFile(默认 ${workerBase}/workspaces/${wid}/.brief)。 provider/model/reasoningEffort 缺省取自配置默认。工作区图纸落盘(.brief/.taskname/ .workspace/.retries 初始化,镜像 spawn-worker.sh)供看门狗重派复用。除 /health 外的请求 在配置了 token 时需带 x-celes-center-token 头。

3.5 DSH agent 预设(agentPreset,可选)

spawnWorker / POST /spawn / 工具 spawn_worker 三入口均支持可选 agentPreset,原样透传给 session.create 的 payload(DSH 原生字段);缺省不传该字段 = 走 DSH 默认预设。内置预设 id 映射:

| 预设 id | 模式 | |---|---| | standard | 标准模式 | | cordis | 创造模式 | | minimal | 极简模式 | | code | PTC 模式 |

预设仅是 session.create 的一次性入参(非持久全局设置),每个 worker 独立指定。

已存在会话的预设切换(W120 新增):POST {prefix}/preset body {sessionId, agentPreset} 走 宿主 agentPreset.select RPC。DSH 限制:仅 blank(未开始) 会话可切换——一旦会话已有 turn/start 事件即固定,宿主返回 agent-preset-locked。因此 worker 的预设应在创建时经 agentPreset 指定(三入口已透传);/preset 仅用于「先建会话、后切换」的 blank 场景。 GET {prefix}/presets 返回实际枚举(含 name/trust/isDefault/broken)。

3.6 worker 完成自动反馈(reportTo,可选)

spawnWorker / POST /spawn / 工具 spawn_worker 三入口均支持 reportTo(主会话 id)。 非空时,插件在投递简报前自动把下面一段追加到简报末尾(spawnWorker 内部注入,不改变 brief 原文本):

【完成反馈(自动注入)】任务完成后:
1) 完整报告写入 ${workerBase}/results/<wid>-<taskname>.md(Markdown:成果/验证/产出路径)。
2) 用 session_send_message 工具(target=<reportTo>)发一条完成消息:成功 → WORKER_<wid>_DONE <成果一句话 + 关键验证/产出路径>;失败 → WORKER_<wid>_FAILED <原因>。
3) (可选)用 report_usage 工具读取本会话上下文/token 占用(缺省即本会话),把占用信息附到报告末尾或回执括号/分号后缀中。
  • 工具入口缺省自动:spawn_worker 的 execute 在调用方未显式给 reportTo 时,自动用 exec.agent.session.id(发起会话 id)——AI 拉起 worker 时回报自动回到发起会话;
  • HTTP 入口:body 可选 reportTo,有则注入,无则不注入(脚本/外部调用可自行决定);
  • <taskname> 用于结果文件名,含路径分隔符/控制字符时消毒为 -(防注入工作区外路径);
  • 收口纪律与 LTS「worker-dispatch · 收口」一致:报告落 results/<wid>-*.md + 日志末行 WORKER_<wid>_DONE|FAILED 回执,看门狗以该文件为交付物判定依据。

3.7 worker 上下文/token 占用上报(report_usage / usage,W120 新增)

数据源:宿主 session.list 每项自带的 projections.values(由 dsh-token-meter + dsh-session-stats 两个投影单元随会话事件实时折合,本 profile 已挂载;冷会话经 sessionProjectionCache 读取)。四个键均为宿主权威数据,无需客户端配合:

| 键 | 内容 | |---|---| | tokenUsage | {uncachedInputTokens, outputTokens, cacheReadTokens, cacheWriteTokens} —— provider 报告的用量累计 | | contextPressure | {pressureTokens?, projectedTokens?, contextWindow?} —— 最近一次请求的输入/缓存压力与上下文窗口 | | contextBreakdown | {systemTokens, toolsTokens, messageTokens} —— 系统/工具/消息占用估计 | | sessionStats | {turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens} —— 轮数/步数与耗时分桶 |

上报接口:

  • 代理工具 report_usage:任一会话可调,{sessionId?} 缺省 = 发起者本会话;返回 {ok, sessionId, running, blank, agentPreset?, asOfSeq?, usage:{tokenUsage, contextPressure, contextBreakdown, sessionStats}} ——worker 将其附到完成报告 / WORKER_<wid>_DONE 回执即完成「上报给主会话」;
  • HTTP GET {prefix}/usage?sessionId=<id> 或 ?wid=<wid>(wid 经 registry extra 的 sess= 解析): 主会话/脚本可随时轮询任意 worker 的占用——「写入可查位置」。
  • 完成反馈注入段(§3.6)已加第 3 条提醒 worker 使用该工具。

3.8 汇报完成后自动归档(autoDelete,W120 新增)

spawnWorker / POST /spawn / 工具 spawn_worker 支持布尔 autoDelete(别名 deleteAfterReport)。 设为 true 时:

  1. 拉起成功后落 .autoDelete 侧车标记并登记 RUNNING 行(extra 带 autodel=1);
  2. watch 巡检到该行「会话结束 + 交付物在案 → DONE」时,自动调 workspace.archiveSession(sessionId) 归档该新 worker 会话;成功/失败均记 watcher.log / alerts.log,registry extra 标注归档结果;
  3. 重派(retryWorker)保留 .autoDelete 语义(读侧车标记透传给新会话)。

DSH 限制(如实注明):宿主无 session.delete RPC;归档 = workspace.archiveSession: 会话从 web 侧边栏隐藏、日志与持久化保留、可逆(/archive 端点或对端 unarchive 恢复)。 存量 worker 不改动;autoDelete 仅作用于本次拉起的新会话,且只在「有交付物」时归档 (无交付物 → FAILED,不归档)。

3.9 硬删除 worker 会话(agent 工具 delete_worker,W720 新增)

文件级真删(不是 §3.8 的归档):会话日志目录连同 sidecar 登记一并从盘上删除,不可恢复。 只面向本插件自己拉起的 worker(wid)或显式 sessionId;{wid?, sessionId?, reason?} 三参可选, sessionId 优先,否则用 wid 经 registry extra 的 sess= 解析;两者都无 → {ok:false, error:"wid or sessionId required"}。

执行顺序(隐藏优先,顺序即语义):

| 步 | 动作 | 说明 | |---|---|---| | 1 | 取消在跑 agent | ctx.get("agents").get(sessionId).cancel({kind:"user"},{keepInbox:true}) —— 与官方停止按钮同一调用;whenIdle() 最多等 5s,异常只告警 | | 2 | 定位会话目录 | sessionPersistence.list() → header → sessionPersistence.locate(header) → loc.path 去最后一段 | | 3 | 盖墓碑 | workspaceRegistry.archiveSession(sessionId)(官方 UI 唯一隐藏通道;此刻会话仍在持久化清单,必然可归档) | | 4 | 真删目录 | rm(dir, {recursive:true, force:true})(node:fs/promises;本插件无 shell、零子进程,不用 rm -rf) | | 5 | 摘工作区记账 | 对仍列该会话的 workspace 记录逐个 entity.detachSession(sessionId)(归档只隐藏、保留 sessionIds 槽位 = 复活向量) | | 6 | 删投影缓存 | storageDomain.get("session_projcache").table("sessions").delete(sessionId)(fail-soft) | | 7 | 清登记 | registry.tsv 删该 wid 行(复用 updateRegistry)+ 删 workspaces/<wid>/ 下 .brief/.reportTo/.autoDelete/.retries/.taskname/.workspace/.reportFile,目录空则删 | | 8 | 审计双写 | watcher.log + alerts.log 各一条 delete_worker <wid> 已硬删除会话 <sid>(dir=..., detached=N) |

返回 {ok:true, wid, sessionId, dir, detached, cacheDropped, registryRow:"removed"|"absent", tombstoned}。 墓碑失败(workspaceRegistry 未注入 / 归档被拒)会写 alerts.log,但不阻断删除。

3.10 本 domain worker 列表(agent 工具 list_workers,W720 新增)

{all?, includeDead?},均缺省 false/true。

  • domain 归属:registry extra 的 source=<发起会话id>(spawn_worker 落行时写入)优先, 缺失回退 report_to=<发起会话id>;调用者 id 取 exec.agent.session.id。 缺省只列本会话自己派的 worker;all:true 不过滤(列 registry 全部)。
  • 每行 {wid, status, sessionId, title, workspace, startedAt, state}: title = <wid>·<taskname>(读 workspaces/<wid>/.taskname sidecar),workspace = registry extra 的 ws=, state = running|gone|unknown(复用 sessState(),对 sessions.list 一次性取回的结果逐行判态)。
  • 排序:startedAt 倒序;includeDead:false 只留 state==="running"。
  • 返回 {ok:true, domain:<调用者会话id|null>, all, count, workers:[...]}。

4. 巡检看门狗(watcher.sh 移植,watch.enabled 才启动)

每 watch.intervalMs(默认 30s)一轮:

  1. (0.3.1)ghost 回收:先扫一遍 registry,把「可证已死」的 RUNNING 行降级为 FAILED(§4.1); 0.5 (0.3.2 / W735)判活边界:sessions.list 只用来判本进程持有的行(extra 的 proc=<本实例 pid>)。proc 存在且 ≠ 本进程 pid(= 另一个 dsh-web 实例持有的行)与 proc 缺失(含 bash 遗留 pid= 行)一律不判活、不判 DONE、不重派,只计数留痕;会话列表 RPC 失败时整轮直接跳过(证据不足,绝不因抖动重派)。跨实例「持有者进程确证已死」的行只由步骤 0 的 ghost 回收判 FAILED(§4.2);
  2. 读 ${workerBase}/registry.tsv(TSV:wid、t0、status、extra…);
  3. 对本进程持有的 status==RUNNING(DONE/FAILED* 跳过)行:从 extra 抽 sess=xxx, sessions.list 判 blank/running/gone;
  4. running → active;blank/gone 且有交付物 → DONE(写 watcher.log + 重写 registry); blank/gone 且无交付物 → anomaly;
  5. anomaly:age = now - t0 < graceMs(默认 10min,防新派会话瞬时 gone 误判)→ 跳过; .retries < maxRetries → 自动重派(用 .brief + .taskname/.workspace 图纸重派, .retries+1,写 watcher.log / alerts.log + RUNNING 行重建);否则 → status=FAILED (写 watcher.log / alerts.log + registry 收口);
  6. 交付物判定:${workerBase}/results/<wid>-*.md 存在(readdir + startsWith)或 ${workerBase}/coplanner/mailbox/coordinator/inbox.jsonl 含 "from":"<wid>";
  7. (W120)DONE 时若该行带 autodel=1/.autoDelete 标记 → workspace.archiveSession 自动归档 (§3.8),并把归档结果写进 registry extra 与 watcher.log / alerts.log。

registry 重写 = 跨进程 O_EXCL 锁 + 唯一 tmp 名 + rename 原子替换(§4.2;同 bash 的 tmp+mv, 但 tmp 名不再固定,且读改写整段进跨进程临界区)。

4.1 ghost 回收(迭代方向 F,0.3.1 新增)

问题:registry 由「进程」写入,但进程会死。死进程留下的 status=RUNNING 行(其进程号已不存在、 其 sess= 会话也已不可用)在跨进程 / 未来视图里就是幽灵:Studio UI 因为按本进程 ownEntries 过滤 看不见,registry 本身却越积越脏。

做法:每轮巡检开头跑一遍独立的 ghost 回收(lib/ghost.js,纯函数 + 依赖注入),只有能证明该行 已死时才动它:

| 判据 | 说明 | |---|---| | 行带进程号 token | proc=<pid>(0.3.1 起 spawnWorker/retryWorker 自动写入,值为宿主 dsh-web 进程号)或遗留 pid=<n>;pid=- 与无 token 一律不动 | | 该进程确证已死 | process.kill(pid, 0) 抛 ESRCH(无 shell、零子进程);EPERM(进程在但无权)判活着,不动 | | 会话确证 gone | sessions.list 查询成功、列表非空、且其中没有该 sess= 会话。查询失败 / 空列表 = 证据不足 → 不动(比 sessState 更严,杜绝 RPC 抖动误判) | | 无交付物在案 | 交付物在案的行仍按原语义判 DONE,不动 | | 已过宽限期 | now - t0 ≥ watch.graceMs,刚派出的行不与主巡检抢判 |

命中 → status=FAILED + extra 前置 看门狗 ghost 回收(<时刻>):proc=<pid> 进程已死 + 会话 gone + 无交付物 (原有 sess=/ws=/source= token 原样保留),并写 watcher.log + alerts.log 审计行。 落盘经 updateRegistry 锁内复检(盘上仍是 RUNNING 才降级),回收自身异常一律吞掉——绝不影响 DONE / 重派 / FAILED 既有语义。只改 status/extra:不杀进程、不重派、不删会话、不动 registry 其它行。

单测/冒烟:node test/ghost-reaper.smoke.mjs(43 项)、node test/ghost-reaper.wiring.mjs(10 项, 假 ctx + 假 registry + 假 pid 直接驱动真插件 apply())。

4.2 registry 跨进程写安全(0.3.2 / W735 新增)

问题(W735 审计):3180/3184/3190 三个 dsh-web 实例的 workerBase 同指 /server-center/runtime/worker-exec,registry.tsv 是跨进程共享可变状态,而旧实现 ① 进程内 Promise 链只挡同进程并发;② tmp 名固定为 registry.tsv.tmp(两个实例同时写会互踩同一个 tmp → rename 后盘上可能是半截表)。真实两进程压测(test/registry-race.mjs,2×60 次并发读改写): 修复前 120 次写入只剩 19 行,修复后 120 行一行不丢。

做法(lib/reglock.js,全部 node 内建;无 shell、零子进程 —— 不 spawn flock(1)):

| 机制 | 细节 | |---|---| | 唯一 tmp 名 | <file>.tmp-<pid>-<seq>(同 crates/workers/src/registry.rs 的 pid+seq 命名),rename 原子替换;tmp 永不互撞 | | 跨进程锁 | <file>.lock 的 O_CREAT|O_EXCL(fs.open(path,"wx"));锁文件内容 pid \t 时刻 \t token | | 死持有者破拆 | 持有者进程 process.kill(pid,0) → ESRCH(确证已死)即破拆重试,不会因持有者崩溃永久死锁 | | 空锁/残留破拆 | 无 pid 的空锁文件、以及本进程上次释放失败的残留,超 registryLock.staleMs(默认 30s)才破拆 | | 活持有者 | 永不按年龄强拆;等到 registryLock.timeoutMs(默认 10s)后放弃本次写入并抛错(不丢行优先于写入成功,绝不退化成无锁读改写),调用方按非致命处理并写 alerts.log | | 释放 | 校验 token 归属,绝不错删别人的锁 |

单测/冒烟(全部零 shell、零子进程):

node test/registry-concurrency.smoke.mjs   # 42 项:唯一 tmp 名 / 丢行对照 / 锁语义 / 两实例并发 spawn / 拿不到锁即放弃写入
node test/watchdog-ownership.smoke.mjs     # 25 项:判活边界(跨进程不重派、本进程才重派 / 判 DONE)
node test/preset-select-noref.smoke.mjs    # 22 项:presetSelectRpc 无 live agent 的结构化错误
node test/ghost-reaper.smoke.mjs           # 43 项(0.3.1 既有)
node test/ghost-reaper.wiring.mjs          # 10 项(0.3.1 既有)
node test/provisioning-ledger.smoke.mjs    # 19 项(0.3.3 / W847:先落盘后创建 + wid 永不复用)
node test/recovery-reconcile.smoke.mjs     # 9 项(0.3.3 / W847:PROVISIONING 恢复对账,不重派)
node test/auto-receipt.smoke.mjs           # 21 项(0.3.3 / W847:自动兜底回执幂等 + 不抢跑 + 重试)
# 真·跨进程:由外部并发启动同一脚本 2 份(脚本自身不起子进程)
BASE=$(mktemp -d); node test/registry-race.mjs A 60 "$BASE" & node test/registry-race.mjs B 60 "$BASE" &
wait; wc -l "$BASE/registry.tsv"           # 期望 120

同版顺带修(W735 第 3 条):presetSelectRpc 的「无 live agent」分支原读未定义变量 noref (ReferenceError → 外层 catch 返回含糊的 "noref is not defined" 且丢掉 code);现返回 {ok:false, code:"agentPreset.select-refused", error:"session <id> has no live agent", detail}。

4.3 台账三件事 + 自动兜底回执(0.3.3 / W847 新增)

先落盘后创建:spawnWorker 在 sessions.create 之前先写一条 status=PROVISIONING 行 (extra 带 proc=<pid>、parent=、report_to=、brief=<sha1-12>);create 成功后立刻把 sess= 写回该行,随后转 RUNNING;任一后续步骤失败转 FAILED(extra 记 fail=<step>: <原因>)。 W847 起所有 spawn 都登记(此前只有 autoDelete 会登记);老格式行(RUNNING/DONE/FAILED、 pid=-、缺字段)解析路径不变。

wid 永不复用:对外入口(工具 / HTTP /spawn)若发现该 wid 在 registry 已有任何历史行 (含 DONE/FAILED/墓碑)→ 返回 {ok:false, step:"validate", error:"wid \"...\" 已占用(registry 已有历史行:<status>),名字永不复用"}, 绝不覆盖/顺延;仅 retryWorker 内部重派传 reuse:true 复用同一 wid。

恢复对账(替代盲重派):巡检对未终结的 PROVISIONING 行只做对账、绝不重派—— 本进程行超 watch.provisioningStaleMs、别的进程行其持有者 process.kill(pid,0) 确证已死时才处理; 先看 registry 的 sess=,缺失则按 parent= 匹配 sessions.list 的 parentSessionId(带 updatedAt ≥ t0); child 存在 → RUNNING,确证 gone / 无匹配 → FAILED。对账本身不再调 spawn/create。 ghost.js 的 fail-safe 五条(无 token / 探活未知 / 会话未确证 gone / 交付物在案 / 宽限期内 一律不动) reapGhosts() 已完整覆盖(lib/ghost.js:94-102、scanGhosts:117-147),本版无需改。

自动兜底回执(回执是运行时行为,不是提示词行为):worker 会话结算(实时态由 running 变 idle/gone)时,由插件的 runWatch 代发一条回执——不依赖 worker 执行三步契约:

| worker 自报路径 | 自动兜底路径 | 最终判定 | |---|---|---| | 报告 + session_send_message + results/<wid>.receipt-ok 三步齐全 | 看到 receipt-ok → 不抢跑,不发兜底回执 | delivered()(报告 + receipt-ok)→ DONE | | 写了报告、但没写 receipt-ok(静默停/中断) | 报告在案 → 发 WORKER_<wid>_AUTO_DONE | DONE | | 报告缺失(重试耗尽 / 无 reportTo / 会话 gone) | 终态 → 发 WORKER_<wid>_AUTO_FAILED(含具体原因 + 报告路径) | FAILED | | worker 自己在途 | — | 会话仍 running → 不动,等结算 |

  • 复用同一个 startWatch()/runWatch() 定时器(config.watch.intervalMs),不新增第二个轮询;
  • 幂等:每条 RUNNING 行至多一条兜底回执,标记 results/<wid>.auto-receipt(dsh-web 重启不重发);
  • 可判别:WORKER_<wid>_AUTO_DONE|AUTO_FAILED + 正文首行「(自动兜底回执 · celes-worker-spawn 0.3.3 代发)」;
  • 投递:走既有 reportTo(session- 前缀校验)经 sessionController.prompt / apiProxy;失败按 watch.autoReceiptRetries 重试并记 watcher.log / alerts.log,失败不写标记(下一轮自然重试);
  • 限制(如实):插件的数据边界是 session.list,读不到会话历史(DSH 会话日志是 session.v3.jsonl.zstd 多帧 zstd,零依赖不解析),故回执正文以「报告路径 + 终态 + 原因」代替 「最后一条 assistant 消息摘要」。

单测/冒烟:node test/provisioning-ledger.smoke.mjs(19 项)、node test/recovery-reconcile.smoke.mjs(9 项)、 node test/auto-receipt.smoke.mjs(21 项);假 ctx + 真 registry + 真 apply(),零 shell。

5. /tmp 卫生脚本(scripts/tmp-hygiene.mjs,0.3.1 新增)

白名单式只读扫描 + 可选执行(默认 dry-run,--apply 才删):

node /src/dsh_plugins/scripts/tmp-hygiene.mjs                 # 只列清单/统计,不删任何东西
node /src/dsh_plugins/scripts/tmp-hygiene.mjs --apply         # 真删(逐条删前复检)
node /src/dsh_plugins/scripts/tmp-hygiene.mjs --all --min-age-hours=72

红线:只认白名单里的目录(ts-*、playwright-*、celestea-*、tsx-*、vitest-*、 i18n-(test|datafolder)*、audit-sink-test*、wg-stubs*、nms-(fixture|closedloop)*)且 mtime ≥ --min-age-hours(默认 24h);符号链接一律跳过;同名普通文件只提示不删;隐藏条目与其余一切 未识别条目(含散落日志、随机名目录、socket)只统计、永不删除。删除前逐条 lstat 复检 (仍白名单、仍够老、仍非符号链接、dev/ino 未变),任何不一致即放弃。

配置

| 字段 | 默认 | 说明 | |---|---|---| | routePrefix | /celes-center/worker | HTTP 路由前缀 | | token | "" | 共享访问令牌;空 = 仅信任本机回环 | | workerBase | /server-center/runtime/worker-exec | worker 运行根(registry.tsv / workspaces / results / watcher.log / alerts.log) | | defaultProvider | celestea | 默认模型 provider(替代 spawn-worker.sh 读 settings.yaml) | | defaultModel | deepseek-v4-flash | 默认模型名 | | defaultEffort | max | 默认推理档位 | | watch.enabled | false | 是否启用巡检看门狗(观察期默认关闭) | | watch.intervalMs | 30000 | 巡检间隔(毫秒) | | watch.maxRetries | 2 | 自动重派次数上限(与 watcher.sh 的 .retries ≤ 2 一致) | | watch.graceMs | 600000 | 新派 worker 宽限期(毫秒),期内异常不重派 | | watch.autoReceipt | true | W847:worker 结算时由插件代发兜底回执(不依赖 worker 自觉) | | watch.autoReceiptRetries | 3 | W847:兜底回执单轮投递重试次数(失败不写标记,下一轮再试) | | watch.autoReceiptRetryMs | 300 | W847:兜底回执投递重试间隔(毫秒) | | watch.provisioningStaleMs | 120000 | W847:本进程 PROVISIONING 行超时多久才对账(防与在飞 spawn 抢) | | registryLock.staleMs | 30000 | 跨进程锁「空锁文件 / 本进程残留」破拆阈值(毫秒);活持有者持有的锁不受此值影响 | | registryLock.timeoutMs | 10000 | 抢锁超时(毫秒);超时放弃本次写入以防丢行(不丢行优先) | | registryLock.pollMs | 25 | 抢锁轮询间隔(毫秒) |

经 settings hub 注册(namespace celestea-center-worker-spawn,live 热更新;watch 改动会 按新配置重拨定时器)。

去 shell 说明

硬约束:禁止 shell —— 不 child_process / 不 exec / 不 bash / 不派生任何子进程。 全部能力由 node 内建模块承担:

  • node:fs/promises:readFile / writeFile / appendFile / readdir / mkdir / rename (registry 原子重写、日志追加、交付物探测、侧车图纸读写);
  • node:path:join(一切路径从 config.workerBase 拼出,无硬编码 Linux 路径);
  • 调度中心 RPC 双后端(W248):0.1.2+ 宿主用 ctx.get("sessionController") 直调官方 RPC(create / rename / selectModel / prompt / list / resolveAgent)+ workspaceRegistry(create/archiveSession)
    • agentPresets(remoteExportList/select),业务对象直接传参;0.1.1 宿主回退 ctx.get("apiProxy") (workspace.create / sessions.create / sessions.rename / sessions.selectModel / sessions.prompt / sessions.list);网关方法为宿主侧 dispatch, 必须传完整 client-request envelope {type:"client-request", rpcId, method, payload}(宿主从 request.payload 取业务字段,缺 type/rpcId/method 会被静默拒绝),返回入参沿用 ctx.get("apiProxy") 原样透传;每条 RPC 结果都做检查(ok === false / prompt 的 accepted !== true → 失败返回并 暴露真实拒绝原因);envelope 防御式解析 const r = env?.result; return r?.value ?? r;;
  • 定时巡检用 setInterval(非 cron/systemd 派生)。

跨平台:不依赖 date/awk/sed/grep/curl/python3;时间戳与 registry 解析全部内建实现 (t0 格式与 bash date +%F_%T 兼容互通)。

与 bash 版的已知偏差 / 边界

  1. 审计登记不落盘:watcher.sh 重派时调 dispatch/bin/audit-log.sh 补登调度审计一条—— 纯 JS 插件内无法调用 shell 脚本,故改为与 alerts.log 同等告警(不写审计通道);如需要, 由外部 cron/调度按 alerts.log 补登记。
  2. workspace.create 失败即失败:bash 版在 workspace.create 拿不到 workspaceId 时会静默 回退 session.create(cwd=工作区);本插件按规格「任一步失败返回 {ok:false, step, error}」 硬失败(更显式)。如需回退可自行改为先建会话。
  3. 默认模型:bash 版读 /opt/dsh/settings.yaml 的 agent-default-model;本插件改为 配置 defaultProvider/defaultModel/defaultEffort(spec 指令)。
  4. 无工作区时的 cwd:bash 默认 /server-center;本插件按规格用 workerBase。
  5. respawn-w100.sh 的「registry 无行重建」不覆盖:watch 只处理 registry 已登记行; W100 重启恢复(registry 行缺失时)需外部用 POST {prefix}/spawn 复现(body 见上)。
  6. registry/简报写入为尽力而为:.brief/.taskname/.workspace/.retries 落盘失败 不阻断拉起(RPC 主链路优先)。
  7. 观察期默认不启用巡检:watch.enabled 默认 false,需显式配置打开(防误伤 b 三件套 退役前的观测窗口)。
  8. registry 锁是「约定式」而非文件系统强制锁(W735):本插件用 O_EXCL 锁文件 (registry.tsv.lock),只有同样遵守该约定的写者才互斥;仍在写的 bash watcher.sh / Rust crates/workers 不取这把锁(各自 tmp+rename),跨生态并发写只靠「唯一 tmp 名 + 原子 rename」这一层兜底(本插件已不再使用固定 tmp 名)。三实例都升到 0.3.2 后风险才完全消除。

许可

MIT