dsh-vibe-math 2.0.18 → 2.0.20

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.18",
4
+ "version": "2.0.20",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -233,7 +233,7 @@ export function apply(ctx) {
233
233
  +'\n团队成员:\n'+banner()+'\n'
234
234
  +'New items:\n'+ (await inboxText(r.rId)) +'\n'
235
235
  +'Reply with ONLY a JSON object:\n'
236
- +'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40}'
236
+ +'{"summary":"<what you did / decided this round, 1-3 sentences>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<task id|null>","task_done":"<task id|null>","contextPct":40}'
237
237
  }
238
238
  function meetingPrompt(r, st){
239
239
  const prior=Object.entries(st.inputs).filter(([k])=>k!==r.rId).map(([k,iv])=>' ['+k+'] '+String(iv.input||iv.summary||'')).join('\n')
@@ -321,10 +321,10 @@ export function apply(ctx) {
321
321
  function byChild(childId){ for(const [,r] of residents){ if(r.childId===childId) return r } return undefined }
322
322
 
323
323
  // ---- artifact writers (resident-facing) ----
324
- async function publishProgress(rId,content){ const rel='Progress/'+rId+'/progress.md'; const prev=(await readText(rel))||''; await writeText(rel, prev+'\n### '+fmtTime()+'|'+rId+'\n'+String(content||'')+'\n'); return {ok:true} }
325
- async function recordProposition(rId,o){ const id=o.id||('p-'+shortId()); const lines=['# 命题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: 命题','- 状态: 未定论','- 概率: '+cl(o.prob!=null?o.prob:0.5),'- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 证明尝试','','## 证伪尝试','']; await writeText('Propos/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 命题 '+id); bumpArtifacts(); return {ok:true,id,file:'Propos/'+rId+'/'+id+'.md'} }
326
- async function recordMethod(rId,o){ const id=o.id||('m-'+shortId()); const lines=['# 方法|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: '+(o.type||'方法'),'- 状态: 经验','- 可信断言: []','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'','## 核心内容',String(o.content||''),'','## 定义与记号',String(o.notation||''),'','## 应用记录','## 改进历史','']; await writeText('Methods/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 方法 '+id); bumpArtifacts(); return {ok:true,id,file:'Methods/'+rId+'/'+id+'.md'} }
327
- async function recordSubproblem(rId,o){ const id=o.id||('s-'+shortId()); const lines=['# 子问题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 状态: 求解中','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 进度','']; await writeText('Subproblems/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 子问题 '+id); bumpArtifacts(); return {ok:true,id,file:'Subproblems/'+rId+'/'+id+'.md'} }
324
+ async function publishProgress(rId,content){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const rel='Progress/'+rId+'/progress.md'; const prev=(await readText(rel))||''; await writeText(rel, prev+'\n### '+fmtTime()+'|'+rId+'\n'+String(content||'')+'\n'); return {ok:true} }
325
+ async function recordProposition(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id||('p-'+shortId()); const lines=['# 命题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: 命题','- 状态: 未定论','- 概率: '+cl(o.prob!=null?o.prob:0.5),'- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 证明尝试','','## 证伪尝试','']; await writeText('Propos/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 命题 '+id); bumpArtifacts(); return {ok:true,id,file:'Propos/'+rId+'/'+id+'.md'} }
326
+ async function recordMethod(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id||('m-'+shortId()); const lines=['# 方法|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: '+(o.type||'方法'),'- 状态: 经验','- 可信断言: []','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'','## 核心内容',String(o.content||''),'','## 定义与记号',String(o.notation||''),'','## 应用记录','## 改进历史','']; await writeText('Methods/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 方法 '+id); bumpArtifacts(); return {ok:true,id,file:'Methods/'+rId+'/'+id+'.md'} }
327
+ async function recordSubproblem(rId,o){ if(!rId||!residents.has(rId)) return {ok:false,message:'no such resident'} ; const id=o.id||('s-'+shortId()); const lines=['# 子问题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 状态: 求解中','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 进度','']; await writeText('Subproblems/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 子问题 '+id); bumpArtifacts(); return {ok:true,id,file:'Subproblems/'+rId+'/'+id+'.md'} }
328
328
  // auto-sync meeting: every meetingKeepEvery new artifacts, convene a general coordination meeting
329
329
  function bumpArtifacts(){ artifactCount+=1; markProgress(); if(!meetingState && !verifyState && !pendingMeeting && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
330
330
  function listResidents(){ return Array.from(residents.values()).map(r=>({id:r.rId,direction:r.direction,status:r.status,rounds:r.rounds,contextPct:r.contextPct,insight:r.insight?r.insight.slice(0,80):''})) }
@@ -338,8 +338,10 @@ export function apply(ctx) {
338
338
  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
339
  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
340
  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
- const r=residents.get(claimer); if(r && !busy.has(claimer)){ currentResident=claimer; await wakeResident(r, (await normalPrompt(r))+'\n\n[YOU CLAIMED TASK '+id+'] '+t.title+' '+t.description,'normal'); await saveAll() }
341
+ // wake the claimer to work on it (framework moves the task, resident decides how).
342
+ // NOT while paused/stopped: a paused run must not start new work the claim is recorded on the
343
+ // board and the resident (who claimed it) picks it up again after resume.
344
+ 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
345
  return {ok:true} }
344
346
  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
347
  async function saveTaskboard(){ await writeJson('State/taskboard.json',taskboard); await writeTaskboard() }
@@ -348,7 +350,7 @@ export function apply(ctx) {
348
350
  // Apply context/compact bookkeeping from a resident's reply, so the flag can clear even when
349
351
  // the reply came through a meeting/verify branch (defensive) as well as the normal branch.
350
352
  function postmark(r, parsed){
351
- if(typeof parsed.contextPct==='number') r.contextPct=clPct(parsed.contextPct)
353
+ const cp=Number(parsed.contextPct); if(Number.isFinite(cp)) r.contextPct=clPct(cp) // tolerate numeric strings ("40")
352
354
  if(parsed.compacted===true || (r.needCompact && parsed.summary)){
353
355
  r.contextSeed=String(parsed.summary||r.contextSeed||'')
354
356
  r.contextPct=Math.min(r.contextPct||15,25)
@@ -444,7 +446,10 @@ export function apply(ctx) {
444
446
  const lines=['# 会议 '+st.id+'|'+fmtTime(),'','**议程**:'+st.agenda,'']
445
447
  for(const [id,iv] of Object.entries(st.inputs)){ lines.push('### '+id); lines.push(iv.input||''); lines.push('') }
446
448
  await writeText('Shared/meetings/'+st.id+'.md', lines.join('\n'))
447
- meetings.push({id:st.id,agenda:st.agenda,at:now(),inputs:st.inputs})
449
+ // the full transcript lives on disk (Shared/meetings/<id>.md); the State array keeps a small
450
+ // index (id/agenda/at) so session.json does not carry a second copy of every transcript and
451
+ // rewrite it on EVERY saveAll during long runs (reports below are capped for the same reason).
452
+ meetings.push({id:st.id,agenda:st.agenda,at:now()}); if(meetings.length>200) meetings.shift()
448
453
  logDecision('meeting',st.agenda)
449
454
  // handle what the meeting produced: task proposals/claims, verify targets, stop vote
450
455
  for(const [id,iv] of Object.entries(st.inputs)){
@@ -653,7 +658,7 @@ export function apply(ctx) {
653
658
  return (params.residentPersona?params.residentPersona+'\n':'')
654
659
  +'Resident researcher '+r.rId+' — CHECKPOINT(团队空闲,请由你们继续自主推进)。当前项目尚未解决(除非你已确认)。团队在等待有人继续:请**继续解决这个问题**——读他人的库对齐、推进某个子问题/引理/方法、尝试一条路线;或向团队发消息(input)、提议任务(propose_task)让大家分工。若你确实认为问题已解决、或已彻底无路可走,才提议开会(propose_meeting)让团队表决/商量、或声明 solved=true。默认立场是:**请推进,而不是停在原地。**\n'
655
660
  +'Reply with ONLY a JSON object:\n'
656
- +'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","claim_task":"<id|null>","contextPct":40}'
661
+ +'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","task_desc":"<optional: why this task matters / what it covers|null>","claim_task":"<id|null>","contextPct":40}'
657
662
  }
658
663
  function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
659
664
  function armHeartbeat(){
@@ -774,7 +779,13 @@ export function apply(ctx) {
774
779
  // ---- resident end handler ----
775
780
  async function onResidentEnd(childId, info){
776
781
  const r=byChild(childId); if(!r) return
777
- busy.delete(r.rId)
782
+ // A turn that is NOT marked busy is a duplicate/stale end (the same subagent/end delivered twice,
783
+ // or an end for a turn already settled). Without this guard every side effect below — task
784
+ // proposal, group relay, verify queueing, meetings.push — would run a SECOND time (the
785
+ // duplicate-task/duplicate-stop class from test9 reappears whenever a host re-delivers an end).
786
+ // Every legitimate end corresponds to a busy turn: busy is added at spawn/wake and cleared only
787
+ // here, on wake failure, on removeMember, or on respawn (whose stale childIds no longer match).
788
+ if(!busy.delete(r.rId)) return
778
789
  // Any resident turn that COMPLETED is real activity for the stall clock (B). Residents frequently
779
790
  // write their libraries via direct fs (not the record* tools), so relying only on
780
791
  // bumpArtifacts/markProgress would leave lastProgressAt stale and B would fire against an active
@@ -817,7 +828,7 @@ export function apply(ctx) {
817
828
  }
818
829
  // normal turn
819
830
  if(r.status==='brainstorm'){ r.insight=parsed.summary||output; r.status='active' }
820
- if(typeof parsed.solved==='boolean') reports.push({rId:r.rId,solved:parsed.solved,summary:parsed.summary||'',at:now()})
831
+ 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)
821
832
  if(parsed.propose_verify) maybeQueueVerify(parsed.propose_verify, r.rId)
822
833
  // group-conversation relay: the resident may choose to speak to the whole team (input) —
823
834
  // forward it to the others so this is a real discussion group, not private monologues.
@@ -859,7 +870,15 @@ export function apply(ctx) {
859
870
  await saveAll(); return {ok:true,message:'v4 started: '+params.residentCount+' resident(s) brainstorming',project:currentProject}
860
871
  }
861
872
  async function resume(){
862
- currentProject=await readCurrentProject(); await ensureDirs(); await loadAll(); await loadSettings()
873
+ currentProject=await readCurrentProject(); await ensureDirs()
874
+ // Resume is only meaningful for a stopped/paused/crashed run. If THIS process is already driving
875
+ // a live run whose disk state belongs to it, loadAll below would overwrite the in-memory state
876
+ // with a slightly stale snapshot (busy marks, wake round counters, mailbox contents from the last
877
+ // saveAll) — a silent clobber for a useless "kick". No-op instead. A cross-process restart is
878
+ // always allowed: its disk epoch differs, so the in-memory state is empty/stale anyway.
879
+ const pre=await readJson('State/session.json')
880
+ if(running && !autoDone && pre && pre.processEpoch===processEpoch) return {ok:true,message:'already running (no-op)'}
881
+ await loadAll(); await loadSettings()
863
882
  // A run the group CONCLUDED (unanimous voteSolved → autoDone) must not be silently revived into
864
883
  // a zombie that keeps waking residents with no consensus that it should still run. The group
865
884
  // decided it is done; continuing means a NEW run (vibe_v4_start / vibe_v4_configure).
@@ -897,12 +916,14 @@ export function apply(ctx) {
897
916
  function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
898
917
  residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
899
918
  meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
919
+ parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
900
920
  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(', ') } }
901
921
  function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
902
922
  residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
903
923
  meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+residents.size}:null,
904
924
  verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.size}:null,
905
925
  pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
926
+ parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
906
927
  meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
907
928
  async function addMember(direction){ const r=newResident(direction||''); await spawnResident(r)
908
929
  // Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
@@ -914,11 +935,14 @@ export function apply(ctx) {
914
935
  return {ok:true,id:r.rId,direction:r.direction} }
915
936
  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=''
916
937
  // Reconcile in-progress coordination so a removed member cannot hang consensus or crash a round:
917
- // drop its meeting speech / verify verdict / pending-verify proposals if it owned them, and
918
- // prune it from the meeting's speaking order so the find() there never selects a ghost.
938
+ // drop its meeting speech / verify verdict and prune it from the meeting's speaking order so the
939
+ // find() there never selects a ghost. Its QUEUED verify proposals are deliberately KEPT: a
940
+ // proposal is a statement about an OBJECT the group can judge on its merits with its CURRENT
941
+ // members (allVoted recomputes over the live residents), and dropping the queue entry would also
942
+ // erase the intent of any OTHER member who independently proposed the same target (dedup keeps
943
+ // only the first entry, which may belong to the removed member).
919
944
  if(meetingState){ delete meetingState.inputs[id]; meetingState.order=(meetingState.order||[]).filter(x=>x!==id) }
920
945
  if(verifyState){ delete verifyState.verdicts[id] }
921
- if(pendingVerify.length) pendingVerify = pendingVerify.filter(p=>p.proposer!==id)
922
946
  await saveAll()
923
947
  // Re-drive the scheduler right away. If the removed member was the ONLY turn in flight (e.g. the
924
948
  // last unspoken meeting speaker / the last unvoted voter, interrupted mid-turn), NO subagent/end
@@ -942,6 +966,11 @@ export function apply(ctx) {
942
966
  // Create/configure a project and set params/problem WITHOUT starting any resident.
943
967
  // The intended flow: vibe_v4_configure {project?, problem?, params?} → vibe_v4_start {}.
944
968
  async function configure(cfg){
969
+ // configure is the PRE-START setup tool (project/problem/params). Switching the project while a
970
+ // run is LIVE would split the run's state across two trees: residents' briefs & libraries point
971
+ // at the OLD frameworkRoot while every subsequent saveAll/transcript/Verified card would go to the
972
+ // NEW project. Params tuning mid-run belongs to vibe_v4_set.
973
+ 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)'}
945
974
  if(cfg && cfg.project && String(cfg.project).trim()) currentProject=String(cfg.project).trim()
946
975
  if(cfg && cfg.problem) problemText=String(cfg.problem)
947
976
  if(cfg && cfg.params && typeof cfg.params==='object') setParams(cfg.params)
@@ -951,7 +980,13 @@ export function apply(ctx) {
951
980
  await saveAll()
952
981
  return {ok:true,project:currentProject,problem:problemText?problemText.slice(0,60):'',params:Object.keys(params).map(k=>k+'='+params[k]).join(', ')}
953
982
  }
954
- 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 } await saveAll(); return {ok:true,message:'aborted'} }
983
+ 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 }
984
+ // Wipe the coordination state too: an aborted run must not report an in-flight meeting/verify,
985
+ // a parked meeting, a verify queue, or busy residents (their childIds are gone, so no end event
986
+ // can ever clear those marks). resume()/start() re-initialize anyway; this keeps status truthful
987
+ // between abort and the next action.
988
+ meetingState=null; verifyState=null; pendingMeeting=null; pendingVerify=[]; busy=new Set(); wakeKind=new Map(); currentResident=''; finalizeLock=null
989
+ await saveAll(); return {ok:true,message:'aborted'} }
955
990
  function setPause(){ clearHeartbeat(); running=false; return {ok:true,message:'paused'} }
956
991
 
957
992
  return {
@@ -959,7 +994,7 @@ export function apply(ctx) {
959
994
  onResidentEnd, start, resume, status, report, addMember, removeMember, setParams,
960
995
  setPause, initAbort, postMessage, startMeeting, saveAll, broadcast, configure, loadSettings,
961
996
  currentResident:()=>currentResident,
962
- residentIdOf:(agent)=>{ const m=residentOfAgent(agent); return m||currentResident },
997
+ residentIdOf:(agent)=>{ const m=residentOfAgent(agent); if(m) return m; const c=currentResident; return (c && residents.has(c)) ? c : '' },
963
998
  useResident:(id)=>{ currentResident=id },
964
999
  publishProgress, recordProposition, recordMethod, recordSubproblem, listResidents, reportContext,
965
1000
  proposeTask, claimTask, taskDone, listTasks,
@@ -570,7 +570,22 @@ VibeMath/Projects/<project>/
570
570
  >
571
571
  > 说明:无法对"某常驻真的拒绝/掉线"的情况达成全体一致时,看门狗会把该对象保留为"未定论/带概率",这是哲学上期望的诚实结果。`session.json` 仍不持久化 `meetingState`(进行中的会议不跨进程恢复),B 的停滞看门狗在重启后仍会触发,因此重启也能自愈。
572
572
  >
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。
573
+ > 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 107/107(T1–T37,后续审计回归见 §27/§28/§29)、v3 100/100、v2 business 18/regression 14、multisession 25/25。
574
+
575
+ ## 29. 第五轮全面深度审计(npm v2.0.20,遗留区域 + 未修改逻辑)
576
+
577
+ 对历轮尚未触达的"牵连对象/未改逻辑"做复查,发现并修复 5 处(新增回归 T33–T37):
578
+
579
+ - **运行中 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**。
580
+ - **configure 在运行中切换项目 → 状态分裂到两棵树**:configure 是"启动前"工具,但运行中调用会把 currentProject 改到新项目:常驻的简报/资料库仍在旧树,而之后的 saveAll/会议纪要/Verified 卡全写到新树。修复:`running && !autoDone` 时拒绝并提示改用 `vibe_v4_set` 调参数。**T34**。
581
+ - **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**。
582
+ - **暂停期间 claim 任务会僵尸唤醒认领者**:v2.0.18 的暂停门控只覆盖了 end 处理器;`claimTask` 的直接唤醒绕过了它——暂停时某个 end 回复里 `claim_task`(或宿主工具认领)会把认领者拉起来做一轮新工作。修复:认领记录照常入板,但唤醒加 `running && !autoDone` 门控(认领者自己知道认领了什么,恢复后自然接续)。**T36**。
583
+ - **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**。
584
+ - **连带小修**:`postmark` 的 contextPct 只认 number,模型报数字字符串("40")时不生效;现兼容数字字符串。
585
+
586
+ > 边界复查(未改,记录):暂停中 `postMessage`(显式宿主动作)仍会投递唤醒(与"认领"不同,是人的明确指令);brainstorm 期首轮 end 永不触发、以及"邮箱在超长共识链后送达"维持 §27/§28 的设计边界。
587
+ >
588
+ > 全套测试仍全绿:`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
589
 
575
590
  ## 27. 第三轮全面深度审计(npm v2.0.18,牵连对象/逻辑)
576
591
 
@@ -587,6 +602,19 @@ VibeMath/Projects/<project>/
587
602
 
588
603
  > 设计边界复查确认(未改):① 会议/验证轮次唤醒**不额外受 maxParallel 限制**——共识需要全员发言/投票;每个调度 pass 只唤醒一个未发言/未投票者,由各 end 事件串行驱动(残留的普通轮可能造成短暂并发,但不会无限累积);② 非一致(平均概率留库)的对象可被后续提议**再次端到端验证**(去重窗口只挡"刚定论为真/假"的对象)——重复提议者是在不知情时提议的,重验一轮不算错;③ 验证中途 `addMember` 的新成员会被要求投票(v2.0.17 注释行为,保持);④ 已定论 run 的成果随时可在磁盘读取,resume 拒绝只针对"自动复活"。
589
604
 
605
+ ## 28. 第四轮全面深度审计(npm v2.0.19,对 v2.0.18 修复的牵连检查)
606
+
607
+ 对 v2.0.18 的修复(FIFO 验证队列、会议暂存、暂停门控、writeJson 串行化等)做连带对象复查,发现并修复 4 处(新增回归 T29–T32):
608
+
609
+ - **同回合重复 `subagent/end` 会把每个副作用跑两遍**:`onResidentEnd` 对"已结算回合的重复/迟到 end"无守卫——重复投递同一 end(宿主重放、桥接层抖动)会让 propose_task / 群聊转发 / 验证入队 / meetings.push **全部二次执行**(test9 的重复任务/重复 stop 一类问题在"重复 end"场景会原样复发)。修复:回合必须以 busy 标记在册为前提——`if(!busy.delete(r.rId)) return`(每次合法 end 都对应一次 spawn/wake 置位的 busy;busy 仅在本处、唤醒失败、removeMember、重生成时清除,而后者 stale childId 已无法 byChild 命中)。**T29**:同一 tick 内连发两次相同 end → 任务只提议一次、群聊只转发一次。
610
+ - **空/失效 rId 的 resident 写入工具会制造库根散卡**:`currentResident` 被复位为 `''`(如移除最后一名常驻后)时,主机/未知代理调用 `vibe_v4_record_*`/`vibe_v4_publish_progress` 会把路径 `Propos//p-x.md` 塌缩成**库根目录的散乱卡片**(正是 test9 曾出现的 0 字节散卡一类问题)。修复:`residentIdOf` 回退仅当 `currentResident` 仍真实存在时生效,否则返回 `''`;四个库写入函数对空/未知 rId 直接拒绝 `{ok:false}`(主机以 `currentResident` 代写这一既有便利不受影响——自驱动测试仍走该回退)。**T30**。
611
+ - **移除者的已排队验证提议被丢弃 → 提议静默蒸发(含"第二提议者意图被去重吞掉"边角)**:v2.0.12 遗留的"移除提议者则清掉其 pendingVerify"在 FIFO 队列下会把该成员的排队提议**整条删除**;且同目标被两人提议时去重只保留第一人的条目——若第一人被移除,第二人的意图同样蒸发。修复:`removeMember` **不再过滤验证队列**——提议是关于对象的陈述,由**当前成员**按对象价值共识判定(allVoted 本就按实时成员集计算,写回扫描也不依赖提出者存续)。**T31**:r-3 提议 p-x 后在 p-y 验证中被移除 → p-x 仍由剩余成员完整验证到 Verified/。
612
+ - **暂存的会议对用户不可见 / 普通轮提议任务永远没有描述**:`status/report` 新增 `parkedMeeting`(暂存会议议程),便于确认"会议没丢、在排队"(**T32** 同时钉住"brainstorm 期开会 → 暂存 → bootstrap 后真正召开",不再被看门狗误杀);普通轮/心跳提示词模板补上 `task_desc` 字段(此前普通轮提议任务时描述恒为空,只有会议轮能带描述)。
613
+
614
+ > 边界复查(未改,记录):brainstorm 阶段若某常驻首轮 end 永不触发,run 停在 brainstorm(brainstorm 无看门狗、无心跳可唤醒"已空闲"者推进)——真实 DSH 的轮次必然以 completed/error/timeout 结束并触发 end,纯属框架外故障,不做可能"静默丢掉慢成员声音"的自动跳过;`Shared/taskboard.md`(人类视图)由调用时快照生成、非逐文件串行,最坏情况短暂滞后于 `State/taskboard.json`(权威源),下次任务动作即自愈。
615
+ >
616
+ > 全套测试仍全绿:`selfdrive-v4` 21/21、`e2e-v4-fixes` 94/94(T1–T32)、v3 100/100、v2 business 18/regression 14、multisession 25/25、selfdrive-v3 0 异常。
617
+
590
618
 
591
619
 
592
620