dsh-vibe-math 2.0.7 → 2.0.9

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/README.md CHANGED
@@ -40,7 +40,7 @@
40
40
  **一句话流水线**:起始产生 N 个**常驻子代理**(continuable,持久上下文)先各自头脑风暴、产出初始见解/方向 → 此后**所有任务安排由它们互相留言 + 集体开会自主决定**(框架只做消息总线/会议/任务板/产物沉淀,**绝不分配任务**);每个常驻把有价值的产物按**价值程度 / 动机用途计划 / 自身概率估计**沉淀到**自己**的 `Progress/<id>/`、`Propos/<id>/`、`Methods/<id>/`、`Subproblems/<id>/` 库,并**可互相阅读**;验证由它们**自行商议**发起,**仅当全体常驻一致(真或假)**才写入 `Verified/`,否则留库附概率;常驻上下文量达阈值(默认 66%)自动 `/compact`;**仅当全体一致认为原问题已解决**才停止;可随时人工干预/增开/关闭常驻,支持断点续跑。
41
41
 
42
42
  > 说明:V4 去掉 v3 的中央规划器与确定性角色(explorer/solver/verifier/planner/method-keeper),把"研究者"本身作为主体。详见 `vibe-math-v4/实现方案.md`。
43
- > 保活机制(分级保活 A+B + 死锁看门狗):团伙空闲超 `activityTimeoutMs` 会收到**自驱动** CHECKPOINT(建议继续解决/发消息/提议任务,而非"是否要停止"),唤醒失败会自动重新武装心跳;若团队空闲且**无新产物**超过 `stallAutoMeetingMs`(默认 6 分钟),框架会自动召集一次同步会议让常驻们自行决定下一步;若某次**会议/验证卡死**(超过 2×`activityTimeoutMs` 仍无新的发言/投票),框架会自动**放弃该会议/验证**并回到正常自组织,避免一个坏掉的会议永久卡住整个团队——框架始终只促成、从不指派任务。
43
+ > 保活机制(分级保活 A+B + 死锁看门狗):团伙空闲超 `activityTimeoutMs` 会收到**自驱动** CHECKPOINT(建议继续解决/发消息/提议任务,而非"是否要停止"),且**并行填充**——A 分支一次尽量填满 `maxParallel` 并发预算(同一时刻唤醒多个空闲常驻,而非"只唤醒 r1、结束后再 r2"的串行),邮箱投递也并行送达多个空闲收件人;唤醒失败会自动重新武装心跳;若团队空闲且**无新产物**超过 `stallAutoMeetingMs`(默认 6 分钟),框架会自动召集一次同步会议让常驻们自行决定下一步;若某次**会议/验证卡死**(超过 2×`activityTimeoutMs` 仍无新的发言/投票),框架会自动**放弃该会议/验证**并回到正常自组织,避免一个坏掉的会议永久卡住整个团队;**会议不抢占验证**——验证进行时会议请求会暂存,验证做完再补开(保持一致共识的"求真"环节不被协调讨论打断)——框架始终只促成、从不指派任务。
44
44
 
45
45
  ---
46
46
 
@@ -97,7 +97,7 @@ dsh plugin --profile <你的 profile> add github:ChongCyrus/Vibe-Mathematics
97
97
 
98
98
  - **形态依赖**:三个 preset 依赖 DSH 的标准 **agent-preset 机制**(`~/.dsh/.agent-presets/<id>/` + preset picker)与 **bundle patch 机制**(`cordis.patch.yml` 注入安装器)。
99
99
  - **宿主插件行**:`agent.cordis.yml` 引用宿主提供的 `@deepseek-ai/dsh-*` 插件行(persona、agent-instructions、tool-bash/pwsh、tool-fs/fs-search、tool-jobs、skill-filesystem、tool-skill、tool-goal、plan-mode、compaction、subagent/workflow、ask-user、todo、web 等,约 21 个唯一包名)。宿主缺行会导致 preset 挂载失败(会话启动时报错)。
