dsh-offhours 0.1.3 → 0.2.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.
@@ -1,266 +0,0 @@
1
- # nightshift — 低谷 + 电脑空闲时,把看板待办真的做掉
2
-
3
- ## 一句话
4
-
5
- **每 5 分钟在本地做一次零 token 的判定**;只有「DeepSeek 低谷 ∧ 这台机器空闲 ∧ 有可跑的待办 ∧ 不在冷却 ∧ 未超预算」全满足时,才启动**一轮** worker(`dsh --profile headless`),在一个窗口内连续做最多 `MAX_CARDS_PER_RUN` 张卡,每张卡**必须留下验证证据**才算完成,结果写回看板并留可见回执,跑完**删掉 worker 会话**。
6
-
7
- ## 判定链(全部本地,零 token)
8
-
9
- | 步 | 条件 | 不满足时 |
10
- | --- | --- | --- |
11
- | 1 | 没有 `DISABLED` 文件(一键停) | 一行日志后退出 |
12
- | 2 | 单实例锁(flock) | 退出 |
13
- | 3 | 低谷:北京时间工作日 09–12 / 14–18 为高峰,其余(含周末/节假日全天)空闲 | 退出 |
14
- | 4 | 剩余窗口 ≥ `MIN_WINDOW_LEFT`(默认 20 分钟) | 退出(不跨进高峰) |
15
- | 5 | 机器空闲:最近 `IDLE_MIN`(**5**) 分钟**无任何会话活动** ∧ load1 ≤ `LOAD_MAX`(2.0) ∧ CPU busy ≤ `CPU_MAX`(40%) ∧ `loginctl` IdleHint 非 no | 退出 |
16
- | 6 | 冷却 `COOLDOWN_MIN`(10) 分钟 | 退出 |
17
- | 7 | 日预算:`MAX_CARDS_PER_DAY`(8) 卡 / `MAX_MINUTES_PER_DAY`(120) 分钟 | 拒绝并在回执写明 |
18
- | 8 | 有可跑的卡:`status=todo` 且 tags **不含 `hold`**;带 `auto` 的优先;**决策类卡**(tags 含 `decision-needed/human-review/blocking/legal/needs-direction/blocked*`,或标题以「拍板/人审/要不要」开头)**不交给无人值守** | 跳过,并在回执/日志里列出这些卡 |
19
-
20
- worker 的单卡超时 = `min(RUN_TIMEOUT, 剩余窗口 − 60s)`。
21
-
22
- ## 契约:worker 只干活,脚本写看板
23
-
24
- worker **不碰**看板文件、也没有 board 工具;它必须在回复末尾给出四行:
25
-
26
- ```
27
- KANBAN-RESULT: done|review|failed
28
- KANBAN-SUMMARY: <一行:做了什么 / 送审或失败原因>
29
- KANBAN-EVIDENCE: <实际跑过的验证命令与观察结果>
30
- KANBAN-NOTE: <review 时:不确定点 + 建议 + 备选;否则「无」>
31
- ```
32
-
33
- `ns.py` 据此落状态(**确定性、带防丢更新**):
34
-
35
- - `done` **且证据非空** → 卡置 done,`summary` 末尾附「证据:…」,缺 `rationale/rejected` 自动补;
36
- - `done` 但**没有证据** → 转**待审**并写明「缺验证证据」;
37
- - `review` → 置**待审**,把「不确定点+建议+备选」写进 description;
38
- - `failed` → 回 todo 并计一次尝试;连续 `MAX_ATTEMPTS`(2) 次 → 转**待审**(附最后错误);worker 超时同理;
39
- - 每轮都更新一张 **「夜间跑批回执(自动维护)」**(done + hold,自己永不被捡起):`summary` 是最近一轮,`description` 保留最近 10 轮;所以「昨晚跑没跑、为什么没跑」在看板上一眼可见。
40
-
41
- ## 为什么不用 dsh 的 cron 插件
42
-
43
- 1. **模型不对**:它「到点就往会话里塞提示词」——不看机器是否空闲、也不管看板有没有活,到点就发 LLM 请求。
44
- 2. **有致命定时器溢出 bug**(本机实测 2026-10-15):它把「下次触发」的毫秒数直接交给 `setTimeout`;超过 32 位上限(≈24.8 天)时 Node 会压成 **1 毫秒** → 立即重发 → 死循环。我用「只匹配一年后某天」的 cron 模拟一次性任务正好踩中:**22:00:26–22:03:17 创建了 195 个会话**(相邻间隔中位数 0.8 秒),直到停掉 dsh-web 才终止;按低谷价约 **1.40 元**。
45
- ⇒ 禁令:不要用「很远才匹配的 cron」当一次性任务;不要在进程外改插件存储(内存定时链不会被取消)。
46
-
47
- ## 前提:headless profile 必须装好这些东西(升级 dsh 后要跟着同步!)
48
-
49
- worker 跑的是 `dsh --profile headless`,它的能力**不是** web profile 的白拿:
50
-
51
- - `dsh-peak-block` 作为 **profile 层**(`bundles` 里)自动挂载 → 高峰期 worker 内的对话请求被硬拦。
52
- - `@deepseek-ai/dsh-hooks-claude-code` **不声明 dsh.bundle**,所以是普通依赖 + 在 `/root/.dsh/profiles/headless/cordis.patch.yml` 里**显式插入一行**(configPath 指向主机级 `/root/.dsh/hooks/dsh-hooks.json`)→ 带来 **feature-guard**。
53
- - 该 hooks 包的 **9 个 peer 依赖必须与运行时同版本**装在 headless profile 里(`@deepseek-ai/dsh-{agent,llm,session,tools,subagent,working-directory,session-projection,hook-protocol}` + `@deepseek-ai/cordis`)。缺任何一个,插件会 `failed to import`,而且**只在 stderr 里留一行 warning**——本次就是这样被骗过去的(见下面「踩过的坑」)。
54
-
55
- ## 安装 / 卸载
56
-
57
- ```bash
58
- /root/ops/nightshift/install.sh # ops 板:service + timer(每 5 分钟,零 token)
59
- /root/ops/nightshift/install.sh --hemou # 额外生成 hemou 单元(默认 disabled,要你手动 enable)
60
- /root/ops/nightshift/install.sh --uninstall # 停用并移除
61
- systemctl list-timers nightshift.timer # 看下一次扫描
62
- tail -f /root/ops/nightshift/nightshift.log # 扫描与每轮结果
63
- python3 /root/ops/nightshift/ns.py status # 状态:上次决策/今日计数/尝试次数/剩余窗口
64
- python3 /root/ops/nightshift/ns.py triage /root/hemou/KANBAN.json # 零 token 可跑性报告
65
- ```
66
-
67
- **hemou 板启用前**:先跑 `triage`(零 token),给不该被无人值守碰的卡打 `hold`,再 `systemctl enable --now nightshift-hemou.timer`。默认只服务 ops 板。
68
-
69
- ## 测试
70
-
71
- ```bash
72
- python3 /root/ops/nightshift/selftest.py # 零 token:判定/状态机/证据/计次/回执/防丢更新 + P1 回归
73
- cd mechanism && NIGHTSHIFT_HOME="$PWD" bash verify-all.sh # 测 worktree 当前代码
74
- /root/ops/nightshift/verify-all.sh --token <面板token> # 测活路径;期望 0 失败
75
- /root/ops/nightshift/scan.sh --dry-run # 只判定(我在用时必须跳过)
76
- /root/ops/nightshift/scan.sh --force --dry-run # 跳过时段/空闲,只看会跑哪张
77
- /root/ops/nightshift/scan.sh --force --max-cards=1 --only=<cardId> # 真跑一轮(花钱,慎用)
78
- # 高峰硬拦负向测试(期望:worker 被拦、卡回 todo、几乎不花钱)
79
- NIGHTSHIFT_OVERLAY=/root/ops/nightshift/test-force-peak.yml \
80
- /root/ops/nightshift/scan.sh --force --max-cards=1 --only=<cardId>
81
- ```
82
- P1 的不变量、配置键、原因码与回滚见 `docs/p1-hardening.md`。
83
-
84
- ## 旋钮(systemd unit 的 `Environment=`)
85
-
86
- | 变量 | 默认 | 含义 |
87
- | --- | --- | --- |
88
- | `NIGHTSHIFT_BOARD` / `NIGHTSHIFT_WORKDIR` | `/root/ops/KANBAN.json` / `/root/ops` | 目标看板 / worker 工作目录(也决定文件沙箱边界) |
89
- | `IDLE_MIN` / `LOAD_MAX` / `CPU_MAX` | **5** / 2.0 / 40 | 空闲判定(`IDLE_MIN`=5 表示"最近 5 分钟没有任何会话活动"就算人离开了;2026-10-16 用户选定,原为 15) |
90
- | `RUN_TIMEOUT` | 1200 | 单卡上限(还会被剩余窗口收紧) |
91
- | `MAX_CARDS_PER_RUN` | 3 | 一次扫描最多做几张 |
92
- | `MIN_WINDOW_LEFT` | 1200 | 剩余窗口不足就不开跑 |
93
- | `COOLDOWN_MIN` | 10 | 两次扫描真正开跑之间的最短间隔 |
94
- | `MAX_CARDS_PER_DAY` / `MAX_MINUTES_PER_DAY` | 8 / 120 | 日预算硬上限(`day.json`) |
95
- | `MAX_ATTEMPTS` | 2 | 同卡失败/超时多少次后转待审 |
96
- | `KEEP_RUNS` / `LOG_MAX_KB` | 200 / 1024 | `runs/` 与日志保留上限 |
97
- | `NIGHTSHIFT_OVERLAY` | 空 | 需要额外 profile 补丁时才设(负向测试用) |
98
-
99
- ## 产物与痕迹
100
-
101
- - `nightshift.log`(轮转 `.1`)、`state.json`(上次运行/尝试次数)、`day.json`(当日预算用量)
102
- - `runs/<时间>-<卡id>.{out,err,transcript.jsonl}`:worker 的输出、stderr、**完整会话转录**(会话目录随后被删除,所以界面不留垃圾)
103
- - 看板上的「夜间跑批回执(自动维护)」卡
104
-
105
- ## 怎么把「已确认的任务」放进待办队列
106
-
107
- **跑批读的就是本工作区的看板**(`NIGHTSHIFT_BOARD`,默认 `/root/ops/KANBAN.json`)。所以「放进待办」=**往看板加一张 `status=todo` 的卡**。三个入口:
108
-
109
- | 入口 | 怎么做 | 适合 |
110
- | --- | --- | --- |
111
- | **跟会话说一句**(最常用) | 「把 X 记进待办 / 任务确认了:做 Y」→ 会话调 `board_add`,按 `kanban-use` 技能的纪律写全 `title`+`rationale`(+`rejected`) | 你正在对话里,顺手记 |
112
- | **GUI 看板页** | 左侧「Smoothly Kanban」→ 页面上的新建/添加卡片 | 你只想手动记一条 |
113
- | **命令行(本仓库脚本)** | `python3 /root/ops/nightshift/add-card.py --title "…" --why "…" [--reject "…"] [--tags auto]`;开发任务用 `--dev --feature <特性名> --project <工程> --accept "<验收标准>"`;`--list` 看未完成卡 | 你在终端里,或想脚本化 |
114
-
115
- **需要 skill 吗?** 技能 `kanban-use` 已经存在(每个会话的目录里都有),它管的是**卡怎么写才完整**(为什么/放弃了什么/完成时三字段齐备)——它是给模型看的纪律,不是运行时自动化。**「把卡做掉」的自动化就是 nightshift 本身**(systemd timer 每 5 分钟零 token 判定 → 低谷+空闲才跑)。中间**不需要额外的库**。
116
-
117
- **无人值守要能把一张开发卡做掉,卡里必须有这四样**(缺了就会被转「待审」——2026-10-16 00:17 真实发生过:那张卡因为「未写特性名、所指工程不是 feature 登记工程」被 worker 拒绝猜并送审):
118
-
119
- 1. **特性名 / 工程**(`feature` / `project`,或等价的仓库路径)——worker 才知道该 attach/new 哪个 worktree;
120
- 2. **验收标准**(跑什么命令、看到什么算过)——它要拿这个当 `KANBAN-EVIDENCE`;
121
- 3. **边界**(不许动什么、要不要 `hold`);
122
- 4. **标签**:`auto` = 明确可无人值守;`hold` = 任何情况都不许无人值守碰;都不标也**会**被跑(口径是 `todo` 且非 `hold`),只是优先做 `auto`。
123
-
124
- > 记不住就直接用 `--dev` 模式:它会把「特性/工程、验收、边界」按上面四样生成进 description,并在缺 `--feature`/`--accept` 时**直接拒绝**(而不是让你放一张做不成的卡进去)。
125
-
126
- ## 多个看板 = 多个实例(ops / hemou)
127
-
128
- 看板是**按工作区**分开的,所以跑批也是按看板各起一个实例(各自 systemd 单元),互不干扰:
129
-
130
- | 实例 | 看板 | 单元 | 门槛 | 状态文件 |
131
- | --- | --- | --- | --- | --- |
132
- | ops | `/root/ops/KANBAN.json` | `nightshift.timer` | `todo` 且非 `hold`(默认口径) | `state-ops.json` / `day-ops.json` |
133
- | hemou | `/root/hemou/KANBAN.json` | `nightshift-hemou.timer` | **`NIGHTSHIFT_REQUIRE_AUTO=1`:只认 `auto`** | `state-hemou.json` / `day-hemou.json` |
134
-
135
- **为什么 hemou 用"只认 auto"**:hemou 板是**显式约定**——作者只在"明确可无人值守"时才打 `auto`,其余待办属于别的线或等人拍板(`todo=36` 里只有 7 张 `auto`)。按"非 hold 即可跑"的口径会连带吃掉另外 5 张(含需要设备的验证卡、需要拍板的决策卡),那是白烧 token。
136
-
137
- **隔离要点(2026-10-16 修)**:预算、冷却、尝试计数**按看板分开**(`state-<板>.json` / `day-<板>.json`);日志共用一份但每行带 `[ops]` / `[hemou]` 前缀;`.lock` 是**全局**的——两个实例互斥(不会同时烧钱)。
138
-
139
- **事后侦测的白名单**:`NIGHTSHIFT_GUARD_IGNORE`(默认 `KANBAN.json`)——看板文件本身每次写都会变(还有别的会话的 autosave),若不排除,hemou 实例会**每轮都误报"主检出被改动"**而把卡全部转待审。
140
-
141
- ## 界面:左侧「夜间跑批」面板跟随你看的看板
142
-
143
- 面板(`dsh-nightshift-panel`)顶部有**看板切换器**(`ops` / `hemou`),选择记在浏览器本地;标题下面显示当前看板的**文件路径**。切到 hemou 就能直接看到那 36 张待办与 7 张 `auto`。
144
-
145
- 面板的数据是零 token 的:状态条(低谷/定时器/下次扫描/冷却/今日预算)、**待办队列**(带 `auto 优先` / `永不自动跑` 标注)、**正在跑**、**待审(等你确认)**,以及 暂停 / 立即扫描一次 / 刷新 三个按钮——「立即扫描」会带**当前看板**的环境去跑扫描(修复前它会永远扫 ops,这是 2026-10-16 修的第二个坑)。
146
-
147
- > 面板的宿主半改动需要**重启一次 dsh-web** 才生效;客户端半刷新页面即可。
148
-
149
- ## 事后侦测:替代 feature-guard 的兜底(2026-10-15 实测后确定)
150
-
151
- **为什么跑批路径不能用 feature-guard 钩子**:headless profile 一旦加载 `@deepseek-ai/dsh-hooks-claude-code`,
152
- **任何工具调用都会崩**:
153
-
154
- ```
155
- dsh: UNKNOWN: Cannot read properties of undefined (reading 'prepare')
156
- ```
157
-
158
- 根因:该插件的 peer 依赖让 profile 里多出一份 `@deepseek-ai/dsh-tools`,而工具调度器是按
159
- `Symbol("@deepseek-ai/dsh-tools.scheduler")` 挂在 tools 服务实例上的 —— 两份副本 ⇒ Symbol 身份不一致 ⇒ `undefined`。
160
- **同机同卡 A/B**:systemd 上下文里「带钩子行」失败(`runs/20261015-234324-*.err`)、「禁用钩子行」成功(23:45:50 那轮 done);
161
- 而在继承了 `DSH_*` 环境的 shell 里一直没复现 —— 也就是说它**只会在真正的无人值守路径上炸**。
162
-
163
- **兜底做法(侦测,不是阻止)**:每轮前后对 `NIGHTSHIFT_GUARD_REPOS`(默认 `/root/hemou`)取
164
- `git status --porcelain --untracked-files=all` + HEAD 指纹;一变就把该卡转**待审**,并把**具体变化路径**写进卡片说明
165
- (例:`/root/hemou 新增改动:?? nightshift-probe3.txt`)。已端到端实测(worker 真往主检出写了文件 → 卡转待审并指名路径)。
166
- 注意两点:① 它**不阻止**写入,只保证「主检出被动了你一定看得到」;② 别的会话/看板自身的正常写入也会触发它(会按路径如实列出,由你判断)。
167
-
168
- ## 左菜单面板(dsh-nightshift-panel)
169
-
170
- GUI 左侧「Global panels」里有 **夜间跑批** 面板(与 Plugins / Smoothly Kanban / Automation tasks 并列)。面板显示:
171
-
172
- - 状态条:低谷/高峰、定时器是否 active、**下次扫描时间**、是否在冷却、**今日预算用量**(0/8 张卡 · 0/120 分钟);
173
- - **待办队列**:`todo` 卡(带 `auto 优先` / `永不自动跑`(=hold) 标记);
174
- - **正在跑**:当前 `in_progress` 的卡;
175
- - **待审(等你确认)**:`review` 卡 —— 这就是「不确定、等你拍板」的那些任务;
176
- - 操作:**暂停**(= 建 `DISABLED` 文件)、**立即扫描一次**(按正常门控,不强制)、**刷新**;每 30 秒自动刷新。
177
-
178
- 源码在工作区 `/root/ops/dsh-nightshift-panel/`,已作为 `@local/dsh-nightshift-panel`(`link:` 安装)进入 web profile 的 bundles;
179
- 装完或升级后**硬刷新页面**(Cmd/Ctrl+Shift+R)。卸载:用 `plugin_manager` 移除该 bundle。
180
-
181
- ## 待审 → 讨论确认 → 回到待办(闭环)
182
-
183
- 送审卡里带着 worker 的**编号问题清单**(①…②…,每条附【建议】与【备选】)。确认有三条入口,**结论一律写回卡片本身**(这是关键:下一个 worker 才不会重复问):
184
-
185
- | 入口 | 怎么做 |
186
- | --- | --- |
187
- | **面板(推荐)** | 左侧「夜间跑批」→ 待审区,每张卡下有「结论 / 执行要点」两个输入框 + **「确认 → 回到待办」** 按钮;带 `hold` 的卡可勾选「同时解除 hold」 |
188
- | **会话里讨论** | 直接说「确认 <卡标题>:①准 ②改B …」——会话(或我)把它落成 `ns.py confirm` |
189
- | **命令行** | `python3 /root/ops/nightshift/ns.py confirm <看板> <卡id> "<结论>" "<执行要点>" [--by X] [--unhold]`(加 `--dry` 只看不改) |
190
-
191
- **机械行为**(`ns.py confirm`,零 token):① 在 description 追加 `【确认 <时间> · by <谁>】结论:… / 执行要点:…`,并写明「worker 不得就同一问题再次送审」;② 状态 `review → todo`;③ **清空 attempts**(否则历史失败计数会立刻把它推回待审)。
192
-
193
- **护栏**:只有 `review` 的卡能确认(`todo/done` 会被拒——不许绕过讨论);结论至少 8 个字(太短下一个 worker 没法照做);带 `hold` 的卡默认**保留 hold**(跑批仍不捡它),要放行必须显式勾选/`--unhold`——hold 是"绝不允许无人值守"的安全默认,不能被"确认"顺手推翻。
194
-
195
- **worker 侧的对应义务**(已写进提示词):见到 `【确认 …】` 块就照办、**不得就同一问题再次送审**(只有确认块没覆盖到的新问题才允许再问);送审时必须写编号清单 ①…②… 并附建议与备选,让人能一句话批掉。
196
-
197
- ## 「绝不重复会话风暴」的机械保障(2026-10-16 用户硬要求)
198
-
199
- | 机制 | 机械事实 | 证据 |
200
- | --- | --- | --- |
201
- | **worker 不能造会话** | headless profile 里 `subagent` / `subagent-spawn-in-process` / `workflow-ptc` / `tool-workflow` 按 id 禁用;`schedule`(会话级定时提醒) / `ralph` / `agent-team` / `cron` 本来就不在 | 运行时探针:worker 只有 19 个工具,上述名字出现 0 次 |
202
- | **不能自己排下一次** | 已摘除 cron 插件;headless 无 schedule ⇒ 无"自己触发自己"的通道 | `dsh --profile headless --dump-config` + 探针 |
203
- | **机械熔断** | 每次 tick 先数"最近 `STORM_MIN`(15) 分钟内写入过的会话数",> `STORM_MAX`(10) ⇒ 拒开工 + 写 `DISABLED` 自动暂停 + 日志 + 回执 | 触发路径实测(`STORM_MAX=0`);昨天那种 3 分钟 195 会话会在第一个 tick 被拦 |
204
- | **每 tick 上限** | timer 每 5 分钟一次、两次真跑之间冷却 10 分钟、一轮最多 3 张、每板每天 8 张 / 120 分钟 | `systemctl list-timers` + `day-<板>.json` |
205
- | **两实例互斥** | 全局 flock:ops 与 hemou 不会同时跑(日志里会写"另一个跑批仍在运行,跳过") | 实测日志 |
206
- | **零 token 判定** | tick 本身只做本地文件/命令判断;不满足条件时一行日志退出,**一个 token 不花** | 日志里大量"跳过"行 + 今天只有 3 次真跑 |
207
-
208
- **残余通道(诚实)**:worker 有 `bash`,理论上能自己调 `dsh` 再开会话;工具层堵不死(沙箱因另一会话的需要已放宽为 `danger-full-access`)。兜底=熔断 + 日预算 + 冷却 + 互斥 + 事后侦测。
209
-
210
- ## 代码去向:worker 独占 worktree + 完成时合并回源会话(2026-10-16 用户要求)
211
-
212
- 默认 **关闭**(`NIGHTSHIFT_ISOLATED_WORKTREE=1` 打开),行为对比:
213
-
214
- | | 关闭(默认,今天的行为) | 打开 |
215
- | --- | --- | --- |
216
- | worker 在哪干活 | **源会话共享的特性 worktree**(例如 `/root/features/facet-client/hemou`) | **自己的独占 worktree** `/root/features/ns-<卡id>/<工程>/`(分支 `feat/ns-<卡id>`,从源特性分支 HEAD 拉出) |
217
- | 完成时 | 就地提交(不推送) | 提交 → `git -C <源worktree> merge --no-ff feat/ns-<卡id>` → 在卡里写 `【代码去向】… @ <merge commit>` |
218
- | 送审/失败 | 就地留下改动(可能污染源 worktree) | **不合并**,保留 worker worktree + 分支,卡上写明路径 |
219
- | 源会话没关联特性 | worker 自己 `feature ensure`(可能自起名) | **直接送审**:"无法确定代码去向"(不自起特性名) |
220
-
221
- 实现:`ns.py prep-worktree <board> <卡id> [--go]`(读 `/root/features/.sessions/<源会话id>.json` → `feature ensure <源特性>` → `feature new ns-<卡id> --from feat/<源特性>` → 打印 `WORKDIR=`)、`ns.py merge-back <board> <卡id> <done|review|failed|auto> [--go]`;`scan.sh` 在 `run_one` 开头准备、`apply` 之后按 `auto`(读卡当前状态)决定是否合并。
222
- **为什么必须走 feature 工具**:`feature-guard` 的放行判据是"路径命中**已注册的**特性 worktree"(`feature_of_path` 读 `feature.json` 的 `.repos[].path`)⇒ 手工 `git worktree add` 出来的目录**不在注册表里,提交会被拦**。
223
- **红线**:不推送、不 `feature recycle`(合主干是人的验收决定)、worker 只写自己那块 worktree。
224
-
225
- ## 并行(2026-10-16 用户要求)
226
-
227
- **已生效(L1 跨板并行)**:
228
- - 锁由**全局一把**改为**每板一把**:`.lock-ops` / `.lock-hemou` ⇒ ops 与 hemou 可同时跑(实测:占住 ops 锁时 hemou 照常、ops 被挡)。
229
- - **全局 worker 槽位**:`SLOTS=slots/`(mkdir 原子占位)、`MAX_WORKERS=${NIGHTSHIFT_MAX_WORKERS:-3}`(跨板总并发上限);拿不到槽位就结束本轮;启动前自动清理过期槽(超过 `RUN_TIMEOUT+20min`)。
230
- - 并行正确性前提:临时结果文件 `last-result-<board>.txt` **每板一份**(原为全局一份,会互相覆盖)。
231
-
232
- **待做(L2 同板多卡并行)**:前置条件是 `NIGHTSHIFT_ISOLATED_WORKTREE=1`(每卡独占 worktree,否则抢 git index)+ 特性注册表串行化(`flock $FEATURES_ROOT/.ns-feature.lock`)+ 每卡一份结果文件 + 每启动一个 worker 前复检 load/CPU;上限用 `MAX_PARALLEL_PER_BOARD`(建议默认 2)。日预算与熔断**仍是权威上限**——并行只提高窗口内速率,不提高总额度。
233
-
234
- ## 改完要重启 dsh-web 时(2026-10-16 事故与铁律)
235
-
236
- **事故**:改完宿主半插件后用 `systemd-run --on-active=60 …` 定时重启 `dsh-web`,结果那个时刻正有会话回合在跑 ⇒ 重启把承载会话的进程杀了 ⇒ 用户看到"**跑着跑着突然中断**"。当晚发生两次(21:19、21:54),每次都让一个未完成的大回合当场消失。
237
-
238
- **铁律**:`dsh-web` 承载**所有会话回合**,任何重启都必须**等机器空闲**:
239
- ```bash
240
- /root/ops/nightshift/restart-web-when-idle.sh [等待上限秒=1800] [报告文件]
241
- ```
242
- 它用 `ns.py idle <NEED_MIN=2> 2.0 40` 判定"最近 2 分钟没有任何会话活动",满足才重启;等不到就**放弃重启**(宁可改动晚生效,也绝不打断会话)。实测:不空闲时 5 秒即放弃、dsh-web 未被重启 ✓。
243
-
244
- **第三条经验(2026-10-16 第二次中断的真因)**:同一个文件**不要混用两种写入机制**——我先把 P3 用 bash+python 写进 `index.js`,随后又用 edit 工具改同一文件;edit 的版本快照没看到 bash 的写入,写回时把 P3 的改动**整体回滚**了(只有之后用 bash 写的 P4 幸存),直到看运行态报错才发现。规则:**一个文件从头到尾只用一个机制**(要么全 bash/python,要么全 edit/write);改完必须用 grep/`node --check` 复核标记是否真的在文件里,而不是相信"脚本打印了成功"。
245
-
246
- **另外两条经验**:
247
- - 面板(`@local/dsh-nightshift-panel`)的**客户端**是每请求读文件 ⇒ 只改文案/样式**只需刷新页面**,不用重启;只有**宿主半**(`index.js`)改动才需要重启。
248
- - 看板插件(`dsh-kanban`)的**客户端是启动时装配**的 ⇒ 改了要重启才生效(同样走上面的空闲重启)。
249
-
250
- ## 边界与诚实清单
251
-
252
- - 时段判定只按工作日+时段,**中国法定节假日会被当成高峰**(保守方向:少跑、不多花钱)。
253
- - 「空闲」是代理指标(会话静默 + 负载 + CPU + loginctl)。`xprintidle` 本机没有。
254
- - worker 的 `workspace-write` 沙箱本该挡住工作目录之外的写入,**但 headless profile 现在被另一会话设成了 `danger-full-access`**
255
- (为的是让无人值守会话能按需建 feature worktree,理由见该 profile 的 `cordis.patch.yml` 注释)——所以**沙箱这条也不再有兜底**。
256
- 跑批路径上唯一还起作用的是上面那套**事后侦测**(+ 提示词纪律)。
257
- - 一轮一张卡是刻意保守;吞吐由预算、冷却、可用窗口共同决定。**这台机器夜里常关机**(2026-10-15:01:25 关、08:06 开),实际可用窗口 ≈ 08:06–09:00 / 12:00–14:00 / 18:00–23:00。
258
- - 看板注入的指引文案(BOARD_GUIDANCE)已与本文件口径对齐,但**需要重启一次 dsh-web 才生效**。
259
- - `ns.py` 的写看板是「读-改-写 + 写前指纹核对 + 重放」,能避免覆盖你的编辑;极端连续并发下会放弃本次写入(记日志),不会硬覆盖。
260
-
261
- ## 踩过的坑(都留在 Agent Note 里)
262
-
263
- 1. **`--dump-config` 不等于激活**:用绝对路径 `insert` 的 peak-block / hooks 两行在 headless 里都是 `failed to import`(只打一行 warning),我当时据此以为"能挂"——错。后来改成 profile 层 + 显式依赖才真生效。
264
- 2. **hooks 包的 9 个 peer 缺一个就整包 import 失败**;装 peer 时**必须带确切版本**(我漏传版本,pnpm 装了 `0.0.1-rc.1`,于是报 `does not provide an export named 'selectHookGroups'`)。
265
- 3. **`--only=` 空值会静默退化成"随便选一张"**(曾因此把真实卡跑了一遍);现在空值直接报错退出。
266
- 4. **`pkill -f "dsh --profile headless"` 会匹配到自己的命令行**,把自己那条 bash 杀掉;用 `[d]sh` 或 pgrep 精确匹配。
@@ -1,3 +0,0 @@
1
- - insert:
2
- - id: dsh-nightshift-panel
3
- name: '@local/dsh-nightshift-panel'