dsh-vibe-math 2.0.16 → 2.0.18

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.18",
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, pendingMeeting = null
49
+ let meetingState = null, verifyState = null, pendingVerify = [], pendingMeeting = null // pendingVerify: FIFO queue (several residents may independently propose different objects before any verify runs — a single slot silently DROPPED all but the last proposal)
50
50
  let busy = new Set(), wakeKind = new Map(), currentResident = ''
51
51
  let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
52
52
  const verifiedRecently = new Map() // targetId -> timestamp when it was closed as Verified (dedup re-propose)
@@ -102,7 +102,22 @@ export function apply(ctx) {
102
102
  async function fsTarget(rel){ return await fs.resolve(rel,{cwd:frameworkRoot()}) }
103
103
  async function readText(rel){ try { const t=await fsTarget(rel); if(await fs.stat(t)===undefined) return undefined; return await fs.readText(t) } catch(e){ return undefined } }
104
104
  async function writeText(rel,content){ try { const t=await fsTarget(rel); await fs.writeText(t,content,undefined,undefined,getPolicy()); return true } catch(e){ return false } }
105
- async function writeJson(rel,obj){ return await writeText(rel,JSON.stringify(obj,null,2)) }
105
+ // State files (taskboard/residents/session/mailboxes/decisions) are written by MANY concurrent
106
+ // flows (parallel resident turns + end handlers + tools). Two near-simultaneous writers of the
107
+ // SAME file each stringified their snapshot BEFORE their fs.writeText landed, so the writer with
108
+ // the OLDER snapshot could land LAST and silently erase the other's entry (e.g. two residents
109
+ // proposing tasks in the same tick → one task vanished from taskboard.json until the next save).
110
+ // Fix: serialize writes PER FILE, and defer JSON.stringify until the write actually runs (so the
111
+ // snapshot always reflects the newest in-memory state at execution time — late writers win with
112
+ // the FULL state, never with a stale subset).
113
+ const jsonQueues = new Map() // rel -> tail promise (per-session file write chain)
114
+ function writeJson(rel,obj){
115
+ const key='j:'+rel
116
+ const prev=jsonQueues.get(key)||Promise.resolve(true)
117
+ const run=prev.catch(()=>{}).then(async ()=>{ try { const t=await fsTarget(rel); await fs.writeText(t,JSON.stringify(obj,null,2),undefined,undefined,getPolicy()); return true } catch(e){ return false } })
118
+ jsonQueues.set(key,run.catch(()=>{}))
119
+ return run
120
+ }
106
121
  async function readJson(rel){ const t=await readText(rel); if(t===undefined||t==='') return undefined; try { return JSON.parse(t) } catch(e){ return undefined } }
107
122
  async function ensureDirs(){ const base=frameworkRoot(); const dirs=['Problems','Progress','Propos','Methods','Subproblems','Shared/meetings','Shared/debates','Verified/命题','Verified/问题','Reliable','Notes','State']; return await runShell('New-Item -Force -ItemType Directory -Path '+[vibeRoot()+'/Projects'].concat(dirs.map(d=>base+'/'+d)).map(psQuote).join(',')+' | Out-Null') }
108
123
  async function readTextAbs(path){ try { const t=await fs.resolve(path); const s=await fs.stat(t); if(s===undefined) return undefined; return await fs.readText(t) } catch(e){ return undefined } }
@@ -373,13 +388,17 @@ export function apply(ctx) {
373
388
  // ---- meeting ----
374
389
  async function startMeeting(agenda,type,targetId){
375
390
  if(meetingState) return {ok:false,message:'meeting already in progress'}
376
- // A meeting must NOT preempt an active or pending verification (unanimous-consensus is the
377
- // group's truth-making step; preempting it would let every round resurface the same conflict).
378
- // Wait instead of stealing the floor: park the request and resume it after the verify settles.
379
- if(verifyState || pendingVerify){
380
- // Keep the FIRST deferred request (never overwrite an earlier one with a later agenda).
391
+ // Park-and-resume (never lose a coordination request, never create a zombie): while a
392
+ // verification holds the floor, while the group is still brainstorming (members are busy in
393
+ // their first rounds a meeting started there could not be serviced and the old code let its
394
+ // stall watchdog silently ABANDON it minutes later), or while the run is paused, the meeting
395
+ // request is parked in pendingMeeting (FIRST request wins) and starts as soon as the floor is
396
+ // free. A never-started session (no residents to talk) and a concluded run (autoDone) refuse
397
+ // instead — convening there previously created a meeting nobody could ever be woken into.
398
+ if(!running || autoDone || phase==='brainstorm' || verifyState || pendingVerify.length>0){
399
+ if(autoDone || (!running && residents.size===0)) return {ok:false,message:'run is not active (use vibe_v4_start or vibe_v4_resume first)'}
381
400
  if(!pendingMeeting) pendingMeeting = { agenda, type:type||'general', targetId:targetId||null }
382
- return {ok:true,deferred:true,during:'verify'}
401
+ return {ok:true,deferred:true,during: phase==='brainstorm'?'brainstorm':(!running?'paused':'verify')}
383
402
  }
384
403
  clearHeartbeat()
385
404
  const ids=Array.from(residents.keys())
@@ -405,17 +424,10 @@ export function apply(ctx) {
405
424
  const ids=Array.from(residents.keys()); const allSpoke=ids.every(id=>st.inputs[id]!==undefined)
406
425
  if(allSpoke){ await finalizeMeeting(); return }
407
426
  // only wake IDLE un-spoken residents (rotated order); in-flight ones re-trigger this on end.
427
+ // NOTE: we deliberately do NOT flush mailboxes here — drafting an un-spoken resident into a normal
428
+ // mail round would delay the meeting and can starve the consensus past its watchdog if the mail
429
+ // backlog is large. Mail is delivered on scheduleNext passes when no consensus is in progress.
408
430
  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
431
  const id=order.find(x=>st.inputs[x]===undefined && !busy.has(x))
420
432
  if(!id){ armHeartbeat(); return } // no idle un-spoken resident (a busy/hung one): re-check later
421
433
  const r=residents.get(id)
@@ -425,6 +437,7 @@ export function apply(ctx) {
425
437
  async function finalizeMeeting(){
426
438
  if(finalizeLock) return // reentry guard: two onResidentEnd may both see allSpoke → only finalize once
427
439
  finalizeLock='meeting'
440
+ let doSchedule=false
428
441
  try {
429
442
  const st=meetingState
430
443
  const ids=Array.from(residents.keys()); const allSpoke=ids.length>0 && ids.every(id=>st.inputs[id]!==undefined)
@@ -443,14 +456,28 @@ export function apply(ctx) {
443
456
  const allSolved = allSpoke && votes.length>0 && votes.every(v=>v===true)
444
457
  logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
445
458
  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 }
459
+ meetingState=null; wakeKind.clear(); await saveAll()
460
+ doSchedule=true
461
+ } finally { finalizeLock=null } // release BEFORE scheduling so a chained verify/meeting is not swallowed
462
+ if(doSchedule) await scheduleNext()
448
463
  }
449
464
 
450
465
  // ---- verification (unanimous) ----
451
466
  async function beginVerify(pv){
452
467
  clearHeartbeat()
453
- pendingVerify=null
468
+ // Re-check dedup at ACTUAL start, not just at propose time: a resident may propose object X while
469
+ // X is already being verified (it does not know). That proposal sits in the pendingVerify queue;
470
+ // when the current X verify closes, beginVerify would run X end-to-end a SECOND time (test9:
471
+ // p-r3-04 was verified twice back-to-back). Drop it if X was closed within the dedup window.
472
+ // (pv was already popped from the FIFO queue by scheduleNext — nothing else to clear here.)
473
+ const tgt=pv&&pv.targetId?String(pv.targetId):''
474
+ if(tgt){
475
+ const last=verifiedRecently.get(tgt)
476
+ if(last!==undefined && (now()-last) < recoverStallMs()){
477
+ logActivity('verify',tgt+' queued verify dropped at start (just verified at '+fmtTime(last)+')')
478
+ await saveAll(); await scheduleNext(); return
479
+ }
480
+ }
454
481
  verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},history:{},transcript:[],at:now(),lastVerdictAt:now()}
455
482
  markProgress();
456
483
  logActivity('verify','debate begin: '+pv.targetId+' ('+pv.targetType+')'); await saveAll(); await scheduleNext()
@@ -468,11 +495,9 @@ export function apply(ctx) {
468
495
  }
469
496
  const ids=Array.from(residents.keys()); const allVoted=ids.every(id=>vs.verdicts[id]!==undefined)
470
497
  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
- }
498
+ // NOTE: we deliberately do NOT flush mailboxes here drafting an un-voted resident into a normal
499
+ // mail round would delay its verdict and can starve the verify past its watchdog when the backlog
500
+ // is large. Mail is delivered on scheduleNext passes when no consensus is in progress.
476
501
  const id=ids.find(x=>vs.verdicts[x]===undefined && !busy.has(x))
477
502
  if(!id){ armHeartbeat(); return } // no idle un-voted resident (a busy/hung one): re-check later
478
503
  const r=residents.get(id)
@@ -482,6 +507,7 @@ export function apply(ctx) {
482
507
  async function finalizeVerify(){
483
508
  if(finalizeLock) return // reentry guard (two onResidentEnd may both see allVoted)
484
509
  finalizeLock='verify'
510
+ let doSchedule=false
485
511
  try {
486
512
  const vs=verifyState; const expected=Array.from(residents.keys()).length
487
513
  const allVoted = expected>0 && Object.keys(vs.verdicts).length>=expected
@@ -489,20 +515,23 @@ export function apply(ctx) {
489
515
  // verdict is a PURE 0-1 probability; only ALL=1 (true) or ALL=0 (false) is a binary verdict.
490
516
  const allTrue = allVoted && vals.every(x=>Number(x.prob)===1)
491
517
  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){
518
+ if(allTrue||allFalse){ await closeVerify(vs,allTrue); doSchedule=true }
519
+ else if(vs.round+1<params.verdictMaxRounds){
494
520
  // Move to a REAL debate round: snapshot the current votes into history (so the next round's
495
521
  // prompt can show others' previous stances), then CLEAR verdicts so every resident is asked to
496
522
  // give a fresh independent judgement after seeing the debate. Without the clear, allVoted stays
497
523
  // true and the debate rounds burn through with NOBODY being re-asked (a silent no-op).
498
524
  vs.history=Object.assign({}, vs.verdicts); vs.verdicts={}
499
525
  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
526
+ 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
501
527
  }
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 }
528
+ else {
529
+ const avg=vals.length? vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length : 0.5
530
+ await writeDebateDoc(vs,false,avg); await rewriteSourceProb(vs.targetId, avg, vs.targetOwner); logActivity('verify',vs.targetId+' NOT unanimous → kept unverified (avg '+avg.toFixed(2)+')')
531
+ verifyState=null; wakeKind.clear(); await saveAll(); doSchedule=true
532
+ }
533
+ } finally { finalizeLock=null } // release BEFORE scheduling (chained verifies must not be swallowed)
534
+ if(doSchedule) await scheduleNext()
506
535
  }
507
536
  async function closeVerify(vs,isTrue){
508
537
  await writeDebateDoc(vs,true,isTrue?1:0)
@@ -511,13 +540,17 @@ export function apply(ctx) {
511
540
  await rewriteSource(target,isTrue,vs.targetOwner)
512
541
  verifiedRecently.set(target, now()) // dedup: block an immediate re-proposal of the same object
513
542
  logActivity('verify',target+' → Verified ('+(isTrue?'真':'假')+') by unanimous consensus')
514
- verifyState=null; wakeKind.clear(); await saveAll(); await scheduleNext()
543
+ verifyState=null; wakeKind.clear(); await saveAll()
544
+ // scheduling is done by finalizeVerify AFTER it releases finalizeLock (so a chained verify is
545
+ // never swallowed by the still-held reentry lock)
515
546
  }
516
547
  // Queue a verify proposal UNLESS the same object was just verified (closed as 真/假). In parallel
517
- // self-organization several residents may independently propose the same target while a verify is
518
- // already settling; without the guard the object gets re-verified end-to-end a second time (test9:
519
- // p-r3-04 was Verified twice back-to-back). A resident who genuinely extends the object later can
520
- // still re-propose after the dedup window (recoverStallMs) has passed.
548
+ // self-organization several residents may independently propose targets while a verify is already
549
+ // settling sometimes the SAME object (test9: p-r3-04 was Verified twice back-to-back), sometimes
550
+ // DIFFERENT objects (e.g. a sync meeting where each member proposes its own target). pendingVerify
551
+ // is therefore a FIFO queue with per-target dedup: every distinct proposal is honored in order, and
552
+ // duplicates collapse to one entry. A resident who genuinely extends the object later can still
553
+ // re-propose after the dedup window (recoverStallMs) has passed.
521
554
  function maybeQueueVerify(target, proposer){
522
555
  const t=String(target||'').trim()
523
556
  if(!t) return false
@@ -526,7 +559,8 @@ export function apply(ctx) {
526
559
  logActivity('verify',t+' re-propose ignored (just verified at '+fmtTime(last)+')')
527
560
  return false
528
561
  }
529
- pendingVerify={targetId:t,targetType:guessTargetType(t),proposer:proposer||'',at:now()}
562
+ if(pendingVerify.some(p=>String(p.targetId)===t)) return true // already queued → keep ONE entry
563
+ pendingVerify.push({targetId:t,targetType:guessTargetType(t),proposer:proposer||'',at:now()})
530
564
  return true
531
565
  }
532
566
  async function writeDebateDoc(vs,done,val){
@@ -586,8 +620,8 @@ export function apply(ctx) {
586
620
  const esc=field.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')
587
621
  // one-per-line: `- 状态: ...\n` OR inline: `; - 状态: ...;` / `- 状态: ...; - 概率:`
588
622
  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
623
+ const replaced=text.replace(re,'$1- '+field+': '+newValue)
624
+ return replaced===text ? text : replaced
591
625
  }
592
626
  // non-unanimous verification: keep the object in its library but write back the
593
627
  // average probability (design §8: "留库附概率"), so the card reflects the consensus estimate.
@@ -654,9 +688,9 @@ export function apply(ctx) {
654
688
  if(phase==='brainstorm'){ await maybeFinishBrainstorm(); return }
655
689
  if(meetingState){ await continueMeetingRound(); return }
656
690
  if(verifyState){ await continueVerifyRound(); return }
657
- if(pendingVerify){ const pv=pendingVerify; await beginVerify(pv); return }
691
+ if(pendingVerify.length){ const pv=pendingVerify.shift(); await beginVerify(pv); return }
658
692
  // A meeting requested while a verify held the floor is parked in pendingMeeting; once the
659
- // verify has truly settled (no verifyState / pendingVerify), resume it before anything else.
693
+ // verify queue has truly drained (no verifyState / pendingVerify), resume it before anything else.
660
694
  if(pendingMeeting){ const pm=pendingMeeting; pendingMeeting=null; await startMeeting(pm.agenda, pm.type, pm.targetId); return }
661
695
  // mailbox delivery
662
696
  const delivered=await deliverNextMailbox(); if(delivered) return
@@ -668,7 +702,7 @@ export function apply(ctx) {
668
702
  // (framework convenes & records; residents decide — never assigns work). Only when no
669
703
  // meeting/verify/pending work is active AND no resident is currently working (so it never
670
704
  // preempts an in-flight round).
671
- if(phase==='active' && !meetingState && !verifyState && !pendingVerify && busy.size===0){
705
+ if(phase==='active' && !meetingState && !verifyState && pendingVerify.length===0 && busy.size===0){
672
706
  const stallMs=Number(params.stallAutoMeetingMs)||((Number(params.activityTimeoutMs)||120000)*3)
673
707
  if(now()-lastProgressAt>=stallMs){
674
708
  await startMeeting('团队较长时间没有新进展。请你们自行讨论:当前问题是否已解决、开放难点是什么、谁负责哪部分、下一步如何推进,并自主决定是否继续。框架只负责转达与记录,不替你们决定。','general',null)
@@ -757,21 +791,29 @@ export function apply(ctx) {
757
791
  meetingState.inputs[r.rId]={input:parsed.input||parsed.summary||'',voteSolved:typeof parsed.voteSolved==='boolean'?parsed.voteSolved:null,propose_verify:parsed.propose_verify||null,propose_task:parsed.propose_task||null,task_desc:parsed.task_desc||'',claim_task:parsed.claim_task||null}
758
792
  meetingState.lastInputAt=now()
759
793
  if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
760
- await saveAll(); await continueMeetingRound(); return
794
+ await saveAll()
795
+ // PAUSE/stop: record the in-flight input/verdict but do NOT start any NEW consensus wake —
796
+ // a paused run must stay paused (resume() refreshes the consensus clocks and re-drives).
797
+ if(!running || autoDone) return
798
+ await continueMeetingRound(); return
761
799
  }
762
800
  if((kind==='verif-ind'||kind==='verif-deb') && verifyState){
763
801
  const v=(parsed&&parsed.vote)||{}
764
802
  // verdict = 0-1 probability the object is TRUE (1=绝对真, 0=绝对假, 0.5=不确定);
765
- // also accept legacy 'TRUE'/'FALSE' strings.
803
+ // also accept legacy 'TRUE'/'FALSE' strings AND quoted numeric strings ("0.9"), which LLMs
804
+ // occasionally emit — without this a confident "0.9" was silently misread as 0.5 (uncertainty).
766
805
  let p
767
806
  if(typeof v.verdict==='number'){ p=clamp01(v.verdict) }
768
807
  else if(/^TRUE$/i.test(String(v.verdict))){ p=1 }
769
808
  else if(/^FALSE$/i.test(String(v.verdict))){ p=0 }
809
+ else if(typeof v.verdict==='string' && v.verdict.trim()!=='' && Number.isFinite(Number(v.verdict))){ p=clamp01(Number(v.verdict)) }
770
810
  else { p=clamp01(Number(v.confidence)) }
771
811
  // verdict is a PURE 0-1 probability (a degree); no binary TRUE/FALSE classification.
772
812
  verifyState.verdicts[r.rId]={prob:p,confidence:p,reason:String(v.reason||parsed.summary||'')}
773
813
  verifyState.lastVerdictAt=now()
774
- await saveAll(); await continueVerifyRound(); return
814
+ await saveAll()
815
+ if(!running || autoDone) return // pause: freeze (resume refreshes the clocks and re-drives)
816
+ await continueVerifyRound(); return
775
817
  }
776
818
  // normal turn
777
819
  if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
@@ -805,7 +847,11 @@ export function apply(ctx) {
805
847
  if(residentCount) params.residentCount=Number(residentCount)||4
806
848
  running=true; autoDone=false; phase='brainstorm'
807
849
  await writeText('Problems/'+problemId+'.md','# 问题|'+problemId+'\n- ID: '+problemId+'\n- 类型: 问题\n- 状态: 求解中\n- 优先级: 1\n- 依赖: []\n\n## 陈述\n'+problemText+'\n')
808
- residents=new Map(); mailboxes=new Map(); taskboard=[]; decisions=[]; meetings=[]; reports=[]; verifyState=null; meetingState=null; pendingVerify=null; residentSeq=0; artifactCount=0; clearHeartbeat()
850
+ // A reused session may still have OLD residents in flight from a previous run (start is a FRESH
851
+ // run that reuses the same r-1.. library paths). Interrupt them BEFORE resetting, otherwise their
852
+ // still-running turns keep writing into the same per-resident files the new run is about to use.
853
+ for(const [,or] of residents){ if(or.childId){ try{ subagents.interrupt(or.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } }
854
+ residents=new Map(); mailboxes=new Map(); taskboard=[]; decisions=[]; meetings=[]; reports=[]; verifyState=null; meetingState=null; pendingVerify=[]; residentSeq=0; artifactCount=0; clearHeartbeat()
809
855
  busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; lastSyncMeetingAt=0; finalizeLock=null; verifiedRecently.clear() // fresh run must NOT inherit stale concurrency/coordination state (busy/wakeKind/currentResident/pendingMeeting) from a previous run on the same reused session
810
856
  lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
811
857
  const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
@@ -814,6 +860,10 @@ export function apply(ctx) {
814
860
  }
815
861
  async function resume(){
816
862
  currentProject=await readCurrentProject(); await ensureDirs(); await loadAll(); await loadSettings()
863
+ // A run the group CONCLUDED (unanimous voteSolved → autoDone) must not be silently revived into
864
+ // a zombie that keeps waking residents with no consensus that it should still run. The group
865
+ // decided it is done; continuing means a NEW run (vibe_v4_start / vibe_v4_configure).
866
+ if(autoDone) return {ok:false,message:'This run already concluded (all residents agreed solved). Start a fresh run with vibe_v4_start (vibe_v4_configure a new problem first if needed).'}
817
867
  // After loadAll the residentSeq counter is still whatever THIS process had (0 on a fresh process),
818
868
  // but persisted residents may already be r-1..r-N. Sync it to the max existing id so a later
819
869
  // addMember never collides with an existing resident (it would silently overwrite it).
@@ -832,32 +882,51 @@ export function apply(ctx) {
832
882
  const needRespawn = Array.from(residents.values()).some(r=>!r.childId)
833
883
  if(needRespawn){
834
884
  for(const [,r] of residents){ r.childId=''; r.status='brainstorm'; r.insight=''; r.roundsSinceCompact=0 }
835
- busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; pendingVerify=null; verifyState=null; meetingState=null; finalizeLock=null; verifiedRecently.clear()
885
+ busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; pendingVerify=[]; verifyState=null; meetingState=null; finalizeLock=null; verifiedRecently.clear()
836
886
  }
837
887
  for(const [,r] of residents){ if(!r.childId){ await spawnResident(r) } }
838
888
  if(!running){ running=true; autoDone=false; if(phase==='idle') phase='active' }
839
889
  if(needRespawn && phase!=='brainstorm') phase='brainstorm' // let re-spawned residents re-bootstrap together
890
+ // A pause froze an in-progress meeting/verify with its watchdog clock still running: refresh the
891
+ // clocks so a resumed consensus gets a full fresh stall window instead of being abandoned the
892
+ // instant it is serviced again (a short pause must never silently kill a real discussion).
893
+ if(meetingState && meetingState.lastInputAt) meetingState.lastInputAt=now()
894
+ if(verifyState && verifyState.lastVerdictAt) verifyState.lastVerdictAt=now()
840
895
  logActivity('resume','restarted'+(crossProcess?' (cross-process: re-spawned)':needRespawn?' (re-spawned)':'')); await saveAll(); await scheduleNext(); return {ok:true,message:'resumed',project:currentProject}
841
896
  }
842
897
  function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
843
898
  residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
844
- meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify?pendingVerify.targetId:null,
899
+ meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
845
900
  params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
846
901
  function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
847
902
  residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
848
903
  meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+residents.size}:null,
849
904
  verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
850
- pendingVerify: pendingVerify?pendingVerify.targetId:null,
905
+ pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
851
906
  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} }
853
- 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)
907
+ async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r)
908
+ // Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
909
+ // residents) can never be true for the new member (not in the snapshot order) and the meeting is
910
+ // only ever released by the stuck watchdog instead of finalizing with everyone's input.
911
+ if(meetingState){ if(!Array.isArray(meetingState.order)) meetingState.order=Array.from(residents.keys()); if(!meetingState.order.includes(r.rId)) meetingState.order.push(r.rId) }
912
+ // Mid-verify additions are automatically asked to vote (continueVerifyRound recomputes ids from
913
+ // the live residents map), so no extra handling is needed there.
914
+ return {ok:true,id:r.rId,direction:r.direction} }
915
+ 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); if(currentResident===id) currentResident=''
854
916
  // Reconcile in-progress coordination so a removed member cannot hang consensus or crash a round:
855
- // drop its meeting speech / verify verdict / deferred-meeting / pending-verify if it owned them, and
917
+ // drop its meeting speech / verify verdict / pending-verify proposals if it owned them, and
856
918
  // prune it from the meeting's speaking order so the find() there never selects a ghost.
857
919
  if(meetingState){ delete meetingState.inputs[id]; meetingState.order=(meetingState.order||[]).filter(x=>x!==id) }
858
920
  if(verifyState){ delete verifyState.verdicts[id] }
859
- if(pendingVerify && pendingVerify.proposer===id) pendingVerify=null
860
- await saveAll(); return {ok:true} }
921
+ if(pendingVerify.length) pendingVerify = pendingVerify.filter(p=>p.proposer!==id)
922
+ await saveAll()
923
+ // Re-drive the scheduler right away. If the removed member was the ONLY turn in flight (e.g. the
924
+ // last unspoken meeting speaker / the last unvoted voter, interrupted mid-turn), NO subagent/end
925
+ // will ever arrive to trigger the next pass, and while a consensus is being serviced no heartbeat
926
+ // is armed either — without this kick the meeting/verify would freeze forever behind members that
927
+ // can already conclude. scheduleNext no-ops safely when the run is paused/stopped.
928
+ await scheduleNext()
929
+ return {ok:true} }
861
930
  // Normalize one parameter value to its intended type so a string from /v4 set or configure
862
931
  // becomes the right number/array. Keeps settings.json clean regardless of how it was set.
863
932
  function normalizeParam(k, v){
@@ -561,15 +561,31 @@ 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
  >
570
571
  > 说明:无法对"某常驻真的拒绝/掉线"的情况达成全体一致时,看门狗会把该对象保留为"未定论/带概率",这是哲学上期望的诚实结果。`session.json` 仍不持久化 `meetingState`(进行中的会议不跨进程恢复),B 的停滞看门狗在重启后仍会触发,因此重启也能自愈。
571
572
  >
572
- > 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 61/61(含 T20 源卡回写 / T21 终结重入 / T22 验证去重)、v3 100/100、v2 business 18/regression 14、multisession 25/25。
573
+ > 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 80/80(含 T20 源卡回写 / T21 终结重入 / T22-23 验证去重 / T24-28 后续审计回归,见 §27)、v3 100/100、v2 business 18/regression 14、multisession 25/25。
574
+
575
+ ## 27. 第三轮全面深度审计(npm v2.0.18,牵连对象/逻辑)
576
+
577
+ 对 v2.0.16/17 修复做连带对象与逻辑审计,又发现并修复 6 处真实缺陷(每处均有新回归 T24–T28 钉住):
578
+
579
+ - **验证提议单槽丢失(pendingVerify 改 FIFO 队列)**:旧实现 `pendingVerify` 是单槽,后到覆盖先到——一次同步会议里多位常驻**各自提议不同对象**(或并行普通轮同时提议)时,只有最后一个对象的验证会被执行,前面的提议**静默丢失**(正是"框架忠实转达"哲学最不能丢的一环)。修复:改为 FIFO 队列 + 队内同目标去重(`maybeQueueVerify` 去重窗口后入队;`scheduleNext` 出队头启动;`beginVerify` 的"启动时二次去重"保留;`removeMember` 过滤其名下在队提议;status/report 显示队头 + `pendingVerifyCount`)。**T25**:一次会议两个成员各提议一个对象 → 两个都完整验证到 Verified/。
580
+ - **removeMember 后不再驱动调度 → 共识永久冻结**:会议/验证在服务中不武装心跳,靠"常驻 end 事件"推进。若被移除的常驻恰是**唯一在途**的那一个(如最后一个未投票者/未发言者,interrupt 后不会再发 end),其余常驻明明已齐票/齐发言却没人触发 finalize——没有看门狗能救(看门狗也在 end 驱动的轮次里),**永久冻结**。修复:`removeMember` 清理后立即 `await scheduleNext()`(顺带把 `currentResident` 复位)。**T26**:验证中移除在途未投票者(mock 永不发 end)→ 验证在剩余成员间照常达成全票、正常定论。
581
+ - **会议在 brainstorm/暂停期"假启动"后被看门狗误杀**:会议在 brainstorm 阶段(成员都在首轮思考)或暂停期间无法被服务,但它的看门狗时钟在创建时就开始走;几分钟后真正轮到它时已被判定卡死而**放弃**(主持人/常驻的会议请求被静默吞掉)。修复:`startMeeting` 在这两种情形(以及验证占场、暂停)一律**暂存到 `pendingMeeting`**(首条优先、不覆盖),等 brainstorm 完成/验证清空/恢复后再真正开始(时钟从真正开始时算);从未启动(无成员)与已全票定论(autoDone)的运行则**明确拒绝**——旧代码在那里开会只会造出一个谁也唤不醒的僵尸会议。
582
+ - **autoDone 后 resume 把已结束的 run 悄悄复活**:已全票定论(`autoDone`)的 run 被 resume 后会把 autoDone 清掉、继续唤醒常驻工作——违反"只有全组一致认为已解决才停止"的哲学(僵尸复活)。修复:`resume()` 在 autoDone 时拒绝并提示改用 `vibe_v4_start`/`vibe_v4_configure`。**T24**。
583
+ - **暂停(Pause)不冻结共识;恢复后看门狗误杀进行中的会议/验证**:旧 setPause 只停 A/B 自驱动,进行中的会议/验证在常驻 end 后仍继续**新唤醒**(暂停不暂停);而暂停期间时钟照走,恢复后若暂停超过看门狗窗口,进行中的会议/验证会被**立即放弃**。修复:end 处理器在 `!running` 时记录在途发言/投票后**不再启动新唤醒**(真暂停);`resume()` 对仍在进行的 meeting/verify **刷新看门狗时钟**后继续。**T28**:暂停期间在途会议发言被记录但无新唤醒;恢复后会议照常收尾(转录文件写出)。
584
+ - **writeJson 并发丢写(同文件延迟序列化)**:taskboard/session/residents 等 State 文件被并行轮、end 处理器、工具并发写;两个近同时写同一文件的流程各自在 fs.writeText 落地**之前**序列化快照,**旧快照后落地会覆盖新快照**(同一瞬间两位常驻各自 proposeTask → 其中一条从 taskboard.json 消失,直到下次保存才自愈)。修复:`writeJson` 按文件串行化 + **延迟到真正执行时才 JSON.stringify**(执行时取最新内存态,后写者永远带全量状态,不会用旧子集覆盖)。
585
+ - **连带小修**:verdict 为**引号数字串**(`"0.9"`,LLM 常见)时旧解析落到 confidence 缺省 → 静默记成 0.5 不确定;现按数字解析。**T27**。
586
+ - **start() 前中断旧常驻**:同一会话复用启动新 run 时,旧 run 仍在途的常驻会继续往**同名** `r-1..` 库路径写文件,与新 run 的 r-1.. 互相污染;start 重置前先 interrupt 旧常驻。
587
+
588
+ > 设计边界复查确认(未改):① 会议/验证轮次唤醒**不额外受 maxParallel 限制**——共识需要全员发言/投票;每个调度 pass 只唤醒一个未发言/未投票者,由各 end 事件串行驱动(残留的普通轮可能造成短暂并发,但不会无限累积);② 非一致(平均概率留库)的对象可被后续提议**再次端到端验证**(去重窗口只挡"刚定论为真/假"的对象)——重复提议者是在不知情时提议的,重验一轮不算错;③ 验证中途 `addMember` 的新成员会被要求投票(v2.0.17 注释行为,保持);④ 已定论 run 的成果随时可在磁盘读取,resume 拒绝只针对"自动复活"。
573
589
 
574
590
 
575
591