cs-bots 0.1.1 → 0.7.5

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/README.md +65 -10
  3. package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +364 -0
  4. package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +284 -0
  5. package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +567 -0
  6. package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +569 -0
  7. package/docs/ARCHITECTURE.md +277 -0
  8. package/docs/adr/ADR-001-/351/241/265/347/255/276/347/273/223/346/236/204/345/233/236/345/210/260/344/270/203/351/241/265.md +70 -0
  9. package/docs/steps/step-01-/345/216/237/346/236/266/346/236/204/345/244/215/345/216/237/344/270/216IA/350/257/212/346/226/255.md +80 -0
  10. package/docs/steps/step-02-/351/241/265/347/255/276/346/224/266/346/225/233/344/270/216/345/215/207/347/272/247/351/200/232/351/201/223.md +51 -0
  11. package/docs/steps/step-03-/346/236/266/346/236/204/350/265/204/346/226/231/346/210/220/346/226/207.md +16 -0
  12. package/docs/steps/step-04-/344/270/212/347/272/277/344/270/216/351/252/214/350/257/201.md +46 -0
  13. package/docs//344/272/244/344/273/230/350/257/264/346/230/216-v0.2.0.md +95 -0
  14. package/docs//346/223/215/344/275/234/346/211/213/345/206/214-v1.md +509 -0
  15. package/docs//350/277/201/347/247/273/346/226/271/346/241/210-v1.md +64 -0
  16. package/docs//351/225/277/346/234/237/350/256/260/345/277/206/346/226/271/346/241/210-v1.md +85 -0
  17. package/package.json +4 -2
  18. package/src/admission.js +365 -0
  19. package/src/api.js +4097 -420
  20. package/src/board.js +883 -14
  21. package/src/botprofile.js +190 -3
  22. package/src/client.js +6596 -1082
  23. package/src/cycle.js +516 -0
  24. package/src/delivery-watch.js +255 -0
  25. package/src/dirbrowse.js +20 -0
  26. package/src/discipline.js +1188 -0
  27. package/src/fence.js +142 -0
  28. package/src/index.js +387 -4
  29. package/src/kernel.js +636 -24
  30. package/src/library.js +1224 -11
  31. package/src/memory.js +617 -37
  32. package/src/metrics.js +339 -0
  33. package/src/prompts.js +1318 -0
  34. package/src/reviewer.js +709 -0
  35. package/src/room.js +655 -14
  36. package/src/roster.js +346 -26
  37. package/src/routing/complexity.js +320 -0
  38. package/src/routing/hints.js +707 -0
  39. package/src/scratch.js +477 -0
  40. package/src/settings.js +97 -0
  41. package/src/stage.js +263 -0
  42. package/src/store.js +133 -1
  43. package/src/subscription.js +571 -0
  44. package/src/suggest.js +1203 -0
  45. package/src/teams.js +527 -0
  46. package/src/todo.js +1301 -0
  47. package/src/tools.js +734 -10
  48. package/src/verify.js +356 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * cs-bots 投递回执看护(Delivery Acknowledgement Watchdog)
