dsh-vibe-math 2.0.8 → 2.0.10

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
 
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.8",
4
+ "version": "2.0.10",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -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
@@ -367,6 +367,10 @@ export function apply(ctx) {
367
367
  // ---- meeting ----
368
368
  async function startMeeting(agenda,type,targetId){
369
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'} }
370
374
  clearHeartbeat()
371
375
  const ids=Array.from(residents.keys())
372
376
  // Rotate the per-meeting speaking order so the SAME resident isn't always the "first speaker
@@ -415,7 +419,7 @@ export function apply(ctx) {
415
419
  const votes=Object.values(st.inputs).map(x=>x.voteSolved).filter(v=>typeof v==='boolean')
416
420
  const allSolved = allSpoke && votes.length>0 && votes.every(v=>v===true)
417
421
  logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
418
- if(allSolved){ running=false; autoDone=true; phase='done'; logActivity('stop','all residents agree: problem solved'); await saveAll(); return }
422
+ if(allSolved){ running=false; autoDone=true; phase='done'; clearHeartbeat(); logActivity('stop','all residents agree: problem solved'); await saveAll(); return }
419
423
  meetingState=null; wakeKind.clear(); await saveAll(); await scheduleNext()
420
424
  }
421
425
 
@@ -553,6 +557,9 @@ export function apply(ctx) {
553
557
  if(meetingState){ await continueMeetingRound(); return }
554
558
  if(verifyState){ await continueVerifyRound(); return }
555
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 }
556
563
  // mailbox delivery
557
564
  const delivered=await deliverNextMailbox(); if(delivered) return
558
565
  // maxParallel: don't start a new wake when the in-flight cap is reached
@@ -570,21 +577,34 @@ export function apply(ctx) {
570
577
  return
571
578
  }
572
579
  }
573
- // A) heartbeat / coordination: wake the least-recently-active resident after an idle timeout
574
- // to SELF-DRIVE (continue solving / message / propose task / meeting / verify). On a FAILED
575
- // wake we re-arm the heartbeat so a single follow-up error NEVER permanently stops the group
576
- // (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).
577
586
  clearHeartbeat()
578
- let target=null, oldest=-1
579
- for(const [,r] of residents){ if(busy.has(r.rId)) continue; const idle=now()-r.lastActiveAt; if(idle>oldest){ oldest=idle; target=r } }
580
587
  const atOs=Number(params.activityTimeoutMs)||120000
581
- 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
582
601
  let ok=false
583
- 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++
584
604
  await saveAll()
585
- if(!ok) armHeartbeat() // wake failed re-arm so the group never permanently stops
586
- return
605
+ if(!ok) continue // a failed wake must NOT stop the fill; try the next idle resident
587
606
  }
607
+ if(started>0) { return } // at least one started; their onResidentEnd re-drives scheduleNext
588
608
  // everyone is busy or not idle-enough: arm a heartbeat to re-check later (no infinite spin)
589
609
  armHeartbeat()
590
610
  }
@@ -597,13 +617,23 @@ export function apply(ctx) {
597
617
  await writeText('Shared/meetings/brainstorm.md', lines.join('\n'))
598
618
  }
599
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
600
625
  for(const [to,msgs] of mailboxes){
601
626
  if(msgs.length===0) continue
602
- const m=msgs.shift(); const r=residents.get(to); if(!r){ continue }
603
- 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 }
604
- 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
605
635
  }
606
- return false
636
+ return delivered
607
637
  }
608
638
 
609
639
  // ---- resident end handler ----
@@ -653,8 +683,13 @@ export function apply(ctx) {
653
683
  if(parsed.propose_task) await proposeTask(parsed.propose_task, parsed.task_desc||'', r.rId)
654
684
  if(parsed.claim_task) await claimTask(parsed.claim_task, r.rId)
655
685
  if(parsed.task_done) await taskDone(parsed.task_done, r.rId)
656
- // a resident may self-trigger a meeting (resident-driven coordination, closest to the philosophy)
657
- 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
+ }
658
693
  await saveAll(); await scheduleNext()
659
694
  }
660
695
 
