dsh-vibe-math 2.0.20 → 2.0.21

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.20",
4
+ "version": "2.0.21",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -76,7 +76,12 @@ export function apply(ctx) {
76
76
  // deadlocked and abandon it. A meeting round's own signal window is activityTimeoutMs, so a
77
77
  // resident should speak within that; 2× that without ANY new input/verdict means the meeting/verify
78
78
  // is stuck and must not keep the whole group blocked.
79
- function recoverStallMs(){ return (Number(params.activityTimeoutMs)||120000) * 2 }
79
+ // Positive duration with a safe fallback: a NEGATIVE/NaN activityTimeoutMs or stallAutoMeetingMs
80
+ // (misconfigured via vibe_v4_set) would otherwise make recoverStallMs negative → every meeting/
81
+ // verify watchdog fires INSTANTLY (abandoning all consensus) and A-fill's idle window would never
82
+ // elapse (waking everyone every pass). Guard every duration read with this.
83
+ function posMs(v,def){ const n=Number(v); return (Number.isFinite(n)&&n>0)?n:(def||120000) }
84
+ function recoverStallMs(){ return posMs(params.activityTimeoutMs,120000) * 2 }
80
85
  function pickProvider(){ try { const n=subagents.list?subagents.list():[]; if(n.indexOf('spawn')!==-1) return 'spawn'; if(n.indexOf('fork')!==-1) return 'fork' } catch(e){} return 'spawn' }
81
86
  // Per-resident model/provider inheritance: when params.provider / params.model are set,
82
87
  // the resident uses that exact route; when left '' the resident inherits the parent's
@@ -91,11 +96,20 @@ export function apply(ctx) {
91
96
  if(allow.length===0 && deny.length===0) return undefined
92
97
  const f={}; if(allow.length) f.allow=allow; if(deny.length) f.deny=deny; return f
93
98
  }
94
- function makeSignal(ms){ return AbortSignal.timeout(ms||30000) }
99
+ function makeSignal(ms){ return AbortSignal.timeout(posMs(ms,30000)) }
95
100
  function workspaceRoot(){ try { if(rootAgent&&rootAgent.session&&rootAgent.session.header&&rootAgent.session.header.cwd) return rootAgent.session.header.cwd } catch(e){} if(sandboxPolicy&&sandboxPolicy.workspaceRoot) return sandboxPolicy.workspaceRoot; return '.' }
96
101
  function vibeRoot(){ return (workspaceRoot()+'/VibeMath').replace(/\\/g,'/') }
97
102
  function frameworkRoot(){ return vibeRoot()+'/Projects/'+currentProject }
98
103
  function slugify(s){ const t=String(s==null?'':s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g,'-').replace(/^-+|-+$/g,''); return t||'project' }
104
+ // Object ids (verify targets, recorded cards) become FILE NAMES and DIRECTORY PATHS
105
+ // (Verified/命题/<id>.md, Shared/debates/<id>.md, Propos/<r>/<id>.md, source-card scans).
106
+ // A hostile/sloppy id containing path separators ('../../x') or Windows-forbidden chars would
107
+ // escape the project tree. Keep every harmless character (incl. Chinese) and replace only
108
+ // separators/control chars; strip leading/trailing dots/dashes so the name is never '.'/'..'.
109
+ function idSafe(s){
110
+ const t=String(s==null?'':s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g,'-').replace(/-{2,}/g,'-').replace(/^[.\-]+|[.\-]+$/g,'')
111
+ return t||'id'
112
+ }
99
113
  function getPolicy(){ try { if(sandboxPolicy&&rootAgent&&rootAgent.session) return sandboxPolicy.resolve({session:rootAgent.session}) } catch(e){} try { if(sandboxPolicy) return sandboxPolicy.resolve({}) } catch(e){} return undefined }
100
114
  function psQuote(p){ return "'"+String(p).replace(/'/g,"''")+"'" }
101
115
  async function runShell(script,cwd){ if(subprocess===undefined) return {ok:false,error:'no-subprocess'}; try { const h=subprocess.spawn({argv:['powershell','-NoProfile','-NonInteractive','-Command',script],cwd:cwd||workspaceRoot(),stdio:{stdin:'ignore',stdout:'inherit',stderr:'inherit'},graceMs:20000}); const o=await h.done; return {ok:o.exitCode===0,exitCode:o.exitCode} } catch(e){ return {ok:false,error:String((e&&e.message)||e)} } }
@@ -322,9 +336,9 @@ export function apply(ctx) {
322
336
 
323
337
  // ---- artifact writers (resident-facing) ----
324
338
  async function publishProgress(rId,content){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const rel='Progress/'+rId+'/progress.md'; const prev=(await readText(rel))||''; await writeText(rel, prev+'\n### '+fmtTime()+'|'+rId+'\n'+String(content||'')+'\n'); return {ok:true} }
325
- async function recordProposition(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id||('p-'+shortId()); const lines=['# 命题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: 命题','- 状态: 未定论','- 概率: '+cl(o.prob!=null?o.prob:0.5),'- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 证明尝试','','## 证伪尝试','']; await writeText('Propos/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 命题 '+id); bumpArtifacts(); return {ok:true,id,file:'Propos/'+rId+'/'+id+'.md'} }
326
- async function recordMethod(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id||('m-'+shortId()); const lines=['# 方法|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: '+(o.type||'方法'),'- 状态: 经验','- 可信断言: []','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'','## 核心内容',String(o.content||''),'','## 定义与记号',String(o.notation||''),'','## 应用记录','## 改进历史','']; await writeText('Methods/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 方法 '+id); bumpArtifacts(); return {ok:true,id,file:'Methods/'+rId+'/'+id+'.md'} }
327
- async function recordSubproblem(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id||('s-'+shortId()); const lines=['# 子问题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 状态: 求解中','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 进度','']; await writeText('Subproblems/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 子问题 '+id); bumpArtifacts(); return {ok:true,id,file:'Subproblems/'+rId+'/'+id+'.md'} }
339
+ async function recordProposition(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('p-'+shortId()); const lines=['# 命题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: 命题','- 状态: 未定论','- 概率: '+cl(o.prob!=null?o.prob:0.5),'- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 证明尝试','','## 证伪尝试','']; await writeText('Propos/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 命题 '+id); bumpArtifacts(); return {ok:true,id,file:'Propos/'+rId+'/'+id+'.md'} }
340
+ async function recordMethod(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('m-'+shortId()); const lines=['# 方法|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: '+(o.type||'方法'),'- 状态: 经验','- 可信断言: []','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'','## 核心内容',String(o.content||''),'','## 定义与记号',String(o.notation||''),'','## 应用记录','## 改进历史','']; await writeText('Methods/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 方法 '+id); bumpArtifacts(); return {ok:true,id,file:'Methods/'+rId+'/'+id+'.md'} }
341
+ async function recordSubproblem(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('s-'+shortId()); const lines=['# 子问题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 状态: 求解中','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 进度','']; await writeText('Subproblems/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 子问题 '+id); bumpArtifacts(); return {ok:true,id,file:'Subproblems/'+rId+'/'+id+'.md'} }
328
342
  // auto-sync meeting: every meetingKeepEvery new artifacts, convene a general coordination meeting
329
343
  function bumpArtifacts(){ artifactCount+=1; markProgress(); if(!meetingState && !verifyState && !pendingMeeting && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
330
344
  function listResidents(){ return Array.from(residents.values()).map(r=>({id:r.rId,direction:r.direction,status:r.status,rounds:r.rounds,contextPct:r.contextPct,insight:r.insight?r.insight.slice(0,80):''})) }
@@ -370,9 +384,9 @@ export function apply(ctx) {
370
384
  }
371
385
  const mb=mailboxes.get(to)||[]; mb.push({from,at:now(),content}); mailboxes.set(to,mb); await saveAll(); logActivity('message',from+'→'+to+' (queued)'); return {ok:true}
372
386
  }
373
- async function broadcast(content){
387
+ async function broadcast(content, from){
374
388
  let n=0
375
- for(const [,r] of residents){ const res=await postMessage('facilitator',r.rId,content); if(res&&res.ok) n++ }
389
+ for(const [,r] of residents){ if(from && r.rId===from) continue; const res=await postMessage(from||'facilitator',r.rId,content); if(res&&res.ok) n++ }
376
390
  logActivity('broadcast','to '+n+' resident(s)'); await saveAll(); return {ok:true,message:'broadcast to '+n+' resident(s)'}
377
391
  }
378
392
  // group conversation relay: when a resident "speaks" (input in its round), forward its
@@ -460,7 +474,13 @@ export function apply(ctx) {
460
474
  const votes=Object.values(st.inputs).map(x=>x.voteSolved).filter(v=>typeof v==='boolean')
461
475
  const allSolved = allSpoke && votes.length>0 && votes.every(v=>v===true)
462
476
  logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
463
- if(allSolved){ running=false; autoDone=true; phase='done'; clearHeartbeat(); logActivity('stop','all residents agree: problem solved'); await saveAll(); return }
477
+ if(allSolved){
478
+ running=false; autoDone=true; phase='done'; clearHeartbeat()
479
+ // symmetric with initAbort: the STOP path must also release the coordination state, else
480
+ // status keeps reporting a phantom in-progress meeting (and stale wake kinds) forever.
481
+ meetingState=null; wakeKind.clear(); pendingMeeting=null; verifyState=null; pendingVerify=[]
482
+ logActivity('stop','all residents agree: problem solved'); await saveAll(); return
483
+ }
464
484
  meetingState=null; wakeKind.clear(); await saveAll()
465
485
  doSchedule=true
466
486
  } finally { finalizeLock=null } // release BEFORE scheduling so a chained verify/meeting is not swallowed
@@ -557,8 +577,8 @@ export function apply(ctx) {
557
577
  // duplicates collapse to one entry. A resident who genuinely extends the object later can still
558
578
  // re-propose after the dedup window (recoverStallMs) has passed.
559
579
  function maybeQueueVerify(target, proposer){
560
- const t=String(target||'').trim()
561
- if(!t) return false
580
+ const t=idSafe(target) // sanitize BEFORE it becomes file names / dedup keys / status output
581
+ if(!t || t==='id') return false
562
582
  const last=verifiedRecently.get(t)
563
583
  if(last!==undefined && (now()-last) < recoverStallMs()){
564
584
  logActivity('verify',t+' re-propose ignored (just verified at '+fmtTime(last)+')')
@@ -663,8 +683,8 @@ export function apply(ctx) {
663
683
  function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
664
684
  function armHeartbeat(){
665
685
  clearHeartbeat()
666
- const ms=Number(params.activityTimeoutMs)||120000
667
- if(!(ms>0) || typeof ctx.timeout!=='function') return
686
+ const ms=posMs(params.activityTimeoutMs,120000)
687
+ if(typeof ctx.timeout!=='function') return
668
688
  heartbeatDisposer=ctx.timeout(()=>{ heartbeatDisposer=null; scheduleNext().catch(()=>{}) }, ms)
669
689
  }
670
690
  // Real DSH /compact of a resident's OWN session via ctx.compaction (if the host provides it);
@@ -708,7 +728,7 @@ export function apply(ctx) {
708
728
  // meeting/verify/pending work is active AND no resident is currently working (so it never
709
729
  // preempts an in-flight round).
710
730
  if(phase==='active' && !meetingState && !verifyState && pendingVerify.length===0 && busy.size===0){
711
- const stallMs=Number(params.stallAutoMeetingMs)||((Number(params.activityTimeoutMs)||120000)*3)
731
+ const stallMs=posMs(params.stallAutoMeetingMs, posMs(params.activityTimeoutMs,120000)*3)
712
732
  if(now()-lastProgressAt>=stallMs){
713
733
  await startMeeting('团队较长时间没有新进展。请你们自行讨论:当前问题是否已解决、开放难点是什么、谁负责哪部分、下一步如何推进,并自主决定是否继续。框架只负责转达与记录,不替你们决定。','general',null)
714
734
  return
@@ -721,7 +741,7 @@ export function apply(ctx) {
721
741
  // On a FAILED wake we re-arm the heartbeat so a single follow-up error NEVER permanently stops
722
742
  // the group (a successful wake re-drives scheduleNext through its own onResidentEnd, which re-arms).
723
743
  clearHeartbeat()
724
- const atOs=Number(params.activityTimeoutMs)||120000
744
+ const atOs=posMs(params.activityTimeoutMs,120000)
725
745
  // `mp` (maxParallel) is already declared above in this function scope.
726
746
  // Collect idle (not busy) residents sorted by idle time, oldest-first (round-robin fairness).
727
747
  const idleCandidates = Array.from(residents.values())
@@ -764,7 +784,7 @@ export function apply(ctx) {
764
784
  let delivered=false
765
785
  for(const [to,msgs] of mailboxes){
766
786
  if(msgs.length===0) continue
767
- const r=residents.get(to); if(!r){ continue }
787
+ const r=residents.get(to); if(!r){ mailboxes.delete(to); continue } // stale recipient → drop the entry
768
788
  if(busy.has(to)) continue // recipient busy → leave the message queued for a later pass
769
789
  const mp=Number(params.maxParallel)||0
770
790
  if(mp>0 && busy.size>=mp) break // concurrency cap reached → stop delivering more now
@@ -856,6 +876,7 @@ export function apply(ctx) {
856
876
  if(!problemText) return {ok:false,message:'problem text required (pass problem, or use vibe_v4_configure first)'}
857
877
  problemId=slugify(problemText.slice(0,40))||'problem'
858
878
  if(residentCount) params.residentCount=Number(residentCount)||4
879
+ if(!(Number(params.residentCount)>=1)) params.residentCount=DEFAULT_PARAMS.residentCount // a 0/negative count (settings misconfig) would spawn nobody & idle forever
859
880
  running=true; autoDone=false; phase='brainstorm'
860
881
  await writeText('Problems/'+problemId+'.md','# 问题|'+problemId+'\n- ID: '+problemId+'\n- 类型: 问题\n- 状态: 求解中\n- 优先级: 1\n- 依赖: []\n\n## 陈述\n'+problemText+'\n')
861
882
  // A reused session may still have OLD residents in flight from a previous run (start is a FRESH
@@ -925,7 +946,12 @@ export function apply(ctx) {
925
946
  pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
926
947
  parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
927
948
  meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
928
- async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r)
949
+ async function addMember(direction){
950
+ // Adding a member starts a REAL resident turn (spawnResident → brainstorm) — refuse unless the
951
+ // run is live: on a concluded (autoDone) or never-started/paused run the new member would work
952
+ // with nobody to coordinate (zombie work on a project the group already declared done).
953
+ if(!running || autoDone) return {ok:false,message:'no active run to join (start or resume first)'}
954
+ const r=newResident(direction||''); await spawnResident(r)
929
955
  // Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
930
956
  // residents) can never be true for the new member (not in the snapshot order) and the meeting is
931
957
  // only ever released by the stuck watchdog instead of finalizing with everyone's input.
@@ -994,6 +1020,9 @@ export function apply(ctx) {
994
1020
  onResidentEnd, start, resume, status, report, addMember, removeMember, setParams,
995
1021
  setPause, initAbort, postMessage, startMeeting, saveAll, broadcast, configure, loadSettings,
996
1022
  currentResident:()=>currentResident,
1023
+ // safety kick: drive one scheduler pass (used when an end handler errored, so an exceptional
1024
+ // turn can never leave the group with no end-event and no heartbeat to continue it)
1025
+ nudge:()=>scheduleNext().catch(()=>{}),
997
1026
  residentIdOf:(agent)=>{ const m=residentOfAgent(agent); if(m) return m; const c=currentResident; return (c && residents.has(c)) ? c : '' },
998
1027
  useResident:(id)=>{ currentResident=id },
999
1028
  publishProgress, recordProposition, recordMethod, recordSubproblem, listResidents, reportContext,
@@ -1034,7 +1063,7 @@ export function apply(ctx) {
1034
1063
  registerTool('vibe_v4_set','Set V4 parameters. model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line.',objParams({residentCount:{type:'integer'},compactAfterRounds:{type:'integer'},compactThreshold:{type:'integer'},meetingKeepEvery:{type:'integer'},maxParallel:{type:'integer'},activityTimeoutMs:{type:'integer'},verdictMaxRounds:{type:'integer'},stallAutoMeetingMs:{type:'integer'},provider:{type:'string'},model:{type:'string'},residentPersona:{type:'string'},toolAllow:{type:'array',items:{type:'string'}},toolDeny:{type:'array',items:{type:'string'}}}),(s,a)=>{ s.setParams(a); return {ok:true} })
1035
1064
  // resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
1036
1065
  // fall back to the last-woken resident when called by the host/assistant.
1037
- registerTool('vibe_v4_send_message','(resident) Send a message to another resident.',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a,x)=>s.postMessage(s.residentIdOf(x),a.to,a.content))
1066
+ registerTool('vibe_v4_send_message','(resident) Send a message to another resident (to=all broadcasts to the whole team).',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a,x)=>{ const from=s.residentIdOf(x); if(!from) return {ok:false,message:'no such resident'}; if(String(a.to)==='all') return s.broadcast(a.content, from); return s.postMessage(from,a.to,a.content) })
1038
1067
  registerTool('vibe_v4_publish_progress','(resident) Append to your own progress markdown.',objParams({content:{type:'string'}},['content']),(s,a,x)=>s.publishProgress(s.residentIdOf(x),a.content))
1039
1068
  registerTool('vibe_v4_record_proposition','(resident) Record a proposition to your library.',objParams({id:{type:'string'},title:{type:'string'},statement:{type:'string'},prob:{type:'number'},value:{type:'number'},motivation:{type:'string'}}),(s,a,x)=>s.recordProposition(s.residentIdOf(x),a))
1040
1069
  registerTool('vibe_v4_record_method','(resident) Record a method/theory to your library.',objParams({id:{type:'string'},title:{type:'string'},type:{type:'string'},content:{type:'string'},notation:{type:'string'},value:{type:'number'},motivation:{type:'string'}}),(s,a,x)=>s.recordMethod(s.residentIdOf(x),a))
@@ -1077,6 +1106,6 @@ export function apply(ctx) {
1077
1106
 
1078
1107
  ctx.on('subagent/end', function(info){
1079
1108
  const sid=childOwner.get(info.id); const s=sid!==undefined?sessions.get(sid):undefined
1080
- if(s) s.onResidentEnd(info.id, info).catch(e=>console.error('vibe-v4 end: '+String((e&&e.stack)||e)))
1109
+ if(s) s.onResidentEnd(info.id, info).catch(e=>{ console.error('vibe-v4 end: '+String((e&&e.stack)||e)); if(s.nudge) s.nudge() })
1081
1110
  })
1082
1111
  }
@@ -570,7 +570,23 @@ VibeMath/Projects/<project>/
570
570
  >
571
571
  > 说明:无法对"某常驻真的拒绝/掉线"的情况达成全体一致时,看门狗会把该对象保留为"未定论/带概率",这是哲学上期望的诚实结果。`session.json` 仍不持久化 `meetingState`(进行中的会议不跨进程恢复),B 的停滞看门狗在重启后仍会触发,因此重启也能自愈。
572
572
  >
573
- > 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 107/107(T1–T37,后续审计回归见 §27/§28/§29)、v3 100/100、v2 business 18/regression 14、multisession 25/25。
573
+ > 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 120/120(T1–T41,后续审计回归见 §27–§30)、v3 100/100、v2 business 18/regression 14、multisession 25/25。
574
+
575
+ ## 30. 第六/七轮全面深度审计(npm v2.0.21,迭代至无新发现)
576
+
577
+ 按"不断检查、修复直至没有新问题"的要求连续多轮审计:本轮修复 6 处 + 2 处加固(新增回归 T38–T41),其后两轮全量通读与一致性扫描**未再发现新问题**,审计收敛。
578
+
579
+ - **全票停会后协调态残留**:`finalizeMeeting` 的 allSolved 分支直接 return,留下 `meetingState`/`wakeKind`/pending 态 → status 永久谎报 `meetingInProgress:true`(v2.0.20 修了 initAbort 却漏了 stop 路径)。修复:stop 分支与 initAbort 对称清空全部协调态。**T38**。
580
+ - **已定论/未运行 run 上 addMember 仍开工**:`addMember` → `spawnResident` 会真的启动一轮头脑风暴——在已全票定论的项目上制造僵尸工作。修复:`!running || autoDone` 时拒绝。**T38**。
581
+ - **验证目标/记录 id 路径穿越**:`propose_verify` 或 record 工具传入含 `/`、`\`、`:` 等的 id 时,Verified 卡/辩论文档/来源回写路径可逃出项目树或写入他人目录。修复:`idSafe` 消毒(保留含中文在内的无害字符,仅替换分隔符/非法字符/控制字符,去首尾点与横线),在 `maybeQueueVerify`(唯一入队漏斗)与三个 record 工具处统一应用。**T39**。
582
+ - **resident 工具 `to=all` 声称支持实际报错**:toolList 宣称 `vibe_v4_send_message {to:all}` 可广播,但 resident 版直接 `residents.get('all')` → no such resident。修复:resident 调用 `to=all` 走 `broadcast(content, from)`,且**跳过自己**(host 广播不受影响)。**T40**。
583
+ - **负值/NaN 时长参数灾难**:`vibe_v4_set {activityTimeoutMs:-50}` 会让 `recoverStallMs()` 为负 → 会议/验证看门狗**立即放弃一切共识**;A-fill 空闲窗口永不流逝(每 pass 唤醒全员)。修复:`posMs(v,def)` 兜底所有时长读取(recoverStallMs、makeSignal、armHeartbeat、A-fill atOs、B 停滞阈值)。**T41**。
584
+ - **residentCount<=0 启动空转**:settings 误配 0/负值 → start 不生成任何常驻却 running=true。修复:start 时回落默认值。
585
+ - **加固**:`deliverNextMailbox` 顺带清理已不存在常驻的陈旧邮箱条目;`subagent/end` 处理异常时(理论上所有 IO 均已内捕,此为最后防线)调用会话 `nudge()` 补一针调度,杜绝"异常回合后无 end、无心跳"的静默停摆。
586
+
587
+ > 审计收敛声明:此后两轮对全文件(1111 行)逐函数通读 + 交叉扫描,仅剩以下**已记录设计边界**(均有意识保留、非缺陷):brainstorm 首轮 end 永不触发无看门狗(框架外故障);会议/验证"末位在途成员"不被心跳打断(避免误杀慢成员,真实 DSH 回合必有 end);暂停中显式 `postMessage` 仍投递(人的明确指令);默认工具全继承(T11 钉住,宿主级工具未写入 resident toolList);邮箱在超长共识链后送达(§27);`State` 与进程内共识态不跨进程持久化(§26 说明,重启后 A/B 保活自愈)。
588
+ >
589
+ > 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 120/120(T1–T41)、v3 100/100、v2 business 18/regression 14、multisession 25/25、selfdrive-v3 0 异常。
574
590
 
575
591
  ## 29. 第五轮全面深度审计(npm v2.0.20,遗留区域 + 未修改逻辑)
576
592