3
+ *
4
+ * ## 它补的是哪一层
5
+ * superagent 把「一条输入到底有没有被消费」拆成三级:
6
+ * `inputsDelivered`(到达会话)→ `inputsRead`(进入历史,且后续有 Report/回合结束才算)
7
+ * 我们此前**只有第一级的一半**:`dispatcher` 记 `sent`(= 把消息交给了宿主),
8
+ * 就当作成功。于是出现最坏的一类失败:
9
+ *
10
+ * 现场(2026-11-09):wake 记 `sent`,会话 `turns: 0 / steps: 0`,
11
+ * 事件文件还停在几天前 —— 任务卡在「进行中」,页面看不到任何异常。
12
+ * **投递成功 ≠ 真的在跑。**
13
+ *
14
+ * 本模块补第二级:投递之后盯着会话的**事件水位**。过了宽限期还没涨,
15
+ * 就把「已投递但执行者未开始动作」写进 `task.delivery.error`
16
+ * (`stage.js` 会据此派 `person/delivery`,页面立刻显形并给出下一步)。
17
+ *
18
+ * ## 为什么用宽限期而不是立即判
19
+ * 模型冷启动 + 排队 + 长上下文都可能让「第一回合」晚几十秒才开始。
20
+ * 立刻判会产生大量误报,而**误报比漏报更伤**:人会学会忽略这条提示。
21
+ * 所以默认 90 秒宽限,超时才落失败;期间任何一次水位上涨都算兑现。
22
+ *
23
+ * ## 持久化取舍(诚实说明)
24
+ * 期望表**只在内存里**,进程重启即清空。理由:
25
+ * - 重启会把所有会话一起杀掉,旧的期望本来就已经失效;
26
+ * - 落盘会给每个投递写一条记录,把 `wakes.jsonl` 的语义搅浑;
27
+ * - 漏判的代价只是「这一次没提醒」——下一次唤醒会重新建立期望。
28
+ * 如果将来需要跨重启追踪,再把它做成 append-only 文件不迟。
29
+ *
30
+ * @module delivery-watch
31
+ */
32
+
33
+ /** 默认宽限期:投递后多久还没动静就判「未开始动作」。 */
34
+ export const DEFAULT_GRACE_MS = 90_000
35
+ /** 默认巡检间隔。 */
36
+ export const DEFAULT_TICK_MS = 15_000
37
+ /** 单次最多同时跟踪多少条期望(防内存失控)。 */
38
+ export const DEFAULT_MAX_PENDING = 200
39
+
40
+ /**
41
+ * 创建回执看护。
42
+ *
43
+ * @param {object} deps
44
+ * @param {(sessionId: string) => Promise<object>} deps.watermark 读会话水位(kernel.readSessionWatermark)
45
+ * @param {(taskId: string, reason: string) => Promise<boolean>} deps.markFailure 落 delivery.error
46
+ * @param {{ info?: Function, warn?: Function }} [deps.logger]
47
+ * @param {{ graceMs?: number, tickMs?: number, maxPending?: number,
48
+ * now?: () => number, schedule?: Function, cancel?: Function }} [deps.options]
49
+ * @returns {object} watchdog
50
+ */
51
+ export function createDeliveryWatch(ctx, deps = {}) {
52
+ const logger = deps.logger ?? {}
53
+ const info = (...a) => { try { logger.info?.(...a) } catch { /* */ } }
54
+ const warn = (...a) => { try { logger.warn?.(...a) } catch { /* */ } }
55
+
56
+ const options = deps.options ?? {}
57
+ const graceMs = Number.isFinite(options.graceMs) ? Number(options.graceMs) : DEFAULT_GRACE_MS
58
+ const tickMs = Number.isFinite(options.tickMs) ? Number(options.tickMs) : DEFAULT_TICK_MS
59
+ const maxPending = Number.isFinite(options.maxPending) ? Number(options.maxPending) : DEFAULT_MAX_PENDING
60
+ const now = typeof options.now === 'function' ? options.now : () => Date.now()
61
+ const schedule = typeof options.schedule === 'function'
62
+ ? options.schedule
63
+ : (fn, ms) => {
64
+ const t = setTimeout(fn, ms)
65
+ // 别让看护定时器把进程顶住
66
+ t?.unref?.()
67
+ return t
68
+ }
69
+ const cancel = typeof options.cancel === 'function'
70
+ ? options.cancel
71
+ : (h) => clearTimeout(h)
72
+
73
+ /** @type {Map<string, { taskId: string, sessionId: string, sentAt: number, baselineSeq: number,
74
+ * baselineUserMessages: number, wakeId?: string }>} */
75
+ const pending = new Map()
76
+ const diagnostics = {
77
+ expected: 0, // 累计建立过多少条期望
78
+ fulfilled: 0, // 水位涨了(真的开始动了)
79
+ unfulfilled: 0, // 超时仍没动
80
+ undecidable: 0, // 读不到水位(宿主没持久化层 / 会话不存在)
81
+ dropped: 0, // 被新期望覆盖 / 主动取消
82
+ lastSweepAt: undefined,
83
+ }
84
+ let timer = null
85
+ let disposed = false
86
+
87
+ /** 期望键:同一任务的同一会话只保留最新一条(新唤醒自然重置宽限期)。 */
88
+ const keyOf = (taskId, sessionId) => `${taskId}\u0000${sessionId}`
89
+
90
+ /**
91
+ * 登记一条「投递已发出,等回执」的期望。
92
+ *
93
+ * @param {{ taskId: string, sessionId: string, baselineSeq?: number,
94
+ * baselineUserMessages?: number, wakeId?: string }} input
95
+ * @returns {object} 期望记录(含 `key`,可用于 cancel)
96
+ */
97
+ function expect(input = {}) {
98
+ const taskId = typeof input.taskId === 'string' ? input.taskId : ''
99
+ const sessionId = typeof input.sessionId === 'string' ? input.sessionId : ''
100
+ if (!taskId || !sessionId) {
101
+ return { ok: false, reason: 'expect:taskId 与 sessionId 必填' }
102
+ }
103
+ const key = keyOf(taskId, sessionId)
104
+ if (pending.has(key)) diagnostics.dropped += 1
105
+ if (pending.size >= maxPending && !pending.has(key)) {
106
+ // 满了就丢最旧的一条(只 warn,不抛——看护不该影响主流程)
107
+ const oldest = pending.keys().next().value
108
+ pending.delete(oldest)
109
+ diagnostics.dropped += 1
110
+ warn(`[cs-bots/watch] 期望表已满(${maxPending}),丢弃最旧一条(${oldest})`)
111
+ }
112
+ const entry = {
113
+ key,
114
+ taskId,
115
+ sessionId,
116
+ sentAt: now(),
117
+ baselineSeq: Number.isFinite(input.baselineSeq) ? Number(input.baselineSeq) : -1,
118
+ baselineUserMessages: Number.isFinite(input.baselineUserMessages)
119
+ ? Number(input.baselineUserMessages)
120
+ : 0,
121
+ wakeId: input.wakeId,
122
+ }
123
+ pending.set(key, entry)
124
+ diagnostics.expected += 1
125
+ ensureTimer()
126
+ return { ok: true, ...entry, graceMs }
127
+ }
128
+
129
+ /** 兑现 / 撤销一条期望(例如任务被重开、或人主动重试)。 */
130
+ function cancelByTask(taskId) {
131
+ let removed = 0
132
+ for (const [k, v] of pending) {
133
+ if (v.taskId === taskId) {
134
+ pending.delete(k)
135
+ removed += 1
136
+ }
137
+ }
138
+ if (removed) diagnostics.dropped += removed
139
+ return removed
140
+ }
141
+
142
+ /**
143
+ * 巡检一轮:判定「水位是否涨了」「是否超时」。
144
+ *
145
+ * 判据(两条任一即算兑现):
146
+ * - 最新 seq > 投递前基线 seq(有新事件,说明回合真的开始了)
147
+ * - `user/message` 条数 > 基线(这条输入被吸进历史)
148
+ *
149
+ * @returns {Promise<{ checked: number, fulfilled: number, unfulfilled: number, undecidable: number }>}
150
+ */
151
+ async function sweep() {
152
+ if (disposed) return { checked: 0, fulfilled: 0, unfulfilled: 0, undecidable: 0 }
153
+ diagnostics.lastSweepAt = now()
154
+ const checked = pending.size
155
+ let fulfilled = 0
156
+ let unfulfilled = 0
157
+ let undecidable = 0
158
+ const at = now()
159
+
160
+ for (const [key, entry] of [...pending]) {
161
+ if (!pending.has(key)) continue // 并发期间被删
162
+ let wm
163
+ try {
164
+ wm = typeof deps.watermark === 'function'
165
+ ? await deps.watermark(entry.sessionId)
166
+ : { ok: false, available: false, reason: 'watermark 未注入' }
167
+ } catch (error) {
168
+ wm = { ok: false, available: true, reason: String(error?.message ?? error) }
169
+ }
170
+
171
+ if (!wm || !wm.ok) {
172
+ // 读不到水位 —— 不能判死(可能是宿主没持久化层)。过了宽限期就放弃跟踪并记一笔。
173
+ if (at - entry.sentAt >= graceMs) {
174
+ pending.delete(key)
175
+ diagnostics.undecidable += 1
176
+ undecidable += 1
177
+ warn(
178
+ `[cs-bots/watch] 无法判定 ${entry.taskId} 的投递是否被消费(${wm?.reason ?? 'unknown'}),`
179
+ + '已停止跟踪(不写失败,避免误报)',
180
+ )
181
+ }
182
+ continue
183
+ }
184
+
185
+ const seqGrew = Number.isFinite(wm.seq) && wm.seq > entry.baselineSeq
186
+ const userGrew = Number.isFinite(wm.userMessages) && wm.userMessages > entry.baselineUserMessages
187
+ if (seqGrew || userGrew) {
188
+ pending.delete(key)
189
+ diagnostics.fulfilled += 1
190
+ fulfilled += 1
191
+ continue
192
+ }
193
+
194
+ if (at - entry.sentAt >= graceMs) {
195
+ pending.delete(key)
196
+ diagnostics.unfulfilled += 1
197
+ unfulfilled += 1
198
+ const reason = `已投递但执行者未开始动作(${Math.round((at - entry.sentAt) / 1000)} 秒内会话没有新事件)`
199
+ + '——可能是会话没有真正运行,或它的模型路由不可用;去「Bot 档案」确认这个 Bot 能跑起来,再点「重试唤醒」'
200
+ try {
201
+ const marked = typeof deps.markFailure === 'function'
202
+ ? await deps.markFailure(entry.taskId, reason)
203
+ : false
204
+ info(`[cs-bots/watch] ${entry.taskId} 投递未被消费 → ${marked ? '已写入 task.delivery' : '写入失败(任务可能已结束)'}`)
205
+ } catch (error) {
206
+ warn(`[cs-bots/watch] ${entry.taskId} 写 delivery 失败:${error?.message ?? error}`)
207
+ }
208
+ }
209
+ }
210
+
211
+ if (pending.size === 0) stopTimer()
212
+ return { checked, fulfilled, unfulfilled, undecidable }
213
+ }
214
+
215
+ function ensureTimer() {
216
+ if (disposed || timer != null || pending.size === 0) return
217
+ const tick = async () => {
218
+ timer = null
219
+ try { await sweep() } catch (error) { warn(`[cs-bots/watch] 巡检异常:${error?.message ?? error}`) }
220
+ if (!disposed && pending.size > 0) ensureTimer()
221
+ }
222
+ timer = schedule(tick, tickMs)
223
+ }
224
+
225
+ function stopTimer() {
226
+ if (timer != null) {
227
+ cancel(timer)
228
+ timer = null
229
+ }
230
+ }
231
+
232
+ /** 关停(插件 dispose 时调用):清定时器、清期望表。 */
233
+ function dispose() {
234
+ disposed = true
235
+ stopTimer()
236
+ pending.clear()
237
+ }
238
+
239
+ return {
240
+ expect,
241
+ sweep,
242
+ cancelByTask,
243
+ dispose,
244
+ stats: () => ({
245
+ ...diagnostics,
246
+ pending: pending.size,
247
+ graceMs,
248
+ tickMs,
249
+ }),
250
+ /** 只读快照(调试用)。 */
251
+ list: () => [...pending.values()].map((e) => ({ ...e })),
252
+ }
253
+ }
254
+
255
+ export default createDeliveryWatch
package/src/dirbrowse.js CHANGED
@@ -293,6 +293,26 @@ export function createDirBrowser(config) {
293
293
  { label, path: dir, real: realOrSelf(dir), roots: allowedRoots() },
294
294
  )
