dsh-vibe-math 2.4.1 → 2.5.0
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.en.md +14 -14
- package/README.md +15 -14
- package/cordis.patch.yml +33 -10
- package/docs/AUDIT-CHECKLIST.md +25 -6
- package/docs/COMPAT-AUDIT-ROUND3.md +16 -1
- package/docs/release-notes/RELEASE-NOTES-2.5.0.md +173 -0
- package/docs/test-timing.md +73 -40
- package/installer.js +225 -45
- package/package.json +3 -2
- package/preset-declaration.js +38 -7
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +67 -21
- package/prompt-corpus-v2/formal-verify-v2.json +116 -92
- package/prompt-corpus-v2/formal-verify-v2.md +240 -93
- package/prompt-corpus-v3/formal-verify-v3.json +81 -81
- package/prompt-corpus-v3/formal-verify-v3.md +288 -1111
- package/prompt-corpus-v4/formal-verify-v4.json +38 -8
- package/prompt-corpus-v4/formal-verify-v4.md +157 -25
- package/prompt-corpus-v5/prompt-corpus-v5.json +17 -17
- package/prompt-corpus-v5/prompt-corpus-v5.md +32 -26
- package/tests/audit-installer-compat.test.mjs +70 -4
- package/tests/audit-installer-policy.test.mjs +229 -4
- package/tests/audit-persona-sensitivity.mjs +11 -4
- package/tests/audit-persona-surface.test.mjs +5 -1
- package/tests/audit-v5-sensitivity.mjs +47 -6
- package/tests/e2e-v5-round2.test.mjs +354 -0
- package/tests/formal-verify-v2.test.mjs +54 -2
- package/tests/formal-verify-v3.test.mjs +63 -2
- package/tests/formal-verify-v4.test.mjs +120 -4
- package/tests/run-tests.mjs +104 -23
- package/vibe-math-v2/agent.cordis.yml +6 -0
- package/vibe-math-v2/vibe-math-v2.js +503 -99
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +20 -4
- package/vibe-math-v3/agent.cordis.yml +26 -10
- package/vibe-math-v3/vibe-math-v3.js +555 -175
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +3 -3
- package/vibe-math-v4/agent.cordis.yml +26 -6
- package/vibe-math-v4/vibe-math-v4.js +404 -51
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +91 -9
- package/vibe-math-v5/agent.cordis.yml +8 -4
- package/vibe-math-v5/vibe-math-v5.js +513 -105
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +73 -40
|
@@ -106,6 +106,11 @@ export function apply(ctx) {
|
|
|
106
106
|
let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
|
|
107
107
|
const verifiedRecently = new Map() // targetId -> timestamp when it was closed as Verified (dedup re-propose)
|
|
108
108
|
let lastActivityAt = now(), lastProgressAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
|
|
109
|
+
// `artifactBaseline` = the artifact count observed when the auto-sync-meeting counter was last
|
|
110
|
+
// re-based (see `countArtifacts`/`syncArtifactCount`): the documented behaviour is "every N NEW
|
|
111
|
+
// artifacts", so the modulo test must run against the number of artifacts written SINCE the run
|
|
112
|
+
// started, not against the raw file count of a possibly pre-existing project tree.
|
|
113
|
+
let artifactBaseline = null
|
|
109
114
|
const activityLogCap = 200
|
|
110
115
|
|
|
111
116
|
// ---- utils ----
|
|
@@ -858,10 +863,21 @@ export function apply(ctx) {
|
|
|
858
863
|
// `off` mode must not CREATE Lean state: a run that never used the feature leaves no
|
|
859
864
|
// State/formal.json behind. Seeded/live records (or a mode that is on) still persist.
|
|
860
865
|
if(formalOn()||formalPersisted||Object.keys(formal).length||formalTodos.length) await writeJson('State/formal.json', {records:formal,todo:formalTodos})
|
|
861
|
-
await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount
|
|
866
|
+
await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount,artifactBaseline,
|
|
867
|
+
// In-flight consensus must survive a restart too: a verification that had collected 3 of 4
|
|
868
|
+
// verdicts and a meeting that had collected half the speeches used to evaporate entirely
|
|
869
|
+
// (Shared/debates|meetings/<id>.md are only written by finalize*, so the partial debate was
|
|
870
|
+
// not even on disk) — and the queued proposals in pendingVerify were lost with it.
|
|
871
|
+
meetingState,verifyState,pendingVerify,pendingMeeting})
|
|
862
872
|
}
|
|
863
873
|
async function loadAll(){
|
|
864
|
-
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
|
|
874
|
+
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; artifactBaseline=(s.artifactBaseline===undefined?null:Number(s.artifactBaseline))
|
|
875
|
+
// Restore the in-flight consensus (see saveAll). `resume()` refreshes the watchdog clocks right
|
|
876
|
+
// after this, so a run resumed after a crash gets a fresh stall window instead of being
|
|
877
|
+
// abandoned by the meeting/verify watchdog on its first serviced pass.
|
|
878
|
+
meetingState=s.meetingState||null; verifyState=s.verifyState||null
|
|
879
|
+
pendingVerify=Array.isArray(s.pendingVerify)?s.pendingVerify:[]
|
|
880
|
+
pendingMeeting=s.pendingMeeting||null }
|
|
865
881
|
const rm=await readJson('State/residents.json'); if(rm&&typeof rm==='object') residents=new Map(Object.entries(rm))
|
|
866
882
|
const mb=await readJson('State/mailboxes.json'); if(mb&&typeof mb==='object') mailboxes=new Map(Object.entries(mb))
|
|
867
883
|
const tb=await readJson('State/taskboard.json'); if(Array.isArray(tb)) taskboard=tb
|
|
@@ -878,7 +894,12 @@ export function apply(ctx) {
|
|
|
878
894
|
|
|
879
895
|
// ---- resident prompts ----
|
|
880
896
|
function banner(){ const o=[]; for(const [id,r] of residents) o.push('- '+id+'「'+(r.direction||'(未定)')+'」'+r.status+'·轮'+r.rounds); return o.join('\n') }
|
|
881
|
-
|
|
897
|
+
// Who sent this? `facilitator` is the framework/human messenger (NOT a resident, not on the
|
|
898
|
+
// roster): a resident that reads a bare `[facilitator]` line tries to reply to it and gets
|
|
899
|
+
// `no such resident`. The definition travels WITH the frame, so it is present on the very wake
|
|
900
|
+
// that shows the message — not only in the once-only onboarding brief.
|
|
901
|
+
function senderLabel(from){ return String(from)==='facilitator'?'facilitator(框架/人类信使,不是常驻成员,不要向它回信;要回话请用本轮回执的 "input" 或 vibe_v4_send_message {to:"all"})':String(from) }
|
|
902
|
+
async function inboxText(rId){ const mb=mailboxes.get(rId)||[]; if(mb.length===0) return ' (no new messages)\n'; return mb.map(m=>' ['+senderLabel(m.from)+'] '+m.content).join('\n')+'\n' }
|
|
882
903
|
function residentLibraries(){
|
|
883
904
|
const base=frameworkRoot()
|
|
884
905
|
return '你的资料库根目录:'+base+'/\n'
|
|
@@ -934,6 +955,7 @@ export function apply(ctx) {
|
|
|
934
955
|
s.push('### 规则')
|
|
935
956
|
s.push('- 只有 Verified/(或卡片标"已验证·真/假")算已确立;其余都是你的实验性工作,请区分"猜想/已知"。')
|
|
936
957
|
s.push('- 验证必须**全组一致**(全真或全假);你只信全票结果。未全票的对象留在库里带概率。')
|
|
958
|
+
s.push('- `facilitator` 是**框架/人类介入的信使名**(不是常驻成员,不在编制里):它转达人类或框架的话,但**不要向它回信**(会返回 no such resident);要回话请用本轮回执的 "input" 字段(会转给全组)或 `vibe_v4_send_message {to:"all"}`。')
|
|
937
959
|
s.push('- 你自己决定做什么,但**优先级/分工由团队讨论决定**,不是固定模式。若你认为问题已解决或接近解决,请**发起会议**让团队表决。')
|
|
938
960
|
s.push('- 退出时**只**输出一个 JSON 对象(放在 ```json 代码围栏内;围栏外不要有文字)。')
|
|
939
961
|
return s.join('\n')
|
|
@@ -947,6 +969,7 @@ export function apply(ctx) {
|
|
|
947
969
|
// dynamic: after a /compact the resident must re-anchor on the rules it is actually
|
|
948
970
|
// living under right now.
|
|
949
971
|
return '[核心规则重申] 只有 Verified/(及标记"已验证·真/假")算已确立;验证须全组一致(全真或全假)才作数,否则留库附平均概率;你只写自己的库('+base+'/ 的 Progress/<你>/、Propos/<你>/、Methods/<你>/、Subproblems/<你>/),可只读任何人的库;任务分工由团队讨论决定;退出只输出一个 JSON 对象。'
|
|
972
|
+
+'\n`facilitator` 是**框架/人类介入的信使名**,不是常驻成员,也不在编制里——**不要向它回信**(`vibe_v4_send_message` 会返回 no such resident);要回话请用本轮回执的 "input" 字段(会转给全组)或 `vibe_v4_send_message {to:"all"}`。'
|
|
950
973
|
+(formalOn()?('\n'+formalWorkLine()):'')
|
|
951
974
|
}
|
|
952
975
|
function brainstormPrompt(r){
|
|
@@ -975,11 +998,10 @@ export function apply(ctx) {
|
|
|
975
998
|
const prior=Object.entries(st.inputs).filter(([k])=>k!==r.rId).map(([k,iv])=>' ['+k+'] '+String(iv.input||iv.summary||'')).join('\n')
|
|
976
999
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
977
1000
|
+'Resident '+r.rId+' — 团队会议进行中。 A meeting is in progress (agenda: '+st.agenda+').'
|
|
978
|
-
+(st.type==='verify'?('\n团队正在验证对象:'+st.targetId+'('+st.targetType+',提出者 '+st.targetOwner+')。请先看他人意见,再给独立判断。'):'')
|
|
979
1001
|
+'\n这是一场真实讨论:下面已有人发言(转给你),请先看,然后**加入讨论/补充/反驳/表决**。'
|
|
980
1002
|
+(prior?('\n\n### 已有发言(他人 input,已转发给你)\n'+prior):'\n(目前还没人发言,你先说。)')
|
|
981
1003
|
+'\n\n你可以:提议任务(propose_task)、认领开放任务(claim_task)、提议验证对象(propose_verify)、或对"原问题是否已解决"表决(voteSolved)。请把**你的实际发言**写进 "input"。'
|
|
982
|
-
+'\nReply with ONLY a JSON object:\n'
|
|
1004
|
+
+'\n**停止表决必须是绝对票**:`voteSolved:true` 只表示你认为原问题**已解决**;只要有一名在册常驻没投 `true`(投 false、弃权、或漏写这个字段),run 就**不会**停止。不确定就投 false。' +'\nReply with ONLY a JSON object:\n'
|
|
983
1005
|
+'{"input":"<your real contribution to this discussion>","propose_task":"<task title or null>","task_desc":"...","claim_task":"<task id or null>","propose_verify":"<id or null>","voteSolved":true}'
|
|
984
1006
|
}
|
|
985
1007
|
function verifyPrompt(r, vs){
|
|
@@ -990,9 +1012,19 @@ export function apply(ctx) {
|
|
|
990
1012
|
const others=Object.entries(src).map(([k,v])=>'- '+k+': 正确概率 '+String(v.prob!=null?Number(v.prob).toFixed(2):0.5)+' → '+v.reason).join('\n')
|
|
991
1013
|
const L=[]
|
|
992
1014
|
L.push((params.residentPersona?params.residentPersona+'\n':'')
|
|
993
|
-
+'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+vs.targetType+',提出者 '+vs.targetOwner+')。\n'
|
|
1015
|
+
+'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+targetTypeWord(vs.targetType)+',提出者 '+vs.targetOwner+')。\n'
|
|
994
1016
|
+'请给出你对「该对象为真」的**正确概率 `verdict`**,仅一个 0–1 数值:**1 = 绝对为真,0 = 绝对为假,0.5 = 完全不确定,其余为介于其间的程度**(不要给 TRUE/FALSE,就给一个数值)。\n'
|
|
995
|
-
|
|
1017
|
+
// ── THE VOTE CONTRACT (why the example below is 1 and not 0.9) ──────────────────────────
|
|
1018
|
+
// Only an EXACT 1 or 0 is a vote; anything strictly between is the model's honest
|
|
1019
|
+
// probability of truth, i.e. an ABSTENTION. The old text demonstrated `verdict:0.9` while
|
|
1020
|
+
// the code required `Number(prob)===1`, so a cautious-but-convinced group could never
|
|
1021
|
+
// converge: every round burned and the object stayed unverified forever. The rule is now
|
|
1022
|
+
// stated HERE, the example shows `1`, and 0.9 is explained instead of merely tolerated.
|
|
1023
|
+
+'**投票契约**:只有**恰好 1**(你认为是**绝对**为真)和**恰好 0**(你认为是**绝对**为假)算表决;**严格介于 0 与 1 之间**(例如 0.9、0.95、0.5)是**弃权**——它是你对"该对象为真"的**概率估计**,不是你的一票。\n'
|
|
1024
|
+
+' · 有把握认为它为真就投 **1**;不要为了"留一点余地"投 0.9——那会让全组永远无法定论。\n'
|
|
1025
|
+
+' · 弃权会被如实统计:本轮没有人全票 → 对象**不会**停止验证,而是把全组的**平均概率**写回它在库中的卡片(`- 概率:`),带概率继续留在库里。\n'
|
|
1026
|
+
+' · 弃权的两种合法用途:① 你确实不确定(用 0.5 附近的值表达);② 已归档的机器检查证明与命题原文不一致、你**不能**用 0 表达"命题为假"(此时请用 `formal` 回执的 `decision:"defect"` 报告偏差,见下方形式化段)。\n'
|
|
1027
|
+
+'判定规则:**仅当全体在册常驻都恰好给 1(都认为是真)、或都恰好给 0(都认为是假)**,才按「真/假」写入 Verified/ 并回写来源卡的「已验证·真/假」;否则**只按概率数值(一种程度)保留在库中**,附全组平均正确概率,不写成真/假。\n'
|
|
996
1028
|
+'请给出你**诚实独立的判断**'
|
|
997
1029
|
+(vs.stage==='debate'?',并参考他人意见:\n':'。\n')
|
|
998
1030
|
+(vs.stage==='debate'&&others?('### 他人上一轮意见(已转发给你)\n'+others+'\n'):''))
|
|
@@ -1004,7 +1036,7 @@ export function apply(ctx) {
|
|
|
1004
1036
|
}
|
|
1005
1037
|
L.push('')
|
|
1006
1038
|
L.push('Reply with ONLY a JSON object:')
|
|
1007
|
-
L.push('{"vote":{"verdict":
|
|
1039
|
+
L.push('{"vote":{"verdict":1,"reason":"<your logic>"}}')
|
|
1008
1040
|
if(formalOn()){
|
|
1009
1041
|
// The formal field belongs in the VOTING contract too: voters are exactly the agents who
|
|
1010
1042
|
// must either formalize the object or record why they judged it infeasible.
|
|
@@ -1068,6 +1100,33 @@ export function apply(ctx) {
|
|
|
1068
1100
|
|
|
1069
1101
|
// ---- resident lifecycle ----
|
|
1070
1102
|
let residentSeq = 0
|
|
1103
|
+
// Spawns that are currently IN FLIGHT, plus the `subagent/start` payloads seen while they were.
|
|
1104
|
+
//
|
|
1105
|
+
// ORDERING FACT (verified against the installed host, dsh-subagent 0.2.0-rc.2): `subagent/start`
|
|
1106
|
+
// is emitted SYNCHRONOUSLY from inside `startContinuable`'s await — `emit('subagent/start', …)` at
|
|
1107
|
+
// `dsh-subagent/lib/index.js:279` (and again from the lifecycle observer at `:306` once
|
|
1108
|
+
// `observer.start(handle.agent)` runs at `:1116`) — and `startContinuable` resolves only
|
|
1109
|
+
// afterwards. `spawnResident` therefore cannot know `started.childId` when the event fires, and
|
|
1110
|
+
// the payload carries no label (see `SubagentRunInfo`: runId/provider/id/local only), so the ONLY
|
|
1111
|
+
// information available at that moment is "a child of mine just started".
|
|
1112
|
+
//
|
|
1113
|
+
// So: the start handler STASHES the live Agent by child id (no ownership guess at all), and
|
|
1114
|
+
// `spawnResident` CLAIMS the record for its child id as soon as the call resolves — at which point
|
|
1115
|
+
// ownership is known exactly. `labelHints` bridges the (mock-ish) hosts that embed the label in
|
|
1116
|
+
// the id for the handler's own `ownsChild` check.
|
|
1117
|
+
const pendingSpawnLabels = new Set()
|
|
1118
|
+
const pendingStartAgents = new Map() // childId -> Agent, seen but not yet owned
|
|
1119
|
+
function stashStartAgent(childId, agent){
|
|
1120
|
+
if(!childId || !agent) return
|
|
1121
|
+
if(pendingStartAgents.size>=64) pendingStartAgents.delete(pendingStartAgents.keys().next().value)
|
|
1122
|
+
pendingStartAgents.set(String(childId), agent)
|
|
1123
|
+
}
|
|
1124
|
+
function claimStartAgent(childId){
|
|
1125
|
+
const id=String(childId==null?'':childId)
|
|
1126
|
+
const agent=pendingStartAgents.get(id)
|
|
1127
|
+
if(agent) pendingStartAgents.delete(id)
|
|
1128
|
+
return agent
|
|
1129
|
+
}
|
|
1071
1130
|
function newResident(dir){ const rId='r-'+(++residentSeq); return {rId,childId:'',direction:dir||'',status:'brainstorm',rounds:0,roundsSinceCompact:0,lastActiveAt:now(),insight:'',contextPct:0,contextSeed:'',needCompact:false} }
|
|
1072
1131
|
async function spawnResident(r){
|
|
1073
1132
|
// Skip BEFORE touching the host when we already KNOW the ceiling and our own live children
|
|
@@ -1076,6 +1135,8 @@ export function apply(ctx) {
|
|
|
1076
1135
|
if(hostChildLimit!==undefined && liveChildCount()>=hostChildLimit){ noteSpawnRefused(r, hostChildLimit); return false }
|
|
1077
1136
|
const ao=residentAgentOptions(); const tf=residentToolFilter()
|
|
1078
1137
|
let started
|
|
1138
|
+
// Register the label for the whole in-flight window: `subagent/start` fires inside this await.
|
|
1139
|
+
pendingSpawnLabels.add(r.rId)
|
|
1079
1140
|
try {
|
|
1080
1141
|
started=await startWithToolFilter(tf, function(f){ return {provider:pickProvider(),label:r.rId,request:{prompt:[textBlock(brainstormPrompt(r))],parent:rootAgent,agentOptions:ao,...(f?{toolFilter:f}:{})},signal:makeSignal(params.activityTimeoutMs||60000)} })
|
|
1081
1142
|
} catch(e){
|
|
@@ -1085,17 +1146,46 @@ export function apply(ctx) {
|
|
|
1085
1146
|
// failure keeps its previous behaviour and still propagates.
|
|
1086
1147
|
if(!isActivationLimitReached(e)) throw e
|
|
1087
1148
|
noteSpawnRefused(r, noteChildLimit(e)); return false
|
|
1088
|
-
}
|
|
1149
|
+
} finally { pendingSpawnLabels.delete(r.rId) }
|
|
1150
|
+
// The child just started, so its `subagent/start` payload has already been seen (the host emits
|
|
1151
|
+
// it inside the await above). Claim the stashed Agent for it NOW — ownership is exact here — and
|
|
1152
|
+
// that is what makes the REAL `/compact` path reachable (see the `subagent/start` listener).
|
|
1153
|
+
const claimed=claimStartAgent(started.childId)
|
|
1154
|
+
if(claimed) rememberAgent(started.childId, claimed)
|
|
1089
1155
|
r.childId=started.childId; r.status='brainstorm'; r.lastActiveAt=now()
|
|
1090
1156
|
childOwner.set(started.childId,sessionId); busy.add(r.rId); wakeKind.set(r.rId,'normal'); currentResident=r.rId
|
|
1091
1157
|
residents.set(r.rId,r); await saveAll(); logActivity('spawn',r.rId+' ('+(r.direction||'brainstorm')+')')
|
|
1092
1158
|
return true
|
|
1093
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Is `childId` one of THIS session's residents — including a spawn that is still IN FLIGHT?
|
|
1162
|
+
*
|
|
1163
|
+
* `spawnResident` sets `r.childId` (and `childOwner`) only after `startContinuable` resolves, but
|
|
1164
|
+
* the host emits `subagent/start` from inside that await. So "is this mine?" must also consult the
|
|
1165
|
+
* pending-spawn labels registered around the call; without that, the start handler rejects every
|
|
1166
|
+
* child of a fresh spawn and `liveAgents` never gets populated.
|
|
1167
|
+
*/
|
|
1168
|
+
function ownsChild(childId){
|
|
1169
|
+
const id=String(childId)
|
|
1170
|
+
for(const [,r] of residents){ if(r.childId===id) return true }
|
|
1171
|
+
for(const r of pendingSpawns){ if(r.childId===id) return true }
|
|
1172
|
+
// During the spawn await the child's id is not recorded anywhere yet, but the framework chose
|
|
1173
|
+
// the spawn's LABEL and `spawnResident` registers it for exactly this window. A host id that
|
|
1174
|
+
// embeds the label (`child-r-1`, the common test/mock shape) is matched by containment; an
|
|
1175
|
+
// opaque host id cannot be matched here, which is why the label is only a
|
|
1176
|
+
// best-effort bridge in addition to the roster checks above.
|
|
1177
|
+
for(const label of pendingSpawnLabels){ if(id===label || id.indexOf(label)>=0) return true }
|
|
1178
|
+
return false
|
|
1179
|
+
}
|
|
1094
1180
|
async function wakeResident(r, promptText, kind){
|
|
1095
1181
|
if(!r || !r.childId) return false // a removed resident must never be woken (else r.childId would crash)
|
|
1096
1182
|
clearHeartbeat()
|
|
1097
1183
|
busy.add(r.rId); wakeKind.set(r.rId,kind||'normal'); currentResident=r.rId
|
|
1098
|
-
|
|
1184
|
+
// `rounds`/`roundsSinceCompact` are advanced only AFTER the send actually succeeded (below), so
|
|
1185
|
+
// a wake that never reached the child cannot consume a "round" of the compaction heuristic nor
|
|
1186
|
+
// inflate the round number the resident is told. `lastActiveAt` still moves here on purpose: a
|
|
1187
|
+
// failed wake must back the A-fill off for one activityTimeoutMs instead of hammering the child.
|
|
1188
|
+
r.lastActiveAt=now()
|
|
1099
1189
|
// Context compaction has TWO distinct needs. Confusing them is the bug that made
|
|
1100
1190
|
// '[核心规则重申]+[CONTEXT COMPACT]' repeat at the start of nearly every prompt:
|
|
1101
1191
|
// (a) r.needCompact (set by a REAL /compact) => the resident's rules may be blurred, so
|
|
@@ -1135,6 +1225,8 @@ export function apply(ctx) {
|
|
|
1135
1225
|
} else {
|
|
1136
1226
|
throw new Error('no subagent continuation API (need sendMessage or followup)')
|
|
1137
1227
|
}
|
|
1228
|
+
// The turn is really in flight now: count it (F10 — a failed send must not consume a round).
|
|
1229
|
+
r.rounds+=1; r.roundsSinceCompact+=1
|
|
1138
1230
|
return true
|
|
1139
1231
|
}
|
|
1140
1232
|
catch(e){ console.error('vibe-v4 wake '+r.rId+' failed: '+String((e&&e.message)||e)); busy.delete(r.rId); return false }
|
|
@@ -1146,18 +1238,74 @@ export function apply(ctx) {
|
|
|
1146
1238
|
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'} }
|
|
1147
1239
|
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'} }
|
|
1148
1240
|
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'} }
|
|
1149
|
-
// auto-sync meeting: every meetingKeepEvery
|
|
1241
|
+
// ── auto-sync meeting: every `meetingKeepEvery` NEW artifacts, convene a coordination meeting ──
|
|
1150
1242
|
//
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
|
|
1243
|
+
// The counting basis is the RESIDENTS' card libraries themselves (Propos/Methods/Subproblems),
|
|
1244
|
+
// NOT the three `record_*` convenience tools. The prompt tells residents verbatim that those tools
|
|
1245
|
+
// are optional and that they are recommended to write their own files directly with `fs`, so a
|
|
1246
|
+
// team that follows the recommendation never called `bumpArtifacts` and
|
|
1247
|
+
// `artifactCount % meetingKeepEvery === 0` could never fire — the documented "every N new
|
|
1248
|
+
// artifacts auto-sync" was UNREACHABLE in the very workflow the prompt recommends, leaving the
|
|
1249
|
+
// stall watchdog as the only path that ever convenes an auto-meeting.
|
|
1250
|
+
//
|
|
1251
|
+
// Setting `artifactBaseline` also means a tile count of the correct sign, since residents write
|
|
1252
|
+
// the paths `Propos/<r>/<id>.md` explicitly.
|
|
1253
|
+
async function countArtifacts(){
|
|
1254
|
+
let n=0
|
|
1255
|
+
for(const base of ['Propos','Methods','Subproblems']){
|
|
1256
|
+
try {
|
|
1257
|
+
const t=await fs.resolve(base,{cwd:frameworkRoot()})
|
|
1258
|
+
if(await fs.stat(t)===undefined) continue
|
|
1259
|
+
const ents=await fs.listDir(t)
|
|
1260
|
+
for(const e of ents||[]){
|
|
1261
|
+
if(!e||e.type!=='directory') continue
|
|
1262
|
+
try {
|
|
1263
|
+
const dt=await fs.resolve(base+'/'+e.name,{cwd:frameworkRoot()})
|
|
1264
|
+
if(await fs.stat(dt)===undefined) continue
|
|
1265
|
+
const files=await fs.listDir(dt)
|
|
1266
|
+
for(const f of files||[]) if(f&&f.type==='file'&&/\.md$/.test(String(f.name))) n++
|
|
1267
|
+
} catch(e2){ /* a single unreadable library must not stop the scan */ }
|
|
1268
|
+
}
|
|
1269
|
+
} catch(e){ /* best-effort: an absent library counts as zero */ }
|
|
1270
|
+
}
|
|
1271
|
+
return n
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Re-base the artifact counter on what is on disk. Called from `start()` (a fresh run: the count
|
|
1275
|
+
* of NEW artifacts starts at 0 even if the project tree already held cards) and from
|
|
1276
|
+
* `scheduleNext` (the single choke point of every scheduling pass, right before the auto-meeting
|
|
1277
|
+
* test), so a group that writes its cards with `fs` still drives the documented cadence.
|
|
1278
|
+
* Costs no writes and never throws into the scheduler.
|
|
1279
|
+
*/
|
|
1280
|
+
async function syncArtifactCount(){
|
|
1281
|
+
const n=await countArtifacts()
|
|
1282
|
+
if(artifactBaseline===null) artifactBaseline=n
|
|
1283
|
+
artifactCount=Math.max(artifactCount, n-(artifactBaseline||0))
|
|
1284
|
+
return artifactCount
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* The `meetingKeepEvery` cadence: every N newly accumulated artifacts, convene ONE coordination
|
|
1288
|
+
* meeting. Called from BOTH paths that can observe the count:
|
|
1289
|
+
* · `bumpArtifacts` (a `record_*` convenience-tool call — the counter moves immediately), and
|
|
1290
|
+
* · `scheduleNext` right after `syncArtifactCount()` re-bases the counter on the files on disk,
|
|
1291
|
+
* which is the ONLY way an `fs`-written card can be seen.
|
|
1292
|
+
* Without the second call site the documented cadence is unreachable in the very workflow the
|
|
1293
|
+
* prompt recommends (`record_*` 可选;推荐直接用 fs 写自己的文件). `lastSyncMeetingAt` is reused as
|
|
1294
|
+
* the count at which the last sync meeting was convened, so the same crossing cannot fire twice.
|
|
1295
|
+
*/
|
|
1296
|
+
function maybeArtifactSyncMeeting(){
|
|
1297
|
+
const every=Number(params.meetingKeepEvery)
|
|
1298
|
+
if(!(every>0)) return false
|
|
1299
|
+
if(meetingState || verifyState || pendingMeeting) return false
|
|
1300
|
+
if(!(artifactCount>0) || artifactCount % every !== 0) return false
|
|
1301
|
+
if(artifactCount<=lastSyncMeetingAt) return false // this crossing already convened a meeting
|
|
1302
|
+
lastSyncMeetingAt=artifactCount
|
|
1303
|
+
logActivity('meeting','定期同步触发:新增产物达到 '+every+' 的倍数(累计 '+artifactCount+')')
|
|
1304
|
+
startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{})
|
|
1305
|
+
return true
|
|
1306
|
+
}
|
|
1307
|
+
function bumpArtifacts(){ artifactCount+=1; markProgress(); maybeArtifactSyncMeeting() }
|
|
1308
|
+
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):'',roundsSinceCompact:r.roundsSinceCompact||0,needCompact:!!r.needCompact,wakeKind:wakeKind.get(r.rId)||''})) }
|
|
1161
1309
|
// identify WHICH resident is calling a resident-facing tool: match the caller's
|
|
1162
1310
|
// subagent id to a resident's childId. Fall back to the last-woken resident when
|
|
1163
1311
|
// the caller is the host/assistant (or an unknown agent). This makes per-resident
|
|
@@ -1171,17 +1319,28 @@ export function apply(ctx) {
|
|
|
1171
1319
|
// wake the claimer to work on it (framework moves the task, resident decides how).
|
|
1172
1320
|
// NOT while paused/stopped: a paused run must not start new work — the claim is recorded on the
|
|
1173
1321
|
// board and the resident (who claimed it) picks it up again after resume.
|
|
1174
|
-
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() }
|
|
1322
|
+
const r=residents.get(claimer); if(r && !busy.has(claimer) && running && !autoDone){ currentResident=claimer; const ok=await wakeResident(r, (await normalPrompt(r))+'\n\n[YOU CLAIMED TASK '+id+'] '+t.title+' — '+t.description,'normal'); await saveAll(); if(!ok) armHeartbeat() }
|
|
1175
1323
|
return {ok:true} }
|
|
1176
1324
|
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} }
|
|
1177
1325
|
async function saveTaskboard(){ await writeJson('State/taskboard.json',taskboard); await writeTaskboard() }
|
|
1178
1326
|
function listTasks(){ return taskboard.filter(t=>t.status!=='done') }
|
|
1179
1327
|
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} }
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1328
|
+
/**
|
|
1329
|
+
* Apply context/compact bookkeeping from a resident's reply.
|
|
1330
|
+
*
|
|
1331
|
+
* `kind` is the wake kind. It matters because the OLD version treated ANY non-empty `summary`
|
|
1332
|
+
* on ANY turn as the acknowledgement of a requested compaction: a meeting/verify reply that
|
|
1333
|
+
* happened to echo a summary (their contracts do not forbid it) cleared `needCompact`, zeroed
|
|
1334
|
+
* `roundsSinceCompact` and clamped `contextPct` to <=25 WITHOUT any compaction happening — so the
|
|
1335
|
+
* soft-compact trigger stayed below threshold and the compaction the flag was asking for was
|
|
1336
|
+
* never issued. Only a real normal research turn can acknowledge the soft directive it received
|
|
1337
|
+
* (`wakeResident` injects `[CONTEXT COMPACT …]` on normal rounds only); `parsed.compacted===true`
|
|
1338
|
+
* stays honoured on every kind, because that is the resident explicitly saying it condensed.
|
|
1339
|
+
*/
|
|
1340
|
+
function postmark(r, parsed, kind){
|
|
1183
1341
|
const cp=Number(parsed.contextPct); if(Number.isFinite(cp)) r.contextPct=clPct(cp) // tolerate numeric strings ("40")
|
|
1184
|
-
|
|
1342
|
+
const isNormalTurn=(kind||'normal')==='normal'
|
|
1343
|
+
if(parsed.compacted===true || (isNormalTurn && r.needCompact && parsed.summary)){
|
|
1185
1344
|
r.contextSeed=String(parsed.summary||r.contextSeed||'')
|
|
1186
1345
|
r.contextPct=Math.min(r.contextPct||15,25)
|
|
1187
1346
|
r.roundsSinceCompact=0
|
|
@@ -1195,8 +1354,22 @@ export function apply(ctx) {
|
|
|
1195
1354
|
const r=residents.get(to); if(!r) return {ok:false,message:'no such resident'}
|
|
1196
1355
|
if(!busy.has(to)){
|
|
1197
1356
|
currentResident=r.rId
|
|
1198
|
-
await wakeResident(r, (await normalPrompt(r))+'\n\n[NEW MESSAGE from '+from+']\n'+content,'normal')
|
|
1199
|
-
await saveAll(); markProgress()
|
|
1357
|
+
const ok=await wakeResident(r, (await normalPrompt(r))+'\n\n[NEW MESSAGE from '+from+']\n'+content,'normal')
|
|
1358
|
+
await saveAll(); markProgress()
|
|
1359
|
+
if(ok){ logActivity('message',from+'→'+to); return {ok:true} }
|
|
1360
|
+
// The immediate wake FAILED (and `wakeResident` already cleared the `busy` mark, so no turn
|
|
1361
|
+
// actually started). Round C: the message must fall back to the MAILBOX instead of being
|
|
1362
|
+
// discarded — the old code logged "(wake failed; heartbeat re-armed)" and then dropped the
|
|
1363
|
+
// text, so the re-armed heartbeat had nothing left to do and the message was lost forever
|
|
1364
|
+
// while the recipient stayed cold (this is what the audit's F1 probe measured: it reported
|
|
1365
|
+
// "queued mail already consumed: false" but the mailbox was in fact already empty, so no
|
|
1366
|
+
// later pass could ever deliver anything). The mailbox is the one place a pass is guaranteed
|
|
1367
|
+
// to look again — including during `brainstorm`, see the delivery call at the top of
|
|
1368
|
+
// scheduleNext — and a successful retry marks the recipient busy and clears the queue.
|
|
1369
|
+
const mb=mailboxes.get(to)||[]; mb.push({from,at:now(),content}); mailboxes.set(to,mb)
|
|
1370
|
+
await saveAll(); armHeartbeat()
|
|
1371
|
+
logActivity('message',from+'→'+to+' (immediate wake failed; queued for retry)')
|
|
1372
|
+
return {ok:true}
|
|
1200
1373
|
}
|
|
1201
1374
|
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}
|
|
1202
1375
|
}
|
|
@@ -1287,8 +1460,16 @@ export function apply(ctx) {
|
|
|
1287
1460
|
if(iv.claim_task) await claimTask(iv.claim_task, id)
|
|
1288
1461
|
if(iv.propose_verify) maybeQueueVerify(iv.propose_verify, id)
|
|
1289
1462
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1463
|
+
// ── the STOP vote must be UNANIMOUS, abstentions included ────────────────────────────────
|
|
1464
|
+
// `onResidentEnd` stores `voteSolved` only when it is a real boolean, otherwise `null`. The
|
|
1465
|
+
// old code filtered those nulls OUT and then asked `votes.every(v=>v===true)`, so a resident
|
|
1466
|
+
// that omitted the field (the meeting contract makes every key except `input` optional) or
|
|
1467
|
+
// wrote the string "true" simply dropped out of the electorate — and 3-of-4 voting true read
|
|
1468
|
+
// as "the whole team agrees the problem is solved" and STOPPED the run. `finalizeVerify` was
|
|
1469
|
+
// already written correctly (every resident must have a verdict); this mirrors it: every
|
|
1470
|
+
// speaker must have produced an explicit `true`.
|
|
1471
|
+
const speakers=Object.values(st.inputs)
|
|
1472
|
+
const allSolved = allSpoke && speakers.length>0 && speakers.every(iv=>iv.voteSolved===true)
|
|
1292
1473
|
logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
|
|
1293
1474
|
if(allSolved){
|
|
1294
1475
|
running=false; autoDone=true; phase='done'; clearHeartbeat()
|
|
@@ -1369,14 +1550,18 @@ export function apply(ctx) {
|
|
|
1369
1550
|
else await closeVerify(vs,allTrue)
|
|
1370
1551
|
doSchedule=true
|
|
1371
1552
|
}
|
|
1372
|
-
else if(vs.round
|
|
1553
|
+
else if(vs.round<params.verdictMaxRounds){
|
|
1554
|
+
// `verdictMaxRounds` is documented (README / 实现方案 / persona) as the maximum number of
|
|
1555
|
+
// verification rounds: 1 independent first vote + (verdictMaxRounds-1) debate re-votes. The
|
|
1556
|
+
// old `vs.round+1<verdictMaxRounds` gave one round FEWER than the name and the docs promise
|
|
1557
|
+
// (default 3 → only 2), so the parameter read as an off-by-one from every documented surface.
|
|
1373
1558
|
// Move to a REAL debate round: snapshot the current votes into history (so the next round's
|
|
1374
1559
|
// prompt can show others' previous stances), then CLEAR verdicts so every resident is asked to
|
|
1375
1560
|
// give a fresh independent judgement after seeing the debate. Without the clear, allVoted stays
|
|
1376
1561
|
// true and the debate rounds burn through with NOBODY being re-asked (a silent no-op).
|
|
1377
1562
|
vs.history=Object.assign({}, vs.verdicts); vs.verdicts={}
|
|
1378
1563
|
vs.lastVerdictAt=now() // fresh deadlock window for the re-vote round
|
|
1379
|
-
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
|
|
1564
|
+
vs.stage='debate'; vs.round+=1; vs.asked=[]; logActivity('verify',vs.targetId+' round '+vs.round+'/'+params.verdictMaxRounds+' → debate (re-vote after seeing others)'); await saveAll(); doSchedule=true
|
|
1380
1565
|
}
|
|
1381
1566
|
else {
|
|
1382
1567
|
const avg=vals.length? vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length : 0.5
|
|
@@ -1440,13 +1625,25 @@ export function apply(ctx) {
|
|
|
1440
1625
|
function maybeQueueVerify(target, proposer){
|
|
1441
1626
|
const t=idSafe(target) // sanitize BEFORE it becomes file names / dedup keys / status output
|
|
1442
1627
|
if(!t || t==='id') return false
|
|
1628
|
+
// The object KIND is derived from the id prefix, and it decides BOTH the directory the Verified
|
|
1629
|
+
// card lands in (Verified/命题/ vs Verified/问题/) and the `- 类型:` line of the source card's
|
|
1630
|
+
// write-back. The old fallback mapped EVERY unrecognised id to 'proposition', so proposing a
|
|
1631
|
+
// method/sub-problem whose id does not start with m-/s- (e.g. `2.1`, `lemma-A`) silently wrote a
|
|
1632
|
+
// card into Verified/命题/ carrying `类型: 命题` — a card whose location and declaration disagree.
|
|
1633
|
+
// The prefix IS the coordination contract (the prompt hands residents `p-`/`m-`/`s-`), so an id
|
|
1634
|
+
// that does not carry one is refused HERE, at the single queueing funnel, instead of guessing.
|
|
1635
|
+
const tt=guessTargetType(t)
|
|
1636
|
+
if(!tt){
|
|
1637
|
+
logActivity('verify',t+' 的提议被拒绝:对象 id 必须以 p-(命题)/ m-(方法)/ s-(子问题)开头,否则框架无法确定它属于哪个库(V4_INVALID_ARGUMENT)')
|
|
1638
|
+
return false
|
|
1639
|
+
}
|
|
1443
1640
|
const last=verifiedRecently.get(t)
|
|
1444
1641
|
if(last!==undefined && (now()-last) < recoverStallMs()){
|
|
1445
1642
|
logActivity('verify',t+' re-propose ignored (just verified at '+fmtTime(last)+')')
|
|
1446
1643
|
return false
|
|
1447
1644
|
}
|
|
1448
1645
|
if(pendingVerify.some(p=>String(p.targetId)===t)) return true // already queued → keep ONE entry
|
|
1449
|
-
pendingVerify.push({targetId:t,targetType:
|
|
1646
|
+
pendingVerify.push({targetId:t,targetType:tt,proposer:proposer||'',at:now()})
|
|
1450
1647
|
return true
|
|
1451
1648
|
}
|
|
1452
1649
|
async function writeDebateDoc(vs,done,val){
|
|
@@ -1532,7 +1729,11 @@ export function apply(ctx) {
|
|
|
1532
1729
|
next=rewriteCardField(next,'概率',isTrue?'1':'0')
|
|
1533
1730
|
await writeText(rel,next||text)
|
|
1534
1731
|
}
|
|
1535
|
-
function guessTargetType(id){ if(/^p-/.test(id)) return 'proposition'; if(/^m-/.test(id)) return 'method'; if(/^s-/.test(id)) return 'subproblem'; return '
|
|
1732
|
+
function guessTargetType(id){ if(/^p-/.test(id)) return 'proposition'; if(/^m-/.test(id)) return 'method'; if(/^s-/.test(id)) return 'subproblem'; return '' }
|
|
1733
|
+
// Human-readable word for the target kind, used in the prompts. Never print the RAW internal
|
|
1734
|
+
// token: the voter is a model, and `proposition`/`method`/`subproblem` in an otherwise Chinese
|
|
1735
|
+
// contract is exactly the kind of untranslated placeholder these audits look for.
|
|
1736
|
+
function targetTypeWord(t){ return t==='method'?'方法':(t==='subproblem'?'子问题':'命题') }
|
|
1536
1737
|
|
|
1537
1738
|
// ---- heartbeat / liveness helpers (boundary-A: event-driven + gated heartbeat) ----
|
|
1538
1739
|
// A checkpoint wake is NOT "keep working forever": it nudges the least-recently-active
|
|
@@ -1653,6 +1854,18 @@ export function apply(ctx) {
|
|
|
1653
1854
|
// scheduling pass goes through — so a refused spawn is deferred by a whole round (and paced by
|
|
1654
1855
|
// the heartbeat) instead of being retried inside the loop that discovered the refusal.
|
|
1655
1856
|
if(pendingSpawns.length) await retryPendingSpawns()
|
|
1857
|
+
// Queued mail MUST also be delivered while the group is still brainstorming (Round C, F1).
|
|
1858
|
+
// A human/team message is not "work": the brainstorm phase used to return at the next line
|
|
1859
|
+
// before ever reaching the delivery point below, so a wake that FAILED left the message
|
|
1860
|
+
// re-queued and every subsequent heartbeat firing a no-op (`maybeFinishBrainstorm` only flips
|
|
1861
|
+
// the phase once every resident has an insight). The scheduler then stayed cold with mail
|
|
1862
|
+
// still queued — the exact permanent stall F1 exists to remove, narrowed to this phase
|
|
1863
|
+
// (_oneoff/probe-v4-mailbox-stall.mjs used to print REPRODUCED here).
|
|
1864
|
+
// Deliberately conditioned on "no consensus in progress": continueMeetingRound /
|
|
1865
|
+
// continueVerifyRound refuse to flush mailboxes on purpose (a backlog must not starve a
|
|
1866
|
+
// consensus past its watchdog, see their comments), so a parked meeting/verify keeps that
|
|
1867
|
+
// guarantee and the mail goes out on the first pass after the consensus closes.
|
|
1868
|
+
if(!meetingState && !verifyState){ const delivered=await deliverNextMailbox(); if(delivered) return }
|
|
1656
1869
|
if(phase==='brainstorm'){ await maybeFinishBrainstorm(); return }
|
|
1657
1870
|
if(meetingState){ await continueMeetingRound(); return }
|
|
1658
1871
|
if(verifyState){ await continueVerifyRound(); return }
|
|
@@ -1660,11 +1873,24 @@ export function apply(ctx) {
|
|
|
1660
1873
|
// A meeting requested while a verify held the floor is parked in pendingMeeting; once the
|
|
1661
1874
|
// verify queue has truly drained (no verifyState / pendingVerify), resume it before anything else.
|
|
1662
1875
|
if(pendingMeeting){ const pm=pendingMeeting; pendingMeeting=null; await startMeeting(pm.agenda, pm.type, pm.targetId); return }
|
|
1663
|
-
// mailbox delivery
|
|
1876
|
+
// mailbox delivery. `wakeResident` CLEARS the heartbeat as its first act, so a pass that delivers
|
|
1877
|
+
// NOTHING must not return without re-arming one: a rejected `sendMessage`
|
|
1878
|
+
// (subagent/delivery-unavailable, a cold-resumed child whose Activation closed, a transient
|
|
1879
|
+
// persistence failure) re-queues the message, refreshes the recipient's lastActiveAt (so the fill
|
|
1880
|
+
// loop below skips it for a whole activityTimeoutMs) and used to end the pass with no wake, no
|
|
1881
|
+
// heartbeat and no in-flight turn — nothing else could ever re-drive the scheduler, so the whole
|
|
1882
|
+
// group stopped permanently with mail still queued. Falling through instead lets the branches
|
|
1883
|
+
// below re-arm (and B/A still get their chance); on a SUCCESSFUL delivery the recipient's own
|
|
1884
|
+
// subagent/end re-drives the pump, so the pass can end here as before.
|
|
1664
1885
|
const delivered=await deliverNextMailbox(); if(delivered) return
|
|
1665
1886
|
// maxParallel: don't start a new wake when the in-flight cap is reached
|
|
1666
1887
|
const mp=Number(params.maxParallel)||0
|
|
1667
1888
|
if(mp>0 && busy.size>=mp){ armHeartbeat(); return }
|
|
1889
|
+
// Refresh the artifact counter from the residents' own card libraries (fs-written cards count
|
|
1890
|
+
// too) before the auto-sync test below, so `meetingKeepEvery` measures NEW artifacts — and
|
|
1891
|
+
// convene the sync meeting here, because this is the only place an `fs`-written card is seen.
|
|
1892
|
+
await syncArtifactCount()
|
|
1893
|
+
if(maybeArtifactSyncMeeting()) return
|
|
1668
1894
|
// B) stall auto-sync meeting (分级保活 B): the group has been idle with NO progress for
|
|
1669
1895
|
// stallAutoMeetingMs → convene a sync meeting so the residents coordinate their next move
|
|
1670
1896
|
// (framework convenes & records; residents decide — never assigns work). Only when no
|
|
@@ -1763,8 +1989,8 @@ export function apply(ctx) {
|
|
|
1763
1989
|
realCompact(r).catch(()=>{}) // best-effort real DSH /compact of this resident while idle
|
|
1764
1990
|
const output=blocksToText(info&&info.lastAssistantMessage)
|
|
1765
1991
|
const parsed=parseReply(output)
|
|
1766
|
-
postmark(r, parsed) // context/compact bookkeeping, regardless of wake kind (clears any leak)
|
|
1767
1992
|
const kind=wakeKind.get(r.rId)||'normal'
|
|
1993
|
+
postmark(r, parsed, kind) // context/compact bookkeeping — a mid-consensus turn may NOT ack a compaction it never received
|
|
1768
1994
|
if(kind==='meeting' && meetingState){
|
|
1769
1995
|
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}
|
|
1770
1996
|
meetingState.lastInputAt=now()
|
|
@@ -1843,6 +2069,9 @@ export function apply(ctx) {
|
|
|
1843
2069
|
// carrying a previous run's records over would let a stale `passed` open the new gate.
|
|
1844
2070
|
formal={}; formalTodos=[]
|
|
1845
2071
|
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
|
|
2072
|
+
// Re-base the artifact counter on the cards already on disk, so the auto-sync meeting counts
|
|
2073
|
+
// artifacts written by THIS run (the project tree may already hold cards from an earlier run).
|
|
2074
|
+
artifactBaseline=null; artifactCount=0; await syncArtifactCount()
|
|
1846
2075
|
lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
|
|
1847
2076
|
const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
|
|
1848
2077
|
pendingSpawns=[] // a fresh run replaces the roster: nothing is queued from before
|
|
@@ -1883,7 +2112,15 @@ export function apply(ctx) {
|
|
|
1883
2112
|
// If the persisted State came from a DIFFERENT process (crash/restart), the saved
|
|
1884
2113
|
// childIds are stale; clear them so residents re-spawn (their libraries persist on
|
|
1885
2114
|
// disk and re-seed the resumed run). Same-process pause→resume keeps continuable ids.
|
|
2115
|
+
//
|
|
2116
|
+
// `persistedEpoch` is only ever READ here, so before this fix a same-process pause→resume
|
|
2117
|
+
// compared this process's random epoch against the epoch saved by ITSELF and always concluded
|
|
2118
|
+
// "cross-process": every resident was re-spawned, `busy`/`wakeKind` were thrown away while the
|
|
2119
|
+
// OLD children were still running (`resume()` never interrupts them, so the late
|
|
2120
|
+
// `subagent/end` that the code's own comment warns about was guaranteed, not hypothetical).
|
|
2121
|
+
// Re-sync it on a same-process load, which is the contract `paused` already assumes.
|
|
1886
2122
|
const crossProcess = persistedEpoch !== processEpoch
|
|
2123
|
+
if(!crossProcess) persistedEpoch = processEpoch
|
|
1887
2124
|
if(crossProcess){ for(const [,r] of residents){ r.childId=''; r.status='brainstorm'; r.roundsSinceCompact=0 } }
|
|
1888
2125
|
// ANY re-spawn (cross-process OR a same-process abort that already cleared childIds) must get a FRESH
|
|
1889
2126
|
// coordination/concurrency state and a brainstorm phase. Otherwise: re-spawned brainstorm residents run
|
|
@@ -1913,9 +2150,18 @@ export function apply(ctx) {
|
|
|
1913
2150
|
pendingSpawns: pendingSpawns.length, hostChildLimit: (hostChildLimit===undefined?null:hostChildLimit),
|
|
1914
2151
|
meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
|
|
1915
2152
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
2153
|
+
// The coordination counters (`spoke k/N`, `voted k/N`, the verification round) used to live only
|
|
2154
|
+
// in `report()`: from `status()` alone an operator could not tell "the group is progressing"
|
|
2155
|
+
// from "the group is stuck at 2/4 votes", which is exactly how F1/F2/F7 stay invisible.
|
|
2156
|
+
consensus: meetingState?{kind:'meeting',id:meetingState.id,agenda:meetingState.agenda,round:meetingState.round,spoke:Object.keys(meetingState.inputs).length,expected:residents.size,solvedVotes:Object.values(meetingState.inputs).filter(iv=>iv.voteSolved===true).length}
|
|
2157
|
+
:(verifyState?{kind:'verify',target:verifyState.targetId,targetType:verifyState.targetType,stage:verifyState.stage,round:verifyState.round,voted:Object.keys(verifyState.verdicts).length,expected:residents.size}:null),
|
|
2158
|
+
artifactCount: artifactCount, artifactBaseline: artifactBaseline,
|
|
1916
2159
|
// The Lean knobs and the per-object formal records are part of the readable status: without
|
|
1917
2160
|
// them a `require`-mode run that keeps returning 未定论 would be undiagnosable from outside.
|
|
1918
2161
|
formal: formalView(),
|
|
2162
|
+
// The parameter NAMES (not just the rendered string) so a caller — the `/v4 set` handler in
|
|
2163
|
+
// particular — can reject an unknown key instead of silently dropping it.
|
|
2164
|
+
paramsKeys: Object.keys(params),
|
|
1919
2165
|
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny','formalVerify','leanCommand','leanArgs','leanTimeoutMs'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
1920
2166
|
function formalReportText(){
|
|
1921
2167
|
if(!formalOn()) return '- 未启用(`formalVerify` = off;可用 vibe_v4_set 切到 encourage / require)'
|
|
@@ -2003,9 +2249,30 @@ export function apply(ctx) {
|
|
|
2003
2249
|
if(min===undefined) return n
|
|
2004
2250
|
return n < min ? min : n
|
|
2005
2251
|
}
|
|
2006
|
-
|
|
2252
|
+
/**
|
|
2253
|
+
* Apply a parameter update. Keys the parameter layer does not know are NOT silently dropped: the
|
|
2254
|
+
* caller gets them back (`ok:false` + `ignored`), because `{ok:true}` for a call that changed
|
|
2255
|
+
* nothing is the "declared but not received" silent failure AUDIT-CHECKLIST §1.9 names.
|
|
2256
|
+
*/
|
|
2257
|
+
function setParams(upd){
|
|
2258
|
+
const keys=Object.keys(upd||{})
|
|
2259
|
+
// `k in params` 会沿原型链判真:`toString` / `constructor` / `__proto__` 这类键会被当成"已知参数"
|
|
2260
|
+
// 写进 params(`params['__proto__']=…` 还会真的改掉 params 的原型),而 schema 的
|
|
2261
|
+
// additionalProperties:false 在 v4 上**不产生运行期拒绝**(裸 tools.register,宿主不校验实参)。
|
|
2262
|
+
// 已知参数一律以**自有属性**为准(AUDIT-CHECKLIST §1.9 的"声明=接收"只能在自有键上成立)。
|
|
2263
|
+
const has = (o,k)=>Object.prototype.hasOwnProperty.call(o,k)
|
|
2264
|
+
const ignored=keys.filter(k=>!has(params,k))
|
|
2265
|
+
for(const k of keys){ if(has(params,k)){ const nv=normalizeParam(k, upd[k]); params[k]= (typeof nv==='number') ? clampInt(k, nv) : nv } }
|
|
2266
|
+
saveSettings().catch(()=>{})
|
|
2267
|
+
if(ignored.length){
|
|
2268
|
+
logActivity('set','忽略未知参数:'+ignored.join(', '))
|
|
2269
|
+
return {ok:false,message:'unknown parameter(s): '+ignored.join(', ')+' — nothing was changed'+(ignored.length<keys.length?('(已应用:'+keys.filter(k=>!(ignored.indexOf(k)>=0)).join(', ')+')'):''),ignored,applied:keys.filter(k=>!(ignored.indexOf(k)>=0))}
|
|
2270
|
+
}
|
|
2271
|
+
return {ok:true,applied:keys}
|
|
2272
|
+
}
|
|
2007
2273
|
// ---- create / configure (no auto-start) + settings-file persistence ----
|
|
2008
|
-
|
|
2274
|
+
// 同一把守护:State/settings.json 是用户可编辑的,原型链上的键名(toString/__proto__)不是参数。
|
|
2275
|
+
async function loadSettings(){ const s=await readJson('State/settings.json'); if(s&&typeof s==='object'){ for(const k of Object.keys(s)){ if(Object.prototype.hasOwnProperty.call(params,k)){ const nv=normalizeParam(k, s[k]); params[k]= (typeof nv==='number') ? clampInt(k, nv) : nv } } } }
|
|
2009
2276
|
async function saveSettings(){ await writeJson('State/settings.json', params) }
|
|
2010
2277
|
// Create/configure a project and set params/problem WITHOUT starting any resident.
|
|
2011
2278
|
// The intended flow: vibe_v4_configure {project?, problem?, params?} → vibe_v4_start {}.
|
|
@@ -2041,10 +2308,27 @@ export function apply(ctx) {
|
|
|
2041
2308
|
rememberAgent, forgetAgent,
|
|
2042
2309
|
setPause, initAbort, postMessage, startMeeting, saveAll, broadcast, configure, loadSettings,
|
|
2043
2310
|
currentResident:()=>currentResident,
|
|
2311
|
+
/** Does this session own the given child id? (used by the `subagent/start` capture) */
|
|
2312
|
+
ownsChild,
|
|
2313
|
+
/** Stash a live Agent seen at `subagent/start` time; claimed by `spawnResident` on resolve. */
|
|
2314
|
+
stashStartAgent,
|
|
2044
2315
|
// safety kick: drive one scheduler pass (used when an end handler errored, so an exceptional
|
|
2045
2316
|
// turn can never leave the group with no end-event and no heartbeat to continue it)
|
|
2046
2317
|
nudge:()=>scheduleNext().catch(()=>{}),
|
|
2047
|
-
|
|
2318
|
+
/**
|
|
2319
|
+
* Which resident is calling a resident-facing tool?
|
|
2320
|
+
* 1. the caller's subagent id matched against a resident's childId — the exact answer;
|
|
2321
|
+
* 2. the last-woken resident, but ONLY when no caller identity exists at all (the mock/harness
|
|
2322
|
+
* path, where `exec.agent` is undefined).
|
|
2323
|
+
* An agent that IS present but matches no resident is NOT guessed at: attributing a host call to
|
|
2324
|
+
* whichever resident happened to be woken last writes into that resident's private library with
|
|
2325
|
+
* no indication that the attribution was invented (AUDIT-CHECKLIST §0.2: identity is never
|
|
2326
|
+
* inferred from global mutable state). Such a call now fails explicitly with 'no such resident'.
|
|
2327
|
+
*/
|
|
2328
|
+
residentIdOf:(agent)=>{ const m=residentOfAgent(agent); if(m) return m
|
|
2329
|
+
let hasId=false; try { hasId=!!(agent&&agent.id) } catch(e){ hasId=false }
|
|
2330
|
+
if(hasId) return ''
|
|
2331
|
+
const c=currentResident; return (c && residents.has(c)) ? c : '' },
|
|
2048
2332
|
useResident:(id)=>{ currentResident=id },
|
|
2049
2333
|
publishProgress, recordProposition, recordMethod, recordSubproblem, listResidents, reportContext,
|
|
2050
2334
|
proposeTask, claimTask, taskDone, listTasks,
|
|
@@ -2071,6 +2355,12 @@ export function apply(ctx) {
|
|
|
2071
2355
|
// `vs` mirrors the live verification state ({targetId,targetType,targetOwner,stage,history,verdicts});
|
|
2072
2356
|
// pass one explicitly to ask "what WOULD the voters read for this object right now?".
|
|
2073
2357
|
verify:(rId,vs)=>{ const r=residents.get(String(rId)); return r?verifyPrompt(r, vs||verifyState||{targetId:'',targetType:'proposition',targetOwner:'',stage:'independent',verdicts:{}}):'' },
|
|
2358
|
+
// The meeting prompt is the ONLY group-chat / task-allocation / stop-vote surface this preset
|
|
2359
|
+
// has, so a host (or the audit corpus) must be able to READ it. Pass `st` to ask "what would
|
|
2360
|
+
// the next speaker read right now?"; the live meeting state is the default.
|
|
2361
|
+
meeting:(rId,st)=>{ const r=residents.get(String(rId)); if(!r) return ''
|
|
2362
|
+
const s=st||meetingState||{agenda:'(无进行中的会议)',type:'general',inputs:{},order:[]}
|
|
2363
|
+
return meetingPrompt(r,s) },
|
|
2074
2364
|
formalBlock:(target)=>formalPromptBlock(target),
|
|
2075
2365
|
formalWorkLine:()=>formalWorkLine(),
|
|
2076
2366
|
},
|
|
@@ -2081,9 +2371,15 @@ export function apply(ctx) {
|
|
|
2081
2371
|
if(which==='brainstorm') return brainstormPrompt(r)
|
|
2082
2372
|
if(which==='heartbeat') return heartbeatPrompt(r)
|
|
2083
2373
|
if(which==='coreRules') return coreRulesBrief()
|
|
2374
|
+
if(which==='meeting') return (arg&&arg.meeting)?meetingPrompt(r,arg.meeting):(meetingState?meetingPrompt(r,meetingState):meetingPrompt(r,{agenda:'(无进行中的会议)',type:'general',inputs:{},order:[]}))
|
|
2084
2375
|
if(which==='verify'){
|
|
2085
2376
|
const target=idSafe(String((arg&&arg.target)||''))
|
|
2086
|
-
|
|
2377
|
+
// F14 的同一条尺子:前缀(p-/m-/s-)就是对象归属的契约。此前的兜底把**任何**认不出的 id
|
|
2378
|
+
// 都当成 proposition,于是这个"读真实提示词"的工具会展示一条带着错误对象类型的投票提示词
|
|
2379
|
+
// (而真正入队的入口 maybeQueueVerify 已经拒绝这种 id)。认不出就如实拒绝,不猜。
|
|
2380
|
+
const tt=guessTargetType(target)
|
|
2381
|
+
if(!tt) return 'V4_INVALID_ARGUMENT: target must start with p- (proposition) / m- (method) / s- (subproblem) — cannot render a verification prompt for an unknown object kind'
|
|
2382
|
+
return verifyPrompt(r,{targetId:target,targetType:tt,targetOwner:'',stage:String((arg&&arg.stage)||'independent'),history:{},verdicts:{}})
|
|
2087
2383
|
}
|
|
2088
2384
|
return normalPrompt(r)
|
|
2089
2385
|
},
|
|
@@ -2117,7 +2413,7 @@ export function apply(ctx) {
|
|
|
2117
2413
|
// The exact text a resident would receive. "成员读到的文字就是产品" (AUDIT-CHECKLIST §0.1): a host
|
|
2118
2414
|
// (or an audit) must be able to READ the prompt, not just the tool return values, or a prompt
|
|
2119
2415
|
// defect stays invisible. Pure builder calls — no side effects on the run.
|
|
2120
|
-
registerTool('vibe_v4_prompts','Read the exact prompt text a resident would receive (which: brainstorm|normal|heartbeat|verify|coreRules). member = resident id; target/stage describe the object for `verify`. Prompt text is the product — this makes it auditable.',objParams({which:{type:'string',enum:['brainstorm','normal','heartbeat','verify','coreRules']},member:{type:'string'},target:{type:'string'},stage:{type:'string'}},['which']),async (s,a)=>{
|
|
2416
|
+
registerTool('vibe_v4_prompts','Read the exact prompt text a resident would receive (which: brainstorm|normal|heartbeat|meeting|verify|coreRules). member = resident id; target/stage describe the object for `verify`. Prompt text is the product — this makes it auditable.',objParams({which:{type:'string',enum:['brainstorm','normal','heartbeat','meeting','verify','coreRules']},member:{type:'string'},target:{type:'string'},stage:{type:'string'}},['which']),async (s,a)=>{
|
|
2121
2417
|
const which=String(a.which||'normal')
|
|
2122
2418
|
if(which==='coreRules') return {ok:true,which,text:await s.promptApi.coreRules()}
|
|
2123
2419
|
const text=await s.promptFor(which,String(a.member||'r-1'),a)
|
|
@@ -2135,7 +2431,7 @@ export function apply(ctx) {
|
|
|
2135
2431
|
// MODE is dynamic — switching it changes the very next prompt), leanCommand/leanArgs select the
|
|
2136
2432
|
// executable, leanTimeoutMs bounds one run. Invalid values fall back to the defaults and an
|
|
2137
2433
|
// unknown mode degrades to 'off' (never to a STRONGER mode).
|
|
2138
|
-
registerTool('vibe_v4_set','Set V4 parameters
|
|
2434
|
+
registerTool('vibe_v4_set','Set V4 parameters: residentCount (how many residents a start spawns), compactThreshold (resident context % that triggers a compaction) and compactAfterRounds (rounds between soft compactions), meetingKeepEvery (every N newly accumulated artifacts an automatic sync meeting is convened), maxParallel (how many residents may be woken concurrently), activityTimeoutMs (idle window before a resident is nudged; also the heartbeat/watchdog period), stallAutoMeetingMs (how long the group may make NO progress before an auto sync meeting is convened), verdictMaxRounds (how many verification rounds one object gets: 1 independent round + re-vote debate rounds), model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line; formalVerify: "off" (default, a true no-op) | "encourage" (residents decide by implementation difficulty whether to formalize in Lean; a passing Lean run turns the vote into a FIDELITY review of the Lean statements) | "require" (same, plus a gate: a unanimous true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record); leanCommand/leanArgs/leanTimeoutMs configure the toolchain. A key outside this list is refused (ok:false, ignored:[...]) instead of being silently dropped.',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'}},formalVerify:{type:'string',enum:['off','encourage','require']},leanCommand:{type:'string'},leanArgs:{type:'array',items:{type:'string'}},leanTimeoutMs:{type:'integer'}}),(s,a)=>{ const r=s.setParams(a); const st=s.status(); if(r&&r.ok===false) return Object.assign({},st,{ok:false,warning:r.message,ignored:r.ignored}); return st })
|
|
2139
2435
|
// resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
|
|
2140
2436
|
// fall back to the last-woken resident when called by the host/assistant.
|
|
2141
2437
|
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) })
|
|
@@ -2152,8 +2448,16 @@ export function apply(ctx) {
|
|
|
2152
2448
|
registerTool('vibe_v4_list_tasks','(resident) List open tasks.',objParams({}),(s)=>({ok:true,tasks:s.listTasks()}))
|
|
2153
2449
|
// context / compact (resident reports its context usage so the framework can /compact-equivalent)
|
|
2154
2450
|
registerTool('vibe_v4_report_context','(resident) Report your context usage %; the framework compacts (self-summary) when it reaches compactThreshold.',objParams({pct:{type:'number'}},['pct']),(s,a,x)=>s.reportContext(s.residentIdOf(x),a.pct))
|
|
2155
|
-
|
|
2156
|
-
|
|
2451
|
+
// NOT IMPLEMENTED — and the description says so. These two tools were registered with
|
|
2452
|
+
// "Reserved: shared-file write lock", which reads as "the lock exists and works": a model following
|
|
2453
|
+
// the v3 convention called `claim_write`, got `{ok:true}` and concluded it held a lock that no code
|
|
2454
|
+
// anywhere implements (this preset has no shared file a resident writes — see the write-scope rule
|
|
2455
|
+
// in `contextBrief`, which restricts every resident to its OWN Progress/Propos/Methods/Subproblems
|
|
2456
|
+
// library, while the framework alone writes Shared/ and State/). They stay registered (removing them
|
|
2457
|
+
// would change the model-visible tool surface) and keep echoing the key, but they now tell the
|
|
2458
|
+
// truth: nothing is reserved, nothing is serialized, write only your own files.
|
|
2459
|
+
registerTool('vibe_v4_claim_write','NOT IMPLEMENTED: no lock exists. This returns {ok:true} without reserving anything. Each resident writes only its own library (Progress/<you>/, Propos/<you>/, Methods/<you>/, Subproblems/<you>/), so overlapping writers are not expected; the framework alone writes Shared/ and State/ (serially, per file). Do not rely on this to exclude another resident.',objParams({target:{type:'string'}},['target']),(s,a)=>({ok:true,key:a.target,locked:false,note:'no lock is implemented; write only your own library files'}))
|
|
2460
|
+
registerTool('vibe_v4_release_write','NOT IMPLEMENTED: there is no lock to release. Returns {ok:true} as a no-op so an agent that calls it out of habit is not misled into thinking it held a reservation.',objParams({target:{type:'string'}},['target']),(s,a)=>({ok:true,key:a.target,locked:false,note:'no lock is implemented; nothing was reserved'}))
|
|
2157
2461
|
|
|
2158
2462
|
// ── Lean formal verification (docs/formal-verification.md §5) ─────────────
|
|
2159
2463
|
// These three tools are registered UNCONDITIONALLY. Registration is STATIC (a mode-dependent
|
|
@@ -2180,7 +2484,10 @@ export function apply(ctx) {
|
|
|
2180
2484
|
const line=String(inv&&inv.rawInput?inv.rawInput:'').trim(); const parts=line.split(/\s+/); const cmd=parts[0]||''; const rest=parts.slice(1)
|
|
2181
2485
|
let r
|
|
2182
2486
|
if(cmd==='configure') r=await s.configure({project:rest[0]||'', problem:parts.slice(2).join(' ')})
|
|
2183
|
-
|
|
2487
|
+
// `/v4 start <problem>` is the natural way to try this out and the tool it mirrors accepts a
|
|
2488
|
+
// problem, so the arguments are honoured instead of silently dropped (a bare `/v4 start` keeps
|
|
2489
|
+
// using the configured problem).
|
|
2490
|
+
else if(cmd==='start') r=await s.start(rest.length?{problem:rest.join(' ')}:{})
|
|
2184
2491
|
else if(cmd==='resume') r=await s.resume()
|
|
2185
2492
|
else if(cmd==='pause') r=s.setPause()
|
|
2186
2493
|
else if(cmd==='abort') r=await s.initAbort()
|
|
@@ -2191,9 +2498,32 @@ export function apply(ctx) {
|
|
|
2191
2498
|
else if(cmd==='members') r={ok:true,residents:s.listResidents()}
|
|
2192
2499
|
else if(cmd==='add') r=await s.addMember(rest.join(' '))
|
|
2193
2500
|
else if(cmd==='remove') r=await s.removeMember(rest[0]||'')
|
|
2194
|
-
else if(cmd==='set'){
|
|
2195
|
-
|
|
2196
|
-
|
|
2501
|
+
else if(cmd==='set'){
|
|
2502
|
+
// Keys are validated HERE and the value is handed to the same `normalizeParam` the tool path
|
|
2503
|
+
// uses, so `/v4 set` cannot (a) coerce `provider=123` into a number for a string-typed key,
|
|
2504
|
+
// or (b) report success for a typo that `setParams` would silently ignore.
|
|
2505
|
+
const upd={}
|
|
2506
|
+
const known=s.status().paramsKeys||[]
|
|
2507
|
+
for(const tok of rest){
|
|
2508
|
+
const eq=tok.indexOf('='); if(eq<=0) continue
|
|
2509
|
+
const k=tok.slice(0,eq)
|
|
2510
|
+
if(known.indexOf(k)<0){ r={ok:false,message:'unknown parameter: '+k+'(用 /v4 status 查看可调参数)'}; break }
|
|
2511
|
+
upd[k]=tok.slice(eq+1)
|
|
2512
|
+
}
|
|
2513
|
+
if(r===undefined){
|
|
2514
|
+
// Apply, then answer with the SAME shape the `vibe_v4_set` TOOL answers with (the current
|
|
2515
|
+
// status + the applied/ignored bookkeeping): reporting only the raw setParams result would
|
|
2516
|
+
// make the slash command a different, less useful surface than the tool it mirrors.
|
|
2517
|
+
const res=s.setParams(upd)
|
|
2518
|
+
const st=s.status()
|
|
2519
|
+
r=(res&&res.ok===false)?Object.assign({},st,{ok:false,warning:res.message,ignored:res.ignored}):Object.assign({},st,{applied:res.applied})
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
else r={ok:false,usage:'configure|start [problem]|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|set <k=v>...'}
|
|
2523
|
+
// `kind:'error'` on a rejected business action, exactly as v2/v3 do: every branch above used to
|
|
2524
|
+
// report `success`, so the human's only non-tool control surface read as success on failure.
|
|
2525
|
+
const failed = !r || r.ok === false || (r.ok === undefined && !!r.error)
|
|
2526
|
+
return {kind: failed?'error':'success', text:JSON.stringify(r,null,2)}
|
|
2197
2527
|
},
|
|
2198
2528
|
}))
|
|
2199
2529
|
|
|
@@ -2202,13 +2532,36 @@ export function apply(ctx) {
|
|
|
2202
2532
|
// registry (dsh-subagent:1231 dispose -> dsh-agent:508 store.delete ->
|
|
2203
2533
|
// dsh-subagent:1241 settle/emit), so an end-time `agents.get(childId)` can never
|
|
2204
2534
|
// resolve. See `liveAgents` in the session body.
|
|
2535
|
+
//
|
|
2536
|
+
// DO NOT key this handler on `childOwner`: the host emits `subagent/start` INSIDE
|
|
2537
|
+
// `startContinuable`'s await (dsh-subagent/lib/index.js:1116 → `observer.start` → emit at `:306`,
|
|
2538
|
+
// and the direct `emit('subagent/start', …)` at `:279`), i.e. BEFORE that call resolves — and
|
|
2539
|
+
// `spawnResident` can only learn `started.childId` after the resolve. Keying on it made this
|
|
2540
|
+
// handler return for EVERY child, so `liveAgents` stayed empty for the whole run and the real
|
|
2541
|
+
// `/compact` path (`liveAgentOf` → `if(!agent || !agent.session) return`) was dead code.
|
|
2542
|
+
//
|
|
2543
|
+
// Ownership is therefore resolved the other way round: the handler finds the (usually single)
|
|
2544
|
+
// session whose spawn window is open and STASHES the Agent under the child id from the payload;
|
|
2545
|
+
// `spawnResident` claims that stash the moment its call resolves. For a host that embeds the spawn
|
|
2546
|
+
// label in the id, `ownsChild` can be decided from the payload alone — that path is used when no
|
|
2547
|
+
// spawn window is open, so a `subagent/start` for a child we did not spawn is never recorded.
|
|
2205
2548
|
ctx.on('subagent/start', function(info){
|
|
2206
2549
|
if(!info || !info.id) return
|
|
2207
|
-
const sid=childOwner.get(info.id); const s=sid!==undefined?sessions.get(sid):undefined
|
|
2208
|
-
if(!s) return
|
|
2209
2550
|
let agent
|
|
2210
2551
|
try { agent = agents.get(info.id) } catch(e){ agent = undefined }
|
|
2211
|
-
if(agent)
|
|
2552
|
+
if(!agent) return
|
|
2553
|
+
let s
|
|
2554
|
+
try {
|
|
2555
|
+
for(const [,ss] of sessions){
|
|
2556
|
+
const open=ss.ownsChild && ss.ownsChild(info.id)
|
|
2557
|
+
if(open){ s=ss; break }
|
|
2558
|
+
}
|
|
2559
|
+
if(!s){ const sid=childOwner.get(info.id); if(sid!==undefined) s=sessions.get(sid) }
|
|
2560
|
+
} catch(e){ s=undefined }
|
|
2561
|
+
if(s){ s.rememberAgent(info.id, agent); return }
|
|
2562
|
+
// No owner yet — the spawn this child belongs to is still inside `startContinuable`. Stash it for
|
|
2563
|
+
// the first session whose spawn window is open, so the claim after the resolve can find it.
|
|
2564
|
+
for(const [,ss] of sessions){ if(ss.stashStartAgent){ ss.stashStartAgent(info.id, agent); return } }
|
|
2212
2565
|
})
|
|
2213
2566
|
|
|
2214
2567
|
ctx.on('subagent/end', function(info){
|