dsh-vibe-math 2.0.15 → 2.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vibe-math",
|
|
3
3
|
"description": "Multi-agent mathematical problem-solving & verification frameworks for DeepSeek Harness — THREE agent presets in one install: vibe-math-v2 (probability-driven: qs.json + Propos knowledge base + explorer→solver→review/debate verdict), vibe-math-v3 (THIRD-generation, recommended: paper-style Markdown knowledge base with Problems/Progress/Propos/Methods/Verified + planner-agent scheduling that decides the next N actions + universal theory/method invention library + agents write their own Markdown directly via a per-file write lock), and vibe-math-v4 (FOURTH-generation: persistent self-organizing resident subagents that message & meet to decide all tasks, verify only by unanimous consensus, /compact at a context threshold, and stop only when all agree the problem is solved). Installing this bundle auto-installs all three presets (v1 was removed at v2.0.0).",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.17",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -48,6 +48,8 @@ export function apply(ctx) {
|
|
|
48
48
|
let problemText = '', problemId = 'problem', runId = 'run-' + shortId()
|
|
49
49
|
let meetingState = null, verifyState = null, pendingVerify = null, pendingMeeting = null
|
|
50
50
|
let busy = new Set(), wakeKind = new Map(), currentResident = ''
|
|
51
|
+
let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
|
|
52
|
+
const verifiedRecently = new Map() // targetId -> timestamp when it was closed as Verified (dedup re-propose)
|
|
51
53
|
let lastActivityAt = now(), lastProgressAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
|
|
52
54
|
const activityLogCap = 200
|
|
53
55
|
|
|
@@ -403,6 +405,9 @@ export function apply(ctx) {
|
|
|
403
405
|
const ids=Array.from(residents.keys()); const allSpoke=ids.every(id=>st.inputs[id]!==undefined)
|
|
404
406
|
if(allSpoke){ await finalizeMeeting(); return }
|
|
405
407
|
// only wake IDLE un-spoken residents (rotated order); in-flight ones re-trigger this on end.
|
|
408
|
+
// NOTE: we deliberately do NOT flush mailboxes here — drafting an un-spoken resident into a normal
|
|
409
|
+
// mail round would delay the meeting and can starve the consensus past its watchdog if the mail
|
|
410
|
+
// backlog is large. Mail is delivered on scheduleNext passes when no consensus is in progress.
|
|
406
411
|
const order=st.order||ids
|
|
407
412
|
const id=order.find(x=>st.inputs[x]===undefined && !busy.has(x))
|
|
408
413
|
if(!id){ armHeartbeat(); return } // no idle un-spoken resident (a busy/hung one): re-check later
|
|
@@ -411,29 +416,48 @@ export function apply(ctx) {
|
|
|
411
416
|
if(!ok) armHeartbeat() // a failed meeting wake must NOT silently hang the meeting
|
|
412
417
|
}
|
|
413
418
|
async function finalizeMeeting(){
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
419
|
+
if(finalizeLock) return // reentry guard: two onResidentEnd may both see allSpoke → only finalize once
|
|
420
|
+
finalizeLock='meeting'
|
|
421
|
+
let doSchedule=false
|
|
422
|
+
try {
|
|
423
|
+
const st=meetingState
|
|
424
|
+
const ids=Array.from(residents.keys()); const allSpoke=ids.length>0 && ids.every(id=>st.inputs[id]!==undefined)
|
|
425
|
+
const lines=['# 会议 '+st.id+'|'+fmtTime(),'','**议程**:'+st.agenda,'']
|
|
426
|
+
for(const [id,iv] of Object.entries(st.inputs)){ lines.push('### '+id); lines.push(iv.input||''); lines.push('') }
|
|
427
|
+
await writeText('Shared/meetings/'+st.id+'.md', lines.join('\n'))
|
|
428
|
+
meetings.push({id:st.id,agenda:st.agenda,at:now(),inputs:st.inputs})
|
|
429
|
+
logDecision('meeting',st.agenda)
|
|
430
|
+
// handle what the meeting produced: task proposals/claims, verify targets, stop vote
|
|
431
|
+
for(const [id,iv] of Object.entries(st.inputs)){
|
|
432
|
+
if(iv.propose_task) await proposeTask(iv.propose_task, iv.task_desc||'', id)
|
|
433
|
+
if(iv.claim_task) await claimTask(iv.claim_task, id)
|
|
434
|
+
if(iv.propose_verify) maybeQueueVerify(iv.propose_verify, id)
|
|
435
|
+
}
|
|
436
|
+
const votes=Object.values(st.inputs).map(x=>x.voteSolved).filter(v=>typeof v==='boolean')
|
|
437
|
+
const allSolved = allSpoke && votes.length>0 && votes.every(v=>v===true)
|
|
438
|
+
logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
|
|
439
|
+
if(allSolved){ running=false; autoDone=true; phase='done'; clearHeartbeat(); logActivity('stop','all residents agree: problem solved'); await saveAll(); return }
|
|
440
|
+
meetingState=null; wakeKind.clear(); await saveAll()
|
|
441
|
+
doSchedule=true
|
|
442
|
+
} finally { finalizeLock=null } // release BEFORE scheduling so a chained verify/meeting is not swallowed
|
|
443
|
+
if(doSchedule) await scheduleNext()
|
|
432
444
|
}
|
|
433
445
|
|
|
434
446
|
// ---- verification (unanimous) ----
|
|
435
447
|
async function beginVerify(pv){
|
|
436
448
|
clearHeartbeat()
|
|
449
|
+
// Re-check dedup at ACTUAL start, not just at propose time: a resident may propose object X while
|
|
450
|
+
// X is already being verified (it does not know). That proposal sits in pendingVerify; when the
|
|
451
|
+
// current X verify closes, beginVerify would run X end-to-end a SECOND time (test9: p-r3-04 was
|
|
452
|
+
// verified twice back-to-back). Drop it if X was closed within the dedup window.
|
|
453
|
+
const tgt=pv&&pv.targetId?String(pv.targetId):''
|
|
454
|
+
if(tgt){
|
|
455
|
+
const last=verifiedRecently.get(tgt)
|
|
456
|
+
if(last!==undefined && (now()-last) < recoverStallMs()){
|
|
457
|
+
logActivity('verify',tgt+' queued verify dropped at start (just verified at '+fmtTime(last)+')')
|
|
458
|
+
pendingVerify=null; await saveAll(); await scheduleNext(); return
|
|
459
|
+
}
|
|
460
|
+
}
|
|
437
461
|
pendingVerify=null
|
|
438
462
|
verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},history:{},transcript:[],at:now(),lastVerdictAt:now()}
|
|
439
463
|
markProgress();
|
|
@@ -452,6 +476,9 @@ export function apply(ctx) {
|
|
|
452
476
|
}
|
|
453
477
|
const ids=Array.from(residents.keys()); const allVoted=ids.every(id=>vs.verdicts[id]!==undefined)
|
|
454
478
|
if(allVoted){ await finalizeVerify(); return }
|
|
479
|
+
// NOTE: we deliberately do NOT flush mailboxes here — drafting an un-voted resident into a normal
|
|
480
|
+
// mail round would delay its verdict and can starve the verify past its watchdog when the backlog
|
|
481
|
+
// is large. Mail is delivered on scheduleNext passes when no consensus is in progress.
|
|
455
482
|
const id=ids.find(x=>vs.verdicts[x]===undefined && !busy.has(x))
|
|
456
483
|
if(!id){ armHeartbeat(); return } // no idle un-voted resident (a busy/hung one): re-check later
|
|
457
484
|
const r=residents.get(id)
|
|
@@ -459,33 +486,60 @@ export function apply(ctx) {
|
|
|
459
486
|
if(!ok) armHeartbeat() // a failed verify wake must NOT silently hang the verification
|
|
460
487
|
}
|
|
461
488
|
async function finalizeVerify(){
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
489
|
+
if(finalizeLock) return // reentry guard (two onResidentEnd may both see allVoted)
|
|
490
|
+
finalizeLock='verify'
|
|
491
|
+
let doSchedule=false
|
|
492
|
+
try {
|
|
493
|
+
const vs=verifyState; const expected=Array.from(residents.keys()).length
|
|
494
|
+
const allVoted = expected>0 && Object.keys(vs.verdicts).length>=expected
|
|
495
|
+
const vals=Object.values(vs.verdicts)
|
|
496
|
+
// verdict is a PURE 0-1 probability; only ALL=1 (true) or ALL=0 (false) is a binary verdict.
|
|
497
|
+
const allTrue = allVoted && vals.every(x=>Number(x.prob)===1)
|
|
498
|
+
const allFalse = allVoted && vals.every(x=>Number(x.prob)===0)
|
|
499
|
+
if(allTrue||allFalse){ await closeVerify(vs,allTrue); doSchedule=true }
|
|
500
|
+
else if(vs.round+1<params.verdictMaxRounds){
|
|
501
|
+
// Move to a REAL debate round: snapshot the current votes into history (so the next round's
|
|
502
|
+
// prompt can show others' previous stances), then CLEAR verdicts so every resident is asked to
|
|
503
|
+
// give a fresh independent judgement after seeing the debate. Without the clear, allVoted stays
|
|
504
|
+
// true and the debate rounds burn through with NOBODY being re-asked (a silent no-op).
|
|
505
|
+
vs.history=Object.assign({}, vs.verdicts); vs.verdicts={}
|
|
506
|
+
vs.lastVerdictAt=now() // fresh deadlock window for the re-vote round
|
|
507
|
+
vs.stage='debate'; vs.round+=1; vs.asked=[]; logActivity('verify',vs.targetId+' round '+vs.round+' → debate (re-vote after seeing others)'); await saveAll(); doSchedule=true
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
const avg=vals.length? vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length : 0.5
|
|
511
|
+
await writeDebateDoc(vs,false,avg); await rewriteSourceProb(vs.targetId, avg, vs.targetOwner); logActivity('verify',vs.targetId+' NOT unanimous → kept unverified (avg '+avg.toFixed(2)+')')
|
|
512
|
+
verifyState=null; wakeKind.clear(); await saveAll(); doSchedule=true
|
|
513
|
+
}
|
|
514
|
+
} finally { finalizeLock=null } // release BEFORE scheduling (chained verifies must not be swallowed)
|
|
515
|
+
if(doSchedule) await scheduleNext()
|
|
481
516
|
}
|
|
482
517
|
async function closeVerify(vs,isTrue){
|
|
483
518
|
await writeDebateDoc(vs,true,isTrue?1:0)
|
|
484
519
|
const target=vs.targetId
|
|
485
520
|
await writeVerifiedCard(vs,isTrue)
|
|
486
521
|
await rewriteSource(target,isTrue,vs.targetOwner)
|
|
522
|
+
verifiedRecently.set(target, now()) // dedup: block an immediate re-proposal of the same object
|
|
487
523
|
logActivity('verify',target+' → Verified ('+(isTrue?'真':'假')+') by unanimous consensus')
|
|
488
|
-
verifyState=null; wakeKind.clear(); await saveAll()
|
|
524
|
+
verifyState=null; wakeKind.clear(); await saveAll()
|
|
525
|
+
// scheduling is done by finalizeVerify AFTER it releases finalizeLock (so a chained verify is
|
|
526
|
+
// never swallowed by the still-held reentry lock)
|
|
527
|
+
}
|
|
528
|
+
// 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.
|
|
533
|
+
function maybeQueueVerify(target, proposer){
|
|
534
|
+
const t=String(target||'').trim()
|
|
535
|
+
if(!t) return false
|
|
536
|
+
const last=verifiedRecently.get(t)
|
|
537
|
+
if(last!==undefined && (now()-last) < recoverStallMs()){
|
|
538
|
+
logActivity('verify',t+' re-propose ignored (just verified at '+fmtTime(last)+')')
|
|
539
|
+
return false
|
|
540
|
+
}
|
|
541
|
+
pendingVerify={targetId:t,targetType:guessTargetType(t),proposer:proposer||'',at:now()}
|
|
542
|
+
return true
|
|
489
543
|
}
|
|
490
544
|
async function writeDebateDoc(vs,done,val){
|
|
491
545
|
const lines=['# 验证辩论|'+vs.targetId+'('+vs.targetType+')|'+fmtTime(),'',(done?('**结论**:'+(val===1?'全体一致为真':'全体一致为假')):('**未达成全体一致**,平均概率 '+val.toFixed(2))),'','## 各常驻意见']
|
|
@@ -499,31 +553,71 @@ export function apply(ctx) {
|
|
|
499
553
|
const text='# 已验证|'+vs.targetId+'\n- ID: '+vs.targetId+'\n- 类型: '+type+'\n- 结论: '+(isTrue?'真':'假')+'\n- 概率: '+(isTrue?1:0)+'\n- 来源: 全体常驻一致\n## 陈述\n参见来源卡。\n'
|
|
500
554
|
await writeText('Verified/'+dir+'/'+vs.targetId+'.md', text)
|
|
501
555
|
}
|
|
556
|
+
// Does `content` declare the target as its card ID? Accept both the exact `- ID: <id>` and the
|
|
557
|
+
// compact single-line form (`- ID: <id>; - 状态: ...`). Residents write cards by hand via fs with
|
|
558
|
+
// varying formats and (crucially) sometimes put a DIFFERENT file name than the declared ID (e.g.
|
|
559
|
+
// Propos/r-3/p-01.md declares "- ID: p-r3-01"). Matching only on the file name then silently loses
|
|
560
|
+
// the verified-status write-back, so we scan candidates' declared ID too.
|
|
561
|
+
function cardDeclaresId(content, target){
|
|
562
|
+
if(!content || !target) return false
|
|
563
|
+
const m=/-\s*ID:\s*([^;\n]+)/.exec(content)
|
|
564
|
+
return !!(m && String(m[1]).trim()===String(target).trim())
|
|
565
|
+
}
|
|
502
566
|
async function findSourceRel(target, owner){
|
|
503
|
-
//
|
|
567
|
+
// 1) exact file name in the owner's library (fast path), then every resident's library
|
|
504
568
|
const order = owner ? [owner, ...Array.from(residents.keys()).filter(k=>k!==owner)] : Array.from(residents.keys())
|
|
505
569
|
for(const rid of order){
|
|
506
570
|
for(const base of ['Propos','Methods','Subproblems']){
|
|
507
|
-
const cand=base+'/'+rid+'/'+target+'.md'
|
|
571
|
+
const cand=base+'/'+rid+'/'+target+'.md'
|
|
572
|
+
const t0=await readText(cand); if(t0!==undefined) return cand
|
|
508
573
|
}
|
|
509
574
|
}
|
|
510
|
-
|
|
575
|
+
// 2) declared-ID scan: residents sometimes name the file differently from the declared card ID
|
|
576
|
+
// (e.g. p-01.md declares ID p-r3-01). Look inside every card of every library for the target ID.
|
|
577
|
+
try {
|
|
578
|
+
for(const rid of order){
|
|
579
|
+
for(const base of ['Propos','Methods','Subproblems']){
|
|
580
|
+
const dirT=await fs.resolve(base+'/'+rid, {cwd: frameworkRoot()})
|
|
581
|
+
if(await fs.stat(dirT)===undefined) continue
|
|
582
|
+
const entries=await fs.listDir(dirT)
|
|
583
|
+
for(const e of entries||[]){
|
|
584
|
+
if(!e || e.type!=='file' || !/\.md$/.test(String(e.name))) continue
|
|
585
|
+
const c=await readText(base+'/'+rid+'/'+e.name)
|
|
586
|
+
if(c!==undefined && cardDeclaresId(c,target)) return base+'/'+rid+'/'+e.name
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} catch(e){ /* scanning is best-effort */ }
|
|
591
|
+
return null // NOT 'Propos/'+target+'.md': writing there would create a stray empty card
|
|
592
|
+
}
|
|
593
|
+
// Update the `- 状态:` / `- 概率:` fields of a source card. Residents hand-write cards in two
|
|
594
|
+
// shapes: one field per line, or one line with `; `-separated fields. Accept both by allowing the
|
|
595
|
+
// anchor anywhere on a line and consuming up to the next `;` when fields share the line.
|
|
596
|
+
function rewriteCardField(text, field, newValue){
|
|
597
|
+
if(!text) return text
|
|
598
|
+
const esc=field.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')
|
|
599
|
+
// one-per-line: `- 状态: ...\n` OR inline: `; - 状态: ...;` / `- 状态: ...; - 概率:`
|
|
600
|
+
const re=new RegExp('(^|\\n|;\\s*)-\\s*'+esc+':[^;\\n]*','gm')
|
|
601
|
+
const replaced=text.replace(re,'$1- '+field+': '+newValue)
|
|
602
|
+
return replaced===text ? text : replaced
|
|
511
603
|
}
|
|
512
604
|
// non-unanimous verification: keep the object in its library but write back the
|
|
513
605
|
// average probability (design §8: "留库附概率"), so the card reflects the consensus estimate.
|
|
514
606
|
async function rewriteSourceProb(target,prob,owner){
|
|
515
607
|
const rel=await findSourceRel(target,owner)
|
|
608
|
+
if(!rel){ logActivity('verify',target+' source card NOT found; avg prob '+Number(prob).toFixed(2)+' not written back'); return }
|
|
516
609
|
let text=(await readText(rel))||''
|
|
517
|
-
|
|
518
|
-
await writeText(rel,text)
|
|
610
|
+
const next=rewriteCardField(text,'概率',Number(prob).toFixed(2))
|
|
611
|
+
await writeText(rel,next||text)
|
|
519
612
|
}
|
|
520
613
|
async function rewriteSource(target,isTrue,owner){
|
|
521
614
|
// find & update the source card status/prob; best effort across per-resident libs
|
|
522
615
|
const rel=await findSourceRel(target,owner)
|
|
616
|
+
if(!rel){ logActivity('verify',target+' source card NOT found; verified status not written back'); return }
|
|
523
617
|
let text=(await readText(rel))||''
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
await writeText(rel,text)
|
|
618
|
+
let next=rewriteCardField(text,'状态',isTrue?'已验证·真':'已验证·假')
|
|
619
|
+
next=rewriteCardField(next,'概率',isTrue?'1':'0')
|
|
620
|
+
await writeText(rel,next||text)
|
|
527
621
|
}
|
|
528
622
|
function guessTargetType(id){ if(/^p-/.test(id)) return 'proposition'; if(/^m-/.test(id)) return 'method'; if(/^s-/.test(id)) return 'subproblem'; return 'proposition' }
|
|
529
623
|
|
|
@@ -674,7 +768,7 @@ export function apply(ctx) {
|
|
|
674
768
|
if(kind==='meeting' && meetingState){
|
|
675
769
|
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}
|
|
676
770
|
meetingState.lastInputAt=now()
|
|
677
|
-
if(parsed.propose_verify)
|
|
771
|
+
if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
|
|
678
772
|
await saveAll(); await continueMeetingRound(); return
|
|
679
773
|
}
|
|
680
774
|
if((kind==='verif-ind'||kind==='verif-deb') && verifyState){
|
|
@@ -694,7 +788,7 @@ export function apply(ctx) {
|
|
|
694
788
|
// normal turn
|
|
695
789
|
if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
|
|
696
790
|
if(typeof parsed.solved==='boolean') reports.push({rId:r.rId,solved:parsed.solved,summary:parsed.summary||'',at:now()})
|
|
697
|
-
if(parsed.propose_verify)
|
|
791
|
+
if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
|
|
698
792
|
// group-conversation relay: the resident may choose to speak to the whole team (input) —
|
|
699
793
|
// forward it to the others so this is a real discussion group, not private monologues.
|
|
700
794
|
if(typeof parsed.input==='string' && parsed.input.trim()) await relayToGroup(r.rId, parsed.input.trim())
|
|
@@ -724,7 +818,7 @@ export function apply(ctx) {
|
|
|
724
818
|
running=true; autoDone=false; phase='brainstorm'
|
|
725
819
|
await writeText('Problems/'+problemId+'.md','# 问题|'+problemId+'\n- ID: '+problemId+'\n- 类型: 问题\n- 状态: 求解中\n- 优先级: 1\n- 依赖: []\n\n## 陈述\n'+problemText+'\n')
|
|
726
820
|
residents=new Map(); mailboxes=new Map(); taskboard=[]; decisions=[]; meetings=[]; reports=[]; verifyState=null; meetingState=null; pendingVerify=null; residentSeq=0; artifactCount=0; clearHeartbeat()
|
|
727
|
-
busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; lastSyncMeetingAt=0 // fresh run must NOT inherit stale concurrency/coordination state (busy/wakeKind/currentResident/pendingMeeting) from a previous run on the same reused session
|
|
821
|
+
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
|
|
728
822
|
lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
|
|
729
823
|
const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
|
|
730
824
|
for(let i=0;i<params.residentCount;i++){ const r=newResident(dirs[i]||''); await spawnResident(r) }
|
|
@@ -750,7 +844,7 @@ export function apply(ctx) {
|
|
|
750
844
|
const needRespawn = Array.from(residents.values()).some(r=>!r.childId)
|
|
751
845
|
if(needRespawn){
|
|
752
846
|
for(const [,r] of residents){ r.childId=''; r.status='brainstorm'; r.insight=''; r.roundsSinceCompact=0 }
|
|
753
|
-
busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; pendingVerify=null; verifyState=null; meetingState=null
|
|
847
|
+
busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; pendingVerify=null; verifyState=null; meetingState=null; finalizeLock=null; verifiedRecently.clear()
|
|
754
848
|
}
|
|
755
849
|
for(const [,r] of residents){ if(!r.childId){ await spawnResident(r) } }
|
|
756
850
|
if(!running){ running=true; autoDone=false; if(phase==='idle') phase='active' }
|
|
@@ -767,7 +861,14 @@ export function apply(ctx) {
|
|
|
767
861
|
verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
|
|
768
862
|
pendingVerify: pendingVerify?pendingVerify.targetId:null,
|
|
769
863
|
meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
|
|
770
|
-
async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r)
|
|
864
|
+
async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r)
|
|
865
|
+
// Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
|
|
866
|
+
// residents) can never be true for the new member (not in the snapshot order) and the meeting is
|
|
867
|
+
// only ever released by the stuck watchdog instead of finalizing with everyone's input.
|
|
868
|
+
if(meetingState){ if(!Array.isArray(meetingState.order)) meetingState.order=Array.from(residents.keys()); if(!meetingState.order.includes(r.rId)) meetingState.order.push(r.rId) }
|
|
869
|
+
// Mid-verify additions are automatically asked to vote (continueVerifyRound recomputes ids from
|
|
870
|
+
// the live residents map), so no extra handling is needed there.
|
|
871
|
+
return {ok:true,id:r.rId,direction:r.direction} }
|
|
771
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)
|
|
772
873
|
// Reconcile in-progress coordination so a removed member cannot hang consensus or crash a round:
|
|
773
874
|
// drop its meeting speech / verify verdict / deferred-meeting / pending-verify if it owned them, and
|
|
@@ -559,11 +559,18 @@ VibeMath/Projects/<project>/
|
|
|
559
559
|
- 并行填充常驻后,一个常驻可能"提议验证(propose_verify)"而另一个同时"提议开会(propose_meeting)"。**会议不得抢占验证**:`startMeeting` 在 `verifyState || pendingVerify` 时改为把会议请求**暂存**(`pendingMeeting`,仅记录 agenda/type/target)并返回 `{deferred:true}`;`scheduleNext` 在所有 meeting/verify/pending 都清空后才恢复该暂存会议。这样统一共识验证(全真/全假)作为"求真"环节不会被会议的协调讨论打断,验证做完后再开会开会协调下一步——两者互斥但都不丢失(暂存会议之后补开)。**暂存只保留第一条**(`!pendingMeeting` 才写入),避免后到的请求覆盖先到的。
|
|
560
560
|
- **进行中增删常驻的一致性**:在会议/验证进行中 `removeMember` 会**同步清理**该常驻的会议发言(`delete meetingState.inputs[id]`)、验证投票(`delete verifyState.verdicts[id]`)、若它是待验证的提出者则清 `pendingVerify`,并从会议发言顺序 `st.order` 里剔除它;`wakeResident` 对已经不存在的常驻**防御性返回 false**(不再因 `r.childId` 抛 TypeError)。这样移除一个常驻不会让共识**卡在幽灵身上**,也不会让会议/验证循环去唤醒一个已删除的常驻而导致崩溃。
|
|
561
561
|
|
|
562
|
+
### 实战审计修复(test9)
|
|
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` 重入锁(第一次执行期间再次进入直接返回),且**锁在调用 `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 收尾期有积压)。曾尝试在会议/验证轮间投递,但会把未投票/未发言的常驻拉去做普通轮,令验证在看门狗窗口内等不到票而**误放弃**——故回退,改为**仅在无进行中共识时投递**(消息持久化不丢失,下次非共识调度/恢复时送达)。
|
|
568
|
+
|
|
562
569
|
> 效果:一个卡死/坏掉的会议或验证最多阻塞 `activityTimeoutMs`×2 后自动释放,团队重新回到 A/B 分级保活,**不会永久停死**。仍符合"框架只促成、从不指派任务"(放弃只是终止一个无法推进的会议,把控制权交还团队的自组织循环)。
|
|
563
570
|
>
|
|
564
571
|
> 说明:无法对"某常驻真的拒绝/掉线"的情况达成全体一致时,看门狗会把该对象保留为"未定论/带概率",这是哲学上期望的诚实结果。`session.json` 仍不持久化 `meetingState`(进行中的会议不跨进程恢复),B 的停滞看门狗在重启后仍会触发,因此重启也能自愈。
|
|
565
572
|
>
|
|
566
|
-
> 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes`
|
|
573
|
+
> 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 61/61(含 T20 源卡回写 / T21 终结重入 / T22 验证去重)、v3 100/100、v2 business 18/regression 14、multisession 25/25。
|
|
567
574
|
|
|
568
575
|
|
|
569
576
|
|