dsh-vibe-math 2.0.16 → 2.0.17

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/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.16",
4
+ "version": "2.0.17",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -405,17 +405,10 @@ export function apply(ctx) {
405
405
  const ids=Array.from(residents.keys()); const allSpoke=ids.every(id=>st.inputs[id]!==undefined)
406
406
  if(allSpoke){ await finalizeMeeting(); return }
407
407
  // only wake IDLE un-spoken residents (rotated order); in-flight ones re-trigger this on end.
408
+ // NOTE: we deliberately do NOT flush mailboxes here — drafting an un-spoken resident into a normal
409
+ // mail round would delay the meeting and can starve the consensus past its watchdog if the mail
410
+ // backlog is large. Mail is delivered on scheduleNext passes when no consensus is in progress.
408
411
  const order=st.order||ids
409
- // Bounded mailbox flush BETWEEN meeting rounds: if an idle resident has queued messages, let it
410
- // read/answer them before being drafted into the meeting, otherwise a long chain of meetings &
411
- // verifies can starve the mailboxes for many minutes (test9: 33 msgs sat undelivered >12 min).
412
- // The mail recipient completes as a normal round; its onResidentEnd re-enters scheduleNext → the
413
- // meeting continues (watchdog still bounds it). Only flush when there IS an idle un-spoken target,
414
- // so we never burn the meeting's own turn budget on unrelated mail.
415
- if(order.find(x=>st.inputs[x]===undefined && !busy.has(x))){
416
- const flushed=await deliverNextMailbox()
417
- if(flushed){ await saveAll(); armHeartbeat(); return }
418
- }
419
412
  const id=order.find(x=>st.inputs[x]===undefined && !busy.has(x))
420
413
  if(!id){ armHeartbeat(); return } // no idle un-spoken resident (a busy/hung one): re-check later
421
414
  const r=residents.get(id)
@@ -425,6 +418,7 @@ export function apply(ctx) {
425
418
  async function finalizeMeeting(){
426
419
  if(finalizeLock) return // reentry guard: two onResidentEnd may both see allSpoke → only finalize once
427
420
  finalizeLock='meeting'
421
+ let doSchedule=false
428
422
  try {
429
423
  const st=meetingState
430
424
  const ids=Array.from(residents.keys()); const allSpoke=ids.length>0 && ids.every(id=>st.inputs[id]!==undefined)
@@ -443,13 +437,27 @@ export function apply(ctx) {
443
437
  const allSolved = allSpoke && votes.length>0 && votes.every(v=>v===true)
444
438
  logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
445
439
  if(allSolved){ running=false; autoDone=true; phase='done'; clearHeartbeat(); logActivity('stop','all residents agree: problem solved'); await saveAll(); return }
446
- meetingState=null; wakeKind.clear(); await saveAll(); await scheduleNext()
447
- } finally { finalizeLock=null }
440
+ meetingState=null; wakeKind.clear(); await saveAll()
441
+ doSchedule=true
442
+ } finally { finalizeLock=null } // release BEFORE scheduling so a chained verify/meeting is not swallowed
443
+ if(doSchedule) await scheduleNext()
448
444
  }
449
445
 
450
446
  // ---- verification (unanimous) ----
451
447
  async function beginVerify(pv){
452
448
  clearHeartbeat()
449
+ // Re-check dedup at ACTUAL start, not just at propose time: a resident may propose object X while
450
+ // X is already being verified (it does not know). That proposal sits in pendingVerify; when the
451
+ // current X verify closes, beginVerify would run X end-to-end a SECOND time (test9: p-r3-04 was
452
+ // verified twice back-to-back). Drop it if X was closed within the dedup window.
453
+ const tgt=pv&&pv.targetId?String(pv.targetId):''
454
+ if(tgt){
455
+ const last=verifiedRecently.get(tgt)
456
+ if(last!==undefined && (now()-last) < recoverStallMs()){
457
+ logActivity('verify',tgt+' queued verify dropped at start (just verified at '+fmtTime(last)+')')
458
+ pendingVerify=null; await saveAll(); await scheduleNext(); return
459
+ }
460
+ }
453
461
  pendingVerify=null
454
462
  verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},history:{},transcript:[],at:now(),lastVerdictAt:now()}
455
463
  markProgress();
@@ -468,11 +476,9 @@ export function apply(ctx) {
468
476
  }
469
477
  const ids=Array.from(residents.keys()); const allVoted=ids.every(id=>vs.verdicts[id]!==undefined)
470
478
  if(allVoted){ await finalizeVerify(); return }
471
- // Bounded mailbox flush BETWEEN verify rounds (same rationale as the meeting flush above).
472
- if(ids.find(x=>vs.verdicts[x]===undefined && !busy.has(x))){
473
- const flushed=await deliverNextMailbox()
474
- if(flushed){ await saveAll(); armHeartbeat(); return }
475
- }
479
+ // NOTE: we deliberately do NOT flush mailboxes here drafting an un-voted resident into a normal
480
+ // mail round would delay its verdict and can starve the verify past its watchdog when the backlog
481
+ // is large. Mail is delivered on scheduleNext passes when no consensus is in progress.
476
482
  const id=ids.find(x=>vs.verdicts[x]===undefined && !busy.has(x))
477
483
  if(!id){ armHeartbeat(); return } // no idle un-voted resident (a busy/hung one): re-check later
478
484
  const r=residents.get(id)
@@ -482,6 +488,7 @@ export function apply(ctx) {
482
488
  async function finalizeVerify(){
483
489
  if(finalizeLock) return // reentry guard (two onResidentEnd may both see allVoted)
484
490
  finalizeLock='verify'
491
+ let doSchedule=false
485
492
  try {
486
493
  const vs=verifyState; const expected=Array.from(residents.keys()).length
487
494
  const allVoted = expected>0 && Object.keys(vs.verdicts).length>=expected
@@ -489,20 +496,23 @@ export function apply(ctx) {
489
496
  // verdict is a PURE 0-1 probability; only ALL=1 (true) or ALL=0 (false) is a binary verdict.
490
497
  const allTrue = allVoted && vals.every(x=>Number(x.prob)===1)
491
498
  const allFalse = allVoted && vals.every(x=>Number(x.prob)===0)
492
- if(allTrue||allFalse){ await closeVerify(vs,allTrue); return }
493
- if(vs.round+1<params.verdictMaxRounds){
499
+ if(allTrue||allFalse){ await closeVerify(vs,allTrue); doSchedule=true }
500
+ else if(vs.round+1<params.verdictMaxRounds){
494
501
  // Move to a REAL debate round: snapshot the current votes into history (so the next round's
495
502
  // prompt can show others' previous stances), then CLEAR verdicts so every resident is asked to
496
503
  // give a fresh independent judgement after seeing the debate. Without the clear, allVoted stays
497
504
  // true and the debate rounds burn through with NOBODY being re-asked (a silent no-op).
498
505
  vs.history=Object.assign({}, vs.verdicts); vs.verdicts={}
499
506
  vs.lastVerdictAt=now() // fresh deadlock window for the re-vote round
500
- vs.stage='debate'; vs.round+=1; vs.asked=[]; logActivity('verify',vs.targetId+' round '+vs.round+' → debate (re-vote after seeing others)'); await saveAll(); await scheduleNext(); return
507
+ vs.stage='debate'; vs.round+=1; vs.asked=[]; logActivity('verify',vs.targetId+' round '+vs.round+' → debate (re-vote after seeing others)'); await saveAll(); doSchedule=true
508
+ }
509
+ else {
510
+ const avg=vals.length? vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length : 0.5
511
+ await writeDebateDoc(vs,false,avg); await rewriteSourceProb(vs.targetId, avg, vs.targetOwner); logActivity('verify',vs.targetId+' NOT unanimous → kept unverified (avg '+avg.toFixed(2)+')')
512
+ verifyState=null; wakeKind.clear(); await saveAll(); doSchedule=true
501
513
  }
502
- const avg=vals.length? vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length : 0.5
503
- await writeDebateDoc(vs,false,avg); await rewriteSourceProb(vs.targetId, avg, vs.targetOwner); logActivity('verify',vs.targetId+' NOT unanimous → kept unverified (avg '+avg.toFixed(2)+')')
504
- verifyState=null; wakeKind.clear(); await saveAll(); await scheduleNext()
505
- } finally { finalizeLock=null }
514
+ } finally { finalizeLock=null } // release BEFORE scheduling (chained verifies must not be swallowed)
515
+ if(doSchedule) await scheduleNext()
506
516
  }
507
517
  async function closeVerify(vs,isTrue){
508
518
  await writeDebateDoc(vs,true,isTrue?1:0)
@@ -511,7 +521,9 @@ export function apply(ctx) {
511
521
  await rewriteSource(target,isTrue,vs.targetOwner)
512
522
  verifiedRecently.set(target, now()) // dedup: block an immediate re-proposal of the same object
513
523
  logActivity('verify',target+' → Verified ('+(isTrue?'真':'假')+') by unanimous consensus')
514
- verifyState=null; wakeKind.clear(); await saveAll(); await scheduleNext()
524
+ verifyState=null; wakeKind.clear(); await saveAll()
525
+ // scheduling is done by finalizeVerify AFTER it releases finalizeLock (so a chained verify is
526
+ // never swallowed by the still-held reentry lock)
515
527
  }
516
528
  // Queue a verify proposal UNLESS the same object was just verified (closed as 真/假). In parallel
517
529
  // self-organization several residents may independently propose the same target while a verify is
@@ -586,8 +598,8 @@ export function apply(ctx) {
586
598
  const esc=field.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')
587
599
  // one-per-line: `- 状态: ...\n` OR inline: `; - 状态: ...;` / `- 状态: ...; - 概率:`
588
600
  const re=new RegExp('(^|\\n|;\\s*)-\\s*'+esc+':[^;\\n]*','gm')
589
- if(re.test(text)) return text.replace(re,'$1- '+field+': '+newValue)
590
- return text
601
+ const replaced=text.replace(re,'$1- '+field+': '+newValue)
602
+ return replaced===text ? text : replaced
591
603
  }
592
604
  // non-unanimous verification: keep the object in its library but write back the
593
605
  // average probability (design §8: "留库附概率"), so the card reflects the consensus estimate.
@@ -849,7 +861,14 @@ export function apply(ctx) {
849
861
  verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
850
862
  pendingVerify: pendingVerify?pendingVerify.targetId:null,
851
863
  meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
852
- async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r); return {ok:true,id:r.rId,direction:r.direction} }
864
+ async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r)
865
+ // Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
866
+ // residents) can never be true for the new member (not in the snapshot order) and the meeting is
867
+ // only ever released by the stuck watchdog instead of finalizing with everyone's input.
868
+ if(meetingState){ if(!Array.isArray(meetingState.order)) meetingState.order=Array.from(residents.keys()); if(!meetingState.order.includes(r.rId)) meetingState.order.push(r.rId) }
869
+ // Mid-verify additions are automatically asked to vote (continueVerifyRound recomputes ids from
870
+ // the live residents map), so no extra handling is needed there.
871
+ return {ok:true,id:r.rId,direction:r.direction} }
853
872
  async function removeMember(id){ const r=residents.get(id); if(!r) return {ok:false}; if(r.childId){ try{ subagents.interrupt(r.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } residents.delete(id); busy.delete(id); mailboxes.delete(id); wakeKind.delete(id)
854
873
  // Reconcile in-progress coordination so a removed member cannot hang consensus or crash a round:
855
874
  // drop its meeting speech / verify verdict / deferred-meeting / pending-verify if it owned them, and
@@ -561,9 +561,10 @@ VibeMath/Projects/<project>/
561
561
 
562
562
  ### 实战审计修复(test9)
563
563
  - **源卡回写定位与格式兼容**:常驻用 fs 直写卡片时,文件名可能与卡内声明的 `- ID:` 不一致(如 `Propos/r-3/p-01.md` 声明 `ID: p-r3-01`),且元数据可能是单行 `; ` 分隔(`- ID: p-105; - 状态: 未定论; - 概率: 0.95; …`)。旧 `findSourceRel` 只按文件名匹配 → 找不到 → 回退到顶层 `Propos/<target>.md` 并对空串执行写回,产生 **0 字节垃圾文件**、真实源卡状态不更新;旧回写正则只匹配"每行一个字段"的格式,对单行 `; ` 分隔卡失效。修复:① 文件名快查后增加**按卡内 `- ID:` 扫描**各常驻库;② 找不到时**跳过写回并记日志**(不再造空文件);③ 回写正则兼容"行首字段"与"行内 `; ` 分隔字段"两种格式。
564
- - **会议/验证终结幂等(并发重入)**:两个常驻的 `onResidentEnd` 同时看到 `allSpoke/allVoted` 时可能**并发执行两次 finalizeMeeting/finalizeVerify**,重复 `meetings.push`/`logDecision`/`propose_task`/`stop`(test9:同一次会议在 decisions/session 里出现两次、生成两条相同任务、重复 stop)。修复:会话级 `finalizeLock` 重入锁(第一次执行期间再次进入直接返回)。
565
- - **重复验证同一对象去重**:并行自组织下多个常驻可能各自对同一对象 `propose_verify`,导致对象被端到端验证两次(test9:p-r3-04 被 Verified 两次)。修复:`verifiedRecently` 记录刚定论对象;在去重窗口(`recoverStallMs()`)内忽略对同一对象的再次提议(`maybeQueueVerify`);扩展后仍可在窗口外重新提议。
566
- - **邮箱被长会议/长验证链饿死**:`scheduleNext` 只在"无 meeting/verify/pending"时投递邮箱;若 verify→meeting→verify 链条不断,邮箱消息可能被无限推迟(test9:11:09–11:21 收尾期 33 条群聊消息全部积压未送达)。修复:会议/验证**每轮之间**做一次有界邮箱投递(存在空闲未发言/未投票者时先投递一批,收件人完成后经 onResidentEnd 重回会议/验证;看门狗仍兜底)。
564
+ - **会议/验证终结幂等(并发重入)**:两个常驻的 `onResidentEnd` 同时看到 `allSpoke/allVoted` 时可能**并发执行两次 finalizeMeeting/finalizeVerify**,重复 `meetings.push`/`logDecision`/`propose_task`/`stop`(test9:同一次会议在 decisions/session 里出现两次、生成两条相同任务、重复 stop)。修复:会话级 `finalizeLock` 重入锁(第一次执行期间再次进入直接返回),且**锁在调用 `scheduleNext()` 之前释放**——否则锁会跨 await 误吞紧随其后的链式验证/会议终结(牵连修正)。
565
+ - **重复验证同一对象去重**:并行自组织下多个常驻可能各自对同一对象 `propose_verify`,导致对象被端到端验证两次(test9:p-r3-04 被 Verified 两次)。修复:`verifiedRecently` 记录刚定论对象;在去重窗口(`recoverStallMs()`)内忽略对同一对象的再次提议(`maybeQueueVerify`);`beginVerify` 启动时**再查一次**去重窗口(丢弃"验证进行中排进 pendingVerify、前一个刚关闭"的重复请求);扩展后仍可在窗口外重新提议。
566
+ - **会议进行中加入成员**:新成员不在会议发言快照 `order` 中会导致 `allSpoke`(按当前成员集判定)永不成立,会议只能靠看门狗超时放弃。修复:`addMember` 在会议进行中把新成员**追加进 `order`**(验证进行中则自动被要求投票,无需处理)。
567
+ - **邮箱被长会议/长验证链饿死(记录为设计边界,未在会议轮内打断)**:`scheduleNext` 只在"无 meeting/verify/pending"时投递邮箱;verify→meeting→verify 长链可能让邮箱消息长时间未送达(test9 收尾期有积压)。曾尝试在会议/验证轮间投递,但会把未投票/未发言的常驻拉去做普通轮,令验证在看门狗窗口内等不到票而**误放弃**——故回退,改为**仅在无进行中共识时投递**(消息持久化不丢失,下次非共识调度/恢复时送达)。
567
568
 
568
569
  > 效果:一个卡死/坏掉的会议或验证最多阻塞 `activityTimeoutMs`×2 后自动释放,团队重新回到 A/B 分级保活,**不会永久停死**。仍符合"框架只促成、从不指派任务"(放弃只是终止一个无法推进的会议,把控制权交还团队的自组织循环)。
569
570
  >