@@ -670,6 +705,7 @@ export function apply(ctx) {
670
705
  running=true; autoDone=false; phase='brainstorm'
671
706
  await writeText('Problems/'+problemId+'.md','# 问题|'+problemId+'\n- ID: '+problemId+'\n- 类型: 问题\n- 状态: 求解中\n- 优先级: 1\n- 依赖: []\n\n## 陈述\n'+problemText+'\n')
672
707
  residents=new Map(); mailboxes=new Map(); taskboard=[]; decisions=[]; meetings=[]; reports=[]; verifyState=null; meetingState=null; pendingVerify=null; residentSeq=0; artifactCount=0; clearHeartbeat()
708
+ busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; lastSyncMeetingAt=0 // fresh run must NOT inherit stale concurrency/coordination state (busy/wakeKind/currentResident/pendingMeeting) from a previous run on the same reused session
673
709
  lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
674
710
  const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
675
711
  for(let i=0;i<params.residentCount;i++){ const r=newResident(dirs[i]||''); await spawnResident(r) }
@@ -527,7 +527,9 @@ VibeMath/Projects/<project>/
527
527
 
528
528
  ### A · 自驱动心跳(修复推进力 + 永不永久停死)
529
529
  - `heartbeatPrompt` 改为**自驱动**:不是"是否要停止",而是"**请继续解决这个问题**——读他人库、推进子问题/引理/方法、尝试路线;或向团队发消息(input)、提议任务(propose_task);确实已解决/无路可走才提议开会/声明 solved。默认立场是推进而非停在原地。" 心跳回复 schema 增加 `input`、`propose_task`、`contextPct`,使一轮心跳能产生群聊消息/任务从而带动后续工作。
530
+ - **并行填充**(修复"只有 r1、再只有 r2"串行):`scheduleNext` A 分支不再是"一次只唤醒一个最闲常驻",而是收集所有空闲常驻(按空闲时长从大到小)**一次尽量填满 `maxParallel` 并发预算**——每个 `wakeResident` 只 sendMessage 发送(立即返回,等各自 `subagent/end` 异步回归),因此一轮可同时唤醒多个常驻,真正并行推进。失败的唤醒只跳过该常驻、继续填满预算;唤醒失败则重新武装心跳。
530
531
  - **唤醒失败重新武装心跳**:`scheduleNext` 心跳分支、`continueMeetingRound`、`continueVerifyRound` 在 `wakeResident` 失败(返回 false)时调用 `armHeartbeat()`,保证**任何一次唤醒失败都不会让小组永久停住**(会稍后重试其它常驻)。
532
+ - **并发邮箱投递**:`deliverNextMailbox` 一次投递所有当前空闲收件人的消息(受 `maxParallel` 上限),避免 `relayToGroup` 广播到多个常驻时被"一次一个"串行化;忙碌收件人保留其消息队列稍后再投(避免饿死其它常驻)。
531
533
 
532
534
  ### B · 停滞自动同步会议(分级保活 B)
533
535
  - 新增 `lastProgressAt`(会话状态,随产物/会议/验证/任务/群聊/消息更新,`markProgress()`),并持久化到 `State/session.json`。
@@ -550,6 +552,9 @@ VibeMath/Projects/<project>/
550
552
  - 同时:**找不到空闲的未发言/未投票常驻时不再静默 `return`,改为 `armHeartbeat()`**(稍后重查),并在发现"没有空闲者"时也武装心跳,杜绝"一直等待一个永不空闲的常驻"造成的隐性挂起。
551
553
  - 分级保活 B 增加 `busy.size===0` 守卫:任何常驻正在工作时不触发,避免预抢占在途轮次。
552
554
 
555
+ ### 会议与验证互斥(不抢占)
556
+ - 并行填充常驻后,一个常驻可能"提议验证(propose_verify)"而另一个同时"提议开会(propose_meeting)"。**会议不得抢占验证**:`startMeeting` 在 `verifyState || pendingVerify` 时改为把会议请求**暂存**(`pendingMeeting`,仅记录 agenda/type/target)并返回 `{deferred:true}`;`scheduleNext` 在所有 meeting/verify/pending 都清空后才恢复该暂存会议。这样统一共识验证(全真/全假)作为"求真"环节不会被会议的协调讨论打断,验证做完后再开会开会协调下一步——两者互斥但都不丢失(暂存会议之后补开)。
557
+
553
558
  > 效果:一个卡死/坏掉的会议或验证最多阻塞 `activityTimeoutMs`×2 后自动释放,团队重新回到 A/B 分级保活,**不会永久停死**。仍符合"框架只促成、从不指派任务"(放弃只是终止一个无法推进的会议,把控制权交还团队的自组织循环)。
554
559
  >
555
560
  > 说明:无法对"某常驻真的拒绝/掉线"的情况达成全体一致时,看门狗会把该对象保留为"未定论/带概率",这是哲学上期望的诚实结果。`session.json` 仍不持久化 `meetingState`(进行中的会议不跨进程恢复),B 的停滞看门狗在重启后仍会触发,因此重启也能自愈。