dsh-vibe-math 2.0.17 → 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.17",
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())
@@ -447,18 +466,18 @@ export function apply(ctx) {
447
466
  async function beginVerify(pv){
448
467
  clearHeartbeat()
449
468
  // 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.
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.)
453
473
  const tgt=pv&&pv.targetId?String(pv.targetId):''
454
474
  if(tgt){
455
475
  const last=verifiedRecently.get(tgt)
456
476
  if(last!==undefined && (now()-last) < recoverStallMs()){
457
477
  logActivity('verify',tgt+' queued verify dropped at start (just verified at '+fmtTime(last)+')')
458
- pendingVerify=null; await saveAll(); await scheduleNext(); return
478
+ await saveAll(); await scheduleNext(); return
459
479
  }
460
480
  }
461
- pendingVerify=null
462
481
  verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},history:{},transcript:[],at:now(),lastVerdictAt:now()}
463
482
  markProgress();
464
483
  logActivity('verify','debate begin: '+pv.targetId+' ('+pv.targetType+')'); await saveAll(); await scheduleNext()
@@ -526,10 +545,12 @@ export function apply(ctx) {
526
545
  // never swallowed by the still-held reentry lock)
527
546
  }
528
547
  // Queue a verify proposal UNLESS the same object was just verified (closed as 真/假). In parallel
529
- // self-organization several residents may independently propose the same target while a verify is
530
- // already settling; without the guard the object gets re-verified end-to-end a second time (test9:
531
- // p-r3-04 was Verified twice back-to-back). A resident who genuinely extends the object later can
532
- // 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.
533
554
  function maybeQueueVerify(target, proposer){
534
555
  const t=String(target||'').trim()
535
556
  if(!t) return false
@@ -538,7 +559,8 @@ export function apply(ctx) {
538
559
  logActivity('verify',t+' re-propose ignored (just verified at '+fmtTime(last)+')')
539
560
  return false
540
561
  }
541
- 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()})
542
564
  return true
543
565
  }
