dsh-vibe-math 2.0.19 → 2.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vibe-math",
|
|
3
3
|
"description": "Multi-agent mathematical problem-solving & verification frameworks for DeepSeek Harness — THREE agent presets in one install: vibe-math-v2 (probability-driven: qs.json + Propos knowledge base + explorer→solver→review/debate verdict), vibe-math-v3 (THIRD-generation, recommended: paper-style Markdown knowledge base with Problems/Progress/Propos/Methods/Verified + planner-agent scheduling that decides the next N actions + universal theory/method invention library + agents write their own Markdown directly via a per-file write lock), and vibe-math-v4 (FOURTH-generation: persistent self-organizing resident subagents that message & meet to decide all tasks, verify only by unanimous consensus, /compact at a context threshold, and stop only when all agree the problem is solved). Installing this bundle auto-installs all three presets (v1 was removed at v2.0.0).",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.21",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -76,7 +76,12 @@ export function apply(ctx) {
|
|
|
76
76
|
// deadlocked and abandon it. A meeting round's own signal window is activityTimeoutMs, so a
|
|
77
77
|
// resident should speak within that; 2× that without ANY new input/verdict means the meeting/verify
|
|
78
78
|
// is stuck and must not keep the whole group blocked.
|
|
79
|
-
|
|
79
|
+
// Positive duration with a safe fallback: a NEGATIVE/NaN activityTimeoutMs or stallAutoMeetingMs
|
|
80
|
+
// (misconfigured via vibe_v4_set) would otherwise make recoverStallMs negative → every meeting/
|
|
81
|
+
// verify watchdog fires INSTANTLY (abandoning all consensus) and A-fill's idle window would never
|
|
82
|
+
// elapse (waking everyone every pass). Guard every duration read with this.
|
|
83
|
+
function posMs(v,def){ const n=Number(v); return (Number.isFinite(n)&&n>0)?n:(def||120000) }
|
|
84
|
+
function recoverStallMs(){ return posMs(params.activityTimeoutMs,120000) * 2 }
|
|
80
85
|
function pickProvider(){ try { const n=subagents.list?subagents.list():[]; if(n.indexOf('spawn')!==-1) return 'spawn'; if(n.indexOf('fork')!==-1) return 'fork' } catch(e){} return 'spawn' }
|
|
81
86
|
// Per-resident model/provider inheritance: when params.provider / params.model are set,
|
|
82
87
|
// the resident uses that exact route; when left '' the resident inherits the parent's
|
|
@@ -91,11 +96,20 @@ export function apply(ctx) {
|
|
|
91
96
|
if(allow.length===0 && deny.length===0) return undefined
|
|
92
97
|
const f={}; if(allow.length) f.allow=allow; if(deny.length) f.deny=deny; return f
|
|
93
98
|
}
|
|
94
|
-
function makeSignal(ms){ return AbortSignal.timeout(ms
|
|
99
|
+
function makeSignal(ms){ return AbortSignal.timeout(posMs(ms,30000)) }
|
|
95
100
|
function workspaceRoot(){ try { if(rootAgent&&rootAgent.session&&rootAgent.session.header&&rootAgent.session.header.cwd) return rootAgent.session.header.cwd } catch(e){} if(sandboxPolicy&&sandboxPolicy.workspaceRoot) return sandboxPolicy.workspaceRoot; return '.' }
|
|
96
101
|
function vibeRoot(){ return (workspaceRoot()+'/VibeMath').replace(/\\/g,'/') }
|
|
97
102
|
function frameworkRoot(){ return vibeRoot()+'/Projects/'+currentProject }
|
|
98
103
|
function slugify(s){ const t=String(s==null?'':s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g,'-').replace(/^-+|-+$/g,''); return t||'project' }
|
|
104
|
+
// Object ids (verify targets, recorded cards) become FILE NAMES and DIRECTORY PATHS
|
|
105
|
+
// (Verified/命题/<id>.md, Shared/debates/<id>.md, Propos/<r>/<id>.md, source-card scans).
|
|
106
|
+
// A hostile/sloppy id containing path separators ('../../x') or Windows-forbidden chars would
|
|
107
|
+
// escape the project tree. Keep every harmless character (incl. Chinese) and replace only
|
|
108
|
+
// separators/control chars; strip leading/trailing dots/dashes so the name is never '.'/'..'.
|
|
109
|
+
function idSafe(s){
|
|
110
|
+
const t=String(s==null?'':s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g,'-').replace(/-{2,}/g,'-').replace(/^[.\-]+|[.\-]+$/g,'')
|
|
111
|
+
return t||'id'
|
|
112
|
+
}
|
|
99
113
|
function getPolicy(){ try { if(sandboxPolicy&&rootAgent&&rootAgent.session) return sandboxPolicy.resolve({session:rootAgent.session}) } catch(e){} try { if(sandboxPolicy) return sandboxPolicy.resolve({}) } catch(e){} return undefined }
|
|
100
114
|
function psQuote(p){ return "'"+String(p).replace(/'/g,"''")+"'" }
|
|
101
115
|
async function runShell(script,cwd){ if(subprocess===undefined) return {ok:false,error:'no-subprocess'}; try { const h=subprocess.spawn({argv:['powershell','-NoProfile','-NonInteractive','-Command',script],cwd:cwd||workspaceRoot(),stdio:{stdin:'ignore',stdout:'inherit',stderr:'inherit'},graceMs:20000}); const o=await h.done; return {ok:o.exitCode===0,exitCode:o.exitCode} } catch(e){ return {ok:false,error:String((e&&e.message)||e)} } }
|
|
@@ -322,9 +336,9 @@ export function apply(ctx) {
|
|
|
322
336
|
|
|
323
337
|
// ---- artifact writers (resident-facing) ----
|
|
324
338
|
async function publishProgress(rId,content){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const rel='Progress/'+rId+'/progress.md'; const prev=(await readText(rel))||''; await writeText(rel, prev+'\n### '+fmtTime()+'|'+rId+'\n'+String(content||'')+'\n'); return {ok:true} }
|
|
325
|
-
async function recordProposition(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id
|
|
326
|
-
async function recordMethod(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id
|
|
327
|
-
async function recordSubproblem(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id
|
|
339
|
+
async function recordProposition(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('p-'+shortId()); const lines=['# 命题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: 命题','- 状态: 未定论','- 概率: '+cl(o.prob!=null?o.prob:0.5),'- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 证明尝试','','## 证伪尝试','']; await writeText('Propos/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 命题 '+id); bumpArtifacts(); return {ok:true,id,file:'Propos/'+rId+'/'+id+'.md'} }
|
|
340
|
+
async function recordMethod(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('m-'+shortId()); const lines=['# 方法|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: '+(o.type||'方法'),'- 状态: 经验','- 可信断言: []','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'','## 核心内容',String(o.content||''),'','## 定义与记号',String(o.notation||''),'','## 应用记录','## 改进历史','']; await writeText('Methods/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 方法 '+id); bumpArtifacts(); return {ok:true,id,file:'Methods/'+rId+'/'+id+'.md'} }
|
|
341
|
+
async function recordSubproblem(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id?idSafe(o.id):('s-'+shortId()); const lines=['# 子问题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 状态: 求解中','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 进度','']; await writeText('Subproblems/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 子问题 '+id); bumpArtifacts(); return {ok:true,id,file:'Subproblems/'+rId+'/'+id+'.md'} }
|
|
328
342
|
// auto-sync meeting: every meetingKeepEvery new artifacts, convene a general coordination meeting
|
|
329
343
|
function bumpArtifacts(){ artifactCount+=1; markProgress(); if(!meetingState && !verifyState && !pendingMeeting && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
|
|
330
344
|
function listResidents(){ return Array.from(residents.values()).map(r=>({id:r.rId,direction:r.direction,status:r.status,rounds:r.rounds,contextPct:r.contextPct,insight:r.insight?r.insight.slice(0,80):''})) }
|
|
@@ -338,8 +352,10 @@ export function apply(ctx) {
|
|
|
338
352
|
async function writeTaskboard(){ const lines=['# 任务板','']; for(const t of taskboard){ lines.push('- ['+t.status+'] '+t.title+(t.claimer?('(认领:'+t.claimer+')'):'')+(t.proposer?('(提议:'+t.proposer+')'):'')+(t.description?(':'+t.description):'')) } await writeText('Shared/taskboard.md',lines.join('\n')) }
|
|
339
353
|
async function proposeTask(title,description,proposer){ const id='t-'+shortId(); taskboard.push({id,title:String(title),description:String(description||''),status:'open',proposer:proposer||'',claimer:'',source:''}); await saveTaskboard(); markProgress(); logActivity('task','proposed '+id+'「'+title+'」'); return {ok:true,id} }
|
|
340
354
|
async function claimTask(id,claimer){ const t=taskboard.find(x=>x.id===id); if(!t) return {ok:false,message:'no such task'}; if(t.status!=='open') return {ok:false,message:'task already '+t.status}; t.status='claimed'; t.claimer=claimer; await saveTaskboard(); markProgress(); logActivity('task',claimer+' claimed '+id);
|
|
341
|
-
// wake the claimer to work on it (framework moves the task, resident decides how)
|
|
342
|
-
|
|
355
|
+
// wake the claimer to work on it (framework moves the task, resident decides how).
|
|
356
|
+
// NOT while paused/stopped: a paused run must not start new work — the claim is recorded on the
|
|
357
|
+
// board and the resident (who claimed it) picks it up again after resume.
|
|
358
|
+
const r=residents.get(claimer); if(r && !busy.has(claimer) && running && !autoDone){ currentResident=claimer; await wakeResident(r, (await normalPrompt(r))+'\n\n[YOU CLAIMED TASK '+id+'] '+t.title+' — '+t.description,'normal'); await saveAll() }
|
|
343
359
|
return {ok:true} }
|
|
344
360
|
async function taskDone(id,claimer){ const t=taskboard.find(x=>x.id===id); if(!t) return {ok:false}; t.status='done'; t.doneBy=claimer; await saveTaskboard(); await writeTaskboard(); markProgress(); logActivity('task','done '+id); return {ok:true} }
|
|
345
361
|
async function saveTaskboard(){ await writeJson('State/taskboard.json',taskboard); await writeTaskboard() }
|
|
@@ -348,7 +364,7 @@ export function apply(ctx) {
|
|
|
348
364
|
// Apply context/compact bookkeeping from a resident's reply, so the flag can clear even when
|
|
349
365
|
// the reply came through a meeting/verify branch (defensive) as well as the normal branch.
|
|
350
366
|
function postmark(r, parsed){
|
|
351
|
-
|
|
367
|
+
const cp=Number(parsed.contextPct); if(Number.isFinite(cp)) r.contextPct=clPct(cp) // tolerate numeric strings ("40")
|
|
352
368
|
if(parsed.compacted===true || (r.needCompact && parsed.summary)){
|
|
353
369
|
r.contextSeed=String(parsed.summary||r.contextSeed||'')
|
|
354
370
|
r.contextPct=Math.min(r.contextPct||15,25)
|
|
@@ -368,9 +384,9 @@ export function apply(ctx) {
|
|
|
368
384
|
}
|
|
369
385
|
const mb=mailboxes.get(to)||[]; mb.push({from,at:now(),content}); mailboxes.set(to,mb); await saveAll(); logActivity('message',from+'→'+to+' (queued)'); return {ok:true}
|
|
370
386
|
}
|
|
371
|
-
async function broadcast(content){
|
|
387
|
+
async function broadcast(content, from){
|
|
372
388
|
let n=0
|
|
373
|
-
for(const [,r] of residents){ const res=await postMessage('facilitator',r.rId,content); if(res&&res.ok) n++ }
|
|
389
|
+
for(const [,r] of residents){ if(from && r.rId===from) continue; const res=await postMessage(from||'facilitator',r.rId,content); if(res&&res.ok) n++ }
|
|
374
390
|
logActivity('broadcast','to '+n+' resident(s)'); await saveAll(); return {ok:true,message:'broadcast to '+n+' resident(s)'}
|
|
375
391
|
}
|
|
376
392
|
// group conversation relay: when a resident "speaks" (input in its round), forward its
|
|
@@ -444,7 +460,10 @@ export function apply(ctx) {
|
|
|
444
460
|
const lines=['# 会议 '+st.id+'|'+fmtTime(),'','**议程**:'+st.agenda,'']
|
|
445
461
|
for(const [id,iv] of Object.entries(st.inputs)){ lines.push('### '+id); lines.push(iv.input||''); lines.push('') }
|
|
446
462
|
await writeText('Shared/meetings/'+st.id+'.md', lines.join('\n'))
|
|
447
|
-
meetings
|
|
463
|
+
// the full transcript lives on disk (Shared/meetings/<id>.md); the State array keeps a small
|
|
464
|
+
// index (id/agenda/at) so session.json does not carry a second copy of every transcript and
|
|
465
|
+
// rewrite it on EVERY saveAll during long runs (reports below are capped for the same reason).
|
|
466
|
+
meetings.push({id:st.id,agenda:st.agenda,at:now()}); if(meetings.length>200) meetings.shift()
|
|
448
467
|
logDecision('meeting',st.agenda)
|
|
449
468
|
// handle what the meeting produced: task proposals/claims, verify targets, stop vote
|
|
450
469
|
for(const [id,iv] of Object.entries(st.inputs)){
|
|
@@ -455,7 +474,13 @@ export function apply(ctx) {
|
|
|
455
474
|
const votes=Object.values(st.inputs).map(x=>x.voteSolved).filter(v=>typeof v==='boolean')
|
|
456
475
|
const allSolved = allSpoke && votes.length>0 && votes.every(v=>v===true)
|
|
457
476
|
logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
|
|
458
|
-
if(allSolved){
|
|
477
|
+
if(allSolved){
|
|
478
|
+
running=false; autoDone=true; phase='done'; clearHeartbeat()
|
|
479
|
+
// symmetric with initAbort: the STOP path must also release the coordination state, else
|
|
480
|
+
// status keeps reporting a phantom in-progress meeting (and stale wake kinds) forever.
|
|
481
|
+
meetingState=null; wakeKind.clear(); pendingMeeting=null; verifyState=null; pendingVerify=[]
|
|
482
|
+
logActivity('stop','all residents agree: problem solved'); await saveAll(); return
|
|
483
|
+
}
|
|
459
484
|
meetingState=null; wakeKind.clear(); await saveAll()
|
|
460
485
|
doSchedule=true
|
|
461
486
|
} finally { finalizeLock=null } // release BEFORE scheduling so a chained verify/meeting is not swallowed
|
|
@@ -552,8 +577,8 @@ export function apply(ctx) {
|
|
|
552
577
|
// duplicates collapse to one entry. A resident who genuinely extends the object later can still
|
|
553
578
|
// re-propose after the dedup window (recoverStallMs) has passed.
|
|
554
579
|
function maybeQueueVerify(target, proposer){
|
|
555
|
-
const t=
|
|
556
|
-
if(!t) return false
|
|
580
|
+
const t=idSafe(target) // sanitize BEFORE it becomes file names / dedup keys / status output
|
|
581
|
+
if(!t || t==='id') return false
|
|
557
582
|
const last=verifiedRecently.get(t)
|
|
558
583
|
if(last!==undefined && (now()-last) < recoverStallMs()){
|
|
559
584
|
logActivity('verify',t+' re-propose ignored (just verified at '+fmtTime(last)+')')
|
|
@@ -658,8 +683,8 @@ export function apply(ctx) {
|
|
|
658
683
|
function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
|
|
659
684
|
function armHeartbeat(){
|
|
660
685
|
clearHeartbeat()
|
|
661
|
-
const ms=
|
|
662
|
-
if(
|
|
686
|
+
const ms=posMs(params.activityTimeoutMs,120000)
|
|
687
|
+
if(typeof ctx.timeout!=='function') return
|
|
663
688
|
heartbeatDisposer=ctx.timeout(()=>{ heartbeatDisposer=null; scheduleNext().catch(()=>{}) }, ms)
|
|
664
689
|
}
|
|
665
690
|
// Real DSH /compact of a resident's OWN session via ctx.compaction (if the host provides it);
|
|
@@ -703,7 +728,7 @@ export function apply(ctx) {
|
|
|
703
728
|
// meeting/verify/pending work is active AND no resident is currently working (so it never
|
|
704
729
|
// preempts an in-flight round).
|
|
705
730
|
if(phase==='active' && !meetingState && !verifyState && pendingVerify.length===0 && busy.size===0){
|
|
706
|
-
const stallMs=
|
|
731
|
+
const stallMs=posMs(params.stallAutoMeetingMs, posMs(params.activityTimeoutMs,120000)*3)
|
|
707
732
|
if(now()-lastProgressAt>=stallMs){
|
|
708
733
|
await startMeeting('团队较长时间没有新进展。请你们自行讨论:当前问题是否已解决、开放难点是什么、谁负责哪部分、下一步如何推进,并自主决定是否继续。框架只负责转达与记录,不替你们决定。','general',null)
|
|
709
734
|
return
|
|
@@ -716,7 +741,7 @@ export function apply(ctx) {
|
|
|
716
741
|
// On a FAILED wake we re-arm the heartbeat so a single follow-up error NEVER permanently stops
|
|
717
742
|
// the group (a successful wake re-drives scheduleNext through its own onResidentEnd, which re-arms).
|
|
718
743
|
clearHeartbeat()
|
|
719
|
-
const atOs=
|
|
744
|
+
const atOs=posMs(params.activityTimeoutMs,120000)
|
|
720
745
|
// `mp` (maxParallel) is already declared above in this function scope.
|
|
721
746
|
// Collect idle (not busy) residents sorted by idle time, oldest-first (round-robin fairness).
|
|
722
747
|
const idleCandidates = Array.from(residents.values())
|
|
@@ -759,7 +784,7 @@ export function apply(ctx) {
|
|
|
759
784
|
let delivered=false
|
|
760
785
|
for(const [to,msgs] of mailboxes){
|
|
761
786
|
if(msgs.length===0) continue
|
|
762
|
-
const r=residents.get(to); if(!r){ continue }
|
|
787
|
+
const r=residents.get(to); if(!r){ mailboxes.delete(to); continue } // stale recipient → drop the entry
|
|
763
788
|
if(busy.has(to)) continue // recipient busy → leave the message queued for a later pass
|
|
764
789
|
const mp=Number(params.maxParallel)||0
|
|
765
790
|
if(mp>0 && busy.size>=mp) break // concurrency cap reached → stop delivering more now
|
|
@@ -823,7 +848,7 @@ export function apply(ctx) {
|
|
|
823
848
|
}
|
|
824
849
|
// normal turn
|
|
825
850
|
if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
|
|
826
|
-
if(typeof parsed.solved==='boolean') reports.push({rId:r.rId,solved:parsed.solved,summary:parsed.summary||'',at:now()})
|
|
851
|
+
if(typeof parsed.solved==='boolean'){ reports.push({rId:r.rId,solved:parsed.solved,summary:parsed.summary||'',at:now()}); if(reports.length>100) reports.shift() } // solved-signal ring (not surfaced in status/report)
|
|
827
852
|
if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
|
|
828
853
|
// group-conversation relay: the resident may choose to speak to the whole team (input) —
|
|
829
854
|
// forward it to the others so this is a real discussion group, not private monologues.
|
|
@@ -851,6 +876,7 @@ export function apply(ctx) {
|
|
|
851
876
|
if(!problemText) return {ok:false,message:'problem text required (pass problem, or use vibe_v4_configure first)'}
|
|
852
877
|
problemId=slugify(problemText.slice(0,40))||'problem'
|
|
853
878
|
if(residentCount) params.residentCount=Number(residentCount)||4
|
|
879
|
+
if(!(Number(params.residentCount)>=1)) params.residentCount=DEFAULT_PARAMS.residentCount // a 0/negative count (settings misconfig) would spawn nobody & idle forever
|
|
854
880
|
running=true; autoDone=false; phase='brainstorm'
|
|
855
881
|
await writeText('Problems/'+problemId+'.md','# 问题|'+problemId+'\n- ID: '+problemId+'\n- 类型: 问题\n- 状态: 求解中\n- 优先级: 1\n- 依赖: []\n\n## 陈述\n'+problemText+'\n')
|
|
856
882
|
// A reused session may still have OLD residents in flight from a previous run (start is a FRESH
|
|
@@ -865,7 +891,15 @@ export function apply(ctx) {
|
|
|
865
891
|
await saveAll(); return {ok:true,message:'v4 started: '+params.residentCount+' resident(s) brainstorming',project:currentProject}
|
|
866
892
|
}
|
|
867
893
|
async function resume(){
|
|
868
|
-
currentProject=await readCurrentProject(); await ensureDirs()
|
|
894
|
+
currentProject=await readCurrentProject(); await ensureDirs()
|
|
895
|
+
// Resume is only meaningful for a stopped/paused/crashed run. If THIS process is already driving
|
|
896
|
+
// a live run whose disk state belongs to it, loadAll below would overwrite the in-memory state
|
|
897
|
+
// with a slightly stale snapshot (busy marks, wake round counters, mailbox contents from the last
|
|
898
|
+
// saveAll) — a silent clobber for a useless "kick". No-op instead. A cross-process restart is
|
|
899
|
+
// always allowed: its disk epoch differs, so the in-memory state is empty/stale anyway.
|
|
900
|
+
const pre=await readJson('State/session.json')
|
|
901
|
+
if(running && !autoDone && pre && pre.processEpoch===processEpoch) return {ok:true,message:'already running (no-op)'}
|
|
902
|
+
await loadAll(); await loadSettings()
|
|
869
903
|
// A run the group CONCLUDED (unanimous voteSolved → autoDone) must not be silently revived into
|
|
870
904
|
// a zombie that keeps waking residents with no consensus that it should still run. The group
|
|
871
905
|
// decided it is done; continuing means a NEW run (vibe_v4_start / vibe_v4_configure).
|
|
@@ -912,7 +946,12 @@ export function apply(ctx) {
|
|
|
912
946
|
pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
|
|
913
947
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
914
948
|
meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
|
|
915
|
-
async function addMember(direction){
|
|
949
|
+
async function addMember(direction){
|
|
950
|
+
// Adding a member starts a REAL resident turn (spawnResident → brainstorm) — refuse unless the
|
|
951
|
+
// run is live: on a concluded (autoDone) or never-started/paused run the new member would work
|
|
952
|
+
// with nobody to coordinate (zombie work on a project the group already declared done).
|
|
953
|
+
if(!running || autoDone) return {ok:false,message:'no active run to join (start or resume first)'}
|
|
954
|
+
const r=newResident(direction||''); await spawnResident(r)
|
|
916
955
|
// Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
|
|
917
956
|
// residents) can never be true for the new member (not in the snapshot order) and the meeting is
|
|
918
957
|
// only ever released by the stuck watchdog instead of finalizing with everyone's input.
|
|
@@ -953,6 +992,11 @@ export function apply(ctx) {
|
|
|
953
992
|
// Create/configure a project and set params/problem WITHOUT starting any resident.
|
|
954
993
|
// The intended flow: vibe_v4_configure {project?, problem?, params?} → vibe_v4_start {}.
|
|
955
994
|
async function configure(cfg){
|
|
995
|
+
// configure is the PRE-START setup tool (project/problem/params). Switching the project while a
|
|
996
|
+
// run is LIVE would split the run's state across two trees: residents' briefs & libraries point
|
|
997
|
+
// at the OLD frameworkRoot while every subsequent saveAll/transcript/Verified card would go to the
|
|
998
|
+
// NEW project. Params tuning mid-run belongs to vibe_v4_set.
|
|
999
|
+
if(running && !autoDone) return {ok:false,message:'cannot configure while a run is running (pause or abort first; use vibe_v4_set to tune params)'}
|
|
956
1000
|
if(cfg && cfg.project && String(cfg.project).trim()) currentProject=String(cfg.project).trim()
|
|
957
1001
|
if(cfg && cfg.problem) problemText=String(cfg.problem)
|
|
958
1002
|
if(cfg && cfg.params && typeof cfg.params==='object') setParams(cfg.params)
|
|
@@ -962,7 +1006,13 @@ export function apply(ctx) {
|
|
|
962
1006
|
await saveAll()
|
|
963
1007
|
return {ok:true,project:currentProject,problem:problemText?problemText.slice(0,60):'',params:Object.keys(params).map(k=>k+'='+params[k]).join(', ')}
|
|
964
1008
|
}
|
|
965
|
-
async function initAbort(){ clearHeartbeat(); running=false; phase='idle'; autoDone=false; for(const [,r] of residents){ if(r.childId){ try{ subagents.interrupt(r.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } r.childId=''; r.lastActiveAt=0; r.roundsSinceCompact=0 }
|
|
1009
|
+
async function initAbort(){ clearHeartbeat(); running=false; phase='idle'; autoDone=false; for(const [,r] of residents){ if(r.childId){ try{ subagents.interrupt(r.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } r.childId=''; r.lastActiveAt=0; r.roundsSinceCompact=0 }
|
|
1010
|
+
// Wipe the coordination state too: an aborted run must not report an in-flight meeting/verify,
|
|
1011
|
+
// a parked meeting, a verify queue, or busy residents (their childIds are gone, so no end event
|
|
1012
|
+
// can ever clear those marks). resume()/start() re-initialize anyway; this keeps status truthful
|
|
1013
|
+
// between abort and the next action.
|
|
1014
|
+
meetingState=null; verifyState=null; pendingMeeting=null; pendingVerify=[]; busy=new Set(); wakeKind=new Map(); currentResident=''; finalizeLock=null
|
|
1015
|
+
await saveAll(); return {ok:true,message:'aborted'} }
|
|
966
1016
|
function setPause(){ clearHeartbeat(); running=false; return {ok:true,message:'paused'} }
|
|
967
1017
|
|
|
968
1018
|
return {
|
|
@@ -970,6 +1020,9 @@ export function apply(ctx) {
|
|
|
970
1020
|
onResidentEnd, start, resume, status, report, addMember, removeMember, setParams,
|
|
971
1021
|
setPause, initAbort, postMessage, startMeeting, saveAll, broadcast, configure, loadSettings,
|
|
972
1022
|
currentResident:()=>currentResident,
|
|
1023
|
+
// safety kick: drive one scheduler pass (used when an end handler errored, so an exceptional
|
|
1024
|
+
// turn can never leave the group with no end-event and no heartbeat to continue it)
|
|
1025
|
+
nudge:()=>scheduleNext().catch(()=>{}),
|
|
973
1026
|
residentIdOf:(agent)=>{ const m=residentOfAgent(agent); if(m) return m; const c=currentResident; return (c && residents.has(c)) ? c : '' },
|
|
974
1027
|
useResident:(id)=>{ currentResident=id },
|
|
975
1028
|
publishProgress, recordProposition, recordMethod, recordSubproblem, listResidents, reportContext,
|
|
@@ -1010,7 +1063,7 @@ export function apply(ctx) {
|
|
|
1010
1063
|
registerTool('vibe_v4_set','Set V4 parameters. model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line.',objParams({residentCount:{type:'integer'},compactAfterRounds:{type:'integer'},compactThreshold:{type:'integer'},meetingKeepEvery:{type:'integer'},maxParallel:{type:'integer'},activityTimeoutMs:{type:'integer'},verdictMaxRounds:{type:'integer'},stallAutoMeetingMs:{type:'integer'},provider:{type:'string'},model:{type:'string'},residentPersona:{type:'string'},toolAllow:{type:'array',items:{type:'string'}},toolDeny:{type:'array',items:{type:'string'}}}),(s,a)=>{ s.setParams(a); return {ok:true} })
|
|
1011
1064
|
// resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
|
|
1012
1065
|
// fall back to the last-woken resident when called by the host/assistant.
|
|
1013
|
-
registerTool('vibe_v4_send_message','(resident) Send a message to another resident.',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a,x)=>s.
|
|
1066
|
+
registerTool('vibe_v4_send_message','(resident) Send a message to another resident (to=all broadcasts to the whole team).',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a,x)=>{ const from=s.residentIdOf(x); if(!from) return {ok:false,message:'no such resident'}; if(String(a.to)==='all') return s.broadcast(a.content, from); return s.postMessage(from,a.to,a.content) })
|
|
1014
1067
|
registerTool('vibe_v4_publish_progress','(resident) Append to your own progress markdown.',objParams({content:{type:'string'}},['content']),(s,a,x)=>s.publishProgress(s.residentIdOf(x),a.content))
|
|
1015
1068
|
registerTool('vibe_v4_record_proposition','(resident) Record a proposition to your library.',objParams({id:{type:'string'},title:{type:'string'},statement:{type:'string'},prob:{type:'number'},value:{type:'number'},motivation:{type:'string'}}),(s,a,x)=>s.recordProposition(s.residentIdOf(x),a))
|
|
1016
1069
|
registerTool('vibe_v4_record_method','(resident) Record a method/theory to your library.',objParams({id:{type:'string'},title:{type:'string'},type:{type:'string'},content:{type:'string'},notation:{type:'string'},value:{type:'number'},motivation:{type:'string'}}),(s,a,x)=>s.recordMethod(s.residentIdOf(x),a))
|
|
@@ -1053,6 +1106,6 @@ export function apply(ctx) {
|
|
|
1053
1106
|
|
|
1054
1107
|
ctx.on('subagent/end', function(info){
|
|
1055
1108
|
const sid=childOwner.get(info.id); const s=sid!==undefined?sessions.get(sid):undefined
|
|
1056
|
-
if(s) s.onResidentEnd(info.id, info).catch(e=>console.error('vibe-v4 end: '+String((e&&e.stack)||e)))
|
|
1109
|
+
if(s) s.onResidentEnd(info.id, info).catch(e=>{ console.error('vibe-v4 end: '+String((e&&e.stack)||e)); if(s.nudge) s.nudge() })
|
|
1057
1110
|
})
|
|
1058
1111
|
}
|
|
@@ -570,7 +570,38 @@ VibeMath/Projects/<project>/
|
|
|
570
570
|
>
|
|
571
571
|
> 说明:无法对"某常驻真的拒绝/掉线"的情况达成全体一致时,看门狗会把该对象保留为"未定论/带概率",这是哲学上期望的诚实结果。`session.json` 仍不持久化 `meetingState`(进行中的会议不跨进程恢复),B 的停滞看门狗在重启后仍会触发,因此重启也能自愈。
|
|
572
572
|
>
|
|
573
|
-
> 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes`
|
|
573
|
+
> 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 120/120(T1–T41,后续审计回归见 §27–§30)、v3 100/100、v2 business 18/regression 14、multisession 25/25。
|
|
574
|
+
|
|
575
|
+
## 30. 第六/七轮全面深度审计(npm v2.0.21,迭代至无新发现)
|
|
576
|
+
|
|
577
|
+
按"不断检查、修复直至没有新问题"的要求连续多轮审计:本轮修复 6 处 + 2 处加固(新增回归 T38–T41),其后两轮全量通读与一致性扫描**未再发现新问题**,审计收敛。
|
|
578
|
+
|
|
579
|
+
- **全票停会后协调态残留**:`finalizeMeeting` 的 allSolved 分支直接 return,留下 `meetingState`/`wakeKind`/pending 态 → status 永久谎报 `meetingInProgress:true`(v2.0.20 修了 initAbort 却漏了 stop 路径)。修复:stop 分支与 initAbort 对称清空全部协调态。**T38**。
|
|
580
|
+
- **已定论/未运行 run 上 addMember 仍开工**:`addMember` → `spawnResident` 会真的启动一轮头脑风暴——在已全票定论的项目上制造僵尸工作。修复:`!running || autoDone` 时拒绝。**T38**。
|
|
581
|
+
- **验证目标/记录 id 路径穿越**:`propose_verify` 或 record 工具传入含 `/`、`\`、`:` 等的 id 时,Verified 卡/辩论文档/来源回写路径可逃出项目树或写入他人目录。修复:`idSafe` 消毒(保留含中文在内的无害字符,仅替换分隔符/非法字符/控制字符,去首尾点与横线),在 `maybeQueueVerify`(唯一入队漏斗)与三个 record 工具处统一应用。**T39**。
|
|
582
|
+
- **resident 工具 `to=all` 声称支持实际报错**:toolList 宣称 `vibe_v4_send_message {to:all}` 可广播,但 resident 版直接 `residents.get('all')` → no such resident。修复:resident 调用 `to=all` 走 `broadcast(content, from)`,且**跳过自己**(host 广播不受影响)。**T40**。
|
|
583
|
+
- **负值/NaN 时长参数灾难**:`vibe_v4_set {activityTimeoutMs:-50}` 会让 `recoverStallMs()` 为负 → 会议/验证看门狗**立即放弃一切共识**;A-fill 空闲窗口永不流逝(每 pass 唤醒全员)。修复:`posMs(v,def)` 兜底所有时长读取(recoverStallMs、makeSignal、armHeartbeat、A-fill atOs、B 停滞阈值)。**T41**。
|
|
584
|
+
- **residentCount<=0 启动空转**:settings 误配 0/负值 → start 不生成任何常驻却 running=true。修复:start 时回落默认值。
|
|
585
|
+
- **加固**:`deliverNextMailbox` 顺带清理已不存在常驻的陈旧邮箱条目;`subagent/end` 处理异常时(理论上所有 IO 均已内捕,此为最后防线)调用会话 `nudge()` 补一针调度,杜绝"异常回合后无 end、无心跳"的静默停摆。
|
|
586
|
+
|
|
587
|
+
> 审计收敛声明:此后两轮对全文件(1111 行)逐函数通读 + 交叉扫描,仅剩以下**已记录设计边界**(均有意识保留、非缺陷):brainstorm 首轮 end 永不触发无看门狗(框架外故障);会议/验证"末位在途成员"不被心跳打断(避免误杀慢成员,真实 DSH 回合必有 end);暂停中显式 `postMessage` 仍投递(人的明确指令);默认工具全继承(T11 钉住,宿主级工具未写入 resident toolList);邮箱在超长共识链后送达(§27);`State` 与进程内共识态不跨进程持久化(§26 说明,重启后 A/B 保活自愈)。
|
|
588
|
+
>
|
|
589
|
+
> 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 120/120(T1–T41)、v3 100/100、v2 business 18/regression 14、multisession 25/25、selfdrive-v3 0 异常。
|
|
590
|
+
|
|
591
|
+
## 29. 第五轮全面深度审计(npm v2.0.20,遗留区域 + 未修改逻辑)
|
|
592
|
+
|
|
593
|
+
对历轮尚未触达的"牵连对象/未改逻辑"做复查,发现并修复 5 处(新增回归 T33–T37):
|
|
594
|
+
|
|
595
|
+
- **运行中 resume 用陈旧磁盘快照覆盖内存态(静默 clobber)**:resume 先 `loadAll` 再用磁盘状态覆盖内存——若某进程已在本进程内驱动着活 run(有人把 resume 当"踢一脚"用),内存里比磁盘新一个事件的 busy/轮次/邮箱状态会被回退到最近一次 saveAll 的快照。修复:resume 开头预读 `session.json`,若内存 `running && !autoDone` **且磁盘 epoch 属于本进程**(同进程活 run),直接返回 `{ok:true,message:'already running (no-op)'}`——不做任何加载;跨进程重启(epoch 不同)不受影响(A4 场景依旧放行)。**T33**。
|
|
596
|
+
- **configure 在运行中切换项目 → 状态分裂到两棵树**:configure 是"启动前"工具,但运行中调用会把 currentProject 改到新项目:常驻的简报/资料库仍在旧树,而之后的 saveAll/会议纪要/Verified 卡全写到新树。修复:`running && !autoDone` 时拒绝并提示改用 `vibe_v4_set` 调参数。**T34**。
|
|
597
|
+
- **abort 后 status 谎报在途协调态**:initAbort 只停调度并清 childId,会议/验证/暂存会议/验证队列/busy 全部残留——被中断常驻的 end 因 childId 已清无法命中,busy 印记永远无人清除,status 显示"会议进行中/验证中/busy=[r-1]",而 run 早已 running=false。修复:initAbort 一并清空 `meetingState/verifyState/pendingMeeting/pendingVerify/busy/wakeKind/currentResident/finalizeLock`。**T35**。
|
|
598
|
+
- **暂停期间 claim 任务会僵尸唤醒认领者**:v2.0.18 的暂停门控只覆盖了 end 处理器;`claimTask` 的直接唤醒绕过了它——暂停时某个 end 回复里 `claim_task`(或宿主工具认领)会把认领者拉起来做一轮新工作。修复:认领记录照常入板,但唤醒加 `running && !autoDone` 门控(认领者自己知道认领了什么,恢复后自然接续)。**T36**。
|
|
599
|
+
- **reports/meetings 无界膨胀(session.json 每事件全量重写)**:`reports` 只写不读,且每个普通轮 end(模板自带 `solved:false`)都 push 一条;`meetings` 数组把**完整会议全文**再存一份(磁盘 `Shared/meetings/<id>.md` 已有),长 run 中 session.json 越来越大、每次 saveAll 都重写整份。修复:reports 上限 100(环形),meetings 只存 {id,agenda,at} 小索引(上限 200)——全文仍在磁盘,会议计数/报告不受影响。**T37**。
|
|
600
|
+
- **连带小修**:`postmark` 的 contextPct 只认 number,模型报数字字符串("40")时不生效;现兼容数字字符串。
|
|
601
|
+
|
|
602
|
+
> 边界复查(未改,记录):暂停中 `postMessage`(显式宿主动作)仍会投递唤醒(与"认领"不同,是人的明确指令);brainstorm 期首轮 end 永不触发、以及"邮箱在超长共识链后送达"维持 §27/§28 的设计边界。
|
|
603
|
+
>
|
|
604
|
+
> 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 107/107(T1–T37)、v3 100/100、v2 business 18/regression 14、multisession 25/25、selfdrive-v3 0 异常。
|
|
574
605
|
|
|
575
606
|
## 27. 第三轮全面深度审计(npm v2.0.18,牵连对象/逻辑)
|
|
576
607
|
|