dsh-vibe-math 2.0.3 → 2.0.4

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/README.md CHANGED
@@ -40,6 +40,7 @@
40
40
  **一句话流水线**:起始产生 N 个**常驻子代理**(continuable,持久上下文)先各自头脑风暴、产出初始见解/方向 → 此后**所有任务安排由它们互相留言 + 集体开会自主决定**(框架只做消息总线/会议/任务板/产物沉淀,**绝不分配任务**);每个常驻把有价值的产物按**价值程度 / 动机用途计划 / 自身概率估计**沉淀到**自己**的 `Progress/<id>/`、`Propos/<id>/`、`Methods/<id>/`、`Subproblems/<id>/` 库,并**可互相阅读**;验证由它们**自行商议**发起,**仅当全体常驻一致(真或假)**才写入 `Verified/`,否则留库附概率;常驻上下文量达阈值(默认 66%)自动 `/compact`;**仅当全体一致认为原问题已解决**才停止;可随时人工干预/增开/关闭常驻,支持断点续跑。
41
41
 
42
42
  > 说明:V4 去掉 v3 的中央规划器与确定性角色(explorer/solver/verifier/planner/method-keeper),把"研究者"本身作为主体。详见 `vibe-math-v4/实现方案.md`。
43
+ > 保活机制(分级保活 A+B):团伙空闲超 `activityTimeoutMs` 会收到**自驱动** CHECKPOINT(建议继续解决/发消息/提议任务,而非"是否要停止"),唤醒失败会自动重新武装心跳;若团队空闲且**无新产物**超过 `stallAutoMeetingMs`(默认 6 分钟),框架会自动召集一次同步会议让常驻们自行决定下一步——因此小组不会"跑完就永久停滞"(框架只促成、从不指派任务)。
43
44
 
44
45
  ---
45
46
 
@@ -416,7 +417,8 @@ dsh plugin --profile <你的 profile> add github:ChongCyrus/Vibe-Mathematics
416
417
  | `compactAfterRounds` | 8 | 常驻每累计 N 轮(未压缩)触发一次软压缩 |
417
418
  | `meetingKeepEvery` | 5 | 每积累 N 个新产物自动触发一次同步会议 |
418
419
  | `maxParallel` | 3 | 同时唤醒的常驻上限(框架侧并发闸,非指派) |
419
- | `activityTimeoutMs` | 120000 | 空闲心跳间隔(超时才触发 CHECKPOINT 唤醒,推动收敛) |
420
+ | `activityTimeoutMs` | 120000 | 空闲心跳间隔(超时才触发**自驱动** CHECKPOINT 唤醒,推动常驻继续推进;唤醒失败会自动重新武装心跳,保证小组永不永久停死) |
421
+ | `stallAutoMeetingMs` | 360000 | **停滞自动同步会议阈值**(分级保活 B):团队空闲且无新产物超过该时长时,框架自动召集一次同步会议,让常驻们自行决定下一步路线/分工(框架只促成,不指派) |
420
422
  | `verdictMaxRounds` | 3 | 验证在独立初评后进入辩论的最大轮数 |
421
423
  | `provider` / `model` | 空 | **常驻 LLM 路由**(空 = 常驻继承主代理的 provider/model;此前声明未用,v1.4.1 真正接入) |
422
424
  | `residentPersona` | 空 | 注入每个常驻提示词开头的人格/要求 |
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.3",
4
+ "version": "2.0.4",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -61,7 +61,7 @@
61
61
  Main controls (recommended flow: configure FIRST, then start):
62
62
  - vibe_v4_configure {project?, problem?, params?} — create/configure the project (name, problem, params) WITHOUT starting a run; set everything here first.
63
63
  - vibe_v4_start {problem?, residentCount?, seedDirections?} — begin the run (spawn residents, brainstorm). If problem was configured, omit it.
