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