100
- - **宿主服务 API**:预设插件消费 `subagents`(startContinuable / followup / interrupt)、`agents`(roots)、`tools`(register)、`commands`(register)、`fs`(resolve/stat/readText/writeText/listDir)、可选 `subprocess` / `sandboxPolicy`。这些 API 形状随 DSH 版本演进;本项目**已充分测试并确认适配 `dsh-v0.1.2-rc.1`**(`package.json` 的 `dsh.testedVersion`;`minVersion` 为 `0.1.2-rc.1`)。**注意:DSH 0.1.2 起 `subagents.startContinuable` 的 `agentOptions` / `toolFilter` 需要宿主 provider 声明对应 capability**(spawn / fork 进程内 provider 均支持,v4 指定常驻模型/路由与工具权限依赖于此)。
100
+ - **宿主服务 API**:预设插件消费 `subagents`(startContinuable / **sendMessage**(续做/唤醒;`followup` 仅为 `Agent` 对象方法、**不是** `subagents` 服务方法)/ interrupt)、`agents`(roots)、`tools`(register)、`commands`(register)、`fs`(resolve/stat/readText/writeText/listDir)、可选 `subprocess` / `sandboxPolicy`。这些 API 形状随 DSH 版本演进;本项目**已充分测试并确认适配 `dsh-v0.1.2-rc.1`**(`package.json` 的 `dsh.testedVersion`;`minVersion` 为 `0.1.2-rc.1`)。**注意:DSH 0.1.2 起 `subagents.startContinuable` 的 `agentOptions` / `toolFilter` 需要宿主 provider 声明对应 capability**(spawn / fork 进程内 provider 均支持,v4 指定常驻模型/路由与工具权限依赖于此)。
101
101
  - **DSH STORE 兼容声明**:`package.json` 的 `dsh.compatibility.dshReleases` 对每个完整 DSH 版本逐项声明 `compatible` / `incompatible` / `unknown`(当前 `0.1.2-alpha.4`、`0.1.2-alpha.5`、`0.1.2-rc.1` 均标 `compatible`);`engines.node` 为 `^22.19.0 || >=24.0.0`(与 DSH 0.1.2 相同)。
102
102
  - **运行时自检(能力 + 版本双检)**:安装器(bundle 插件)每次启动时:**① 尽力探测 DSH 版本**(读 `@deepseek-ai/dsh/package.json` 或 `DSH_VERSION` 环境变量;DSH 未通过公开 service/context 暴露版本,故为尽力而为,探测不到就跳过)。若探测到且该版本未被 `dshReleases` 声明为 `compatible`,会给出明确"DSH 版本不匹配,请使用 `dsh-v0.1.2-rc.1`(或 `0.1.2-alpha.4/alpha.5`)"提示;**② 再对上述服务与关键 API 做能力自检**(这是真正的挂载门槛,含 `fs.resolve` 返回形状检测与 subagent `agentOptions`/`toolFilter` capability 检测),不满足时打 warning。preset 挂载失败时先看 DSH 日志里的自检 warning。
103
103
  - **升级路径**:DSH 升级后无需重装本包;升级本包用 `dsh plugin update dsh-vibe-math`,重启 DSH 后安装器会自动把 preset 更新到新版本(见上文「安装」说明)。