64
- - vibe_v4_set {residentCount, compactThreshold, compactAfterRounds, meetingKeepEvery, maxParallel, activityTimeoutMs, verdictMaxRounds, provider, model, residentPersona, toolAllow, toolDeny} — tune params (persisted to the settings file). provider/model override the residents' LLM route (empty = they inherit YOUR model/provider); toolAllow/toolDeny are per-resident tool permissions (empty = they inherit all tools).
64
+ - vibe_v4_set {residentCount, compactThreshold, compactAfterRounds, meetingKeepEvery, maxParallel, activityTimeoutMs, stallAutoMeetingMs, verdictMaxRounds, provider, model, residentPersona, toolAllow, toolDeny} — tune params (persisted to the settings file). provider/model override the residents' LLM route (empty = they inherit YOUR model/provider); toolAllow/toolDeny are per-resident tool permissions (empty = they inherit all tools). stallAutoMeetingMs is the stalled-group auto-sync-meeting threshold (分级保活 B).
65
65
  - vibe_v4_resume / vibe_v4_pause / vibe_v4_abort / vibe_v4_status / vibe_v4_report.
66
66
  - vibe_v4_message {to|all, content} — inject a message to a resident (human/assistant intervention).
67
67
  - vibe_v4_meeting {agenda} — force a meeting.
@@ -32,6 +32,7 @@ export function apply(ctx) {
32
32
  residentCount: 4, compactThreshold: 66, compactAfterRounds: 8,
33
33
  maxParallel: 3, activityTimeoutMs: 120000, verdictMaxRounds: 3,
34
34
  meetingKeepEvery: 5, // 每积累 N 个新产物自动触发一次同步会议
35
+ stallAutoMeetingMs: 360000, // 团队空闲且无新产物的"停滞阈值":超过则自动召集同步会议(分级保活 B)
35
36
  // model/provider inheritance: '' = the resident inherits the parent (main assistant)
36
37
  // route (provider + model). Set them to override the resident's LLM backend/model.
37
38
  provider: '', model: '', residentPersona: '',
@@ -47,7 +48,7 @@ export function apply(ctx) {
47
48
  let problemText = '', problemId = 'problem', runId = 'run-' + shortId()
48
49
  let meetingState = null, verifyState = null, pendingVerify = null
49
50
  let busy = new Set(), wakeKind = new Map(), currentResident = ''
50
- let lastActivityAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
51
+ let lastActivityAt = now(), lastProgressAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
51
52
  const activityLogCap = 200
52
53
 
53
54
  // ---- utils ----
@@ -64,6 +65,11 @@ export function apply(ctx) {
64
65
  function blocksToText(b){ if(!b) return ''; let out=''; for(const x of b){ if(x&&x.type==='text'&&typeof x.text==='string') out+=x.text+'\n' } return out.trim() }
65
66
  function logActivity(event,detail){ activityLog.push({at:now(),event,detail:String(detail||'')}); if(activityLog.length>activityLogCap) activityLog.shift() }
66
67
  function logDecision(kind,detail){ decisions.push({at:now(),kind,detail:String(detail||'')}) }
68
+ // Record that the project made real progress (new artifact, meeting, verify, task, or a
69
+ // resident speaking to the group). The stall auto-sync meeting (B) fires only when this has
70
+ // NOT advanced for stallAutoMeetingMs, so a group that is genuinely producing keeps working
71
+ // and only a truly stalled group gets a coordination meeting to reboot itself.
72
+ function markProgress(){ lastProgressAt = now(); }
67
73
  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' }
68
74
  // Per-resident model/provider inheritance: when params.provider / params.model are set,
69
75
  // the resident uses that exact route; when left '' the resident inherits the parent's
@@ -110,10 +116,10 @@ export function apply(ctx) {
110
116
  await writeJson('State/mailboxes.json', Object.fromEntries(mailboxes))
111
117
  await writeJson('State/taskboard.json', taskboard)
112
118
  await writeJson('State/decisions.json', decisions)
113
- await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,activityLog,processEpoch,artifactCount})
119
+ await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount})
114
120
  }