544
566
  async function writeDebateDoc(vs,done,val){
@@ -666,9 +688,9 @@ export function apply(ctx) {
666
688
  if(phase==='brainstorm'){ await maybeFinishBrainstorm(); return }
667
689
  if(meetingState){ await continueMeetingRound(); return }
668
690
  if(verifyState){ await continueVerifyRound(); return }
669
- if(pendingVerify){ const pv=pendingVerify; await beginVerify(pv); return }
691
+ if(pendingVerify.length){ const pv=pendingVerify.shift(); await beginVerify(pv); return }
670
692
  // A meeting requested while a verify held the floor is parked in pendingMeeting; once the
671
- // 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.
672
694
  if(pendingMeeting){ const pm=pendingMeeting; pendingMeeting=null; await startMeeting(pm.agenda, pm.type, pm.targetId); return }
673
695
  // mailbox delivery
674
696
  const delivered=await deliverNextMailbox(); if(delivered) return
@@ -680,7 +702,7 @@ export function apply(ctx) {
680
702
  // (framework convenes & records; residents decide — never assigns work). Only when no
681
703
  // meeting/verify/pending work is active AND no resident is currently working (so it never
682
704
  // preempts an in-flight round).
683
- if(phase==='active' && !meetingState && !verifyState && !pendingVerify && busy.size===0){
705
+ if(phase==='active' && !meetingState && !verifyState && pendingVerify.length===0 && busy.size===0){
684
706
  const stallMs=Number(params.stallAutoMeetingMs)||((Number(params.activityTimeoutMs)||120000)*3)
685
707
  if(now()-lastProgressAt>=stallMs){
686
708
  await startMeeting('团队较长时间没有新进展。请你们自行讨论:当前问题是否已解决、开放难点是什么、谁负责哪部分、下一步如何推进,并自主决定是否继续。框架只负责转达与记录,不替你们决定。','general',null)
@@ -769,21 +791,29 @@ export function apply(ctx) {
769
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}
770
792
  meetingState.lastInputAt=now()
771
793
  if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
772
- 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
773
799
  }
774
800
  if((kind==='verif-ind'||kind==='verif-deb') && verifyState){
775
801
  const v=(parsed&&parsed.vote)||{}
776
802
  // verdict = 0-1 probability the object is TRUE (1=绝对真, 0=绝对假, 0.5=不确定);
777
- // 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).
778
805
  let p
779
806
  if(typeof v.verdict==='number'){ p=clamp01(v.verdict) }
780
807
  else if(/^TRUE$/i.test(String(v.verdict))){ p=1 }
781
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)) }
782
810
  else { p=clamp01(Number(v.confidence)) }
783
811
  // verdict is a PURE 0-1 probability (a degree); no binary TRUE/FALSE classification.
784
812
  verifyState.verdicts[r.rId]={prob:p,confidence:p,reason:String(v.reason||parsed.summary||'')}
785
813
  verifyState.lastVerdictAt=now()
786
- 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
787
817
  }
788
818
  // normal turn
789
819
  if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
@@ -817,7 +847,11 @@ export function apply(ctx) {
817
847
  if(residentCount) params.residentCount=Number(residentCount)||4
818
848
  running=true; autoDone=false; phase='brainstorm'
819
849
  await writeText('Problems/'+problemId+'.md','# 问题|'+problemId+'\n- ID: '+problemId+'\n- 类型: 问题\n- 状态: 求解中\n- 优先级: 1\n- 依赖: []\n\n## 陈述\n'+problemText+'\n')
820
- 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()
821
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
822
856
  lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
823
857
  const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
@@ -826,6 +860,10 @@ export function apply(ctx) {
826
860
  }
827
861
  async function resume(){
828
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).'}
829
867
  // After loadAll the residentSeq counter is still whatever THIS process had (0 on a fresh process),
830
868
  // but persisted residents may already be r-1..r-N. Sync it to the max existing id so a later
831
869
  // addMember never collides with an existing resident (it would silently overwrite it).
@@ -844,22 +882,27 @@ export function apply(ctx) {
844
882
  const needRespawn = Array.from(residents.values()).some(r=>!r.childId)
845
883
  if(needRespawn){
846
884
  for(const [,r] of residents){ r.childId=''; r.status='brainstorm'; r.insight=''; r.roundsSinceCompact=0 }
847
- 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()
848
886
  }
849
887
  for(const [,r] of residents){ if(!r.childId){ await spawnResident(r) } }
850
888
  if(!running){ running=true; autoDone=false; if(phase==='idle') phase='active' }
851
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()
852
895
  logActivity('resume','restarted'+(crossProcess?' (cross-process: re-spawned)':needRespawn?' (re-spawned)':'')); await saveAll(); await scheduleNext(); return {ok:true,message:'resumed',project:currentProject}
853
896
  }
854
897
  function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
855
898
  residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
856
- meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify?pendingVerify.targetId:null,
899
+ meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
857
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(', ') } }
858
901
  function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
859
902
  residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
860
903
  meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+residents.size}:null,
861
904
  verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
862
- pendingVerify: pendingVerify?pendingVerify.targetId:null,
905
+ pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
863
906
  meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
864
907
  async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r)
865
908
  // Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
@@ -869,14 +912,21 @@ export function apply(ctx) {
869
912
  // Mid-verify additions are automatically asked to vote (continueVerifyRound recomputes ids from
870
913
  // the live residents map), so no extra handling is needed there.
871
914
  return {ok:true,id:r.rId,direction:r.direction} }
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)
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=''
873
916
  // Reconcile in-progress coordination so a removed member cannot hang consensus or crash a round:
874
- // 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
875
918
  // prune it from the meeting's speaking order so the find() there never selects a ghost.
876
919
  if(meetingState){ delete meetingState.inputs[id]; meetingState.order=(meetingState.order||[]).filter(x=>x!==id) }
877
920
  if(verifyState){ delete verifyState.verdicts[id] }
878
- if(pendingVerify && pendingVerify.proposer===id) pendingVerify=null
879
- 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} }
880
930
  // Normalize one parameter value to its intended type so a string from /v4 set or configure
881
931
  // becomes the right number/array. Keeps settings.json clean regardless of how it was set.
882
932
  function normalizeParam(k, v){
@@ -570,7 +570,22 @@ VibeMath/Projects/<project>/
570
570
  >
571
571
  > 说明:无法对"某常驻真的拒绝/掉线"的情况达成全体一致时,看门狗会把该对象保留为"未定论/带概率",这是哲学上期望的诚实结果。`session.json` 仍不持久化 `meetingState`(进行中的会议不跨进程恢复),B 的停滞看门狗在重启后仍会触发,因此重启也能自愈。
572
572
  >
573
- > 全套测试仍全绿:`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 拒绝只针对"自动复活"。
574
589
 
575
590
 
576
591