package/installer.js CHANGED
@@ -113,7 +113,9 @@ async function checkHostCapabilities(ctx, logger) {
113
113
  // 2) capability self-check (the authoritative mounting gate; also covers hosts whose version
114
114
  // could not be read). subagents / agents / tools / commands / fs shapes + v4 capabilities.
115
115
  const checks = [
116
- ['subagents', ['startContinuable', 'followup', 'interrupt']],
116
+ // subagents 服务的续做/唤醒方法是 sendMessage(sender, targetId, content, {signal});
117
+ // followup 不是 subagents 服务的方法(它只是 Agent 对象方法)。同时探测两者,能用一个即可。
118
+ ['subagents', ['startContinuable', 'interrupt']],
117
119
  ['agents', ['roots']],
118
120
  ['tools', ['register']],
119
121
  ['commands', ['register']],
@@ -128,6 +130,10 @@ async function checkHostCapabilities(ctx, logger) {
128
130
  for (let j = 0; j < methods.length; j++) {
129
131
  if (typeof s[methods[j]] !== 'function') problems.push(svc + '.' + methods[j] + ' 不可用(宿主版本可能过旧)')
130
132
  }
133
+ // subagents continuation (wake) API: sendMessage (modern) OR followup (legacy) must exist.
134
+ if (svc === 'subagents' && typeof s.sendMessage !== 'function' && typeof s.followup !== 'function') {
135
+ problems.push('subagents 缺少续做/唤醒方法(需 sendMessage 或 followup 至少其一)')
136
+ }
131
137
  }
132
138
  // fs API shape: DSH 0.1.1 起 resolve 返回 {targetKey, displayPath} 对象(旧版返回字符串路径)
133
139
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-vibe-math",
3
3
  "description": "Multi-agent mathematical problem-solving & verification frameworks for DeepSeek Harness — THREE agent presets in one install: vibe-math-v2 (probability-driven: qs.json + Propos knowledge base + explorer→solver→review/debate verdict), vibe-math-v3 (THIRD-generation, recommended: paper-style Markdown knowledge base with Problems/Progress/Propos/Methods/Verified + planner-agent scheduling that decides the next N actions + universal theory/method invention library + agents write their own Markdown directly via a per-file write lock), and vibe-math-v4 (FOURTH-generation: persistent self-organizing resident subagents that message & meet to decide all tasks, verify only by unanimous consensus, /compact at a context threshold, and stop only when all agree the problem is solved). Installing this bundle auto-installs all three presets (v1 was removed at v2.0.0).",
4
- "version": "2.0.7",
4
+ "version": "2.0.9",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -444,7 +444,19 @@ export function apply(ctx) {
444
444
  scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1
445
445
  await saveAll(); return started.childId
446
446
  }
447
- async function followupChild(childId, promptText) { await subagents.followup(rootAgent, childId, [textBlock(promptText)], { source: { kind: 'user' }, signal: makeSignal(30000) }); scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1; await saveAll() }
447
+ // DSH continuable-wake API is subagents.sendMessage(sender, targetId, content, {signal}); subagents.followup
448
+ // does NOT exist on the subagents service (it is only Agent.followup). Calling the missing method threw
449
+ // TypeError and made every wake fail silently. Prefer sendMessage, fall back to a legacy followup.
450
+ async function followupChild(childId, promptText) {
451
+ const blocks = [textBlock(promptText)]
452
+ try {
453
+ if (typeof subagents.sendMessage === 'function') await subagents.sendMessage(rootAgent, childId, blocks, { signal: makeSignal(30000) })
454
+ else if (typeof subagents.followup === 'function') await subagents.followup(rootAgent, childId, blocks, { source: { kind: 'user' }, signal: makeSignal(30000) })
455
+ else throw new Error('no subagent continuation API')
456
+ } catch (e) { console.error('vibe-math-v2: wake ' + childId + ' failed: ' + String((e && e.message) || e)); throw e }
457
+ scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1
458
+ await saveAll()
459
+ }
448
460
  async function interruptChild(childId) { try { subagents.interrupt(childId, { kind: 'ancestor', agent: rootAgent }) } catch (e) {} }
449
461
 
450
462
  // ================= prompts =================
@@ -888,7 +888,19 @@ export function apply(ctx) {
888
888
  await saveAll()
889
889
  return started.childId
890
890
  }
891
- async function followupChild(childId, promptText) { await subagents.followup(rootAgent, childId, [textBlock(promptText)], { source: { kind: 'user' }, signal: makeSignal(30000) }); scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1; await saveAll() }
891
+ // DSH continuable-wake API is subagents.sendMessage(sender, targetId, content, {signal}); subagents.followup
892
+ // does NOT exist on the subagents service (it is only Agent.followup). Calling the missing method threw
893
+ // TypeError and made every wake fail silently. Prefer sendMessage, fall back to a legacy followup.
894
+ async function followupChild(childId, promptText) {
895
+ const blocks = [textBlock(promptText)]
896
+ try {
897
+ if (typeof subagents.sendMessage === 'function') await subagents.sendMessage(rootAgent, childId, blocks, { signal: makeSignal(30000) })
898
+ else if (typeof subagents.followup === 'function') await subagents.followup(rootAgent, childId, blocks, { source: { kind: 'user' }, signal: makeSignal(30000) })
899
+ else throw new Error('no subagent continuation API')
900
+ } catch (e) { console.error('vibe-math-v3: wake ' + childId + ' failed: ' + String((e && e.message) || e)); throw e }
901
+ scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1
902
+ await saveAll()
903
+ }
892
904
  async function interruptChild(childId) { try { subagents.interrupt(childId, { kind: 'ancestor', agent: rootAgent }) } catch (e) {} }
893
905
 
894
906
  // ================= prompts =================
@@ -46,7 +46,7 @@ export function apply(ctx) {
46
46
  let residents = new Map(), mailboxes = new Map(), taskboard = [], decisions = []
47
47
  let meetings = [], reports = [], activityLog = []
48
48
  let problemText = '', problemId = 'problem', runId = 'run-' + shortId()
49
- let meetingState = null, verifyState = null, pendingVerify = null
49
+ let meetingState = null, verifyState = null, pendingVerify = null, pendingMeeting = null
50
50
  let busy = new Set(), wakeKind = new Map(), currentResident = ''
51
51
  let lastActivityAt = now(), lastProgressAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
52
52
  const activityLogCap = 200
@@ -280,7 +280,21 @@ export function apply(ctx) {
280
280
  prompt = coreRulesBrief() + '\n' + prompt
281
281
  r.needCompact = false
282
282
  }
283
- try { await subagents.followup(rootAgent,r.childId,[textBlock(prompt)],{source:{kind:'user'},signal:makeSignal(params.activityTimeoutMs||60000)}); return true }
283
+ // The DSH continuable-wake API is subagents.sendMessage(sender, targetId, content, {signal}),
284
+ // NOT subagents.followup (which is only Agent.followup, and does NOT exist on the subagents
285
+ // service). Using a non-existent method threw TypeError and made EVERY wake fail silently →
286
+ // the group went idle forever. Prefer sendMessage; fall back to a legacy followup if a host
287
+ // still exposes it (older deployments), so this works across versions.
288
+ try {
289
+ if(typeof subagents.sendMessage==='function'){
290
+ await subagents.sendMessage(rootAgent, r.childId, [textBlock(prompt)], {signal: makeSignal(params.activityTimeoutMs||60000)})
291
+ } else if(typeof subagents.followup==='function'){
292
+ await subagents.followup(rootAgent, r.childId, [textBlock(prompt)], {source:{kind:'user'},signal:makeSignal(params.activityTimeoutMs||60000)})
293
+ } else {
294
+ throw new Error('no subagent continuation API (need sendMessage or followup)')
295
+ }
296
+ return true
297
+ }
284
298
  catch(e){ console.error('vibe-v4 wake '+r.rId+' failed: '+String((e&&e.message)||e)); busy.delete(r.rId); return false }
285
299
  }
286
300
  function byChild(childId){ for(const [,r] of residents){ if(r.childId===childId) return r } return undefined }
@@ -353,6 +367,10 @@ export function apply(ctx) {
353
367
  // ---- meeting ----
354
368
  async function startMeeting(agenda,type,targetId){
355
369
  if(meetingState) return {ok:false,message:'meeting already in progress'}
370
+ // A meeting must NOT preempt an active or pending verification (unanimous-consensus is the
371
+ // group's truth-making step; preempting it would let every round resurface the same conflict).
372
+ // Wait instead of stealing the floor: park the request and resume it after the verify settles.
373
+ if(verifyState || pendingVerify){ pendingMeeting = { agenda, type:type||'general', targetId:targetId||null }; return {ok:true,deferred:true,during:'verify'} }
356
374
  clearHeartbeat()
357
375
  const ids=Array.from(residents.keys())
358
376
  // Rotate the per-meeting speaking order so the SAME resident isn't always the "first speaker
@@ -539,6 +557,9 @@ export function apply(ctx) {
539
557
  if(meetingState){ await continueMeetingRound(); return }
540
558
  if(verifyState){ await continueVerifyRound(); return }
541
559
  if(pendingVerify){ const pv=pendingVerify; await beginVerify(pv); return }
560
+ // A meeting requested while a verify held the floor is parked in pendingMeeting; once the
561
+ // verify has truly settled (no verifyState / pendingVerify), resume it before anything else.
562
+ if(pendingMeeting){ const pm=pendingMeeting; pendingMeeting=null; await startMeeting(pm.agenda, pm.type, pm.targetId); return }
542
563
  // mailbox delivery
543
564
  const delivered=await deliverNextMailbox(); if(delivered) return
544
565
  // maxParallel: don't start a new wake when the in-flight cap is reached
@@ -556,21 +577,34 @@ export function apply(ctx) {
556
577
  return
557
578
  }
558
579
  }
559
- // A) heartbeat / coordination: wake the least-recently-active resident after an idle timeout
560
- // to SELF-DRIVE (continue solving / message / propose task / meeting / verify). On a FAILED
561
- // wake we re-arm the heartbeat so a single follow-up error NEVER permanently stops the group
562
- // (a successful wake re-drives scheduleNext through its own onResidentEnd, which re-arms).
580
+ // A) heartbeat / coordination: wake IDLE residents after an idle timeout to SELF-DRIVE (continue
581
+ // solving / message / propose task / meeting / verify). This is a CONCURRENCY FILL, not a
582
+ // single nudge: scheduleNext should wake up to `maxParallel` idle residents in one pass so the
583
+ // group can progress in parallel (design §A: "同一时刻可唤醒多个空闲常驻,受 maxParallel 上限").
584
+ // On a FAILED wake we re-arm the heartbeat so a single follow-up error NEVER permanently stops
585
+ // the group (a successful wake re-drives scheduleNext through its own onResidentEnd, which re-arms).
563
586
  clearHeartbeat()
564
- let target=null, oldest=-1
565
- for(const [,r] of residents){ if(busy.has(r.rId)) continue; const idle=now()-r.lastActiveAt; if(idle>oldest){ oldest=idle; target=r } }
566
587
  const atOs=Number(params.activityTimeoutMs)||120000
567
- if(target && oldest>=atOs){
588
+ // `mp` (maxParallel) is already declared above in this function scope.
589
+ // Collect idle (not busy) residents sorted by idle time, oldest-first (round-robin fairness).
590
+ const idleCandidates = Array.from(residents.values())
591
+ .filter(r=>!busy.has(r.rId))
592
+ .sort((a,b)=>(now()-b.lastActiveAt)-(now()-a.lastActiveAt))
593
+ // Fill the concurrency budget: keep waking the most-idle resident until either everyone idle is
594
+ // started OR the in-flight cap (maxParallel) is reached. This turns the previous "one at a time"
595
+ // serialization into genuine parallel progress.
596
+ let started=0
597
+ for(const r of idleCandidates){
598
+ const free = mp>0 ? (mp - busy.size) : Number.MAX_SAFE_INTEGER
599
+ if(free<=0) break // concurrency cap reached → stop filling
600
+ if((now()-r.lastActiveAt)<atOs) break // the remaining are all busy-or-not-idle-enough
568
601
  let ok=false
569
- try { ok = await wakeResident(target, await heartbeatPrompt(target), 'normal') } catch(e){ ok=false }
602
+ try { ok = await wakeResident(r, await heartbeatPrompt(r), 'normal') } catch(e){ ok=false }
603
+ if(ok) started++
570
604
  await saveAll()
571
- if(!ok) armHeartbeat() // wake failed re-arm so the group never permanently stops
572
- return
605
+ if(!ok) continue // a failed wake must NOT stop the fill; try the next idle resident
573
606
  }
607
+ if(started>0) { return } // at least one started; their onResidentEnd re-drives scheduleNext
574
608
  // everyone is busy or not idle-enough: arm a heartbeat to re-check later (no infinite spin)
575
609
  armHeartbeat()
576
610
  }
@@ -583,19 +617,36 @@ export function apply(ctx) {
583
617
  await writeText('Shared/meetings/brainstorm.md', lines.join('\n'))
584
618
  }
585
619
  async function deliverNextMailbox(){
620
+ // Deliver queued messages to ALL currently-idle recipients in one pass (parallel), bounded by the
621
+ // same maxParallel concurrency cap, so a group chat (relayToGroup → many non-busy recipients) is
622
+ // not serialized one-message-at-a-time. Returns true if anything was delivered. A busy recipient
623
+ // keeps its message queued (avoid starving others).
624
+ let delivered=false
586
625
  for(const [to,msgs] of mailboxes){
587
626
  if(msgs.length===0) continue
588
- const m=msgs.shift(); const r=residents.get(to); if(!r){ continue }
589
- if(!busy.has(to)){ currentResident=to; await wakeResident(r, (await normalPrompt(r))+'\n\n[MESSAGE from '+m.from+']\n'+m.content,'normal'); await saveAll(); return true }
590
- msgs.push(m); continue // recipient busy → keep the message queued, try another mailbox (avoid starving others)
627
+ const r=residents.get(to); if(!r){ continue }
628
+ if(busy.has(to)) continue // recipient busy leave the message queued for a later pass
629
+ const mp=Number(params.maxParallel)||0
630
+ if(mp>0 && busy.size>=mp) break // concurrency cap reached → stop delivering more now
631
+ const m=msgs.shift()
632
+ currentResident=to
633
+ const ok = await wakeResident(r, (await normalPrompt(r))+'\n\n[MESSAGE from '+m.from+']\n'+m.content,'normal')
634
+ await saveAll(); if(!ok) msgs.unshift(m); delivered=delivered||ok
591
635
  }
592
- return false
636
+ return delivered
593
637
  }
594
638
 
595
639
  // ---- resident end handler ----
596
640
  async function onResidentEnd(childId, info){
597
641
  const r=byChild(childId); if(!r) return
598
642
  busy.delete(r.rId)
643
+ // Any resident turn that COMPLETED is real activity for the stall clock (B). Residents frequently
644
+ // write their libraries via direct fs (not the record* tools), so relying only on
645
+ // bumpArtifacts/markProgress would leave lastProgressAt stale and B would fire against an active
646
+ // team. We count only a clean 'completed' turn: an error/max-tokens/refusal did NOT meaningfully
647
+ // advance the work, so it must NOT mask a truly stalled group (B can then convene a recovery
648
+ // meeting). A completed turn also refreshes the meeting/verify deadlock clock through lastInputAt.
649
+ if(info && info.stopReason==='completed') markProgress()
599
650
  realCompact(r).catch(()=>{}) // best-effort real DSH /compact of this resident while idle
600
651
  const output=blocksToText(info&&info.lastAssistantMessage)
601
652
  const parsed=parseReply(output)
@@ -632,8 +683,13 @@ export function apply(ctx) {
632
683
  if(parsed.propose_task) await proposeTask(parsed.propose_task, parsed.task_desc||'', r.rId)
633
684
  if(parsed.claim_task) await claimTask(parsed.claim_task, r.rId)
634
685
  if(parsed.task_done) await taskDone(parsed.task_done, r.rId)
635
- // a resident may self-trigger a meeting (resident-driven coordination, closest to the philosophy)
636
- if(parsed.propose_meeting && !meetingState){ await startMeeting(String(parsed.propose_meeting),'general',null); await saveAll(); return }
686
+ // a resident may self-trigger a meeting (resident-driven coordination, closest to the philosophy).
687
+ // If a verify is holding the floor the meeting is deferred (pendingMeeting) and we fall through
688
+ // so the pending verify (or mailbox/heartbeat) still advances rather than being stuck behind it.
689
+ if(parsed.propose_meeting && !meetingState){
690
+ const mr=await startMeeting(String(parsed.propose_meeting),'general',null); await saveAll()
691
+ if(mr && !mr.deferred) return
692
+ }
637
693
  await saveAll(); await scheduleNext()
638
694
  }
639
695
 
@@ -88,7 +88,7 @@ flowchart TD
88
88
  ### 4.1 本质
89
89
  - 每个常驻 = 一个 DSH **continuable 子代理**:`subagents.startContinuable({ parent: 会话根代理, request, agentOptions, toolFilter })`。
90
90
  - **持久上下文**(continuable):跨多次唤醒记住前文,直到被 `/compact` 压缩。
91
- - **唤醒** = `subagents.followup(root, <childId>, [textBlock(prompt)])`;一轮收益 = 该常驻提交一次完整思考(经 `ctx.on('subagent/end')` 回归)。
91
+ - **唤醒** = `subagents.sendMessage(root, <childId>, [textBlock(prompt)], {signal})`(DSH 的 `subagents` 服务没有 `followup` 方法——`followup` 只是 `Agent` 对象的方法;`sendMessage` 才是服务暴露的续做 API。旧代码 `subagents.followup(...)` 在真实运行时抛 `TypeError`,导致每次唤醒都失败、小组整体停摆);一轮收益 = 该常驻提交一次完整思考(经 `ctx.on('subagent/end')` 回归,`stateOf`=settled 时 `watchSettlement` 每次 dispose 都会重新触发一次 `/end`)。
92
92
  - **工具**:`tools.register` 对会话内所有代理可用(处理器按 `exec.agent` 路由),故常驻可直接用 `fs` + `vibe_v4_*` 工具(写自己的库、读他人的库、发消息、提会议、记命题、发起验证)。
93
93
 
94
94
  ### 4.2 身份与上下文
@@ -117,7 +117,7 @@ VibeMath/Projects/<project>/
117
117
  | 能力 | 机制 | 关键点 |
118
118
  |---|---|---|
119
119
  | **常驻生命周期** | start 时 brainstorm→spawn N(各带初始方向);add/remove;resume 重建 | 常驻上下文断点后需 re-spawn 并用其 `progress.md` 重种化 |
120
- | **消息总线** | `vibe_v4_send_message(to, content)` → 入目标邮件箱 → 若空闲则 `followup` 唤醒 | 常驻间不直接互调,全靠框架 relay(模拟收件箱);支持 `broadcast(all)` |
120
+ | **消息总线** | `vibe_v4_send_message(to, content)` → 入目标邮件箱 → 若空闲则 `sendMessage` 唤醒 | 常驻间不直接互调,全靠框架 relay(模拟收件箱);支持 `broadcast(all)` |
121
121
  | **会议** | `vibe_v4_meeting(agenda)` → 向全体发会议 prompt → 收齐发言 → 写 `Shared/meetings/<id>.md` → 广播结论 | 会议用于分工/方向/任务分配/提出验证/表决"是否已解决" |
122
122
  | **任务板** | 常驻在会议/留言提议任务 → 框架记 `Shared/taskboard.md`;认领后被唤醒 | 框架只搬运,不决定谁做什么 |
123
123
  | **产物沉淀** | `vibe_v4_publish_progress` / `record_proposition` / `record_method` / `record_subproblem` | **必填**:价值程度 / 动机用途计划 / 自身概率估计(框架校验,缺则提示) |
@@ -254,7 +254,7 @@ VibeMath/Projects/<project>/
254
254
  | DSH 能力 | 在本架构中的用途 |
255
255
  |---|---|
256
256
  | `subagents.startContinuable({parent, request, agentOptions, toolFilter})` | 产生常驻(continuable,持久上下文) |
257
- | `subagents.followup(root, childId, blocks)` | 唤醒常驻做一轮 / 投递一条留言 / 开会向某常驻收集发言 |
257
+ | `subagents.sendMessage(root, childId, blocks, {signal})` | 唤醒常驻做一轮 / 投递一条留言 / 开会向某常驻收集发言(**服务暴露的续做 API**;旧 `subagents.followup` 不存在) |
258
258
  | `ctx.on('subagent/end')` | 一常驻完成一轮(收其回复,续驱动) |
259
259
  | `subagents.interrupt / list` | 中断 / 枚举常驻 |
260
260
  | `tools.register`(会话内所有代理可用,`exec.agent` 路由) | 常驻可直接调用 `vibe_v4_*` 与 `fs` |
@@ -521,12 +521,15 @@ VibeMath/Projects/<project>/
521
521
 
522
522
  针对一次真实 run("跑完就停")的诊断:三名常驻第一轮就把问题归约到**同一个硬核引理**并一致给出 `solved=false`——这是**自然的难点停滞**,而非 bug。但框架的活性机制太弱且会"死":
523
523
  - 空闲后唯一驱动是心跳(`activityTimeoutMs` 唤醒一个常驻、CHECKPOINT 提示词偏向"是否要停止"),**缺乏推进力**;
524
- - **唤醒失败会永久停死**(`scheduleNext` 在心跳分支 `await wakeResident(...)` 后无条件 `return`,而 `wakeResident` 捕获异常后返回 false 但**不重新武装心跳**)→ 一旦某次 `subagents.followup` 抛异常,心跳不再武装,小组**永不再被唤醒**;
524
+ - **唤醒失败会永久停死**(`scheduleNext` 在心跳分支 `await wakeResident(...)` 后无条件 `return`,而 `wakeResident` 捕获异常后返回 false 但**不重新武装心跳**)→ 一旦某次唤醒抛异常,心跳不再武装,小组**永不再被唤醒**;
525
+ - **唤醒 API 用错**(历史根因):代码曾调 `subagents.followup(...)`,但 DSH 的 `subagents` 服务**没有** `followup`(那只是 `Agent` 对象的方法),真实运行时每次唤醒抛 `TypeError: is not a function` → 被 catch 后所有后续唤醒全部失败。**已改为** `subagents.sendMessage(root, childId, blocks, {signal})`(服务暴露的续做 API),并保留 `followup` 作为旧宿主兜底。
525
526
  - `session.json` 不持久化 `meetingState`(进行中会议只在进程内存),进程重启或会议唤醒失败都可能让会议悬停。
526
527
 
527
528
  ### A · 自驱动心跳(修复推进力 + 永不永久停死)
528
529
  - `heartbeatPrompt` 改为**自驱动**:不是"是否要停止",而是"**请继续解决这个问题**——读他人库、推进子问题/引理/方法、尝试路线;或向团队发消息(input)、提议任务(propose_task);确实已解决/无路可走才提议开会/声明 solved。默认立场是推进而非停在原地。" 心跳回复 schema 增加 `input`、`propose_task`、`contextPct`,使一轮心跳能产生群聊消息/任务从而带动后续工作。
530
+ - **并行填充**(修复"只有 r1、再只有 r2"串行):`scheduleNext` A 分支不再是"一次只唤醒一个最闲常驻",而是收集所有空闲常驻(按空闲时长从大到小)**一次尽量填满 `maxParallel` 并发预算**——每个 `wakeResident` 只 sendMessage 发送(立即返回,等各自 `subagent/end` 异步回归),因此一轮可同时唤醒多个常驻,真正并行推进。失败的唤醒只跳过该常驻、继续填满预算;唤醒失败则重新武装心跳。
529
531
  - **唤醒失败重新武装心跳**:`scheduleNext` 心跳分支、`continueMeetingRound`、`continueVerifyRound` 在 `wakeResident` 失败(返回 false)时调用 `armHeartbeat()`,保证**任何一次唤醒失败都不会让小组永久停住**(会稍后重试其它常驻)。
532
+ - **并发邮箱投递**:`deliverNextMailbox` 一次投递所有当前空闲收件人的消息(受 `maxParallel` 上限),避免 `relayToGroup` 广播到多个常驻时被"一次一个"串行化;忙碌收件人保留其消息队列稍后再投(避免饿死其它常驻)。
530
533
 
531
534
  ### B · 停滞自动同步会议(分级保活 B)
532
535
  - 新增 `lastProgressAt`(会话状态,随产物/会议/验证/任务/群聊/消息更新,`markProgress()`),并持久化到 `State/session.json`。
@@ -549,6 +552,9 @@ VibeMath/Projects/<project>/
549
552
  - 同时:**找不到空闲的未发言/未投票常驻时不再静默 `return`,改为 `armHeartbeat()`**(稍后重查),并在发现"没有空闲者"时也武装心跳,杜绝"一直等待一个永不空闲的常驻"造成的隐性挂起。
550
553
  - 分级保活 B 增加 `busy.size===0` 守卫:任何常驻正在工作时不触发,避免预抢占在途轮次。
551
554
 
555
+ ### 会议与验证互斥(不抢占)
556
+ - 并行填充常驻后,一个常驻可能"提议验证(propose_verify)"而另一个同时"提议开会(propose_meeting)"。**会议不得抢占验证**:`startMeeting` 在 `verifyState || pendingVerify` 时改为把会议请求**暂存**(`pendingMeeting`,仅记录 agenda/type/target)并返回 `{deferred:true}`;`scheduleNext` 在所有 meeting/verify/pending 都清空后才恢复该暂存会议。这样统一共识验证(全真/全假)作为"求真"环节不会被会议的协调讨论打断,验证做完后再开会开会协调下一步——两者互斥但都不丢失(暂存会议之后补开)。
557
+
552
558
  > 效果:一个卡死/坏掉的会议或验证最多阻塞 `activityTimeoutMs`×2 后自动释放,团队重新回到 A/B 分级保活,**不会永久停死**。仍符合"框架只促成、从不指派任务"(放弃只是终止一个无法推进的会议,把控制权交还团队的自组织循环)。
553
559
  >
554
560
  > 说明:无法对"某常驻真的拒绝/掉线"的情况达成全体一致时,看门狗会把该对象保留为"未定论/带概率",这是哲学上期望的诚实结果。`session.json` 仍不持久化 `meetingState`(进行中的会议不跨进程恢复),B 的停滞看门狗在重启后仍会触发,因此重启也能自愈。