115
121
  async function loadAll(){
116
- const s=await readJson('State/session.json'); if(s){ running=!!s.running; autoDone=!!s.autoDone; phase=s.phase||'idle'; problemId=s.problemId||problemId; problemText=s.problemText||problemText; runId=s.runId||runId; meetings=s.meetings||[]; reports=s.reports||[]; lastActivityAt=s.lastActivityAt||now(); activityLog=s.activityLog||activityLog; persistedEpoch=s.processEpoch||''; artifactCount=s.artifactCount||0 }
122
+ const s=await readJson('State/session.json'); if(s){ running=!!s.running; autoDone=!!s.autoDone; phase=s.phase||'idle'; problemId=s.problemId||problemId; problemText=s.problemText||problemText; runId=s.runId||runId; meetings=s.meetings||[]; reports=s.reports||[]; lastActivityAt=s.lastActivityAt||now(); lastProgressAt=s.lastProgressAt||now(); activityLog=s.activityLog||activityLog; persistedEpoch=s.processEpoch||''; artifactCount=s.artifactCount||0 }
117
123
  const rm=await readJson('State/residents.json'); if(rm&&typeof rm==='object') residents=new Map(Object.entries(rm))
118
124
  const mb=await readJson('State/mailboxes.json'); if(mb&&typeof mb==='object') mailboxes=new Map(Object.entries(mb))
119
125
  const tb=await readJson('State/taskboard.json'); if(Array.isArray(tb)) taskboard=tb
@@ -280,7 +286,7 @@ export function apply(ctx) {
280
286
  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'} }
281
287
  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'} }
282
288
  // auto-sync meeting: every meetingKeepEvery new artifacts, convene a general coordination meeting
283
- function bumpArtifacts(){ artifactCount+=1; if(!meetingState && !verifyState && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
289
+ function bumpArtifacts(){ artifactCount+=1; markProgress(); if(!meetingState && !verifyState && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
284
290
  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):''})) }
285
291
  // identify WHICH resident is calling a resident-facing tool: match the caller's
286
292
  // subagent id to a resident's childId. Fall back to the last-woken resident when
@@ -290,12 +296,12 @@ export function apply(ctx) {
290
296
 
291
297
  // ---- task board (residents propose / claim / complete; framework wakes the claimer) ----
292
298
  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')) }
293
- 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(); logActivity('task','proposed '+id+'「'+title+'」'); return {ok:true,id} }
294
- 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(); logActivity('task',claimer+' claimed '+id);
299
+ 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} }
300
+ 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);
295
301
  // wake the claimer to work on it (framework moves the task, resident decides how)
296
302
  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() }
297
303
  return {ok:true} }
298
- 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(); logActivity('task','done '+id); return {ok:true} }
304
+ 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} }
299
305
  async function saveTaskboard(){ await writeJson('State/taskboard.json',taskboard); await writeTaskboard() }
300
306
  function listTasks(){ return taskboard.filter(t=>t.status!=='done') }
301
307
  async function reportContext(rId,pct){ const r=residents.get(rId); if(r){ r.contextPct=clPct(pct); if(Number(pct)<30) r.needCompact=false; } return {ok:true} }
@@ -318,7 +324,7 @@ export function apply(ctx) {
318
324
  if(!busy.has(to)){
319
325
  currentResident=r.rId
320
326
  await wakeResident(r, (await normalPrompt(r))+'\n\n[NEW MESSAGE from '+from+']\n'+content,'normal')
321
- await saveAll(); logActivity('message',from+'→'+to); return {ok:true}
327
+ await saveAll(); markProgress(); logActivity('message',from+'→'+to); return {ok:true}
322
328
  }
323
329
  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}
324
330
  }
@@ -336,7 +342,7 @@ export function apply(ctx) {
336
342
  if(r.rId===from) continue
337
343
  const mb=mailboxes.get(r.rId)||[]; mb.push({from,at:now(),content:'[群聊] '+text}); mailboxes.set(r.rId,mb)
338
344
  }
339
- logActivity('relay',from+' → 团队: '+text.slice(0,60)); await saveAll()
345
+ logActivity('relay',from+' → 团队: '+text.slice(0,60)); markProgress(); await saveAll()
340
346
  }
341
347
 
342
348
  // ---- meeting ----
@@ -349,6 +355,7 @@ export function apply(ctx) {
349
355
  const rot=Math.floor(Math.random()*Math.max(1,ids.length))
350
356
  const order=ids.slice(rot).concat(ids.slice(0,rot))
351
357
  meetingState={id:'mt-'+shortId(),agenda,type:type||'general',targetId:targetId||null,round:0,asked:[],inputs:{},transcript:[],order}
358
+ markProgress();
352
359
  logActivity('meeting','start: '+agenda); await saveAll(); await scheduleNext(); return {ok:true,id:meetingState.id}
353
360
  }
