dsh-vibe-math 2.0.17 → 2.0.19

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.19",
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 } }
@@ -218,7 +233,7 @@ export function apply(ctx) {
218
233
  +'\n团队成员:\n'+banner()+'\n'
219
234
  +'New items:\n'+ (await inboxText(r.rId)) +'\n'
220
235
  +'Reply with ONLY a JSON object:\n'
221
- +'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40}'
236
+ +'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40}'
222
237
  }
223
238
  function meetingPrompt(r, st){
224
239
  const prior=Object.entries(st.inputs).filter(([k])=>k!==r.rId).map(([k,iv])=>' ['+k+'] '+String(iv.input||iv.summary||'')).join('\n')
@@ -306,10 +321,10 @@ export function apply(ctx) {
306
321
  function byChild(childId){ for(const [,r] of residents){ if(r.childId===childId) return r } return undefined }
307
322
 
308
323
  // ---- artifact writers (resident-facing) ----
309
- async function publishProgress(rId,content){ 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} }
310
- async function recordProposition(rId,o){ 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'} }
311
- async function recordMethod(rId,o){ 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'} }
312
- async function recordSubproblem(rId,o){ 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'} }
324
+ 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'} }
313
328
  // auto-sync meeting: every meetingKeepEvery new artifacts, convene a general coordination meeting
314
329
  function bumpArtifacts(){ artifactCount+=1; markProgress(); if(!meetingState && !verifyState && !pendingMeeting && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
315
330
  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):''})) }
@@ -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){
@@ -631,7 +653,7 @@ export function apply(ctx) {
631
653
  return (params.residentPersona?params.residentPersona+'\n':'')
632
654
  +'Resident researcher '+r.rId+' — CHECKPOINT(团队空闲,请由你们继续自主推进)。当前项目尚未解决(除非你已确认)。团队在等待有人继续:请**继续解决这个问题**——读他人的库对齐、推进某个子问题/引理/方法、尝试一条路线;或向团队发消息(input)、提议任务(propose_task)让大家分工。若你确实认为问题已解决、或已彻底无路可走,才提议开会(propose_meeting)让团队表决/商量、或声明 solved=true。默认立场是:**请推进,而不是停在原地。**\n'
633
655
  +'Reply with ONLY a JSON object:\n'
634
- +'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","claim_task":"<id|null>","contextPct":40}'
656
+ +'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<id|null>","contextPct":40}'
635
657
  }
636
658
  function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
637
659
  function armHeartbeat(){
@@ -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)
@@ -752,7 +774,13 @@ export function apply(ctx) {
752
774
  // ---- resident end handler ----
753
775
  async function onResidentEnd(childId, info){
754
776
  const r=byChild(childId); if(!r) return
755
- busy.delete(r.rId)
777
+ // A turn that is NOT marked busy is a duplicate/stale end (the same subagent/end delivered twice,
778
+ // or an end for a turn already settled). Without this guard every side effect below — task
779
+ // proposal, group relay, verify queueing, meetings.push — would run a SECOND time (the
780
+ // duplicate-task/duplicate-stop class from test9 reappears whenever a host re-delivers an end).
781
+ // Every legitimate end corresponds to a busy turn: busy is added at spawn/wake and cleared only
782
+ // here, on wake failure, on removeMember, or on respawn (whose stale childIds no longer match).
783
+ if(!busy.delete(r.rId)) return
756
784
  // Any resident turn that COMPLETED is real activity for the stall clock (B). Residents frequently
757
785
  // write their libraries via direct fs (not the record* tools), so relying only on
758
786
  // bumpArtifacts/markProgress would leave lastProgressAt stale and B would fire against an active
@@ -769,21 +797,29 @@ export function apply(ctx) {
769
797
  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
798
  meetingState.lastInputAt=now()
771
799
  if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
772
- await saveAll(); await continueMeetingRound(); return
800
+ await saveAll()
801
+ // PAUSE/stop: record the in-flight input/verdict but do NOT start any NEW consensus wake —
802
+ // a paused run must stay paused (resume() refreshes the consensus clocks and re-drives).
803
+ if(!running || autoDone) return
804
+ await continueMeetingRound(); return
773
805
  }
774
806
  if((kind==='verif-ind'||kind==='verif-deb') && verifyState){
775
807
  const v=(parsed&&parsed.vote)||{}
776
808
  // verdict = 0-1 probability the object is TRUE (1=绝对真, 0=绝对假, 0.5=不确定);
777
- // also accept legacy 'TRUE'/'FALSE' strings.
809
+ // also accept legacy 'TRUE'/'FALSE' strings AND quoted numeric strings ("0.9"), which LLMs
810
+ // occasionally emit — without this a confident "0.9" was silently misread as 0.5 (uncertainty).
778
811
  let p
779
812
  if(typeof v.verdict==='number'){ p=clamp01(v.verdict) }
780
813
  else if(/^TRUE$/i.test(String(v.verdict))){ p=1 }
781
814
  else if(/^FALSE$/i.test(String(v.verdict))){ p=0 }
815
+ else if(typeof v.verdict==='string' && v.verdict.trim()!=='' && Number.isFinite(Number(v.verdict))){ p=clamp01(Number(v.verdict)) }
782
816
  else { p=clamp01(Number(v.confidence)) }
783
817
  // verdict is a PURE 0-1 probability (a degree); no binary TRUE/FALSE classification.
784
818
  verifyState.verdicts[r.rId]={prob:p,confidence:p,reason:String(v.reason||parsed.summary||'')}
785
819
  verifyState.lastVerdictAt=now()
786
- await saveAll(); await continueVerifyRound(); return
820
+ await saveAll()
821
+ if(!running || autoDone) return // pause: freeze (resume refreshes the clocks and re-drives)
822
+ await continueVerifyRound(); return
787
823
  }
788
824
  // normal turn
789
825
  if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
@@ -817,7 +853,11 @@ export function apply(ctx) {
817
853
  if(residentCount) params.residentCount=Number(residentCount)||4
818
854
  running=true; autoDone=false; phase='brainstorm'
819
855
  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()
856
+ // A reused session may still have OLD residents in flight from a previous run (start is a FRESH
857
+ // run that reuses the same r-1.. library paths). Interrupt them BEFORE resetting, otherwise their
858
+ // still-running turns keep writing into the same per-resident files the new run is about to use.
859
+ for(const [,or] of residents){ if(or.childId){ try{ subagents.interrupt(or.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } }
860
+ residents=new Map(); mailboxes=new Map(); taskboard=[]; decisions=[]; meetings=[]; reports=[]; verifyState=null; meetingState=null; pendingVerify=[]; residentSeq=0; artifactCount=0; clearHeartbeat()
821
861
  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
862
  lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
823
863
  const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
@@ -826,6 +866,10 @@ export function apply(ctx) {
826
866
  }
827
867
  async function resume(){
828
868
  currentProject=await readCurrentProject(); await ensureDirs(); await loadAll(); await loadSettings()
869
+ // A run the group CONCLUDED (unanimous voteSolved → autoDone) must not be silently revived into
870
+ // a zombie that keeps waking residents with no consensus that it should still run. The group
871
+ // decided it is done; continuing means a NEW run (vibe_v4_start / vibe_v4_configure).
872
+ 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
873
  // After loadAll the residentSeq counter is still whatever THIS process had (0 on a fresh process),
830
874
  // but persisted residents may already be r-1..r-N. Sync it to the max existing id so a later
831
875
  // addMember never collides with an existing resident (it would silently overwrite it).
@@ -844,22 +888,29 @@ export function apply(ctx) {
844
888
  const needRespawn = Array.from(residents.values()).some(r=>!r.childId)
845
889
  if(needRespawn){
846
890
  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()
891
+ busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; pendingVerify=[]; verifyState=null; meetingState=null; finalizeLock=null; verifiedRecently.clear()
848
892
  }
849
893
  for(const [,r] of residents){ if(!r.childId){ await spawnResident(r) } }
850
894
  if(!running){ running=true; autoDone=false; if(phase==='idle') phase='active' }
851
895
  if(needRespawn && phase!=='brainstorm') phase='brainstorm' // let re-spawned residents re-bootstrap together
896
+ // A pause froze an in-progress meeting/verify with its watchdog clock still running: refresh the
897
+ // clocks so a resumed consensus gets a full fresh stall window instead of being abandoned the
898
+ // instant it is serviced again (a short pause must never silently kill a real discussion).
899
+ if(meetingState && meetingState.lastInputAt) meetingState.lastInputAt=now()
900
+ if(verifyState && verifyState.lastVerdictAt) verifyState.lastVerdictAt=now()
852
901
  logActivity('resume','restarted'+(crossProcess?' (cross-process: re-spawned)':needRespawn?' (re-spawned)':'')); await saveAll(); await scheduleNext(); return {ok:true,message:'resumed',project:currentProject}
853
902
  }
854
903
  function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
855
904
  residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
856
- meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify?pendingVerify.targetId:null,
905
+ meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
906
+ parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
857
907
  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
908
  function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
859
909
  residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
860
910
  meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+residents.size}:null,
861
911
  verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
862
- pendingVerify: pendingVerify?pendingVerify.targetId:null,
912
+ pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
913
+ parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
863
914
  meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
864
915
  async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r)
865
916
  // Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
@@ -869,14 +920,24 @@ export function apply(ctx) {
869
920
  // Mid-verify additions are automatically asked to vote (continueVerifyRound recomputes ids from
870
921
  // the live residents map), so no extra handling is needed there.
871
922
  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)
923
+ 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
924
  // 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
875
- // prune it from the meeting's speaking order so the find() there never selects a ghost.
925
+ // drop its meeting speech / verify verdict and prune it from the meeting's speaking order so the
926
+ // find() there never selects a ghost. Its QUEUED verify proposals are deliberately KEPT: a
927
+ // proposal is a statement about an OBJECT the group can judge on its merits with its CURRENT
928
+ // members (allVoted recomputes over the live residents), and dropping the queue entry would also
929
+ // erase the intent of any OTHER member who independently proposed the same target (dedup keeps
930
+ // only the first entry, which may belong to the removed member).
876
931
  if(meetingState){ delete meetingState.inputs[id]; meetingState.order=(meetingState.order||[]).filter(x=>x!==id) }
877
932
  if(verifyState){ delete verifyState.verdicts[id] }
878
- if(pendingVerify && pendingVerify.proposer===id) pendingVerify=null
879
- await saveAll(); return {ok:true} }
933
+ await saveAll()
934
+ // Re-drive the scheduler right away. If the removed member was the ONLY turn in flight (e.g. the
935
+ // last unspoken meeting speaker / the last unvoted voter, interrupted mid-turn), NO subagent/end
936
+ // will ever arrive to trigger the next pass, and while a consensus is being serviced no heartbeat
937
+ // is armed either — without this kick the meeting/verify would freeze forever behind members that
938
+ // can already conclude. scheduleNext no-ops safely when the run is paused/stopped.
939
+ await scheduleNext()
940
+ return {ok:true} }
880
941
  // Normalize one parameter value to its intended type so a string from /v4 set or configure
881
942
  // becomes the right number/array. Keeps settings.json clean regardless of how it was set.
882
943
  function normalizeParam(k, v){
@@ -909,7 +970,7 @@ export function apply(ctx) {
909
970
  onResidentEnd, start, resume, status, report, addMember, removeMember, setParams,
910
971
  setPause, initAbort, postMessage, startMeeting, saveAll, broadcast, configure, loadSettings,
911
972
  currentResident:()=>currentResident,
912
- residentIdOf:(agent)=>{ const m=residentOfAgent(agent); return m||currentResident },
973
+ residentIdOf:(agent)=>{ const m=residentOfAgent(agent); if(m) return m; const c=currentResident; return (c && residents.has(c)) ? c : '' },
913
974
  useResident:(id)=>{ currentResident=id },
914
975
  publishProgress, recordProposition, recordMethod, recordSubproblem, listResidents, reportContext,
915
976
  proposeTask, claimTask, taskDone, listTasks,
@@ -570,7 +570,35 @@ 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` 94/94(T1–T32,后续审计回归见 §27/§28)、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 拒绝只针对"自动复活"。
589
+
590
+ ## 28. 第四轮全面深度审计(npm v2.0.19,对 v2.0.18 修复的牵连检查)
591
+
592
+ 对 v2.0.18 的修复(FIFO 验证队列、会议暂存、暂停门控、writeJson 串行化等)做连带对象复查,发现并修复 4 处(新增回归 T29–T32):
593
+
594
+ - **同回合重复 `subagent/end` 会把每个副作用跑两遍**:`onResidentEnd` 对"已结算回合的重复/迟到 end"无守卫——重复投递同一 end(宿主重放、桥接层抖动)会让 propose_task / 群聊转发 / 验证入队 / meetings.push **全部二次执行**(test9 的重复任务/重复 stop 一类问题在"重复 end"场景会原样复发)。修复:回合必须以 busy 标记在册为前提——`if(!busy.delete(r.rId)) return`(每次合法 end 都对应一次 spawn/wake 置位的 busy;busy 仅在本处、唤醒失败、removeMember、重生成时清除,而后者 stale childId 已无法 byChild 命中)。**T29**:同一 tick 内连发两次相同 end → 任务只提议一次、群聊只转发一次。
595
+ - **空/失效 rId 的 resident 写入工具会制造库根散卡**:`currentResident` 被复位为 `''`(如移除最后一名常驻后)时,主机/未知代理调用 `vibe_v4_record_*`/`vibe_v4_publish_progress` 会把路径 `Propos//p-x.md` 塌缩成**库根目录的散乱卡片**(正是 test9 曾出现的 0 字节散卡一类问题)。修复:`residentIdOf` 回退仅当 `currentResident` 仍真实存在时生效,否则返回 `''`;四个库写入函数对空/未知 rId 直接拒绝 `{ok:false}`(主机以 `currentResident` 代写这一既有便利不受影响——自驱动测试仍走该回退)。**T30**。
596
+ - **移除者的已排队验证提议被丢弃 → 提议静默蒸发(含"第二提议者意图被去重吞掉"边角)**:v2.0.12 遗留的"移除提议者则清掉其 pendingVerify"在 FIFO 队列下会把该成员的排队提议**整条删除**;且同目标被两人提议时去重只保留第一人的条目——若第一人被移除,第二人的意图同样蒸发。修复:`removeMember` **不再过滤验证队列**——提议是关于对象的陈述,由**当前成员**按对象价值共识判定(allVoted 本就按实时成员集计算,写回扫描也不依赖提出者存续)。**T31**:r-3 提议 p-x 后在 p-y 验证中被移除 → p-x 仍由剩余成员完整验证到 Verified/。
597
+ - **暂存的会议对用户不可见 / 普通轮提议任务永远没有描述**:`status/report` 新增 `parkedMeeting`(暂存会议议程),便于确认"会议没丢、在排队"(**T32** 同时钉住"brainstorm 期开会 → 暂存 → bootstrap 后真正召开",不再被看门狗误杀);普通轮/心跳提示词模板补上 `task_desc` 字段(此前普通轮提议任务时描述恒为空,只有会议轮能带描述)。
598
+
599
+ > 边界复查(未改,记录):brainstorm 阶段若某常驻首轮 end 永不触发,run 停在 brainstorm(brainstorm 无看门狗、无心跳可唤醒"已空闲"者推进)——真实 DSH 的轮次必然以 completed/error/timeout 结束并触发 end,纯属框架外故障,不做可能"静默丢掉慢成员声音"的自动跳过;`Shared/taskboard.md`(人类视图)由调用时快照生成、非逐文件串行,最坏情况短暂滞后于 `State/taskboard.json`(权威源),下次任务动作即自愈。
600
+ >
601
+ > 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 94/94(T1–T32)、v3 100/100、v2 business 18/regression 14、multisession 25/25、selfdrive-v3 0 异常。
574
602
 
575
603
 
576
604