295
295
  }
296
+ // ⚠️ 修复(2026-11-04 安全测试,P0):原来只判 `withinAllowedForWrite`(家目录 ∪
297
+ // 进程 cwd ∪ config.roots),而 `validateAsBotCwd` 里的受保护区(DSH 家目录 /
298
+ // ~/.agents / cs-bots 状态目录)**只在那一个校验函数里生效** ⇒ 校验说"不行"、
299
+ // 写入却说"可以"。现在两处口径统一:写闸门也遵守同一组受保护区。
300
+ const real = realOrSelf(dir)
301
+ const guarded = [
302
+ { dir: dshHome, label: 'DSH 家目录' },
303
+ { dir: agentsDir, label: '~/.agents(宿主技能目录)' },
304
+ { dir: stateDir, label: 'cs-bots 状态目录' },
305
+ ]
306
+ for (const { dir: gdir, label: glabel } of guarded) {
307
+ const root = realOrSelf(gdir)
308
+ if (isInside(real, root) || isInside(dir, gdir)) {
309
+ throw fail(
310
+ `拒绝在受保护区写入:${dir}(${glabel})—— 该区域属宿主/插件自身,不接受外部写入`,
311
+ 'DIR_PROTECTED',
312
+ { label, path: dir, real, protected: glabel },
313
+ )
314
+ }
315
+ }
296
316
  }
297
317
 
298
318
  /**