354
361
  async function continueMeetingRound(){
@@ -359,7 +366,8 @@ export function apply(ctx) {
359
366
  const order=meetingState.order||ids
360
367
  const id=order.find(x=>meetingState.inputs[x]===undefined && !busy.has(x)); if(!id) return
361
368
  const r=residents.get(id)
362
- await wakeResident(r, meetingPrompt(r,meetingState), 'meeting'); await saveAll()
369
+ const ok = await wakeResident(r, meetingPrompt(r,meetingState), 'meeting'); await saveAll()
370
+ if(!ok) armHeartbeat() // a failed meeting wake must NOT silently hang the meeting
363
371
  }
364
372
  async function finalizeMeeting(){
365
373
  const st=meetingState
@@ -387,6 +395,7 @@ export function apply(ctx) {
387
395
  clearHeartbeat()
388
396
  pendingVerify=null
389
397
  verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},transcript:[]}
398
+ markProgress();
390
399
  logActivity('verify','debate begin: '+pv.targetId+' ('+pv.targetType+')'); await saveAll(); await scheduleNext()
391
400
  }
392
401
  async function continueVerifyRound(){
@@ -395,7 +404,8 @@ export function apply(ctx) {
395
404
  if(allVoted){ await finalizeVerify(); return }
396
405
  const id=ids.find(x=>verifyState.verdicts[x]===undefined && !busy.has(x)); if(!id) return
397
406
  const r=residents.get(id)
398
- await wakeResident(r, verifyPrompt(r,verifyState), verifyState.stage==='independent'?'verif-ind':'verif-deb'); await saveAll()
407
+ const ok = await wakeResident(r, verifyPrompt(r,verifyState), verifyState.stage==='independent'?'verif-ind':'verif-deb'); await saveAll()
408
+ if(!ok) armHeartbeat() // a failed verify wake must NOT silently hang the verification
399
409
  }
400
410
  async function finalizeVerify(){
401
411
  const vs=verifyState; const expected=Array.from(residents.keys()).length
@@ -460,15 +470,15 @@ export function apply(ctx) {
460
470
 
461
471
  // ---- heartbeat / liveness helpers (boundary-A: event-driven + gated heartbeat) ----
462
472
  // A checkpoint wake is NOT "keep working forever": it nudges the least-recently-active
463
- // resident, after an idle timeout, to either make concrete progress or push the group
464
- // toward a decision (meeting / verify / solved). This adds convergence pressure instead
465
- // of infinite token-burning, matching the "framework never assigns work" philosophy.
473
+ // resident, after an idle timeout, to CONTINUE the work itself (self-drive), and only to
474
+ // propose a meeting/verify/solved when it truly has nothing left. This keeps the group moving
475
+ // on its own (framework never assigns work) but adds convergence pressure instead of letting
476
+ // a stalled group sit idle forever.
466
477
  function heartbeatPrompt(r){
467
478
  return (params.residentPersona?params.residentPersona+'\n':'')
468
- +'Resident researcher '+r.rId+' — CHECKPOINT(空闲):团队在等待方向。'
469
- +'请用一句话说明你下一步做什么;若你已无产出、或认为问题已解决/接近解决,请**提议开会(propose_meeting)**、**提议验证(propose_verify)**、或**声明 solved=true**,让团队能做出决定——不要产出填充性工作。\n'
479
+ +'Resident researcher '+r.rId+' — CHECKPOINT(团队空闲,请由你们继续自主推进)。当前项目尚未解决(除非你已确认)。团队在等待有人继续:请**继续解决这个问题**——读他人的库对齐、推进某个子问题/引理/方法、尝试一条路线;或向团队发消息(input)、提议任务(propose_task)让大家分工。若你确实认为问题已解决、或已彻底无路可走,才提议开会(propose_meeting)让团队表决/商量、或声明 solved=true。默认立场是:**请推进,而不是停在原地。**\n'
470
480
  +'Reply with ONLY a JSON object:\n'
471
- +'{"summary":"<what you do next or a declaration>","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","claim_task":"<id|null>"}'
481
+ +'{"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}'
472
482
  }
473
483
  function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
474
484
  function armHeartbeat(){
@@ -509,14 +519,30 @@ export function apply(ctx) {
509
519
  // maxParallel: don't start a new wake when the in-flight cap is reached
510
520
  const mp=Number(params.maxParallel)||0
511
521
  if(mp>0 && busy.size>=mp){ armHeartbeat(); return }
512
- // heartbeat / coordination wake of the least-recently-active resident, ONLY after idle timeout
522
+ // B) stall auto-sync meeting (分级保活 B): the group has been idle with NO progress for
523
+ // stallAutoMeetingMs → convene a sync meeting so the residents coordinate their next move
524
+ // (framework convenes & records; residents decide — never assigns work). Only when no
525
+ // meeting/verify/pending work is already active.
526
+ if(phase==='active' && !meetingState && !verifyState && !pendingVerify){
527
+ const stallMs=Number(params.stallAutoMeetingMs)||((Number(params.activityTimeoutMs)||120000)*3)
528
+ if(now()-lastProgressAt>=stallMs){
529
+ await startMeeting('团队较长时间没有新进展。请你们自行讨论:当前问题是否已解决、开放难点是什么、谁负责哪部分、下一步如何推进,并自主决定是否继续。框架只负责转达与记录,不替你们决定。','general',null)
530
+ return
531
+ }
532
+ }
533
+ // A) heartbeat / coordination: wake the least-recently-active resident after an idle timeout
534
+ // to SELF-DRIVE (continue solving / message / propose task / meeting / verify). On a failed
535
+ // wake we re-arm the heartbeat so a single follow-up error NEVER permanently stops the group.
513
536
  clearHeartbeat()
514
537
  let target=null, oldest=-1
515
538
  for(const [,r] of residents){ if(busy.has(r.rId)) continue; const idle=now()-r.lastActiveAt; if(idle>oldest){ oldest=idle; target=r } }
516
539
  const atOs=Number(params.activityTimeoutMs)||120000
517
540
  if(target && oldest>=atOs){
518
- // an idle timeout has elapsed: checkpoint wake (event-driven work is unchanged)
519
- await wakeResident(target, await heartbeatPrompt(target), 'normal'); await saveAll(); return
541
+ let ok=false
542
+ try { ok = await wakeResident(target, await heartbeatPrompt(target), 'normal') } catch(e){ ok=false }
543
+ await saveAll()
544
+ if(!ok) armHeartbeat() // wake failed → re-arm so the group never permanently stops
545
+ return
520
546
  }
521
547
  // everyone is busy or not idle-enough: arm a heartbeat to re-check later (no infinite spin)
522
548
  armHeartbeat()
@@ -613,7 +639,7 @@ export function apply(ctx) {
613
639
  }
614
640
  function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
615
641
  residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
616
- params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','provider','model','residentPersona','toolAllow','toolDeny'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
642
+ 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(', ') } }
617
643
  function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
618
644
  residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
619
645
  verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage}:null, meetings:meetings.length,
@@ -623,7 +649,7 @@ export function apply(ctx) {
623
649
  // Normalize one parameter value to its intended type so a string from /v4 set or configure
624
650
  // becomes the right number/array. Keeps settings.json clean regardless of how it was set.
625
651
  function normalizeParam(k, v){
626
- const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery']
652
+ const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery','stallAutoMeetingMs']
627
653
  if(INT_KEYS.includes(k)){ const n=Number(v); return Number.isFinite(n)?n:v }
628
654
  if(k==='toolAllow'||k==='toolDeny'){ if(Array.isArray(v)) return v.map(x=>String(x).trim()).filter(Boolean); if(typeof v==='string') return v.split(',').map(x=>x.trim()).filter(Boolean); return [] }
629
655
  return v
@@ -689,7 +715,7 @@ export function apply(ctx) {
689
715
  // model/provider inheritance: set model/provider to override the residents' LLM route (''=inherit
690
716
  // the main assistant's route). toolAllow/toolDeny are per-resident tool permissions (scoped
691
717
  // restrict). residentPersona prepends a persona line to every resident prompt.
692
- 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'},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} })
718
+ 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} })
693
719
  // resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
694
720
  // fall back to the last-woken resident when called by the host/assistant.
695
721
  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.postMessage(s.residentIdOf(x),a.to,a.content))
@@ -515,6 +515,28 @@ VibeMath/Projects/<project>/
515
515
 
516
516
  > 测试:`selfdrive-v4.mjs` 21/21;`e2e-v4-fixes.test.mjs` 36/36(新增 T10 模型/工具权限接线、T11 默认继承、T12 自主发明理论提示、T13 压缩指令不泄漏进 meetings);v3 E2E 100/100、v2 regression 14/14、v2 business 24/24、multisession 25/25 全绿。
517
517
 
518
+ ---
519
+
520
+ ## 25. 分级保活:自驱动心跳 + 停滞自动同步会议(v1.4.2)
521
+
522
+ 针对一次真实 run("跑完就停")的诊断:三名常驻第一轮就把问题归约到**同一个硬核引理**并一致给出 `solved=false`——这是**自然的难点停滞**,而非 bug。但框架的活性机制太弱且会"死":
523
+ - 空闲后唯一驱动是心跳(`activityTimeoutMs` 唤醒一个常驻、CHECKPOINT 提示词偏向"是否要停止"),**缺乏推进力**;
524
+ - **唤醒失败会永久停死**(`scheduleNext` 在心跳分支 `await wakeResident(...)` 后无条件 `return`,而 `wakeResident` 捕获异常后返回 false 但**不重新武装心跳**)→ 一旦某次 `subagents.followup` 抛异常,心跳不再武装,小组**永不再被唤醒**;
525
+ - `session.json` 不持久化 `meetingState`(进行中会议只在进程内存),进程重启或会议唤醒失败都可能让会议悬停。
526
+
527
+ ### A · 自驱动心跳(修复推进力 + 永不永久停死)
528
+ - `heartbeatPrompt` 改为**自驱动**:不是"是否要停止",而是"**请继续解决这个问题**——读他人库、推进子问题/引理/方法、尝试路线;或向团队发消息(input)、提议任务(propose_task);确实已解决/无路可走才提议开会/声明 solved。默认立场是推进而非停在原地。" 心跳回复 schema 增加 `input`、`propose_task`、`contextPct`,使一轮心跳能产生群聊消息/任务从而带动后续工作。
529
+ - **唤醒失败重新武装心跳**:`scheduleNext` 心跳分支、`continueMeetingRound`、`continueVerifyRound` 在 `wakeResident` 失败(返回 false)时调用 `armHeartbeat()`,保证**任何一次唤醒失败都不会让小组永久停住**(会稍后重试其它常驻)。
530
+
531
+ ### B · 停滞自动同步会议(分级保活 B)
532
+ - 新增 `lastProgressAt`(会话状态,随产物/会议/验证/任务/群聊/消息更新,`markProgress()`),并持久化到 `State/session.json`。
533
+ - `scheduleNext` 在无 meeting/verify/pending、`phase==='active'` 且 `now()-lastProgressAt >= stallAutoMeetingMs`(默认 360000=6 分钟,可调)时,自动 `startMeeting('团队较长时间没有新进展。请你们自行讨论…')`——框架**只促成会议、不指派任务**,让常驻们自行决定下一步路线/分工,从而"复活"一个停滞的团队。
534
+ - 这同时覆盖"进程重启后进行中会议丢失"的场景:重启后 `running:true, phase:active`、无会议 → 若持续无新产物,B 会自动再召集一次协调会议。
535
+
536
+ > 行为:有产出的团队会不断更新 `lastProgressAt`,B 不会触发;真正停滞的团队先被 A(自驱动心跳)持续轻推,若仍无新产物则被 B(自动同步会议)召集起来自主重启。全程框架只"促成",从不指派任务。
537
+ >
538
+ > 新增参数:`stallAutoMeetingMs`(`vibe_v4_set`/`status` 可见)。新增测试:T14(停滞自动会议)、T15(自驱动心跳提示词非旧的"说下一步");`selfdrive-v4` 21/21、`e2e-v4-fixes` 39/39。
539
+
518
540
 
519
541
 
520
542