dsh-vibe-math 2.4.1 → 2.6.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 +53 -14
- package/README.md +54 -14
- package/cordis.patch.yml +54 -17
- package/docs/AUDIT-CHECKLIST.md +26 -7
- package/docs/COMPAT-AUDIT-ROUND3.md +16 -1
- package/docs/final-paper.md +171 -0
- package/docs/release-notes/RELEASE-NOTES-2.5.0.md +173 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
- package/docs/test-timing.md +77 -40
- package/installer.js +225 -45
- package/package.json +6 -3
- package/preset-declaration.js +38 -7
- package/prompt-corpus-persona/persona-corpus.json +13 -13
- package/prompt-corpus-persona/persona-corpus.md +113 -39
- package/prompt-corpus-v2/formal-verify-v2.json +128 -92
- package/prompt-corpus-v2/formal-verify-v2.md +380 -142
- package/prompt-corpus-v3/formal-verify-v3.json +117 -81
- package/prompt-corpus-v3/formal-verify-v3.md +693 -1189
- 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 +68 -8
- package/tests/audit-persona-surface.test.mjs +21 -8
- package/tests/audit-v5-sensitivity.mjs +47 -6
- package/tests/e2e-v5-round2.test.mjs +825 -1
- package/tests/formal-verify-v2.test.mjs +57 -2
- package/tests/formal-verify-v3.test.mjs +67 -2
- package/tests/formal-verify-v4.test.mjs +120 -4
- package/tests/prompt-v5-integrity.test.mjs +9 -3
- package/tests/run-tests.mjs +104 -23
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +20 -4
- package/vibe-math-v2/vibe-math-v2.js +1318 -106
- 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 +36 -14
- package/vibe-math-v3/vibe-math-v3.js +1348 -179
- 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 +36 -8
- package/vibe-math-v4/vibe-math-v4.js +1195 -57
- 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 +16 -8
- package/vibe-math-v5/vibe-math-v5.js +1628 -112
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +143 -41
|
@@ -90,6 +90,18 @@ export function apply(ctx) {
|
|
|
90
90
|
leanCommand: 'lean',
|
|
91
91
|
leanArgs: [], // inserted BEFORE the file name (e.g. ['env','lean'] with lake)
|
|
92
92
|
leanTimeoutMs: 120000,
|
|
93
|
+
// ---- final paper (docs/final-paper.md §1) ----------------------------------
|
|
94
|
+
// The run's conclusion is a PAPER the whole team co-authors once consensus has converged.
|
|
95
|
+
// finalPaper=true triggers it automatically at that moment (idempotent); the other four knobs
|
|
96
|
+
// decide what is produced, in which language, whether PDFs are attempted, and WHO finalises.
|
|
97
|
+
finalPaper: true,
|
|
98
|
+
paperFormat: 'both', // 'both' | 'md' | 'tex'
|
|
99
|
+
paperLanguage: 'zh', // 'zh' | 'en'
|
|
100
|
+
paperCompilePdf: true, // compile a PDF when a LaTeX engine is actually detected
|
|
101
|
+
// v2 §A5: the editor is the SESSION ROOT / human side ("office") by default — v4 has no
|
|
102
|
+
// `facilitator` roster member with an LLM — or one named resident (`resident:<id>`).
|
|
103
|
+
paperEditor: 'office',
|
|
104
|
+
paperLatexCommand: '', // preferred LaTeX executable ('' = the standard detection order)
|
|
93
105
|
}
|
|
94
106
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
95
107
|
let running = false, autoDone = false, phase = 'idle'
|
|
@@ -102,10 +114,20 @@ export function apply(ctx) {
|
|
|
102
114
|
// whether its verdict may take effect.
|
|
103
115
|
let formal = {}, formalTodos = [], formalPersisted = false
|
|
104
116
|
let meetingState = null, verifyState = null, pendingVerify = [], pendingMeeting = null // pendingVerify: FIFO queue (several residents may independently propose different objects before any verify runs — a single slot silently DROPPED all but the last proposal)
|
|
117
|
+
// The final-paper flow (docs/final-paper.md, v1 + v2 amendments). In-memory only: the DURABLE
|
|
118
|
+
// idempotency record is `Paper/<run id>/paper.meta.json` on disk (finalizedAt + per-artifact
|
|
119
|
+
// existence, v2 §C), so a re-trigger after a restart is still recognised even though this object
|
|
120
|
+
// did not survive. While a paper is in flight this holds the whole team-authoring state machine.
|
|
121
|
+
let paperState = null
|
|
105
122
|
let busy = new Set(), wakeKind = new Map(), currentResident = ''
|
|
106
123
|
let finalizeLock = null // 'meeting'|'verify' while a consensus finalize is running (reentry guard)
|
|
107
124
|
const verifiedRecently = new Map() // targetId -> timestamp when it was closed as Verified (dedup re-propose)
|
|
108
125
|
let lastActivityAt = now(), lastProgressAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
|
|
126
|
+
// `artifactBaseline` = the artifact count observed when the auto-sync-meeting counter was last
|
|
127
|
+
// re-based (see `countArtifacts`/`syncArtifactCount`): the documented behaviour is "every N NEW
|
|
128
|
+
// artifacts", so the modulo test must run against the number of artifacts written SINCE the run
|
|
129
|
+
// started, not against the raw file count of a possibly pre-existing project tree.
|
|
130
|
+
let artifactBaseline = null
|
|
109
131
|
const activityLogCap = 200
|
|
110
132
|
|
|
111
133
|
// ---- utils ----
|
|
@@ -858,10 +880,21 @@ export function apply(ctx) {
|
|
|
858
880
|
// `off` mode must not CREATE Lean state: a run that never used the feature leaves no
|
|
859
881
|
// State/formal.json behind. Seeded/live records (or a mode that is on) still persist.
|
|
860
882
|
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
|
|
883
|
+
await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount,artifactBaseline,
|
|
884
|
+
// In-flight consensus must survive a restart too: a verification that had collected 3 of 4
|
|
885
|
+
// verdicts and a meeting that had collected half the speeches used to evaporate entirely
|
|
886
|
+
// (Shared/debates|meetings/<id>.md are only written by finalize*, so the partial debate was
|
|
887
|
+
// not even on disk) — and the queued proposals in pendingVerify were lost with it.
|
|
888
|
+
meetingState,verifyState,pendingVerify,pendingMeeting})
|
|
862
889
|
}
|
|
863
890
|
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
|
|
891
|
+
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))
|
|
892
|
+
// Restore the in-flight consensus (see saveAll). `resume()` refreshes the watchdog clocks right
|
|
893
|
+
// after this, so a run resumed after a crash gets a fresh stall window instead of being
|
|
894
|
+
// abandoned by the meeting/verify watchdog on its first serviced pass.
|
|
895
|
+
meetingState=s.meetingState||null; verifyState=s.verifyState||null
|
|
896
|
+
pendingVerify=Array.isArray(s.pendingVerify)?s.pendingVerify:[]
|
|
897
|
+
pendingMeeting=s.pendingMeeting||null }
|
|
865
898
|
const rm=await readJson('State/residents.json'); if(rm&&typeof rm==='object') residents=new Map(Object.entries(rm))
|
|
866
899
|
const mb=await readJson('State/mailboxes.json'); if(mb&&typeof mb==='object') mailboxes=new Map(Object.entries(mb))
|
|
867
900
|
const tb=await readJson('State/taskboard.json'); if(Array.isArray(tb)) taskboard=tb
|
|
@@ -878,7 +911,12 @@ export function apply(ctx) {
|
|
|
878
911
|
|
|
879
912
|
// ---- resident prompts ----
|
|
880
913
|
function banner(){ const o=[]; for(const [id,r] of residents) o.push('- '+id+'「'+(r.direction||'(未定)')+'」'+r.status+'·轮'+r.rounds); return o.join('\n') }
|
|
881
|
-
|
|
914
|
+
// Who sent this? `facilitator` is the framework/human messenger (NOT a resident, not on the
|
|
915
|
+
// roster): a resident that reads a bare `[facilitator]` line tries to reply to it and gets
|
|
916
|
+
// `no such resident`. The definition travels WITH the frame, so it is present on the very wake
|
|
917
|
+
// that shows the message — not only in the once-only onboarding brief.
|
|
918
|
+
function senderLabel(from){ return String(from)==='facilitator'?'facilitator(框架/人类信使,不是常驻成员,不要向它回信;要回话请用本轮回执的 "input" 或 vibe_v4_send_message {to:"all"})':String(from) }
|
|
919
|
+
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
920
|
function residentLibraries(){
|
|
883
921
|
const base=frameworkRoot()
|
|
884
922
|
return '你的资料库根目录:'+base+'/\n'
|
|
@@ -934,6 +972,7 @@ export function apply(ctx) {
|
|
|
934
972
|
s.push('### 规则')
|
|
935
973
|
s.push('- 只有 Verified/(或卡片标"已验证·真/假")算已确立;其余都是你的实验性工作,请区分"猜想/已知"。')
|
|
936
974
|
s.push('- 验证必须**全组一致**(全真或全假);你只信全票结果。未全票的对象留在库里带概率。')
|
|
975
|
+
s.push('- `facilitator` 是**框架/人类介入的信使名**(不是常驻成员,不在编制里):它转达人类或框架的话,但**不要向它回信**(会返回 no such resident);要回话请用本轮回执的 "input" 字段(会转给全组)或 `vibe_v4_send_message {to:"all"}`。')
|
|
937
976
|
s.push('- 你自己决定做什么,但**优先级/分工由团队讨论决定**,不是固定模式。若你认为问题已解决或接近解决,请**发起会议**让团队表决。')
|
|
938
977
|
s.push('- 退出时**只**输出一个 JSON 对象(放在 ```json 代码围栏内;围栏外不要有文字)。')
|
|
939
978
|
return s.join('\n')
|
|
@@ -947,6 +986,7 @@ export function apply(ctx) {
|
|
|
947
986
|
// dynamic: after a /compact the resident must re-anchor on the rules it is actually
|
|
948
987
|
// living under right now.
|
|
949
988
|
return '[核心规则重申] 只有 Verified/(及标记"已验证·真/假")算已确立;验证须全组一致(全真或全假)才作数,否则留库附平均概率;你只写自己的库('+base+'/ 的 Progress/<你>/、Propos/<你>/、Methods/<你>/、Subproblems/<你>/),可只读任何人的库;任务分工由团队讨论决定;退出只输出一个 JSON 对象。'
|
|
989
|
+
+'\n`facilitator` 是**框架/人类介入的信使名**,不是常驻成员,也不在编制里——**不要向它回信**(`vibe_v4_send_message` 会返回 no such resident);要回话请用本轮回执的 "input" 字段(会转给全组)或 `vibe_v4_send_message {to:"all"}`。'
|
|
950
990
|
+(formalOn()?('\n'+formalWorkLine()):'')
|
|
951
991
|
}
|
|
952
992
|
function brainstormPrompt(r){
|
|
@@ -975,11 +1015,10 @@ export function apply(ctx) {
|
|
|
975
1015
|
const prior=Object.entries(st.inputs).filter(([k])=>k!==r.rId).map(([k,iv])=>' ['+k+'] '+String(iv.input||iv.summary||'')).join('\n')
|
|
976
1016
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
977
1017
|
+'Resident '+r.rId+' — 团队会议进行中。 A meeting is in progress (agenda: '+st.agenda+').'
|
|
978
|
-
+(st.type==='verify'?('\n团队正在验证对象:'+st.targetId+'('+st.targetType+',提出者 '+st.targetOwner+')。请先看他人意见,再给独立判断。'):'')
|
|
979
1018
|
+'\n这是一场真实讨论:下面已有人发言(转给你),请先看,然后**加入讨论/补充/反驳/表决**。'
|
|
980
1019
|
+(prior?('\n\n### 已有发言(他人 input,已转发给你)\n'+prior):'\n(目前还没人发言,你先说。)')
|
|
981
1020
|
+'\n\n你可以:提议任务(propose_task)、认领开放任务(claim_task)、提议验证对象(propose_verify)、或对"原问题是否已解决"表决(voteSolved)。请把**你的实际发言**写进 "input"。'
|
|
982
|
-
+'\nReply with ONLY a JSON object:\n'
|
|
1021
|
+
+'\n**停止表决必须是绝对票**:`voteSolved:true` 只表示你认为原问题**已解决**;只要有一名在册常驻没投 `true`(投 false、弃权、或漏写这个字段),run 就**不会**停止。不确定就投 false。' +'\nReply with ONLY a JSON object:\n'
|
|
983
1022
|
+'{"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
1023
|
}
|
|
985
1024
|
function verifyPrompt(r, vs){
|
|
@@ -990,9 +1029,19 @@ export function apply(ctx) {
|
|
|
990
1029
|
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
1030
|
const L=[]
|
|
992
1031
|
L.push((params.residentPersona?params.residentPersona+'\n':'')
|
|
993
|
-
+'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+vs.targetType+',提出者 '+vs.targetOwner+')。\n'
|
|
1032
|
+
+'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+targetTypeWord(vs.targetType)+',提出者 '+vs.targetOwner+')。\n'
|
|
994
1033
|
+'请给出你对「该对象为真」的**正确概率 `verdict`**,仅一个 0–1 数值:**1 = 绝对为真,0 = 绝对为假,0.5 = 完全不确定,其余为介于其间的程度**(不要给 TRUE/FALSE,就给一个数值)。\n'
|
|
995
|
-
|
|
1034
|
+
// ── THE VOTE CONTRACT (why the example below is 1 and not 0.9) ──────────────────────────
|
|
1035
|
+
// Only an EXACT 1 or 0 is a vote; anything strictly between is the model's honest
|
|
1036
|
+
// probability of truth, i.e. an ABSTENTION. The old text demonstrated `verdict:0.9` while
|
|
1037
|
+
// the code required `Number(prob)===1`, so a cautious-but-convinced group could never
|
|
1038
|
+
// converge: every round burned and the object stayed unverified forever. The rule is now
|
|
1039
|
+
// stated HERE, the example shows `1`, and 0.9 is explained instead of merely tolerated.
|
|
1040
|
+
+'**投票契约**:只有**恰好 1**(你认为是**绝对**为真)和**恰好 0**(你认为是**绝对**为假)算表决;**严格介于 0 与 1 之间**(例如 0.9、0.95、0.5)是**弃权**——它是你对"该对象为真"的**概率估计**,不是你的一票。\n'
|
|
1041
|
+
+' · 有把握认为它为真就投 **1**;不要为了"留一点余地"投 0.9——那会让全组永远无法定论。\n'
|
|
1042
|
+
+' · 弃权会被如实统计:本轮没有人全票 → 对象**不会**停止验证,而是把全组的**平均概率**写回它在库中的卡片(`- 概率:`),带概率继续留在库里。\n'
|
|
1043
|
+
+' · 弃权的两种合法用途:① 你确实不确定(用 0.5 附近的值表达);② 已归档的机器检查证明与命题原文不一致、你**不能**用 0 表达"命题为假"(此时请用 `formal` 回执的 `decision:"defect"` 报告偏差,见下方形式化段)。\n'
|
|
1044
|
+
+'判定规则:**仅当全体在册常驻都恰好给 1(都认为是真)、或都恰好给 0(都认为是假)**,才按「真/假」写入 Verified/ 并回写来源卡的「已验证·真/假」;否则**只按概率数值(一种程度)保留在库中**,附全组平均正确概率,不写成真/假。\n'
|
|
996
1045
|
+'请给出你**诚实独立的判断**'
|
|
997
1046
|
+(vs.stage==='debate'?',并参考他人意见:\n':'。\n')
|
|
998
1047
|
+(vs.stage==='debate'&&others?('### 他人上一轮意见(已转发给你)\n'+others+'\n'):''))
|
|
@@ -1004,7 +1053,7 @@ export function apply(ctx) {
|
|
|
1004
1053
|
}
|
|
1005
1054
|
L.push('')
|
|
1006
1055
|
L.push('Reply with ONLY a JSON object:')
|
|
1007
|
-
L.push('{"vote":{"verdict":
|
|
1056
|
+
L.push('{"vote":{"verdict":1,"reason":"<your logic>"}}')
|
|
1008
1057
|
if(formalOn()){
|
|
1009
1058
|
// The formal field belongs in the VOTING contract too: voters are exactly the agents who
|
|
1010
1059
|
// must either formalize the object or record why they judged it infeasible.
|
|
@@ -1068,6 +1117,33 @@ export function apply(ctx) {
|
|
|
1068
1117
|
|
|
1069
1118
|
// ---- resident lifecycle ----
|
|
1070
1119
|
let residentSeq = 0
|
|
1120
|
+
// Spawns that are currently IN FLIGHT, plus the `subagent/start` payloads seen while they were.
|
|
1121
|
+
//
|
|
1122
|
+
// ORDERING FACT (verified against the installed host, dsh-subagent 0.2.0-rc.2): `subagent/start`
|
|
1123
|
+
// is emitted SYNCHRONOUSLY from inside `startContinuable`'s await — `emit('subagent/start', …)` at
|
|
1124
|
+
// `dsh-subagent/lib/index.js:279` (and again from the lifecycle observer at `:306` once
|
|
1125
|
+
// `observer.start(handle.agent)` runs at `:1116`) — and `startContinuable` resolves only
|
|
1126
|
+
// afterwards. `spawnResident` therefore cannot know `started.childId` when the event fires, and
|
|
1127
|
+
// the payload carries no label (see `SubagentRunInfo`: runId/provider/id/local only), so the ONLY
|
|
1128
|
+
// information available at that moment is "a child of mine just started".
|
|
1129
|
+
//
|
|
1130
|
+
// So: the start handler STASHES the live Agent by child id (no ownership guess at all), and
|
|
1131
|
+
// `spawnResident` CLAIMS the record for its child id as soon as the call resolves — at which point
|
|
1132
|
+
// ownership is known exactly. `labelHints` bridges the (mock-ish) hosts that embed the label in
|
|
1133
|
+
// the id for the handler's own `ownsChild` check.
|
|
1134
|
+
const pendingSpawnLabels = new Set()
|
|
1135
|
+
const pendingStartAgents = new Map() // childId -> Agent, seen but not yet owned
|
|
1136
|
+
function stashStartAgent(childId, agent){
|
|
1137
|
+
if(!childId || !agent) return
|
|
1138
|
+
if(pendingStartAgents.size>=64) pendingStartAgents.delete(pendingStartAgents.keys().next().value)
|
|
1139
|
+
pendingStartAgents.set(String(childId), agent)
|
|
1140
|
+
}
|
|
1141
|
+
function claimStartAgent(childId){
|
|
1142
|
+
const id=String(childId==null?'':childId)
|
|
1143
|
+
const agent=pendingStartAgents.get(id)
|
|
1144
|
+
if(agent) pendingStartAgents.delete(id)
|
|
1145
|
+
return agent
|
|
1146
|
+
}
|
|
1071
1147
|
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
1148
|
async function spawnResident(r){
|
|
1073
1149
|
// Skip BEFORE touching the host when we already KNOW the ceiling and our own live children
|
|
@@ -1076,6 +1152,8 @@ export function apply(ctx) {
|
|
|
1076
1152
|
if(hostChildLimit!==undefined && liveChildCount()>=hostChildLimit){ noteSpawnRefused(r, hostChildLimit); return false }
|
|
1077
1153
|
const ao=residentAgentOptions(); const tf=residentToolFilter()
|
|
1078
1154
|
let started
|
|
1155
|
+
// Register the label for the whole in-flight window: `subagent/start` fires inside this await.
|
|
1156
|
+
pendingSpawnLabels.add(r.rId)
|
|
1079
1157
|
try {
|
|
1080
1158
|
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
1159
|
} catch(e){
|
|
@@ -1085,17 +1163,46 @@ export function apply(ctx) {
|
|
|
1085
1163
|
// failure keeps its previous behaviour and still propagates.
|
|
1086
1164
|
if(!isActivationLimitReached(e)) throw e
|
|
1087
1165
|
noteSpawnRefused(r, noteChildLimit(e)); return false
|
|
1088
|
-
}
|
|
1166
|
+
} finally { pendingSpawnLabels.delete(r.rId) }
|
|
1167
|
+
// The child just started, so its `subagent/start` payload has already been seen (the host emits
|
|
1168
|
+
// it inside the await above). Claim the stashed Agent for it NOW — ownership is exact here — and
|
|
1169
|
+
// that is what makes the REAL `/compact` path reachable (see the `subagent/start` listener).
|
|
1170
|
+
const claimed=claimStartAgent(started.childId)
|
|
1171
|
+
if(claimed) rememberAgent(started.childId, claimed)
|
|
1089
1172
|
r.childId=started.childId; r.status='brainstorm'; r.lastActiveAt=now()
|
|
1090
1173
|
childOwner.set(started.childId,sessionId); busy.add(r.rId); wakeKind.set(r.rId,'normal'); currentResident=r.rId
|
|
1091
1174
|
residents.set(r.rId,r); await saveAll(); logActivity('spawn',r.rId+' ('+(r.direction||'brainstorm')+')')
|
|
1092
1175
|
return true
|
|
1093
1176
|
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Is `childId` one of THIS session's residents — including a spawn that is still IN FLIGHT?
|
|
1179
|
+
*
|
|
1180
|
+
* `spawnResident` sets `r.childId` (and `childOwner`) only after `startContinuable` resolves, but
|
|
1181
|
+
* the host emits `subagent/start` from inside that await. So "is this mine?" must also consult the
|
|
1182
|
+
* pending-spawn labels registered around the call; without that, the start handler rejects every
|
|
1183
|
+
* child of a fresh spawn and `liveAgents` never gets populated.
|
|
1184
|
+
*/
|
|
1185
|
+
function ownsChild(childId){
|
|
1186
|
+
const id=String(childId)
|
|
1187
|
+
for(const [,r] of residents){ if(r.childId===id) return true }
|
|
1188
|
+
for(const r of pendingSpawns){ if(r.childId===id) return true }
|
|
1189
|
+
// During the spawn await the child's id is not recorded anywhere yet, but the framework chose
|
|
1190
|
+
// the spawn's LABEL and `spawnResident` registers it for exactly this window. A host id that
|
|
1191
|
+
// embeds the label (`child-r-1`, the common test/mock shape) is matched by containment; an
|
|
1192
|
+
// opaque host id cannot be matched here, which is why the label is only a
|
|
1193
|
+
// best-effort bridge in addition to the roster checks above.
|
|
1194
|
+
for(const label of pendingSpawnLabels){ if(id===label || id.indexOf(label)>=0) return true }
|
|
1195
|
+
return false
|
|
1196
|
+
}
|
|
1094
1197
|
async function wakeResident(r, promptText, kind){
|
|
1095
1198
|
if(!r || !r.childId) return false // a removed resident must never be woken (else r.childId would crash)
|
|
1096
1199
|
clearHeartbeat()
|
|
1097
1200
|
busy.add(r.rId); wakeKind.set(r.rId,kind||'normal'); currentResident=r.rId
|
|
1098
|
-
|
|
1201
|
+
// `rounds`/`roundsSinceCompact` are advanced only AFTER the send actually succeeded (below), so
|
|
1202
|
+
// a wake that never reached the child cannot consume a "round" of the compaction heuristic nor
|
|
1203
|
+
// inflate the round number the resident is told. `lastActiveAt` still moves here on purpose: a
|
|
1204
|
+
// failed wake must back the A-fill off for one activityTimeoutMs instead of hammering the child.
|
|
1205
|
+
r.lastActiveAt=now()
|
|
1099
1206
|
// Context compaction has TWO distinct needs. Confusing them is the bug that made
|
|
1100
1207
|
// '[核心规则重申]+[CONTEXT COMPACT]' repeat at the start of nearly every prompt:
|
|
1101
1208
|
// (a) r.needCompact (set by a REAL /compact) => the resident's rules may be blurred, so
|
|
@@ -1135,6 +1242,8 @@ export function apply(ctx) {
|
|
|
1135
1242
|
} else {
|
|
1136
1243
|
throw new Error('no subagent continuation API (need sendMessage or followup)')
|
|
1137
1244
|
}
|
|
1245
|
+
// The turn is really in flight now: count it (F10 — a failed send must not consume a round).
|
|
1246
|
+
r.rounds+=1; r.roundsSinceCompact+=1
|
|
1138
1247
|
return true
|
|
1139
1248
|
}
|
|
1140
1249
|
catch(e){ console.error('vibe-v4 wake '+r.rId+' failed: '+String((e&&e.message)||e)); busy.delete(r.rId); return false }
|
|
@@ -1146,18 +1255,74 @@ export function apply(ctx) {
|
|
|
1146
1255
|
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
1256
|
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
1257
|
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
|
|
1258
|
+
// ── auto-sync meeting: every `meetingKeepEvery` NEW artifacts, convene a coordination meeting ──
|
|
1259
|
+
//
|
|
1260
|
+
// The counting basis is the RESIDENTS' card libraries themselves (Propos/Methods/Subproblems),
|
|
1261
|
+
// NOT the three `record_*` convenience tools. The prompt tells residents verbatim that those tools
|
|
1262
|
+
// are optional and that they are recommended to write their own files directly with `fs`, so a
|
|
1263
|
+
// team that follows the recommendation never called `bumpArtifacts` and
|
|
1264
|
+
// `artifactCount % meetingKeepEvery === 0` could never fire — the documented "every N new
|
|
1265
|
+
// artifacts auto-sync" was UNREACHABLE in the very workflow the prompt recommends, leaving the
|
|
1266
|
+
// stall watchdog as the only path that ever convenes an auto-meeting.
|
|
1150
1267
|
//
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1268
|
+
// Setting `artifactBaseline` also means a tile count of the correct sign, since residents write
|
|
1269
|
+
// the paths `Propos/<r>/<id>.md` explicitly.
|
|
1270
|
+
async function countArtifacts(){
|
|
1271
|
+
let n=0
|
|
1272
|
+
for(const base of ['Propos','Methods','Subproblems']){
|
|
1273
|
+
try {
|
|
1274
|
+
const t=await fs.resolve(base,{cwd:frameworkRoot()})
|
|
1275
|
+
if(await fs.stat(t)===undefined) continue
|
|
1276
|
+
const ents=await fs.listDir(t)
|
|
1277
|
+
for(const e of ents||[]){
|
|
1278
|
+
if(!e||e.type!=='directory') continue
|
|
1279
|
+
try {
|
|
1280
|
+
const dt=await fs.resolve(base+'/'+e.name,{cwd:frameworkRoot()})
|
|
1281
|
+
if(await fs.stat(dt)===undefined) continue
|
|
1282
|
+
const files=await fs.listDir(dt)
|
|
1283
|
+
for(const f of files||[]) if(f&&f.type==='file'&&/\.md$/.test(String(f.name))) n++
|
|
1284
|
+
} catch(e2){ /* a single unreadable library must not stop the scan */ }
|
|
1285
|
+
}
|
|
1286
|
+
} catch(e){ /* best-effort: an absent library counts as zero */ }
|
|
1287
|
+
}
|
|
1288
|
+
return n
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Re-base the artifact counter on what is on disk. Called from `start()` (a fresh run: the count
|
|
1292
|
+
* of NEW artifacts starts at 0 even if the project tree already held cards) and from
|
|
1293
|
+
* `scheduleNext` (the single choke point of every scheduling pass, right before the auto-meeting
|
|
1294
|
+
* test), so a group that writes its cards with `fs` still drives the documented cadence.
|
|
1295
|
+
* Costs no writes and never throws into the scheduler.
|
|
1296
|
+
*/
|
|
1297
|
+
async function syncArtifactCount(){
|
|
1298
|
+
const n=await countArtifacts()
|
|
1299
|
+
if(artifactBaseline===null) artifactBaseline=n
|
|
1300
|
+
artifactCount=Math.max(artifactCount, n-(artifactBaseline||0))
|
|
1301
|
+
return artifactCount
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* The `meetingKeepEvery` cadence: every N newly accumulated artifacts, convene ONE coordination
|
|
1305
|
+
* meeting. Called from BOTH paths that can observe the count:
|
|
1306
|
+
* · `bumpArtifacts` (a `record_*` convenience-tool call — the counter moves immediately), and
|
|
1307
|
+
* · `scheduleNext` right after `syncArtifactCount()` re-bases the counter on the files on disk,
|
|
1308
|
+
* which is the ONLY way an `fs`-written card can be seen.
|
|
1309
|
+
* Without the second call site the documented cadence is unreachable in the very workflow the
|
|
1310
|
+
* prompt recommends (`record_*` 可选;推荐直接用 fs 写自己的文件). `lastSyncMeetingAt` is reused as
|
|
1311
|
+
* the count at which the last sync meeting was convened, so the same crossing cannot fire twice.
|
|
1312
|
+
*/
|
|
1313
|
+
function maybeArtifactSyncMeeting(){
|
|
1314
|
+
const every=Number(params.meetingKeepEvery)
|
|
1315
|
+
if(!(every>0)) return false
|
|
1316
|
+
if(meetingState || verifyState || pendingMeeting) return false
|
|
1317
|
+
if(!(artifactCount>0) || artifactCount % every !== 0) return false
|
|
1318
|
+
if(artifactCount<=lastSyncMeetingAt) return false // this crossing already convened a meeting
|
|
1319
|
+
lastSyncMeetingAt=artifactCount
|
|
1320
|
+
logActivity('meeting','定期同步触发:新增产物达到 '+every+' 的倍数(累计 '+artifactCount+')')
|
|
1321
|
+
startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{})
|
|
1322
|
+
return true
|
|
1323
|
+
}
|
|
1324
|
+
function bumpArtifacts(){ artifactCount+=1; markProgress(); maybeArtifactSyncMeeting() }
|
|
1325
|
+
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
1326
|
// identify WHICH resident is calling a resident-facing tool: match the caller's
|
|
1162
1327
|
// subagent id to a resident's childId. Fall back to the last-woken resident when
|
|
1163
1328
|
// the caller is the host/assistant (or an unknown agent). This makes per-resident
|
|
@@ -1171,17 +1336,28 @@ export function apply(ctx) {
|
|
|
1171
1336
|
// wake the claimer to work on it (framework moves the task, resident decides how).
|
|
1172
1337
|
// NOT while paused/stopped: a paused run must not start new work — the claim is recorded on the
|
|
1173
1338
|
// 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() }
|
|
1339
|
+
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
1340
|
return {ok:true} }
|
|
1176
1341
|
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
1342
|
async function saveTaskboard(){ await writeJson('State/taskboard.json',taskboard); await writeTaskboard() }
|
|
1178
1343
|
function listTasks(){ return taskboard.filter(t=>t.status!=='done') }
|
|
1179
1344
|
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
|
-
|
|
1345
|
+
/**
|
|
1346
|
+
* Apply context/compact bookkeeping from a resident's reply.
|
|
1347
|
+
*
|
|
1348
|
+
* `kind` is the wake kind. It matters because the OLD version treated ANY non-empty `summary`
|
|
1349
|
+
* on ANY turn as the acknowledgement of a requested compaction: a meeting/verify reply that
|
|
1350
|
+
* happened to echo a summary (their contracts do not forbid it) cleared `needCompact`, zeroed
|
|
1351
|
+
* `roundsSinceCompact` and clamped `contextPct` to <=25 WITHOUT any compaction happening — so the
|
|
1352
|
+
* soft-compact trigger stayed below threshold and the compaction the flag was asking for was
|
|
1353
|
+
* never issued. Only a real normal research turn can acknowledge the soft directive it received
|
|
1354
|
+
* (`wakeResident` injects `[CONTEXT COMPACT …]` on normal rounds only); `parsed.compacted===true`
|
|
1355
|
+
* stays honoured on every kind, because that is the resident explicitly saying it condensed.
|
|
1356
|
+
*/
|
|
1357
|
+
function postmark(r, parsed, kind){
|
|
1183
1358
|
const cp=Number(parsed.contextPct); if(Number.isFinite(cp)) r.contextPct=clPct(cp) // tolerate numeric strings ("40")
|
|
1184
|
-
|
|
1359
|
+
const isNormalTurn=(kind||'normal')==='normal'
|
|
1360
|
+
if(parsed.compacted===true || (isNormalTurn && r.needCompact && parsed.summary)){
|
|
1185
1361
|
r.contextSeed=String(parsed.summary||r.contextSeed||'')
|
|
1186
1362
|
r.contextPct=Math.min(r.contextPct||15,25)
|
|
1187
1363
|
r.roundsSinceCompact=0
|
|
@@ -1195,8 +1371,22 @@ export function apply(ctx) {
|
|
|
1195
1371
|
const r=residents.get(to); if(!r) return {ok:false,message:'no such resident'}
|
|
1196
1372
|
if(!busy.has(to)){
|
|
1197
1373
|
currentResident=r.rId
|
|
1198
|
-
await wakeResident(r, (await normalPrompt(r))+'\n\n[NEW MESSAGE from '+from+']\n'+content,'normal')
|
|
1199
|
-
await saveAll(); markProgress()
|
|
1374
|
+
const ok=await wakeResident(r, (await normalPrompt(r))+'\n\n[NEW MESSAGE from '+from+']\n'+content,'normal')
|
|
1375
|
+
await saveAll(); markProgress()
|
|
1376
|
+
if(ok){ logActivity('message',from+'→'+to); return {ok:true} }
|
|
1377
|
+
// The immediate wake FAILED (and `wakeResident` already cleared the `busy` mark, so no turn
|
|
1378
|
+
// actually started). Round C: the message must fall back to the MAILBOX instead of being
|
|
1379
|
+
// discarded — the old code logged "(wake failed; heartbeat re-armed)" and then dropped the
|
|
1380
|
+
// text, so the re-armed heartbeat had nothing left to do and the message was lost forever
|
|
1381
|
+
// while the recipient stayed cold (this is what the audit's F1 probe measured: it reported
|
|
1382
|
+
// "queued mail already consumed: false" but the mailbox was in fact already empty, so no
|
|
1383
|
+
// later pass could ever deliver anything). The mailbox is the one place a pass is guaranteed
|
|
1384
|
+
// to look again — including during `brainstorm`, see the delivery call at the top of
|
|
1385
|
+
// scheduleNext — and a successful retry marks the recipient busy and clears the queue.
|
|
1386
|
+
const mb=mailboxes.get(to)||[]; mb.push({from,at:now(),content}); mailboxes.set(to,mb)
|
|
1387
|
+
await saveAll(); armHeartbeat()
|
|
1388
|
+
logActivity('message',from+'→'+to+' (immediate wake failed; queued for retry)')
|
|
1389
|
+
return {ok:true}
|
|
1200
1390
|
}
|
|
1201
1391
|
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
1392
|
}
|
|
@@ -1287,14 +1477,36 @@ export function apply(ctx) {
|
|
|
1287
1477
|
if(iv.claim_task) await claimTask(iv.claim_task, id)
|
|
1288
1478
|
if(iv.propose_verify) maybeQueueVerify(iv.propose_verify, id)
|
|
1289
1479
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1480
|
+
// ── the STOP vote must be UNANIMOUS, abstentions included ────────────────────────────────
|
|
1481
|
+
// `onResidentEnd` stores `voteSolved` only when it is a real boolean, otherwise `null`. The
|
|
1482
|
+
// old code filtered those nulls OUT and then asked `votes.every(v=>v===true)`, so a resident
|
|
1483
|
+
// that omitted the field (the meeting contract makes every key except `input` optional) or
|
|
1484
|
+
// wrote the string "true" simply dropped out of the electorate — and 3-of-4 voting true read
|
|
1485
|
+
// as "the whole team agrees the problem is solved" and STOPPED the run. `finalizeVerify` was
|
|
1486
|
+
// already written correctly (every resident must have a verdict); this mirrors it: every
|
|
1487
|
+
// speaker must have produced an explicit `true`.
|
|
1488
|
+
const speakers=Object.values(st.inputs)
|
|
1489
|
+
const allSolved = allSpoke && speakers.length>0 && speakers.every(iv=>iv.voteSolved===true)
|
|
1292
1490
|
logActivity('meeting', 'concluded'+(allSolved?' → ALL agree solved':' (no unanimous solved vote)'))
|
|
1293
1491
|
if(allSolved){
|
|
1492
|
+
// ── FINAL PAPER ENTRY (docs/final-paper.md §A1) ─────────────────────────────────────────────────
|
|
1493
|
+
// The paper phase MUST start HERE, BEFORE `running=false; autoDone=true` and before the
|
|
1494
|
+
// coordination state is released: afterwards `startMeeting` refuses (autoDone), this
|
|
1495
|
+
// handler's meeting/verify branches return early on `!running||autoDone`, and
|
|
1496
|
+
// `scheduleNext` does nothing — co-writing/merge/cross-review could never run. The run
|
|
1497
|
+
// therefore stays `running` while the team writes; `paperFinalize()` performs the
|
|
1498
|
+
// completion flip once the paper exists (or has degraded safely).
|
|
1499
|
+
meetingState=null; pendingMeeting=null; verifyState=null; pendingVerify=[]; wakeKind.clear()
|
|
1500
|
+
await saveAll()
|
|
1501
|
+
if(params.finalPaper){
|
|
1502
|
+
const paper=await paperStart('auto',{deferCompletion:true})
|
|
1503
|
+
if(paper&&paper.ok){
|
|
1504
|
+
logActivity('paper','收口:一致性停止票通过('+PAPER_CLOSURE_SIGNAL+')→ 进入最终论文阶段,论文完成后才标记完成')
|
|
1505
|
+
return
|
|
1506
|
+
}
|
|
1507
|
+
logActivity('paper','最终论文未能启动('+((paper&&paper.message)||'unknown')+')→ 按原路径收口')
|
|
1508
|
+
} else logActivity('paper','finalPaper=false:跳过最终论文,直接收口')
|
|
1294
1509
|
running=false; autoDone=true; phase='done'; clearHeartbeat()
|
|
1295
|
-
// symmetric with initAbort: the STOP path must also release the coordination state, else
|
|
1296
|
-
// status keeps reporting a phantom in-progress meeting (and stale wake kinds) forever.
|
|
1297
|
-
meetingState=null; wakeKind.clear(); pendingMeeting=null; verifyState=null; pendingVerify=[]
|
|
1298
1510
|
logActivity('stop','all residents agree: problem solved'); await saveAll(); return
|
|
1299
1511
|
}
|
|
1300
1512
|
meetingState=null; wakeKind.clear(); await saveAll()
|
|
@@ -1369,14 +1581,18 @@ export function apply(ctx) {
|
|
|
1369
1581
|
else await closeVerify(vs,allTrue)
|
|
1370
1582
|
doSchedule=true
|
|
1371
1583
|
}
|
|
1372
|
-
else if(vs.round
|
|
1584
|
+
else if(vs.round<params.verdictMaxRounds){
|
|
1585
|
+
// `verdictMaxRounds` is documented (README / 实现方案 / persona) as the maximum number of
|
|
1586
|
+
// verification rounds: 1 independent first vote + (verdictMaxRounds-1) debate re-votes. The
|
|
1587
|
+
// old `vs.round+1<verdictMaxRounds` gave one round FEWER than the name and the docs promise
|
|
1588
|
+
// (default 3 → only 2), so the parameter read as an off-by-one from every documented surface.
|
|
1373
1589
|
// Move to a REAL debate round: snapshot the current votes into history (so the next round's
|
|
1374
1590
|
// prompt can show others' previous stances), then CLEAR verdicts so every resident is asked to
|
|
1375
1591
|
// give a fresh independent judgement after seeing the debate. Without the clear, allVoted stays
|
|
1376
1592
|
// true and the debate rounds burn through with NOBODY being re-asked (a silent no-op).
|
|
1377
1593
|
vs.history=Object.assign({}, vs.verdicts); vs.verdicts={}
|
|
1378
1594
|
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
|
|
1595
|
+
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
1596
|
}
|
|
1381
1597
|
else {
|
|
1382
1598
|
const avg=vals.length? vals.reduce((a,x)=>a+(x.prob!=null?x.prob:0.5),0)/vals.length : 0.5
|
|
@@ -1440,13 +1656,25 @@ export function apply(ctx) {
|
|
|
1440
1656
|
function maybeQueueVerify(target, proposer){
|
|
1441
1657
|
const t=idSafe(target) // sanitize BEFORE it becomes file names / dedup keys / status output
|
|
1442
1658
|
if(!t || t==='id') return false
|
|
1659
|
+
// The object KIND is derived from the id prefix, and it decides BOTH the directory the Verified
|
|
1660
|
+
// card lands in (Verified/命题/ vs Verified/问题/) and the `- 类型:` line of the source card's
|
|
1661
|
+
// write-back. The old fallback mapped EVERY unrecognised id to 'proposition', so proposing a
|
|
1662
|
+
// method/sub-problem whose id does not start with m-/s- (e.g. `2.1`, `lemma-A`) silently wrote a
|
|
1663
|
+
// card into Verified/命题/ carrying `类型: 命题` — a card whose location and declaration disagree.
|
|
1664
|
+
// The prefix IS the coordination contract (the prompt hands residents `p-`/`m-`/`s-`), so an id
|
|
1665
|
+
// that does not carry one is refused HERE, at the single queueing funnel, instead of guessing.
|
|
1666
|
+
const tt=guessTargetType(t)
|
|
1667
|
+
if(!tt){
|
|
1668
|
+
logActivity('verify',t+' 的提议被拒绝:对象 id 必须以 p-(命题)/ m-(方法)/ s-(子问题)开头,否则框架无法确定它属于哪个库(V4_INVALID_ARGUMENT)')
|
|
1669
|
+
return false
|
|
1670
|
+
}
|
|
1443
1671
|
const last=verifiedRecently.get(t)
|
|
1444
1672
|
if(last!==undefined && (now()-last) < recoverStallMs()){
|
|
1445
1673
|
logActivity('verify',t+' re-propose ignored (just verified at '+fmtTime(last)+')')
|
|
1446
1674
|
return false
|
|
1447
1675
|
}
|
|
1448
1676
|
if(pendingVerify.some(p=>String(p.targetId)===t)) return true // already queued → keep ONE entry
|
|
1449
|
-
pendingVerify.push({targetId:t,targetType:
|
|
1677
|
+
pendingVerify.push({targetId:t,targetType:tt,proposer:proposer||'',at:now()})
|
|
1450
1678
|
return true
|
|
1451
1679
|
}
|
|
1452
1680
|
async function writeDebateDoc(vs,done,val){
|
|
@@ -1532,7 +1760,11 @@ export function apply(ctx) {
|
|
|
1532
1760
|
next=rewriteCardField(next,'概率',isTrue?'1':'0')
|
|
1533
1761
|
await writeText(rel,next||text)
|
|
1534
1762
|
}
|
|
1535
|
-
function guessTargetType(id){ if(/^p-/.test(id)) return 'proposition'; if(/^m-/.test(id)) return 'method'; if(/^s-/.test(id)) return 'subproblem'; return '
|
|
1763
|
+
function guessTargetType(id){ if(/^p-/.test(id)) return 'proposition'; if(/^m-/.test(id)) return 'method'; if(/^s-/.test(id)) return 'subproblem'; return '' }
|
|
1764
|
+
// Human-readable word for the target kind, used in the prompts. Never print the RAW internal
|
|
1765
|
+
// token: the voter is a model, and `proposition`/`method`/`subproblem` in an otherwise Chinese
|
|
1766
|
+
// contract is exactly the kind of untranslated placeholder these audits look for.
|
|
1767
|
+
function targetTypeWord(t){ return t==='method'?'方法':(t==='subproblem'?'子问题':'命题') }
|
|
1536
1768
|
|
|
1537
1769
|
// ---- heartbeat / liveness helpers (boundary-A: event-driven + gated heartbeat) ----
|
|
1538
1770
|
// A checkpoint wake is NOT "keep working forever": it nudges the least-recently-active
|
|
@@ -1648,11 +1880,35 @@ export function apply(ctx) {
|
|
|
1648
1880
|
|
|
1649
1881
|
// ---- liveness / scheduling ----
|
|
1650
1882
|
async function scheduleNext(){
|
|
1651
|
-
if(!running||autoDone){
|
|
1883
|
+
if(!running||autoDone){
|
|
1884
|
+
// A MANUALLY triggered final paper may run on an already-concluded run; it is the one thing
|
|
1885
|
+
// that still needs the timer, so it keeps its own pacing here (paperAdvance re-arms the
|
|
1886
|
+
// heartbeat while it waits and clears it once the paper is done).
|
|
1887
|
+
if(paperState&&paperState.status!=='done'&&paperState.status!=='failed'){ try{ await paperAdvance('scheduler-concluded') }catch(e){} return }
|
|
1888
|
+
clearHeartbeat(); return
|
|
1889
|
+
}
|
|
1890
|
+
// The run is CONCLUDING (its closing meeting voted unanimously to stop): the ONLY remaining
|
|
1891
|
+
// work is the final paper. No new research round may be dispatched behind it (docs/final-paper.md §A1).
|
|
1892
|
+
if(paperState&&paperState.status!=='done'&&paperState.status!=='failed'){
|
|
1893
|
+
try{ await paperAdvance('scheduler-paper') }catch(e){ console.error('vibe-math-v4: paper pass failed: '+String((e&&e.message)||e)) }
|
|
1894
|
+
return
|
|
1895
|
+
}
|
|
1652
1896
|
// Residents the host's live-child cap refused are retried here — the single choke point every
|
|
1653
1897
|
// scheduling pass goes through — so a refused spawn is deferred by a whole round (and paced by
|
|
1654
1898
|
// the heartbeat) instead of being retried inside the loop that discovered the refusal.
|
|
1655
1899
|
if(pendingSpawns.length) await retryPendingSpawns()
|
|
1900
|
+
// Queued mail MUST also be delivered while the group is still brainstorming (Round C, F1).
|
|
1901
|
+
// A human/team message is not "work": the brainstorm phase used to return at the next line
|
|
1902
|
+
// before ever reaching the delivery point below, so a wake that FAILED left the message
|
|
1903
|
+
// re-queued and every subsequent heartbeat firing a no-op (`maybeFinishBrainstorm` only flips
|
|
1904
|
+
// the phase once every resident has an insight). The scheduler then stayed cold with mail
|
|
1905
|
+
// still queued — the exact permanent stall F1 exists to remove, narrowed to this phase
|
|
1906
|
+
// (_oneoff/probe-v4-mailbox-stall.mjs used to print REPRODUCED here).
|
|
1907
|
+
// Deliberately conditioned on "no consensus in progress": continueMeetingRound /
|
|
1908
|
+
// continueVerifyRound refuse to flush mailboxes on purpose (a backlog must not starve a
|
|
1909
|
+
// consensus past its watchdog, see their comments), so a parked meeting/verify keeps that
|
|
1910
|
+
// guarantee and the mail goes out on the first pass after the consensus closes.
|
|
1911
|
+
if(!meetingState && !verifyState){ const delivered=await deliverNextMailbox(); if(delivered) return }
|
|
1656
1912
|
if(phase==='brainstorm'){ await maybeFinishBrainstorm(); return }
|
|
1657
1913
|
if(meetingState){ await continueMeetingRound(); return }
|
|
1658
1914
|
if(verifyState){ await continueVerifyRound(); return }
|
|
@@ -1660,11 +1916,24 @@ export function apply(ctx) {
|
|
|
1660
1916
|
// A meeting requested while a verify held the floor is parked in pendingMeeting; once the
|
|
1661
1917
|
// verify queue has truly drained (no verifyState / pendingVerify), resume it before anything else.
|
|
1662
1918
|
if(pendingMeeting){ const pm=pendingMeeting; pendingMeeting=null; await startMeeting(pm.agenda, pm.type, pm.targetId); return }
|
|
1663
|
-
// mailbox delivery
|
|
1919
|
+
// mailbox delivery. `wakeResident` CLEARS the heartbeat as its first act, so a pass that delivers
|
|
1920
|
+
// NOTHING must not return without re-arming one: a rejected `sendMessage`
|
|
1921
|
+
// (subagent/delivery-unavailable, a cold-resumed child whose Activation closed, a transient
|
|
1922
|
+
// persistence failure) re-queues the message, refreshes the recipient's lastActiveAt (so the fill
|
|
1923
|
+
// loop below skips it for a whole activityTimeoutMs) and used to end the pass with no wake, no
|
|
1924
|
+
// heartbeat and no in-flight turn — nothing else could ever re-drive the scheduler, so the whole
|
|
1925
|
+
// group stopped permanently with mail still queued. Falling through instead lets the branches
|
|
1926
|
+
// below re-arm (and B/A still get their chance); on a SUCCESSFUL delivery the recipient's own
|
|
1927
|
+
// subagent/end re-drives the pump, so the pass can end here as before.
|
|
1664
1928
|
const delivered=await deliverNextMailbox(); if(delivered) return
|
|
1665
1929
|
// maxParallel: don't start a new wake when the in-flight cap is reached
|
|
1666
1930
|
const mp=Number(params.maxParallel)||0
|
|
1667
1931
|
if(mp>0 && busy.size>=mp){ armHeartbeat(); return }
|
|
1932
|
+
// Refresh the artifact counter from the residents' own card libraries (fs-written cards count
|
|
1933
|
+
// too) before the auto-sync test below, so `meetingKeepEvery` measures NEW artifacts — and
|
|
1934
|
+
// convene the sync meeting here, because this is the only place an `fs`-written card is seen.
|
|
1935
|
+
await syncArtifactCount()
|
|
1936
|
+
if(maybeArtifactSyncMeeting()) return
|
|
1668
1937
|
// B) stall auto-sync meeting (分级保活 B): the group has been idle with NO progress for
|
|
1669
1938
|
// stallAutoMeetingMs → convene a sync meeting so the residents coordinate their next move
|
|
1670
1939
|
// (framework convenes & records; residents decide — never assigns work). Only when no
|
|
@@ -1763,8 +2032,12 @@ export function apply(ctx) {
|
|
|
1763
2032
|
realCompact(r).catch(()=>{}) // best-effort real DSH /compact of this resident while idle
|
|
1764
2033
|
const output=blocksToText(info&&info.lastAssistantMessage)
|
|
1765
2034
|
const parsed=parseReply(output)
|
|
1766
|
-
postmark(r, parsed) // context/compact bookkeeping, regardless of wake kind (clears any leak)
|
|
1767
2035
|
const kind=wakeKind.get(r.rId)||'normal'
|
|
2036
|
+
postmark(r, parsed, kind) // context/compact bookkeeping — a mid-consensus turn may NOT ack a compaction it never received
|
|
2037
|
+
// The final-paper flow (docs/final-paper.md §A1) has its own wake kinds and its own state machine. It is
|
|
2038
|
+
// handled BEFORE the meeting/verify branches and before their `!running||autoDone` guards,
|
|
2039
|
+
// because a manually triggered paper legitimately runs on an already-concluded run.
|
|
2040
|
+
if(kind.indexOf('paper:')===0){ await onPaperWakeEnd(r,parsed); return }
|
|
1768
2041
|
if(kind==='meeting' && meetingState){
|
|
1769
2042
|
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
2043
|
meetingState.lastInputAt=now()
|
|
@@ -1822,6 +2095,714 @@ export function apply(ctx) {
|
|
|
1822
2095
|
await saveAll(); await scheduleNext()
|
|
1823
2096
|
}
|
|
1824
2097
|
|
|
2098
|
+
// ================= final paper (docs/final-paper.md §3-§5) =================
|
|
2099
|
+
// When v4's consensus has converged (§2: the unanimous "solved" meeting closed, no ballot in
|
|
2100
|
+
// flight, nothing queued) the run must produce a PAPER, not stop at a chat log. The team
|
|
2101
|
+
// co-authors it: every resident writes the part it owns, the facilitator (the framework)
|
|
2102
|
+
// MERGES the parts, every participant CROSS-REVIEWS at least one other part, the `paperEditor`
|
|
2103
|
+
// finalises the merged draft, and the paper is finalised only when every participant has
|
|
2104
|
+
// explicitly stated it is deliverable. Dissent iterates a bounded number of rounds and, at the
|
|
2105
|
+
// cap, the disagreement is recorded in the appendix and the run is warned about.
|
|
2106
|
+
//
|
|
2107
|
+
// Everything the paper states comes from EVIDENCE ALREADY ON DISK (Verified/ cards, Propos/
|
|
2108
|
+
// cards, Methods/ cards, Shared/meetings|debates/, the Lean records) or from the team's own
|
|
2109
|
+
// written parts. Nothing is invented; unresolved/refuted items are flagged as such.
|
|
2110
|
+
const PAPER_FORMATS=['both','md','tex'], PAPER_LANGS=['zh','en']
|
|
2111
|
+
// v2 §A5: v4's `facilitator` is NOT a roster member and has no LLM (it is the framework / the
|
|
2112
|
+
// human-side messenger), so "let the facilitator unify the notation" has no author. The editor is
|
|
2113
|
+
// therefore the SESSION ROOT (the office = the human side) by default, or one named resident.
|
|
2114
|
+
const PAPER_EDITOR_DEFAULT='office'
|
|
2115
|
+
function paperEditorOk(v){ const s=String(v==null?'':v).trim(); return s==='office'||/^resident:[^\s]+$/.test(s) }
|
|
2116
|
+
// v2 §A4: v4's REAL closure signal is the closing meeting's UNANIMOUS stop vote — the branch
|
|
2117
|
+
// below in `finalizeMeeting` (allSolved). The v1 wording ("every verify target decided / mailbox
|
|
2118
|
+
// empty") is stricter than the code and would never fire, so the paper phase is entered from
|
|
2119
|
+
// inside that exact branch and any still-未决 item is flagged in the paper instead.
|
|
2120
|
+
const PAPER_CLOSURE_SIGNAL='finalizeMeeting/allSolved(一致性会议的一致停止票)'
|
|
2121
|
+
const PAPER_MAX_ROUNDS=2 // bounded iterations after dissent (docs/final-paper.md §4: "有上限")
|
|
2122
|
+
const PAPER_NUDGE_TRIES=2 // paced re-asks before the step deadline declares a timeout
|
|
2123
|
+
const PAPER_WAIT_FLOOR_MS=2000 // a step's deadline is at least this (tests run with tiny timeouts)
|
|
2124
|
+
const PAPER_KINDS={part:'paper:part',review:'paper:review',final:'paper:final',vote:'paper:vote',revise:'paper:revise'}
|
|
2125
|
+
const PAPER_MARK={part:'[PAPER PART]',review:'[PAPER REVIEW]',final:'[PAPER FINAL]',vote:'[PAPER DELIVERABLE]',revise:'[PAPER REVISE]'}
|
|
2126
|
+
const PAPER_HEAD_ZH=['标题、作者、日期与摘要','引言与问题背景','原问题的完整解法','已检验通过的命题','已解决的子问题与中间成果','创造或发现的有价值之物','规律总结','讨论、局限与展望','附录:证据与文件索引']
|
|
2127
|
+
const PAPER_HEAD_EN=['Title, Authors, Date and Abstract','Introduction and Problem Background','Complete Solution of the Original Problem','Verified Propositions','Solved Sub-problems and Intermediate Results','Valuable Creations and Discoveries','Patterns and Reusable Principles','Discussion, Limitations and Outlook','Appendix: Evidence and File Index']
|
|
2128
|
+
|
|
2129
|
+
function paperTitles(lang){ return lang==='en'?PAPER_HEAD_EN:PAPER_HEAD_ZH }
|
|
2130
|
+
/** The run id doubles as the v4 "institute id": Paper/<run id>/ (docs/final-paper.md §5). Sanitised through
|
|
2131
|
+
* `idSafe`+`slugify`, so a hostile problem text/title can never produce `../`. */
|
|
2132
|
+
function paperIdFor(){ return idSafe(slugify('v4-'+problemId))+'-'+idSafe(runId) }
|
|
2133
|
+
function paperDirFor(){ return 'Paper/'+paperIdFor() }
|
|
2134
|
+
/** Containment (docs/final-paper.md §6): every paper write must stay inside `Paper/<id>/`. */
|
|
2135
|
+
async function paperWriteRel(rel,content){
|
|
2136
|
+
const dir=paperState?paperState.dir:paperDirFor()
|
|
2137
|
+
const norm=String(rel).replace(/\\/g,'/')
|
|
2138
|
+
if(norm.indexOf(dir+'/')!==0 || norm.indexOf('..')!==-1) return false
|
|
2139
|
+
return await writeText(norm,content)
|
|
2140
|
+
}
|
|
2141
|
+
function paperLog(line){ const ps=paperState; if(ps) ps.log.push(fmtTime()+'|'+String(line)) }
|
|
2142
|
+
function paperStepLimit(){ return Math.max(PAPER_WAIT_FLOOR_MS, recoverStallMs()*4) }
|
|
2143
|
+
function paperCfg(ov){
|
|
2144
|
+
const o=ov||{}
|
|
2145
|
+
const ed=String(o.editor==null?'':o.editor).trim()
|
|
2146
|
+
return {
|
|
2147
|
+
language:PAPER_LANGS.indexOf(String(o.lang))!==-1?String(o.lang):params.paperLanguage,
|
|
2148
|
+
format:PAPER_FORMATS.indexOf(String(o.format))!==-1?String(o.format):params.paperFormat,
|
|
2149
|
+
editor:paperEditorOk(ed)?ed:(paperEditorOk(params.paperEditor)?String(params.paperEditor):PAPER_EDITOR_DEFAULT),
|
|
2150
|
+
compilePdf:params.paperCompilePdf!==false,
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
/** `resident:<id>` -> the id, `office`/absent -> ''. */
|
|
2154
|
+
function paperEditorResidentId(editor){ const m=/^resident:([^\s]+)$/.exec(String(editor||'')); return m?m[1]:'' }
|
|
2155
|
+
// ---- evidence (every claim in §4-§9 of the skeleton must be traceable to one of these) ----
|
|
2156
|
+
async function paperListDirFiles(base){
|
|
2157
|
+
const out=[]
|
|
2158
|
+
try {
|
|
2159
|
+
const t=await fs.resolve(base,{cwd:frameworkRoot()}); if(await fs.stat(t)===undefined) return out
|
|
2160
|
+
for(const e of (await fs.listDir(t))||[]){ if(!e||e.type!=='file'||!/\.md$/.test(String(e.name))) continue
|
|
2161
|
+
const rel=base+'/'+e.name; out.push({rel,text:(await readText(rel))||''}) }
|
|
2162
|
+
} catch(e){ /* best effort: an absent directory is an empty evidence set */ }
|
|
2163
|
+
out.sort((a,b)=>a.rel<b.rel?-1:a.rel>b.rel?1:0); return out
|
|
2164
|
+
}
|
|
2165
|
+
async function paperListCards(base){
|
|
2166
|
+
const out=[]
|
|
2167
|
+
try {
|
|
2168
|
+
const root=await fs.resolve(base,{cwd:frameworkRoot()}); if(await fs.stat(root)===undefined) return out
|
|
2169
|
+
for(const d of (await fs.listDir(root))||[]){ if(!d||d.type!=='directory') continue
|
|
2170
|
+
const dir=base+'/'+d.name
|
|
2171
|
+
try {
|
|
2172
|
+
const t=await fs.resolve(dir,{cwd:frameworkRoot()}); if(await fs.stat(t)===undefined) continue
|
|
2173
|
+
for(const e of (await fs.listDir(t))||[]){ if(!e||e.type!=='file'||!/\.md$/.test(String(e.name))) continue
|
|
2174
|
+
const rel=dir+'/'+e.name; out.push({rel,text:(await readText(rel))||''}) }
|
|
2175
|
+
} catch(e2){ /* one unreadable library must not stop the scan */ }
|
|
2176
|
+
}
|
|
2177
|
+
} catch(e){ /* best effort */ }
|
|
2178
|
+
out.sort((a,b)=>a.rel<b.rel?-1:a.rel>b.rel?1:0); return out
|
|
2179
|
+
}
|
|
2180
|
+
function paperField(text,name){
|
|
2181
|
+
const esc=String(name).replace(/[.*+?^${}()|[\]\\]/g,'\\$&')
|
|
2182
|
+
const re=new RegExp('(?:^|\\n|;\\s*)-\\s*'+esc+':\\s*([^;\\n]*)')
|
|
2183
|
+
const m=re.exec(String(text||'')); return m?m[1].trim():''
|
|
2184
|
+
}
|
|
2185
|
+
async function paperEvidence(){
|
|
2186
|
+
const [verifiedFiles,problemFiles,propos,methods,subproblems,meetings,debates]=await Promise.all([
|
|
2187
|
+
paperListDirFiles('Verified/命题'), paperListDirFiles('Verified/问题'), paperListCards('Propos'),
|
|
2188
|
+
paperListCards('Methods'), paperListCards('Subproblems'), paperListDirFiles('Shared/meetings'), paperListDirFiles('Shared/debates'),
|
|
2189
|
+
])
|
|
2190
|
+
const parse=f=>({rel:f.rel,id:paperField(f.text,'ID'),结论:paperField(f.text,'结论'),概率:paperField(f.text,'概率'),
|
|
2191
|
+
类型:paperField(f.text,'类型'),状态:paperField(f.text,'状态')||paperField(f.text,'状态'),标题:paperField(f.text,'标题'),
|
|
2192
|
+
可信断言:paperField(f.text,'可信断言'),text:f.text})
|
|
2193
|
+
return {
|
|
2194
|
+
problem:problemText, problemId:problemId, runId:runId, project:currentProject, converged:!!autoDone,
|
|
2195
|
+
verified:verifiedFiles.map(parse), verifiedProblems:problemFiles.map(parse),
|
|
2196
|
+
propos:propos.map(parse), methods:methods.map(parse), subproblems:subproblems.map(parse),
|
|
2197
|
+
meetings:meetings.map(f=>f.rel), debates:debates.map(f=>f.rel),
|
|
2198
|
+
formal:formalView().objects.map(o=>({target:o.target,status:o.status,file:o.file,proof:o.proof})),
|
|
2199
|
+
index:[...verifiedFiles,...problemFiles,...propos,...methods,...subproblems,...meetings,...debates].map(f=>f.rel).sort(),
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
function paperHash(s){ let h=5381; const t=String(s==null?'':s); for(let i=0;i<t.length;i++) h=((h*33)^t.charCodeAt(i))>>>0; return h.toString(16).padStart(8,'0') }
|
|
2203
|
+
/** Idempotency is per (run id, finalizedAt) plus per-artifact existence — docs/final-paper.md §C explicitly
|
|
2204
|
+
* drops v1's multi-file inputsHash. This only fingerprints one string for the log, never a
|
|
2205
|
+
* decision. */
|
|
2206
|
+
function paperFingerprint(ev){ return paperHash([ev.problemId,ev.runId,ev.problem,ev.index.join('|')].join('\n')) }
|
|
2207
|
+
// ---- prompts (stable markers so the flow is observable/driveable) ----
|
|
2208
|
+
function paperPrompt(kind,rid,again){
|
|
2209
|
+
const ps=paperState, T=paperTitles(ps.cfg.language), zh=ps.cfg.language!=='en'
|
|
2210
|
+
const head=zh
|
|
2211
|
+
? '【最终论文·团队合写】你('+rid+')是本次运行的参与常驻。'
|
|
2212
|
+
: '[FINAL PAPER — TEAM CO-AUTHORING] You ('+rid+') are a participating resident of this run.'
|
|
2213
|
+
const common=(zh?'\n论文目录:`'+ps.dir+'/`(一切写入必须在这里面)。只整理**已有证据**:你自己的库、Verified/、Shared/meetings|debates/ 与既有卡片;**不得编造**;未决/被否证的条目必须显式标注。'
|
|
2214
|
+
:'\nPaper directory: `'+ps.dir+'/` (everything you write stays inside it). Only organise EXISTING evidence (your own library, Verified/, Shared/meetings|debates/, the recorded cards); never invent; flag unresolved/refuted items explicitly.')
|
|
2215
|
+
if(kind==='part') return PAPER_MARK.part+'\n'+head+'\n'+(again?(zh?'(上一次没有收到你的部分,请重发。)\n':'(no part was received from you last time — please resend.)\n'):'')
|
|
2216
|
+
+(zh?'请撰写**你负责的那一部分**(你库里的命题/方法/子问题/进展,以及你亲自参与的会议与争论中确立的内容),成文即可,不必追求篇幅。\n'
|
|
2217
|
+
:'Write the part YOU own (the propositions/methods/sub-problems/progress in your library and what you personally established in meetings/debates).\n')
|
|
2218
|
+
+common+'\n'+(zh?'回复一个 JSON:`{"paperPart":{"title":"<部分标题>","markdown":"<正文 markdown>"}}`(也可以直接用 fs 写到 `'+ps.dir+'/parts/'+rid+'.md`)。'
|
|
2219
|
+
:'Reply with one JSON object: `{"paperPart":{"title":"<title>","markdown":"<body markdown>"}}` (you may also write `'+ps.dir+'/parts/'+rid+'.md` with fs).')
|
|
2220
|
+
+'\n'+(zh?'九节骨架:':"The paper's nine sections: ")+T.join(' / ')
|
|
2221
|
+
if(kind==='review') return PAPER_MARK.review+'\n'+head+'\n'+(again?(zh?'(上一次的互审没有指名**其他**参与者,请重发。)\n':'(your last review did not name ANOTHER participant — please resend.)\n'):'')
|
|
2222
|
+
+(zh?'合并稿见 `'+ps.dir+'/draft.md`。请**审阅至少一位其他参与者的部分**(不能审自己),指出具体问题并给出可落实的改进。\n'
|
|
2223
|
+
:'The merged draft is at `'+ps.dir+'/draft.md`. Review AT LEAST ONE OTHER participant\'s part (never your own), name concrete problems and give actionable improvements.\n')
|
|
2224
|
+
+common+'\n'+(zh?'回复一个 JSON:`{"paperReview":{"reviewed":"<被审的参与者 id>","points":["<问题>","..."],"improvements":"<你建议的改进>"}}`'
|
|
2225
|
+
:'Reply with one JSON object: `{"paperReview":{"reviewed":"<other participant id>","points":["..."],"improvements":"..."}}`')
|
|
2226
|
+
if(kind==='final') return PAPER_MARK.final+'\n'+head+'\n'
|
|
2227
|
+
+(zh?'你是本次的**定稿代表**(paperEditor)。请把合并稿 `'+ps.dir+'/draft.md` 与互审意见梳理成**最终稿**:去重、统一术语与记号,保留全部证据与未决标注。\n'
|
|
2228
|
+
:'You are the run\'s EDITOR (paperEditor). Turn the merged draft `'+ps.dir+'/draft.md` plus the review points into the FINAL draft: deduplicate, unify terms and notation, keep every evidence pointer and unresolved flag.\n')
|
|
2229
|
+
+common+'\n'+(zh?'回复一个 JSON:`{"paperFinal":{"markdown":"<最终稿正文 markdown>"}}`(或直接写 `'+ps.dir+'/final.md`)。'
|
|
2230
|
+
:'Reply with one JSON object: `{"paperFinal":{"markdown":"<final body markdown>"}}` (or write `'+ps.dir+'/final.md`).')
|
|
2231
|
+
// vote / revise
|
|
2232
|
+
return PAPER_MARK[kind]+'\n'+head+'\n'
|
|
2233
|
+
+(zh?'请阅读最终稿 `'+ps.dir+'/final.md`,然后**明确表态它是否可交付**。有反对必须说明理由(框架会迭代,超限会把分歧写进附录)。\n'
|
|
2234
|
+
:'Read the final draft at `'+ps.dir+'/final.md` and state EXPLICITLY whether it is deliverable. Any objection must state its reason (the framework iterates; at the cap the disagreement goes into the appendix).\n')
|
|
2235
|
+
+common+'\n'+(zh?'回复一个 JSON:`{"paperDeliverable":true|false,"reason":"<理由>"}`'
|
|
2236
|
+
:'Reply with one JSON object: `{"paperDeliverable":true|false,"reason":"<why>"}`')
|
|
2237
|
+
}
|
|
2238
|
+
// ---- state machine ----
|
|
2239
|
+
function paperHas(step,rid){
|
|
2240
|
+
const ps=paperState; if(!ps) return false
|
|
2241
|
+
if(step==='parts') return !!ps.parts[rid]
|
|
2242
|
+
if(step==='review') return !!ps.reviews[rid]
|
|
2243
|
+
if(step==='final') return !!ps.finalDraft
|
|
2244
|
+
if(step==='vote') return typeof ps.deliverable[rid]==='boolean'
|
|
2245
|
+
return false
|
|
2246
|
+
}
|
|
2247
|
+
function paperMarkMissing(step,rid){
|
|
2248
|
+
const ps=paperState
|
|
2249
|
+
if(step==='parts') ps.parts[rid]={text:'',missing:true,source:'missing'}
|
|
2250
|
+
else if(step==='review') ps.reviews[rid]={missing:true}
|
|
2251
|
+
else if(step==='final'){ if(!ps.finalDraft) ps.finalDraft=paperFacilitatorFinal() }
|
|
2252
|
+
else if(step==='vote') ps.deliverable[rid]=false
|
|
2253
|
+
paperLog('超时:'+rid+' 在 '+step+' 阶段未回应,已按"缺少该部分/未表态"记录')
|
|
2254
|
+
}
|
|
2255
|
+
async function paperWakeSome(ids,kind){
|
|
2256
|
+
const ps=paperState
|
|
2257
|
+
for(const rid of ids){
|
|
2258
|
+
const r=residents.get(rid)
|
|
2259
|
+
if(!r||!r.childId){ paperLog('警告:'+rid+' 已不在场,跳过 '+kind); paperMarkMissing(kind==='part'?'parts':kind==='review'?'review':kind==='final'?'final':'vote',rid); continue }
|
|
2260
|
+
if(busy.has(rid)) continue
|
|
2261
|
+
const ok=await wakeResident(r,paperPrompt(kind,rid),PAPER_KINDS[kind])
|
|
2262
|
+
if(!ok) paperLog('警告:无法唤醒 '+rid+' 执行 '+kind)
|
|
2263
|
+
}
|
|
2264
|
+
await paperWriteLog()
|
|
2265
|
+
}
|
|
2266
|
+
async function paperWait(step){
|
|
2267
|
+
const ps=paperState; if(!ps) return
|
|
2268
|
+
const miss=ps.participants.filter(x=>!paperHas(step,x))
|
|
2269
|
+
if(!miss.length) return await paperAdvance('wait-recheck')
|
|
2270
|
+
const nowTs=now()
|
|
2271
|
+
if(nowTs-ps.stepAt>=paperStepLimit()){
|
|
2272
|
+
for(const rid of miss) paperMarkMissing(step,rid)
|
|
2273
|
+
ps.warning=(ps.warning?ps.warning+';':'')+'有参与者在 '+step+' 阶段超时未回应('+miss.join('、')+'),已按"缺少该部分"记录并继续'
|
|
2274
|
+
paperLog('警告:'+ps.warning); ps.stepAt=nowTs
|
|
2275
|
+
await paperWriteLog(); return await paperAdvance('timeout')
|
|
2276
|
+
}
|
|
2277
|
+
// Paced re-ask: at most one nudge per stall window, and only PAPER_NUDGE_TRIES in total, so an
|
|
2278
|
+
// unresponsive host cannot produce a wake storm while the deadline is still open.
|
|
2279
|
+
if(nowTs-(ps.lastNudgeAt||0)>=posMs(params.activityTimeoutMs,120000)){
|
|
2280
|
+
ps.lastNudgeAt=nowTs
|
|
2281
|
+
for(const rid of miss){
|
|
2282
|
+
const key=rid+':'+step
|
|
2283
|
+
ps.attempts[key]=(ps.attempts[key]||0)+1
|
|
2284
|
+
if(ps.attempts[key]>PAPER_NUDGE_TRIES) continue
|
|
2285
|
+
const r=residents.get(rid)
|
|
2286
|
+
if(!r||!r.childId||busy.has(rid)) continue
|
|
2287
|
+
const ok=await wakeResident(r,paperPrompt(step,rid,true),PAPER_KINDS[step])
|
|
2288
|
+
if(!ok) paperLog('警告:无法唤醒 '+rid+'('+step+' 重试)')
|
|
2289
|
+
}
|
|
2290
|
+
await paperWriteLog()
|
|
2291
|
+
}
|
|
2292
|
+
armHeartbeat() // keep the clock running so the step deadline is actually reachable
|
|
2293
|
+
}
|
|
2294
|
+
async function paperAdvance(why){
|
|
2295
|
+
const ps=paperState
|
|
2296
|
+
if(!ps||ps.status==='done'||ps.status==='failed') return
|
|
2297
|
+
try {
|
|
2298
|
+
if(ps.status==='parts'){
|
|
2299
|
+
if(ps.participants.every(x=>paperHas('parts',x))) return await paperMerge()
|
|
2300
|
+
return await paperWait('parts')
|
|
2301
|
+
}
|
|
2302
|
+
if(ps.status==='review'){
|
|
2303
|
+
if(ps.participants.every(x=>paperHas('review',x))) return await paperFinalStep()
|
|
2304
|
+
return await paperWait('review')
|
|
2305
|
+
}
|
|
2306
|
+
if(ps.status==='final'){
|
|
2307
|
+
if(ps.finalDraft) return await paperVoteStep()
|
|
2308
|
+
return await paperWait('final')
|
|
2309
|
+
}
|
|
2310
|
+
if(ps.status==='vote'){
|
|
2311
|
+
if(!ps.participants.every(x=>paperHas('vote',x))) return await paperWait('vote')
|
|
2312
|
+
if(ps.participants.every(x=>ps.deliverable[x]===true)) return await paperFinalize()
|
|
2313
|
+
return await paperIterate()
|
|
2314
|
+
}
|
|
2315
|
+
} catch(e){ console.error('vibe-math-v4: paper step ('+(why||'')+') failed: '+String((e&&e.stack)||e)) }
|
|
2316
|
+
}
|
|
2317
|
+
async function paperMerge(){
|
|
2318
|
+
const ps=paperState
|
|
2319
|
+
const parts=ps.participants.map(rid=>({rid,text:String((ps.parts[rid]&&ps.parts[rid].text)||'')})).filter(p=>p.text.trim())
|
|
2320
|
+
const seen=new Map(), dups=[], terms=new Map(), conflicts=[], body=[]
|
|
2321
|
+
for(const p of parts){
|
|
2322
|
+
const paras=p.text.replace(/\r\n/g,'\n').split(/\n{2,}/)
|
|
2323
|
+
for(const raw of paras){
|
|
2324
|
+
const para=raw.trim(); if(!para) continue
|
|
2325
|
+
const norm=para.replace(/\s+/g,' ').toLowerCase()
|
|
2326
|
+
if(norm.length>=40 && seen.has(norm)){ dups.push({owner:seen.get(norm),dup:p.rid,head:para.slice(0,60)}); continue }
|
|
2327
|
+
if(norm.length>=40) seen.set(norm,p.rid)
|
|
2328
|
+
body.push(para)
|
|
2329
|
+
// Notation/term unification: the FIRST definition of a `- 术语: 定义` / `**术语**:…` line
|
|
2330
|
+
// wins; a different definition of the same term in another part is recorded as a conflict.
|
|
2331
|
+
const m=/^(?:[-*]\s*)?(?:\*\*)?([\u4e00-\u9fa5A-Za-z][^::*]{0,24})(?:\*\*)?\s*[::]\s*(\S.*)$/.exec(para)
|
|
2332
|
+
if(m){ const term=m[1].trim()
|
|
2333
|
+
if(!terms.has(term)) terms.set(term,{owner:p.rid,def:para})
|
|
2334
|
+
else if(terms.get(term).owner!==p.rid && terms.get(term).def!==para) conflicts.push({term,kept:terms.get(term).owner,conflict:p.rid}) }
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
ps.draft=body.join('\n\n')
|
|
2338
|
+
const L=['# 合并稿(facilitator 合并)','','- 论文 id:'+ps.id,'- 参与成员:'+(ps.participants.join('、')||'(无常驻)'),
|
|
2339
|
+
'- 合并规则:跨部分**完全重复**的段落只保留首次出现;同一术语的**首个定义**为准,冲突记入下方。','']
|
|
2340
|
+
for(const p of parts) L.push('## 部分|'+p.rid,'',p.text.trim(),'')
|
|
2341
|
+
L.push('## 合并说明','')
|
|
2342
|
+
L.push('- 去重:'+(dups.length?dups.map(d=>'「'+d.head+'…」('+d.dup+' 重复了 '+d.owner+')').join(';'):'无跨部分重复段落'))
|
|
2343
|
+
L.push('- 术语/记号统一:'+(terms.size?[...terms.entries()].map(([k,v])=>k+'(取 '+v.owner+' 的定义)').join(';'):'未识别到形如「术语:定义」的行'))
|
|
2344
|
+
L.push('- 术语冲突:'+(conflicts.length?conflicts.map(c=>c.term+'(保留 '+c.kept+','+c.conflict+' 的定义不同)').join(';'):'无'))
|
|
2345
|
+
ps.mergeNotes={dups:dups.length,terms:terms.size,conflicts:conflicts.length}
|
|
2346
|
+
paperLog('facilitator 合并完成:'+parts.length+' 个部分,去重 '+dups.length+' 段,统一术语 '+terms.size+' 个,冲突 '+conflicts.length+' 处')
|
|
2347
|
+
await paperWriteRel(ps.dir+'/draft.md',L.join('\n')+'\n')
|
|
2348
|
+
ps.status='review'; ps.stepAt=now(); ps.lastNudgeAt=0
|
|
2349
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2350
|
+
if(!ps.participants.length){ return await paperFinalStep() }
|
|
2351
|
+
await paperWakeSome(ps.participants.slice(),'review')
|
|
2352
|
+
await paperAdvance('review-started')
|
|
2353
|
+
}
|
|
2354
|
+
/** The framework's own finalisation (paperEditor='facilitator'): the merged body plus the review
|
|
2355
|
+
* round APPLIED as an attributed revision block. No new claims are introduced. */
|
|
2356
|
+
function paperFacilitatorFinal(){
|
|
2357
|
+
const ps=paperState
|
|
2358
|
+
const L=[String(ps.draft||'').trim()]
|
|
2359
|
+
const rows=ps.participants.map(rid=>ps.reviews[rid]).filter(Boolean)
|
|
2360
|
+
if(rows.length){
|
|
2361
|
+
L.push('','## 互审改进(已采纳)')
|
|
2362
|
+
for(const rid of ps.participants){
|
|
2363
|
+
const rv=ps.reviews[rid]; if(!rv) continue
|
|
2364
|
+
if(rv.missing){ L.push('- '+rid+':互审超时未提交(已记入附录)'); continue }
|
|
2365
|
+
const pts=(rv.points||[]).map(x=>'「'+x+'」').join('、')
|
|
2366
|
+
L.push('- '+rid+' → 审阅 '+rv.reviewed+':'+(pts||'(未列出具体问题)')+(rv.improvements?(';改进:'+rv.improvements):''))
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return L.join('\n').trim()
|
|
2370
|
+
}
|
|
2371
|
+
async function paperFinalStep(){
|
|
2372
|
+
const ps=paperState
|
|
2373
|
+
ps.status='final'; ps.stepAt=now(); ps.lastNudgeAt=0
|
|
2374
|
+
const wantId=paperEditorResidentId(ps.cfg.editor)
|
|
2375
|
+
const r=wantId?residents.get(wantId):undefined
|
|
2376
|
+
if(wantId && r && r.childId){
|
|
2377
|
+
ps.editorId=wantId
|
|
2378
|
+
paperLog('定稿代表:resident:'+wantId+'(paperEditor 指定)')
|
|
2379
|
+
const ok=await wakeResident(r,paperPrompt('final',wantId),PAPER_KINDS.final)
|
|
2380
|
+
if(ok){ await paperWriteMeta(); await paperWriteLog(); armHeartbeat(); return }
|
|
2381
|
+
ps.warning='无法唤醒定稿代表 resident:'+wantId+',降级到 office'
|
|
2382
|
+
ps.editorDowngraded=true; paperLog('警告:'+ps.warning)
|
|
2383
|
+
} else if(wantId){
|
|
2384
|
+
// v2 §A5: the named editor left the roster (e.g. removeMember) -> downgrade to office and
|
|
2385
|
+
// RECORD the downgrade in meta + paper.log.md (never silently substitute an author).
|
|
2386
|
+
ps.warning='定稿代表 resident:'+wantId+' 已不在编制(可能已被解雇),降级到 office'
|
|
2387
|
+
ps.editorDowngraded=true; paperLog('警告:'+ps.warning)
|
|
2388
|
+
}
|
|
2389
|
+
ps.editorId='office'
|
|
2390
|
+
paperLog('定稿代表:office(会话根/人类侧)—— 框架依 office 之责把合并稿与互审记录整理成最终稿')
|
|
2391
|
+
if(!ps.finalDraft) ps.finalDraft=paperFacilitatorFinal()
|
|
2392
|
+
await paperWriteRel(ps.dir+'/final.md',ps.finalDraft)
|
|
2393
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2394
|
+
return await paperVoteStep()
|
|
2395
|
+
}
|
|
2396
|
+
async function paperVoteStep(){
|
|
2397
|
+
const ps=paperState
|
|
2398
|
+
if(!ps.finalDraft) ps.finalDraft=paperFacilitatorFinal()
|
|
2399
|
+
await paperWriteRel(ps.dir+'/final.md',ps.finalDraft)
|
|
2400
|
+
ps.status='vote'; ps.stepAt=now(); ps.lastNudgeAt=0
|
|
2401
|
+
paperLog('交付表决:请全体参与成员明确表态"可交付"')
|
|
2402
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2403
|
+
if(!ps.participants.length) return await paperFinalize()
|
|
2404
|
+
await paperWakeSome(ps.participants.slice(),'vote')
|
|
2405
|
+
await paperAdvance('vote-started')
|
|
2406
|
+
}
|
|
2407
|
+
async function paperIterate(){
|
|
2408
|
+
const ps=paperState
|
|
2409
|
+
const against=ps.participants.filter(x=>ps.deliverable[x]!==true)
|
|
2410
|
+
ps.dissent=against.slice()
|
|
2411
|
+
ps.round+=1
|
|
2412
|
+
paperLog('第 '+ps.round+' 轮:'+against.join('、')+' 未表示可交付('+(against.map(x=>ps.deliverable[x]===false?'明确反对':'未表态').join(';'))+')')
|
|
2413
|
+
if(ps.round>ps.maxRounds){
|
|
2414
|
+
ps.warning=(ps.warning?ps.warning+';':'')+'互审/交付迭代达到上限('+ps.maxRounds+'):'+against.join('、')+' 的分歧已写入附录'
|
|
2415
|
+
paperLog('警告:'+ps.warning)
|
|
2416
|
+
return await paperFinalize()
|
|
2417
|
+
}
|
|
2418
|
+
for(const rid of against) ps.deliverable[rid]=undefined
|
|
2419
|
+
ps.status='vote'; ps.stepAt=now(); ps.lastNudgeAt=0
|
|
2420
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2421
|
+
await paperWakeSome(against,'revise')
|
|
2422
|
+
await paperAdvance('revise-started')
|
|
2423
|
+
}
|
|
2424
|
+
// ---- content: the nine-section skeleton, built ONLY from evidence + the team's own text ----
|
|
2425
|
+
function paperExtractSection(draft,words){
|
|
2426
|
+
const lines=String(draft||'').split(/\r?\n/)
|
|
2427
|
+
let start=-1,level=0
|
|
2428
|
+
for(let i=0;i<lines.length;i++){
|
|
2429
|
+
const m=/^(#{1,6})\s*(.+)$/.exec(lines[i])
|
|
2430
|
+
if(m&&new RegExp(words,'i').test(m[2])){ start=i+1; level=m[1].length; break }
|
|
2431
|
+
}
|
|
2432
|
+
if(start<0) return ''
|
|
2433
|
+
const out=[]
|
|
2434
|
+
for(let i=start;i<lines.length;i++){
|
|
2435
|
+
const m=/^(#{1,6})\s*/.exec(lines[i])
|
|
2436
|
+
if(m&&m[1].length<=level) break
|
|
2437
|
+
out.push(lines[i])
|
|
2438
|
+
}
|
|
2439
|
+
return out.join('\n').trim()
|
|
2440
|
+
}
|
|
2441
|
+
function paperBullets(items,empty){ return items.length?items.map(x=>'- '+x).join('\n'):('- '+empty) }
|
|
2442
|
+
function paperCompose(ps,ev,cfg){
|
|
2443
|
+
const zh=cfg.language!=='en', T=paperTitles(cfg.language)
|
|
2444
|
+
const r=(a,b)=>zh?a:b
|
|
2445
|
+
const body=String(ps.finalDraft||ps.draft||'').trim()
|
|
2446
|
+
const truth=ev.verified.filter(v=>v.结论!=='假'), refuted=ev.verified.filter(v=>v.结论==='假')
|
|
2447
|
+
const pending=ev.propos.filter(p=>!['已验证·真','已验证·假'].includes(p.状态))
|
|
2448
|
+
const firstLine=String(ev.problem||'').split(/\r?\n/)[0].trim()
|
|
2449
|
+
const conclusion=r(
|
|
2450
|
+
(truth.length?('全体一致检验通过的命题 '+truth.length+' 条'+(refuted.length?(',被否证 '+refuted.length+' 条'):'')):'本轮没有取得全体一致的检验结论')+';未决条目 '+pending.length+' 条。',
|
|
2451
|
+
(truth.length?('Verified propositions: '+truth.length+(refuted.length?('; refuted: '+refuted.length):'')):'No unanimously verified proposition this run')+'; unresolved items: '+pending.length+'.')
|
|
2452
|
+
const sec=[];
|
|
2453
|
+
sec.push({title:T[0],body:[r('**摘要**:原问题——',' **Abstract** — problem: ')+firstLine,
|
|
2454
|
+
r('主要结论:','Main conclusion: ')+conclusion].join('\n')});
|
|
2455
|
+
sec.push({title:T[1],body:[r('原问题(完整陈述):','Original problem (full statement):'),'',String(ev.problem||''),
|
|
2456
|
+
'','- '+r('项目','Project')+':'+ev.project,'- '+r('运行','Run')+':'+ev.runId,'- '+r('状态','Status')+':'+(ev.converged?r('已收敛','converged'):r('未收敛','not converged'))].join('\n')});
|
|
2457
|
+
sec.push({title:T[2],body:[body||r('(团队本轮没有产出正文;以下仅为已有证据的索引。)','(The team authored no body this run; only the evidence index below.)'),
|
|
2458
|
+
'','### '+r('最终答案与结论','Final answer and conclusions'),paperBullets(
|
|
2459
|
+
truth.map(v=>v.id+(v.概率?('('+r('判定为真,估计值','true, estimate')+' '+v.概率+')'):'')+' ← `'+v.rel+'`'),
|
|
2460
|
+
r('本轮没有被全体一致判定为真的命题','No unanimously true proposition this run'))].join('\n')});
|
|
2461
|
+
sec.push({title:T[3],body:[paperBullets(
|
|
2462
|
+
ev.verified.map(v=>{ const verdict=v.结论==='假'?r('已否证','refuted'):r('真','true')
|
|
2463
|
+
return '['+verdict+'] '+v.id+(v.概率?('|'+r('概率','probability')+' '+v.概率):'')+'|'+r('证据','evidence')+' `'+v.rel+'`'+(v.类型?('|'+v.类型):'') }),
|
|
2464
|
+
r('本轮没有被检验通过的命题','No verified proposition this run')),
|
|
2465
|
+
'','### '+r('未决 / 未通过','Unresolved / not passed'),
|
|
2466
|
+
paperBullets(pending.map(p=>'[未决] '+p.id+'('+r('状态','status')+':'+(p.状态||'未定论')+')|`'+p.rel+'`')
|
|
2467
|
+
.concat(ev.formal.filter(f=>f.status!=='passed').map(f=>'[未决·形式化] '+f.target+'('+f.status+')'+(f.file?('|`'+f.file+'`'):''))),
|
|
2468
|
+
r('无','none'))].join('\n')});
|
|
2469
|
+
sec.push({title:T[4],body:[paperBullets(
|
|
2470
|
+
ev.verifiedProblems.map(v=>'[已解决] '+v.id+'|`'+v.rel+'`').concat(ev.subproblems.map(s=>'['+(['已验证·真','已验证·假'].includes(s.状态)?r('已解决','solved'):r('未决','unresolved'))+'] '+s.id+(s.标题?('「'+s.标题+'」'):'')+'|`'+s.rel+'`')),
|
|
2471
|
+
r('本轮没有记录子问题','No sub-problem was recorded this run'))].join('\n')});
|
|
2472
|
+
sec.push({title:T[5],body:[paperBullets(
|
|
2473
|
+
ev.methods.map(m=>m.id+(m.标题?('「'+m.标题+'」'):'')+'('+(m.类型||r('方法','method'))+','+(m.状态||r('经验','experiential'))+')'+(m.可信断言?('|'+r('可信断言','trusted claims')+':'+m.可信断言):'')+'|`'+m.rel+'`'),
|
|
2474
|
+
r('本轮没有记录方法/理论/工具','No method/theory/tool was recorded this run')),
|
|
2475
|
+
'',r('已归档的形式化产物:','Archived formal artifacts: ')+paperBullets(ev.formal.filter(f=>f.status==='passed').map(f=>f.target+(f.proof?('|`'+f.proof+'`'):'')+(f.file?('|`'+f.file+'`'):'')),r('无','none'))].join('\n')});
|
|
2476
|
+
const pattern=paperExtractSection(body,zh?'规律|总结|原理':'pattern|principle')
|
|
2477
|
+
sec.push({title:T[6],body:[pattern||[r('团队本轮没有单独撰写"规律总结";以下为**有证据支撑**的可复用条目(取自方法卡与已验证命题),未作外推:',
|
|
2478
|
+
'The team did not author a separate "patterns" section; below are the EVIDENCE-BACKED reusable items (method cards and verified propositions), with no extrapolation:'),
|
|
2479
|
+
paperBullets(ev.methods.filter(m=>m.可信断言).map(m=>m.可信断言+'(`'+m.rel+'`)')
|
|
2480
|
+
.concat(truth.map(v=>v.id+'(`'+v.rel+'`)')),r('无','none'))].join('\n')].join('\n')});
|
|
2481
|
+
const disc=paperExtractSection(body,zh?'讨论|局限|展望':'discussion|limitation|outlook')
|
|
2482
|
+
sec.push({title:T[7],body:[disc||r('(团队未单独撰写讨论;以下为框架按证据整理的局限。)','(No separate discussion was authored; the framework lists the evidenced limitations.)'),
|
|
2483
|
+
'','### '+r('局限与未决','Limitations and open items'),
|
|
2484
|
+
paperBullets([
|
|
2485
|
+
r('未决/未通过条目 '+pending.length+' 条(见第 4 节,均已标注)','Unresolved items: '+pending.length+' (flagged in §4)'),
|
|
2486
|
+
r('被否证的命题 '+refuted.length+' 条(见第 4 节)','Refuted propositions: '+refuted.length+' (see §4)'),
|
|
2487
|
+
(ps.compile&&ps.compile.result==='failed')?r('PDF 编译失败:已保留 paper.tex 与 paper.md 并上报','PDF compilation failed: paper.tex and paper.md were kept and reported'):'',
|
|
2488
|
+
(ps.compile&&ps.compile.result==='not-detected')?r('本机未检测到 LaTeX 引擎:只产出 tex+md','No LaTeX engine detected: tex+md only'):'',
|
|
2489
|
+
ps.dissent.length?r('交付分歧('+ps.dissent.join('、')+')已写入附录','Deliverability dissent ('+ps.dissent.join(', ')+') is recorded in the appendix'):'',
|
|
2490
|
+
ps.warning?r('过程警告:'+ps.warning,'Process warning: '+ps.warning):'',
|
|
2491
|
+
].filter(Boolean),r('无','none'))].join('\n')});
|
|
2492
|
+
sec.push({title:T[8],body:[r('证据与文件索引(全部相对于项目根):','Evidence and file index (all relative to the project root):'),
|
|
2493
|
+
'',paperBullets(ev.index.map(p=>'`'+p+'`'),r('无','none')),
|
|
2494
|
+
'','- '+r('过程记录','Process log')+':`'+ps.dir+'/paper.log.md`',
|
|
2495
|
+
'- '+r('元数据','Metadata')+':`'+ps.dir+'/paper.meta.json`',
|
|
2496
|
+
'- '+r('各成员撰写部分','Per-member parts')+':`'+ps.dir+'/parts/`',
|
|
2497
|
+
'- '+r('互审记录','Cross-review records')+':`'+ps.dir+'/reviews/`',
|
|
2498
|
+
'- '+r('合并稿/最终稿','Merged / final draft')+':`'+ps.dir+'/draft.md`、`'+ps.dir+'/final.md`',
|
|
2499
|
+
(ps.dissent.length||ps.round>ps.maxRounds)?['','### '+r('附录:交付分歧','Appendix: deliverability dissent'),paperBullets(ps.participants.filter(x=>ps.deliverable[x]!==true).map(x=>x+':'+(ps.deliverable[x]===false?r('反对交付','objected'):r('未表态','no statement'))+'|'+r('迭代轮次','rounds')+' '+ps.round+'/'+ps.maxRounds),r('无','none'))].join('\n'):'']
|
|
2500
|
+
.filter(Boolean).join('\n')});
|
|
2501
|
+
const title=r('Vibe-Mathematics v4 研究报告','Vibe-Mathematics v4 Research Report')+'|'+firstLine.slice(0,60)
|
|
2502
|
+
const md=L=>L
|
|
2503
|
+
const mdText=['# '+title,'','- '+r('作者','Authors')+':Vibe-Mathematics v4('+r('运行','run')+' '+ps.runId+')',
|
|
2504
|
+
'- '+r('日期','Date')+':'+fmtTime(ps.finalizedAt||now()).slice(0,10),
|
|
2505
|
+
'- '+r('参与成员','Participants')+':'+(ps.participants.join('、')||r('(无常驻)','(no residents)')),
|
|
2506
|
+
'- '+r('定稿代表','Editor')+':'+(ps.editorId||cfg.editor),'',
|
|
2507
|
+
...sec.map((s,i)=>['## '+(i+1)+'. '+s.title,'',s.body,''].join('\n'))].join('\n')
|
|
2508
|
+
const texBody=sec.map((s,i)=>'\\section{'+texEsc(s.title)+'}\n'+texEsc(s.body)).join('\n\n')
|
|
2509
|
+
const tex=paperTex(cfg,title,texBody)
|
|
2510
|
+
return {title, sections:sec, md:mdText, tex, texBody}
|
|
2511
|
+
}
|
|
2512
|
+
function paperTex(cfg,title,texBody){
|
|
2513
|
+
const zh=cfg.language!=='en'
|
|
2514
|
+
const cls=zh?'ctexart':'article'
|
|
2515
|
+
const pkgs=['amsmath','amssymb','amsthm','geometry','hyperref','longtable','booktabs']
|
|
2516
|
+
return ['% generated by vibe-math-v4 (final paper)','\\documentclass[11pt]{'+cls+'}',
|
|
2517
|
+
...pkgs.map(p=>'\\usepackage{'+p+'}'),
|
|
2518
|
+
'\\title{'+texEsc(title)+'}','\\author{Vibe-Mathematics v4}','\\date{'+texEsc(fmtTime(now()).slice(0,10))+'}',
|
|
2519
|
+
'\\begin{document}','\\maketitle','',texBody,'','\\end{document}',''].join('\n')
|
|
2520
|
+
}
|
|
2521
|
+
/** tex escaping (docs/final-paper.md §5): every `_ % & # $ { } ~ ^ \\` becomes a safe command/escape. */
|
|
2522
|
+
function texEsc(s){
|
|
2523
|
+
const M={'\\':'\\textbackslash{}','{':'\\{','}':'\\}','$':'\\$','&':'\\&','#':'\\#','^':'\\textasciicircum{}','_':'\\_','%':'\\%','~':'\\textasciitilde{}'}
|
|
2524
|
+
let out=''; for(const ch of String(s==null?'':s)) out+=(M[ch]||ch); return out
|
|
2525
|
+
}
|
|
2526
|
+
// ---- compilation (detect → compile twice → capped repair → degrade; docs/final-paper.md §D/§E) ----
|
|
2527
|
+
// The host fs is TEXT-ONLY (dsh-fs has no writeBytes), so `paper.pdf` can only ever be produced
|
|
2528
|
+
// by the compiler subprocess inside `Paper/<id>/` — the plugin just `stat`s it. The Lean seam
|
|
2529
|
+
// (`sub.resolveExecutable` + `sub.spawn`) is reused verbatim so a fake compiler is injectable.
|
|
2530
|
+
async function paperDetectEngine(lang){
|
|
2531
|
+
const sub=subprocessOf(); if(sub===undefined) return {ok:false,tried:[],reason:'NO_SUBPROCESS'}
|
|
2532
|
+
const prefer=String(params.paperLatexCommand||'').trim()
|
|
2533
|
+
const base=lang==='en'?['pdflatex','latexmk','xelatex','lualatex','tectonic']:['xelatex','latexmk','pdflatex','lualatex','tectonic']
|
|
2534
|
+
const order=(prefer?[prefer]:[]).concat(base.filter(x=>x!==prefer))
|
|
2535
|
+
const tried=[]
|
|
2536
|
+
for(let i=0;i<order.length;i++){
|
|
2537
|
+
const exe=order[i]
|
|
2538
|
+
try { const r=await sub.resolveExecutable(exe); if(r) return {ok:true,exe:String(r),tried,alts:order.slice(i+1)} }
|
|
2539
|
+
catch(e){ tried.push(exe) }
|
|
2540
|
+
}
|
|
2541
|
+
return {ok:false,tried,reason:'LATEX_NOT_FOUND'}
|
|
2542
|
+
}
|
|
2543
|
+
function paperEngineArgv(exe,lang){
|
|
2544
|
+
if(/latexmk$/.test(exe)) return [exe,'-interaction=nonstopmode',lang==='en'?'-pdf':'-xelatex','paper.tex']
|
|
2545
|
+
return [exe,'-interaction=nonstopmode','paper.tex']
|
|
2546
|
+
}
|
|
2547
|
+
async function paperRunEngine(exe,argv,dir,cap){
|
|
2548
|
+
const sub=subprocessOf(); if(sub===undefined) return {ok:false,code:'NO_SUBPROCESS'}
|
|
2549
|
+
let handle
|
|
2550
|
+
try { handle=sub.spawn({argv,cwd:dir,stdio:{stdin:'ignore',stdout:{maxBytes:64*1024},stderr:{maxBytes:64*1024}},graceMs:cap}) }
|
|
2551
|
+
catch(e){ return {ok:false,code:'LATEX_SPAWN_FAILED',message:String((e&&e.message)||e)} }
|
|
2552
|
+
let killed=false,timerDispose=null,outcome
|
|
2553
|
+
try {
|
|
2554
|
+
outcome=await Promise.race([handle.done,new Promise(function(resolve){
|
|
2555
|
+
if(typeof ctx.timeout!=='function') return
|
|
2556
|
+
timerDispose=ctx.timeout(function(){ killed=true; try{ if(typeof handle.terminate==='function') handle.terminate() }catch(e){} resolve({exitCode:null,signal:'SIGTERM'}) },cap)
|
|
2557
|
+
})])
|
|
2558
|
+
} catch(e){ if(timerDispose){ try{ timerDispose() }catch(_e){} } return {ok:false,code:'LATEX_RUN_FAILED',message:String((e&&e.message)||e)} }
|
|
2559
|
+
if(timerDispose){ try{ timerDispose() }catch(e){} }
|
|
2560
|
+
let out='',err=''
|
|
2561
|
+
try { if(handle.collected&&handle.collected.stdout) out=handle.collected.stdout.readFrom(0).text } catch(e){}
|
|
2562
|
+
try { if(handle.collected&&handle.collected.stderr) err=handle.collected.stderr.readFrom(0).text } catch(e){}
|
|
2563
|
+
const exitCode=outcome?outcome.exitCode:null
|
|
2564
|
+
return {ok:exitCode===0,exitCode,timedOut:killed,stdout:tail(out,2000),stderr:tail(err,2000)}
|
|
2565
|
+
}
|
|
2566
|
+
async function paperPdfExists(dir){
|
|
2567
|
+
try { const t=await fs.resolve(dir+'/paper.pdf',{cwd:frameworkRoot()}); return !!(await fs.stat(t)) } catch(e){ return false }
|
|
2568
|
+
}
|
|
2569
|
+
/** v2 §E: at most ONE engine swap, then ONE minimal-template retry, then report + degrade. */
|
|
2570
|
+
async function paperCompile(ps,composed,ev){
|
|
2571
|
+
const dir=ps.dir
|
|
2572
|
+
// v2 §E: `paperFormat=md` produces no tex, so "cannot compile without tex" must NOT be warned.
|
|
2573
|
+
if(ps.cfg.format==='md') return {result:'skipped',reason:'paperFormat=md(未产出 tex,跳过编译)',attempts:[]}
|
|
2574
|
+
if(!ps.cfg.compilePdf) return {result:'skipped',reason:'paperCompilePdf=false',attempts:[]}
|
|
2575
|
+
// Never clobber an existing pdf (v2 §D): a previously compiled artifact is kept as-is.
|
|
2576
|
+
if(await paperPdfExists(dir)) return {result:'ok',engine:'existing',note:'已存在 paper.pdf,未覆盖',attempts:[]}
|
|
2577
|
+
const det=await paperDetectEngine(ps.cfg.language)
|
|
2578
|
+
if(!det.ok) return {result:'not-detected',reason:'未检测到 LaTeX 引擎('+(det.tried.join('/')||'subprocess 不可用')+'):只产出 tex+md,不编译',tried:det.tried,attempts:[]}
|
|
2579
|
+
const cap=posMs(params.activityTimeoutMs,120000)
|
|
2580
|
+
const attempts=[]
|
|
2581
|
+
const runTwice=async(exe,label,passes)=>{
|
|
2582
|
+
const argv=paperEngineArgv(exe,ps.cfg.language)
|
|
2583
|
+
let ok=true,first=null
|
|
2584
|
+
for(let i=0;i<(passes||2);i++){ const a=await paperRunEngine(exe,argv,dir,cap); if(i===0) first=a; if(!a.ok){ ok=false; break } }
|
|
2585
|
+
const pdf=await paperPdfExists(dir)
|
|
2586
|
+
attempts.push({stage:label,engine:exe,ok:!!(ok&&pdf),exitCode:first?first.exitCode:null,pdf,stderr:String((first&&first.stderr)||'').slice(0,300)})
|
|
2587
|
+
return {ok:!!(ok&&pdf),last:first}
|
|
2588
|
+
}
|
|
2589
|
+
let r=await runTwice(det.exe,'initial',2)
|
|
2590
|
+
if(r.ok) return {result:'ok',engine:det.exe,attempts}
|
|
2591
|
+
// repair 1: swap to another RESOLVABLE engine (same tex, nonstopmode, twice) — capped at one swap
|
|
2592
|
+
const sub=subprocessOf()
|
|
2593
|
+
for(const alt of det.alts||[]){
|
|
2594
|
+
let altExe=null
|
|
2595
|
+
try { if(sub) altExe=String(await sub.resolveExecutable(alt)) } catch(e){ continue }
|
|
2596
|
+
if(!altExe||altExe===det.exe) continue
|
|
2597
|
+
const rr=await runTwice(altExe,'engine-switch',2)
|
|
2598
|
+
if(rr.ok) return {result:'ok',engine:altExe,repaired:'engine-switch',attempts}
|
|
2599
|
+
break
|
|
2600
|
+
}
|
|
2601
|
+
// repair 2: ONE minimal-template retry (drops every optional package / custom preamble)
|
|
2602
|
+
await paperWriteRel(dir+'/paper.tex',paperMinimalTemplate(ps.cfg,composed.title,composed.texBody))
|
|
2603
|
+
const rr=await runTwice(det.exe,'minimal-template',1)
|
|
2604
|
+
if(rr.ok) return {result:'ok',engine:det.exe,repaired:'minimal-template',attempts}
|
|
2605
|
+
const last=attempts[attempts.length-1]||{}
|
|
2606
|
+
return {result:'failed',engine:det.exe,attempts,error:'编译失败(已按上限尝试换引擎/最小模板):保留 paper.tex 与 paper.md,不阻塞定稿',tail:String(last.stderr||'')}
|
|
2607
|
+
}
|
|
2608
|
+
function paperMinimalTemplate(cfg,title,texBody){
|
|
2609
|
+
const cls=cfg.language==='en'?'article':'ctexart'
|
|
2610
|
+
return ['% generated by vibe-math-v4 (minimal fallback template)','\\documentclass[11pt]{'+cls+'}',
|
|
2611
|
+
'\\begin{document}','\\section*{'+texEsc(title)+'}','',String(texBody||''),'','\\end{document}',''].join('\n')
|
|
2612
|
+
}
|
|
2613
|
+
async function paperEnsureArtifacts(meta,cfg){
|
|
2614
|
+
const filled=[]
|
|
2615
|
+
const dir=String(meta.dir||('Paper/'+meta.id))
|
|
2616
|
+
const saved=paperState
|
|
2617
|
+
try {
|
|
2618
|
+
if(!paperState||paperState.id!==meta.id){
|
|
2619
|
+
paperState={ id:meta.id, dir, trigger:'recheck', deferCompletion:false, startedAt:meta.startedAt||now(), finalizedAt:meta.finalizedAt||0,
|
|
2620
|
+
status:'done', round:meta.rounds||0, maxRounds:PAPER_MAX_ROUNDS, cfg, fingerprint:meta.fingerprint||'',
|
|
2621
|
+
participants:Array.isArray(meta.participants)?meta.participants:[], parts:{}, reviews:{}, deliverable:{}, dissent:meta.dissent||[],
|
|
2622
|
+
log:[], warning:meta.warning||'', compile:meta.compile||null, draft:'', finalDraft:'', files:meta.files||[], stepAt:now(), lastNudgeAt:0, attempts:{}, editorId:meta.editorId||'' }
|
|
2623
|
+
}
|
|
2624
|
+
const ev=await paperEvidence()
|
|
2625
|
+
const body=await readText(dir+'/final.md')
|
|
2626
|
+
if(body!==undefined&&body!=='') paperState.finalDraft=body
|
|
2627
|
+
const composed=paperCompose(paperState,ev,cfg)
|
|
2628
|
+
const wantMd=cfg.format!=='tex', wantTex=cfg.format!=='md'
|
|
2629
|
+
const mdRaw=await readText(dir+'/paper.md'), texRaw=await readText(dir+'/paper.tex')
|
|
2630
|
+
if(wantMd&&mdRaw===undefined){ if(await paperWriteRel(dir+'/paper.md',composed.md)) filled.push('paper.md') }
|
|
2631
|
+
if(wantTex&&texRaw===undefined){ if(await paperWriteRel(dir+'/paper.tex',composed.tex)) filled.push('paper.tex') }
|
|
2632
|
+
if(cfg.compilePdf&&wantTex&&!(await paperPdfExists(dir))){
|
|
2633
|
+
const c=await paperCompile(paperState,composed,ev)
|
|
2634
|
+
paperState.compile=c
|
|
2635
|
+
if(c.result==='ok') filled.push('paper.pdf')
|
|
2636
|
+
}
|
|
2637
|
+
if(filled.length) await paperWriteMeta()
|
|
2638
|
+
return filled
|
|
2639
|
+
} finally { paperState=saved||null }
|
|
2640
|
+
}
|
|
2641
|
+
async function paperWriteMeta(){
|
|
2642
|
+
const ps=paperState; if(!ps) return false
|
|
2643
|
+
const meta={ id:ps.id, dir:ps.dir, generatedBy:'vibe-math-v4', preset:'v4', trigger:ps.trigger,
|
|
2644
|
+
runId, closureSignal:PAPER_CLOSURE_SIGNAL, startedAt:ps.startedAt, finalizedAt:ps.finalizedAt||0,
|
|
2645
|
+
params:{finalPaper:params.finalPaper,paperFormat:ps.cfg.format,paperLanguage:ps.cfg.language,paperCompilePdf:ps.cfg.compilePdf,paperEditor:ps.cfg.editor,paperLatexCommand:params.paperLatexCommand},
|
|
2646
|
+
editor:ps.cfg.editor, editorId:ps.editorId||'', editorDowngraded:!!ps.editorDowngraded, participants:ps.participants, rounds:ps.round, maxRounds:ps.maxRounds,
|
|
2647
|
+
fingerprint:ps.fingerprint||'', warning:ps.warning||'', files:ps.files||[], compile:ps.compile||null, mergeNotes:ps.mergeNotes||null,
|
|
2648
|
+
parts:Object.keys(ps.parts).sort().map(k=>({id:k,chars:String((ps.parts[k]||{}).text||'').length,missing:!!(ps.parts[k]||{}).missing,source:(ps.parts[k]||{}).source||''})),
|
|
2649
|
+
reviews:Object.keys(ps.reviews).sort().map(k=>({id:k,reviewed:(ps.reviews[k]||{}).reviewed||'',points:((ps.reviews[k]||{}).points||[]).length,improvements:(ps.reviews[k]||{}).improvements||'',missing:!!(ps.reviews[k]||{}).missing})),
|
|
2650
|
+
deliverable:Object.keys(ps.deliverable).sort().map(k=>({id:k,deliverable:ps.deliverable[k]})),
|
|
2651
|
+
dissent:ps.dissent||[] }
|
|
2652
|
+
return await paperWriteRel(ps.dir+'/paper.meta.json',JSON.stringify(meta,null,2))
|
|
2653
|
+
}
|
|
2654
|
+
async function paperWriteLog(){
|
|
2655
|
+
const ps=paperState; if(!ps) return false
|
|
2656
|
+
const L=['# 最终论文过程记录(paper.log.md)','','- 论文 id:'+ps.id,'- 触发:'+ps.trigger,
|
|
2657
|
+
'- 语言 / 格式 / 定稿代表:'+ps.cfg.language+' / '+ps.cfg.format+' / '+ps.cfg.editor,'- 参与成员:'+(ps.participants.join('、')||'(无常驻)'),
|
|
2658
|
+
'- 状态:'+ps.status,'']
|
|
2659
|
+
L.push('## 过程(谁写了什么)'); for(const l of ps.log) L.push('- '+l); L.push('')
|
|
2660
|
+
L.push('## 归属与处理(谁提了什么意见、如何采纳)')
|
|
2661
|
+
for(const rid of ps.participants){ const p=ps.parts[rid]
|
|
2662
|
+
L.push('- 撰写:'+rid+(p?(p.missing?'(超时未提交)':'('+String(p.text||'').length+' 字,来源 '+(p.source||'reply')+')'):'(未提交)')) }
|
|
2663
|
+
for(const rid of ps.participants){ const rv=ps.reviews[rid]
|
|
2664
|
+
if(!rv||rv.missing){ L.push('- 互审:'+rid+'('+(rv?'超时未提交,已记入附录':'未完成')+')'); continue }
|
|
2665
|
+
L.push('- 互审:'+rid+' → '+rv.reviewed+',意见 '+((rv.points||[]).length)+' 条,处理:已采纳并记入合并稿审阅记录'+(rv.improvements?('(改进:'+String(rv.improvements).slice(0,200)+')'):'')) }
|
|
2666
|
+
L.push('- 定稿:'+(ps.editorId||ps.cfg.editor))
|
|
2667
|
+
for(const rid of ps.participants) if(typeof ps.deliverable[rid]==='boolean') L.push('- 交付表态:'+rid+' = '+(ps.deliverable[rid]?'可交付':'不可交付'))
|
|
2668
|
+
if(ps.compile) L.push('- 编译:'+JSON.stringify({result:ps.compile.result,engine:ps.compile.engine||'',repaired:ps.compile.repaired||'',attempts:(ps.compile.attempts||[]).length,error:ps.compile.error||''}))
|
|
2669
|
+
if(ps.warning) L.push('- 警告:'+ps.warning)
|
|
2670
|
+
return await paperWriteRel(ps.dir+'/paper.log.md',L.join('\n')+'\n')
|
|
2671
|
+
}
|
|
2672
|
+
async function paperFinalize(){
|
|
2673
|
+
const ps=paperState
|
|
2674
|
+
const ev=await paperEvidence()
|
|
2675
|
+
ps.fingerprint=paperFingerprint(ev)
|
|
2676
|
+
// final.md / draft.md must exist before composing so a re-run is reproducible
|
|
2677
|
+
if(!ps.finalDraft) ps.finalDraft=paperFacilitatorFinal()
|
|
2678
|
+
await paperWriteRel(ps.dir+'/final.md',ps.finalDraft)
|
|
2679
|
+
const composed=paperCompose(ps,ev,ps.cfg)
|
|
2680
|
+
ps.files=[]
|
|
2681
|
+
if(ps.cfg.format!=='tex'){ if(await paperWriteRel(ps.dir+'/paper.md',composed.md)) ps.files.push('paper.md') }
|
|
2682
|
+
if(ps.cfg.format!=='md'){ if(await paperWriteRel(ps.dir+'/paper.tex',composed.tex)) ps.files.push('paper.tex') }
|
|
2683
|
+
ps.compile=await paperCompile(ps,composed,ev)
|
|
2684
|
+
if(ps.compile&&ps.compile.result==='ok') ps.files.push('paper.pdf')
|
|
2685
|
+
ps.finalizedAt=now(); ps.status='done'
|
|
2686
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2687
|
+
// v2 §A1: the paper phase was entered from INSIDE the closing branch, BEFORE the completion
|
|
2688
|
+
// flags — so the run only becomes "done" here, once the paper exists (or has degraded safely).
|
|
2689
|
+
if(ps.deferCompletion && autoDone!==true){
|
|
2690
|
+
running=false; autoDone=true; phase='done'; clearHeartbeat()
|
|
2691
|
+
wakeKind.clear(); meetingState=null; verifyState=null; pendingMeeting=null; pendingVerify=[]
|
|
2692
|
+
logActivity('stop','all residents agree: problem solved(一致性停止票;最终论文已产出)')
|
|
2693
|
+
await saveAll()
|
|
2694
|
+
}
|
|
2695
|
+
const warn=[ps.warning,ps.compile&&ps.compile.result==='failed'?ps.compile.error:''].filter(Boolean).join(';')
|
|
2696
|
+
if(warn) logActivity('paper','最终论文完成(有警告):'+ps.dir+'|'+warn)
|
|
2697
|
+
else logActivity('paper','最终论文完成:'+ps.dir+'('+ps.files.join('、')+(ps.compile?('|编译 '+(ps.compile.result||'')):'')+')')
|
|
2698
|
+
return {ok:true,id:ps.id,dir:ps.dir,files:ps.files,compile:ps.compile,rounds:ps.round,dissent:ps.dissent,warning:ps.warning}
|
|
2699
|
+
}
|
|
2700
|
+
function paperStatusView(){
|
|
2701
|
+
const ps=paperState
|
|
2702
|
+
return {
|
|
2703
|
+
params:{finalPaper:params.finalPaper,paperFormat:params.paperFormat,paperLanguage:params.paperLanguage,paperCompilePdf:params.paperCompilePdf,paperEditor:params.paperEditor,paperLatexCommand:params.paperLatexCommand},
|
|
2704
|
+
id:ps?ps.id:paperIdFor(), dir:ps?ps.dir:paperDirFor(), status:ps?ps.status:'idle',
|
|
2705
|
+
running:!!(ps&&ps.status!=='done'&&ps.status!=='failed'), trigger:ps?ps.trigger:'',
|
|
2706
|
+
rounds:ps?ps.round:0, maxRounds:ps?ps.maxRounds:PAPER_MAX_ROUNDS, participants:ps?ps.participants:[],
|
|
2707
|
+
editor:ps?(ps.editorId||ps.cfg.editor):params.paperEditor, editorDowngraded:!!(ps&&ps.editorDowngraded),
|
|
2708
|
+
parts:ps?Object.keys(ps.parts).length:0, reviews:ps?Object.keys(ps.reviews).length:0,
|
|
2709
|
+
finalizedAt:ps?ps.finalizedAt:0, warning:ps?ps.warning:'', files:ps?(ps.files||[]):[], compile:ps?ps.compile:null,
|
|
2710
|
+
closureSignal:PAPER_CLOSURE_SIGNAL,
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
function paperStatusSummary(){
|
|
2714
|
+
const v=paperStatusView()
|
|
2715
|
+
return {ok:true, id:v.id, dir:v.dir, status:v.status, running:v.running, trigger:v.trigger, lang:v.params.paperLanguage,
|
|
2716
|
+
format:v.params.paperFormat, editor:v.editor, editorDowngraded:v.editorDowngraded, participants:v.participants,
|
|
2717
|
+
parts:v.parts, reviews:v.reviews, rounds:v.rounds+'/'+v.maxRounds, finalizedAt:v.finalizedAt, files:v.files,
|
|
2718
|
+
params:v.params,
|
|
2719
|
+
compile:v.compile?{result:v.compile.result,engine:v.compile.engine||'',repaired:v.compile.repaired||''}:null,
|
|
2720
|
+
warning:v.warning, closureSignal:v.closureSignal}
|
|
2721
|
+
}
|
|
2722
|
+
async function paperStart(trigger,opts){
|
|
2723
|
+
const o=opts||{}
|
|
2724
|
+
const force=!!o.force
|
|
2725
|
+
const cfg=paperCfg(o)
|
|
2726
|
+
const id=paperIdFor(), dir='Paper/'+id
|
|
2727
|
+
if(paperState && paperState.id===id && paperState.status!=='done' && paperState.status!=='failed' && !force)
|
|
2728
|
+
return {ok:true,id,dir,status:paperState.status,alreadyRunning:true,message:'最终论文正在进行('+paperState.status+')'}
|
|
2729
|
+
if(paperState && paperState.id!==id && paperState.status!=='done' && paperState.status!=='failed' && !force)
|
|
2730
|
+
return {ok:false,code:'V4_PAPER_BUSY',message:'另一篇论文正在进行('+paperState.id+'),完成或 force 后再试'}
|
|
2731
|
+
// Idempotency (docs/final-paper.md §C): the run id IS the directory, so a finalizedAt in THAT directory
|
|
2732
|
+
// means this run already has its paper; only per-artifact existence is re-checked.
|
|
2733
|
+
const meta=await readJson(dir+'/paper.meta.json')
|
|
2734
|
+
if(!force && meta && meta.finalizedAt){
|
|
2735
|
+
const filled=await paperEnsureArtifacts(meta,cfg)
|
|
2736
|
+
return {ok:true,id,dir,idempotent:true,finalizedAt:meta.finalizedAt,filled,
|
|
2737
|
+
message:'论文已定稿(幂等:只补写缺失产物'+(filled.length?(':'+filled.join('、')):':无缺失')+')'+(params.finalPaper===false?'(finalPaper=false:自动触发已关闭,本次为手动命令)':'')}
|
|
2738
|
+
}
|
|
2739
|
+
const prevDeliver={}
|
|
2740
|
+
paperState={ id, dir, trigger, deferCompletion:!!o.deferCompletion, startedAt:now(), finalizedAt:0, status:'parts', round:0, maxRounds:PAPER_MAX_ROUNDS,
|
|
2741
|
+
cfg, fingerprint:'', participants:Array.from(residents.keys()).sort(), parts:{}, reviews:{}, deliverable:prevDeliver,
|
|
2742
|
+
dissent:[], log:[], warning:'', compile:null, draft:'', finalDraft:'', files:[], stepAt:now(), lastNudgeAt:0, attempts:{}, editorId:'', editorDowngraded:false }
|
|
2743
|
+
paperLog('触发('+trigger+'):'+id+'|收敛信号='+PAPER_CLOSURE_SIGNAL+'|语言='+cfg.language+'|格式='+cfg.format+'|定稿='+cfg.editor+'|参与='+(paperState.participants.join('、')||'(无常驻)'))
|
|
2744
|
+
if(params.finalPaper===false) paperLog('注意:finalPaper=false —— 自动触发已关闭;本次为手动命令')
|
|
2745
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
2746
|
+
if(!paperState.participants.length){
|
|
2747
|
+
paperState.warning='本轮没有在场常驻:正文由框架(office 侧)从既有证据整理'
|
|
2748
|
+
paperLog('警告:'+paperState.warning)
|
|
2749
|
+
await paperMerge() // -> paperFinalStep -> vote(no participants) -> finalize
|
|
2750
|
+
return {ok:true,id,dir,status:paperState.status,message:'最终论文已启动(无常驻成员,由框架从既有证据整理)'+(params.finalPaper===false?'(finalPaper=false:自动触发已关闭)':'')}
|
|
2751
|
+
}
|
|
2752
|
+
await paperWakeSome(paperState.participants.slice(),'part')
|
|
2753
|
+
await paperAdvance('parts-started')
|
|
2754
|
+
return {ok:true,id,dir,status:paperState.status,participants:paperState.participants,files:paperState.files,
|
|
2755
|
+
message:'最终论文已启动('+trigger+')'+(params.finalPaper===false?'(finalPaper=false:自动触发已关闭)':'')}
|
|
2756
|
+
}
|
|
2757
|
+
async function runPaperCommand(ov,force){
|
|
2758
|
+
const known=['lang','format','editor']
|
|
2759
|
+
for(const k of Object.keys(ov||{})) if(known.indexOf(k)===-1) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'未知选项 '+k+'(支持 lang=zh|en / format=md|tex|both / editor=office|resident:<id> / force)'}
|
|
2760
|
+
if(ov&&ov.lang&&PAPER_LANGS.indexOf(String(ov.lang))===-1) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'paperLanguage 只能是 zh|en'}
|
|
2761
|
+
if(ov&&ov.format&&PAPER_FORMATS.indexOf(String(ov.format))===-1) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'paperFormat 只能是 both|md|tex'}
|
|
2762
|
+
if(ov&&ov.editor&&!paperEditorOk(ov.editor)) return {ok:false,code:'V4_INVALID_ARGUMENT',message:'paperEditor 只能是 office 或 resident:<id>'}
|
|
2763
|
+
return await paperStart('manual',{force:!!force,lang:ov&&ov.lang,format:ov&&ov.format,editor:ov&&ov.editor})
|
|
2764
|
+
}
|
|
2765
|
+
/** A paper wake's reply. `kind` is read from wakeKind (set by wakeResident, not cleared here). */
|
|
2766
|
+
async function onPaperWakeEnd(r,parsed){
|
|
2767
|
+
const ps=paperState; if(!ps) return
|
|
2768
|
+
const kind=String(wakeKind.get(r.rId)||'')
|
|
2769
|
+
// A paper contribution may arrive as a plain string or as `{markdown:"…"}` — anything else is
|
|
2770
|
+
// NOT accepted (a stray object must never become "[object Object]" in the final draft).
|
|
2771
|
+
const pick=v=>{ if(typeof v==='string') return v; if(v&&typeof v==='object'&&typeof v.markdown==='string') return v.markdown; return '' }
|
|
2772
|
+
if(kind===PAPER_KINDS.part){
|
|
2773
|
+
let text=pick(parsed&&parsed.paperPart).trim()
|
|
2774
|
+
if(!text) text=String((await readText(ps.dir+'/parts/'+r.rId+'.md'))||'').trim()
|
|
2775
|
+
if(text){ ps.parts[r.rId]={text,at:now(),source:'reply'}; await paperWriteRel(ps.dir+'/parts/'+r.rId+'.md',text)
|
|
2776
|
+
paperLog(r.rId+' 提交了自己负责的部分('+text.length+' 字)') }
|
|
2777
|
+
else paperLog('警告:'+r.rId+' 的部分为空(将按超时/重试处理)')
|
|
2778
|
+
} else if(kind===PAPER_KINDS.review){
|
|
2779
|
+
const rv=(parsed&&parsed.paperReview&&typeof parsed.paperReview==='object')?parsed.paperReview:{}
|
|
2780
|
+
const reviewed=String(rv.reviewed||'')
|
|
2781
|
+
const others=ps.participants.filter(x=>x!==r.rId)
|
|
2782
|
+
if(others.indexOf(reviewed)===-1){
|
|
2783
|
+
paperLog('拒绝 '+r.rId+' 的互审:未指名其他参与者(reviewed='+JSON.stringify(rv.reviewed===undefined?null:rv.reviewed)+')——必须审阅至少一位他人')
|
|
2784
|
+
} else {
|
|
2785
|
+
const points=Array.isArray(rv.points)?rv.points.map(String).filter(Boolean):[]
|
|
2786
|
+
ps.reviews[r.rId]={reviewed,points,improvements:String(rv.improvements||''),at:now(),accepted:true}
|
|
2787
|
+
paperLog(r.rId+' 审阅了 '+reviewed+' 的部分:'+points.length+' 条意见'+(rv.improvements?',并给出改进建议':'') )
|
|
2788
|
+
await paperWriteRel(ps.dir+'/reviews/'+r.rId+'.md',['# 互审记录|'+r.rId,'','- 审阅对象:'+reviewed,'- 意见:','',
|
|
2789
|
+
...points.map(p=>'- '+p),'','## 改进建议','',String(rv.improvements||'(无)')].join('\n')+'\n')
|
|
2790
|
+
}
|
|
2791
|
+
} else if(kind===PAPER_KINDS.final){
|
|
2792
|
+
let text=pick(parsed&&parsed.paperFinal).trim()
|
|
2793
|
+
if(!text) text=String((await readText(ps.dir+'/final.md'))||'').trim()
|
|
2794
|
+
if(text&&!/^#\s*互审记录/.test(text)){ ps.finalDraft=text; await paperWriteRel(ps.dir+'/final.md',text); paperLog('定稿代表 '+(ps.editorId||ps.cfg.editor)+' 提交了最终稿('+text.length+' 字)') }
|
|
2795
|
+
else paperLog('警告:定稿代表返回空稿,改由框架(office)定稿')
|
|
2796
|
+
} else if(kind===PAPER_KINDS.vote||kind===PAPER_KINDS.revise){
|
|
2797
|
+
const d=parsed?parsed.paperDeliverable:undefined
|
|
2798
|
+
if(typeof d!=='boolean'){ paperLog('警告:'+r.rId+' 未明确表态"可交付/不可交付"(将按未表态处理)'); }
|
|
2799
|
+
else { ps.deliverable[r.rId]=d
|
|
2800
|
+
paperLog(r.rId+(d?' 表示:可交付':' 表示:不可交付')+(parsed&&parsed.reason?('|理由:'+String(parsed.reason).slice(0,160)):'')) }
|
|
2801
|
+
}
|
|
2802
|
+
await paperWriteLog()
|
|
2803
|
+
await paperAdvance('resident-end:'+kind)
|
|
2804
|
+
}
|
|
2805
|
+
|
|
1825
2806
|
// ---- controls ----
|
|
1826
2807
|
async function start({problem,residentCount,seedDirections}){
|
|
1827
2808
|
await loadSettings()
|
|
@@ -1843,6 +2824,10 @@ export function apply(ctx) {
|
|
|
1843
2824
|
// carrying a previous run's records over would let a stale `passed` open the new gate.
|
|
1844
2825
|
formal={}; formalTodos=[]
|
|
1845
2826
|
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
|
|
2827
|
+
paperState=null // a fresh run gets a fresh paper flow (its durable record lives in Paper/<run id>/)
|
|
2828
|
+
// Re-base the artifact counter on the cards already on disk, so the auto-sync meeting counts
|
|
2829
|
+
// artifacts written by THIS run (the project tree may already hold cards from an earlier run).
|
|
2830
|
+
artifactBaseline=null; artifactCount=0; await syncArtifactCount()
|
|
1846
2831
|
lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
|
|
1847
2832
|
const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
|
|
1848
2833
|
pendingSpawns=[] // a fresh run replaces the roster: nothing is queued from before
|
|
@@ -1883,7 +2868,15 @@ export function apply(ctx) {
|
|
|
1883
2868
|
// If the persisted State came from a DIFFERENT process (crash/restart), the saved
|
|
1884
2869
|
// childIds are stale; clear them so residents re-spawn (their libraries persist on
|
|
1885
2870
|
// disk and re-seed the resumed run). Same-process pause→resume keeps continuable ids.
|
|
2871
|
+
//
|
|
2872
|
+
// `persistedEpoch` is only ever READ here, so before this fix a same-process pause→resume
|
|
2873
|
+
// compared this process's random epoch against the epoch saved by ITSELF and always concluded
|
|
2874
|
+
// "cross-process": every resident was re-spawned, `busy`/`wakeKind` were thrown away while the
|
|
2875
|
+
// OLD children were still running (`resume()` never interrupts them, so the late
|
|
2876
|
+
// `subagent/end` that the code's own comment warns about was guaranteed, not hypothetical).
|
|
2877
|
+
// Re-sync it on a same-process load, which is the contract `paused` already assumes.
|
|
1886
2878
|
const crossProcess = persistedEpoch !== processEpoch
|
|
2879
|
+
if(!crossProcess) persistedEpoch = processEpoch
|
|
1887
2880
|
if(crossProcess){ for(const [,r] of residents){ r.childId=''; r.status='brainstorm'; r.roundsSinceCompact=0 } }
|
|
1888
2881
|
// ANY re-spawn (cross-process OR a same-process abort that already cleared childIds) must get a FRESH
|
|
1889
2882
|
// coordination/concurrency state and a brainstorm phase. Otherwise: re-spawned brainstorm residents run
|
|
@@ -1913,10 +2906,20 @@ export function apply(ctx) {
|
|
|
1913
2906
|
pendingSpawns: pendingSpawns.length, hostChildLimit: (hostChildLimit===undefined?null:hostChildLimit),
|
|
1914
2907
|
meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
|
|
1915
2908
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
2909
|
+
// The coordination counters (`spoke k/N`, `voted k/N`, the verification round) used to live only
|
|
2910
|
+
// in `report()`: from `status()` alone an operator could not tell "the group is progressing"
|
|
2911
|
+
// from "the group is stuck at 2/4 votes", which is exactly how F1/F2/F7 stay invisible.
|
|
2912
|
+
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}
|
|
2913
|
+
:(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),
|
|
2914
|
+
artifactCount: artifactCount, artifactBaseline: artifactBaseline,
|
|
2915
|
+
paper: paperStatusSummary(),
|
|
1916
2916
|
// The Lean knobs and the per-object formal records are part of the readable status: without
|
|
1917
2917
|
// them a `require`-mode run that keeps returning 未定论 would be undiagnosable from outside.
|
|
1918
2918
|
formal: formalView(),
|
|
1919
|
-
|
|
2919
|
+
// The parameter NAMES (not just the rendered string) so a caller — the `/v4 set` handler in
|
|
2920
|
+
// particular — can reject an unknown key instead of silently dropping it.
|
|
2921
|
+
paramsKeys: Object.keys(params),
|
|
2922
|
+
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny','formalVerify','leanCommand','leanArgs','leanTimeoutMs','finalPaper','paperFormat','paperLanguage','paperCompilePdf','paperEditor','paperLatexCommand'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
1920
2923
|
function formalReportText(){
|
|
1921
2924
|
if(!formalOn()) return '- 未启用(`formalVerify` = off;可用 vibe_v4_set 切到 encourage / require)'
|
|
1922
2925
|
const v=formalView()
|
|
@@ -1933,6 +2936,7 @@ export function apply(ctx) {
|
|
|
1933
2936
|
pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
|
|
1934
2937
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
1935
2938
|
formal: formalView(),
|
|
2939
|
+
paper: paperStatusSummary(),
|
|
1936
2940
|
meetings:meetings.length, recentActivity: activityLog.slice(-8) } }
|
|
1937
2941
|
/** Human-readable mirror of the formal state (kept OUT of the JSON report shape). */
|
|
1938
2942
|
function formalReport(){ return {ok:true, formalMode:formalMode(), formalReport:'## Lean 形式化\n'+formalReportText(), formal:formalView()} }
|
|
@@ -1980,6 +2984,21 @@ export function apply(ctx) {
|
|
|
1980
2984
|
// Degrading to a STRONGER mode would let a typo silently gate every conclusion — the exact
|
|
1981
2985
|
// failure mode `require` is supposed to avoid.
|
|
1982
2986
|
if(k==='formalVerify') return FORMAL_MODES.indexOf(String(v))!==-1?String(v):'off'
|
|
2987
|
+
// ---- final paper (docs/final-paper.md §B): booleans and enums are coerced HERE, because `/v4 set` hands
|
|
2988
|
+
// the parameter layer RAW STRINGS ('false' must become false, not stay truthy) and an unknown
|
|
2989
|
+
// value must fall back to the default instead of silently keeping a bogus strong setting. ----
|
|
2990
|
+
if(k==='finalPaper'||k==='paperCompilePdf'){
|
|
2991
|
+
if(typeof v==='boolean') return v
|
|
2992
|
+
const s=String(v==null?'':v).trim().toLowerCase()
|
|
2993
|
+
if(s==='true'||s==='1'||s==='yes'||s==='on') return true
|
|
2994
|
+
if(s==='false'||s==='0'||s==='no'||s==='off') return false
|
|
2995
|
+
return DEFAULT_PARAMS[k]
|
|
2996
|
+
}
|
|
2997
|
+
if(k==='paperFormat') return PAPER_FORMATS.indexOf(String(v))!==-1?String(v):DEFAULT_PARAMS.paperFormat
|
|
2998
|
+
if(k==='paperLanguage') return PAPER_LANGS.indexOf(String(v))!==-1?String(v):DEFAULT_PARAMS.paperLanguage
|
|
2999
|
+
// v2 §A5: `office` (the session root / human side) by default, or `resident:<id>`.
|
|
3000
|
+
if(k==='paperEditor'){ const s=String(v==null?'':v).trim(); return paperEditorOk(s)?s:DEFAULT_PARAMS.paperEditor }
|
|
3001
|
+
if(k==='paperLatexCommand'){ const s=String(v==null?'':v).trim(); return s }
|
|
1983
3002
|
// A blank command would make resolveExecutable('') fail confusingly; fall back to the default.
|
|
1984
3003
|
if(k==='leanCommand'){ const s=String(v==null?'':v).trim(); return s||'lean' }
|
|
1985
3004
|
// A non-positive timeout is meaningless (the run would be killed instantly) → default.
|
|
@@ -2003,9 +3022,30 @@ export function apply(ctx) {
|
|
|
2003
3022
|
if(min===undefined) return n
|
|
2004
3023
|
return n < min ? min : n
|
|
2005
3024
|
}
|
|
2006
|
-
|
|
3025
|
+
/**
|
|
3026
|
+
* Apply a parameter update. Keys the parameter layer does not know are NOT silently dropped: the
|
|
3027
|
+
* caller gets them back (`ok:false` + `ignored`), because `{ok:true}` for a call that changed
|
|
3028
|
+
* nothing is the "declared but not received" silent failure AUDIT-CHECKLIST §1.9 names.
|
|
3029
|
+
*/
|
|
3030
|
+
function setParams(upd){
|
|
3031
|
+
const keys=Object.keys(upd||{})
|
|
3032
|
+
// `k in params` 会沿原型链判真:`toString` / `constructor` / `__proto__` 这类键会被当成"已知参数"
|
|
3033
|
+
// 写进 params(`params['__proto__']=…` 还会真的改掉 params 的原型),而 schema 的
|
|
3034
|
+
// additionalProperties:false 在 v4 上**不产生运行期拒绝**(裸 tools.register,宿主不校验实参)。
|
|
3035
|
+
// 已知参数一律以**自有属性**为准(AUDIT-CHECKLIST §1.9 的"声明=接收"只能在自有键上成立)。
|
|
3036
|
+
const has = (o,k)=>Object.prototype.hasOwnProperty.call(o,k)
|
|
3037
|
+
const ignored=keys.filter(k=>!has(params,k))
|
|
3038
|
+
for(const k of keys){ if(has(params,k)){ const nv=normalizeParam(k, upd[k]); params[k]= (typeof nv==='number') ? clampInt(k, nv) : nv } }
|
|
3039
|
+
saveSettings().catch(()=>{})
|
|
3040
|
+
if(ignored.length){
|
|
3041
|
+
logActivity('set','忽略未知参数:'+ignored.join(', '))
|
|
3042
|
+
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))}
|
|
3043
|
+
}
|
|
3044
|
+
return {ok:true,applied:keys}
|
|
3045
|
+
}
|
|
2007
3046
|
// ---- create / configure (no auto-start) + settings-file persistence ----
|
|
2008
|
-
|
|
3047
|
+
// 同一把守护:State/settings.json 是用户可编辑的,原型链上的键名(toString/__proto__)不是参数。
|
|
3048
|
+
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
3049
|
async function saveSettings(){ await writeJson('State/settings.json', params) }
|
|
2010
3050
|
// Create/configure a project and set params/problem WITHOUT starting any resident.
|
|
2011
3051
|
// The intended flow: vibe_v4_configure {project?, problem?, params?} → vibe_v4_start {}.
|
|
@@ -2030,6 +3070,7 @@ export function apply(ctx) {
|
|
|
2030
3070
|
// can ever clear those marks). resume()/start() re-initialize anyway; this keeps status truthful
|
|
2031
3071
|
// between abort and the next action.
|
|
2032
3072
|
meetingState=null; verifyState=null; pendingMeeting=null; pendingVerify=[]; busy=new Set(); wakeKind=new Map(); currentResident=''; finalizeLock=null
|
|
3073
|
+
paperState=null // an aborted run is over: its paper flow (if any) must not keep driving wakes
|
|
2033
3074
|
await saveAll(); return {ok:true,message:'aborted'} }
|
|
2034
3075
|
function setPause(){ clearHeartbeat(); running=false; return {ok:true,message:'paused'} }
|
|
2035
3076
|
|
|
@@ -2041,10 +3082,30 @@ export function apply(ctx) {
|
|
|
2041
3082
|
rememberAgent, forgetAgent,
|
|
2042
3083
|
setPause, initAbort, postMessage, startMeeting, saveAll, broadcast, configure, loadSettings,
|
|
2043
3084
|
currentResident:()=>currentResident,
|
|
3085
|
+
// Final paper (docs/final-paper.md): the session API used by the `/v4 paper` command and by
|
|
3086
|
+
// the suites. `paperStatus` is the same view `status()`/`report()` expose.
|
|
3087
|
+
runPaperCommand, paperStatus:()=>paperStatusSummary(),
|
|
3088
|
+
/** Does this session own the given child id? (used by the `subagent/start` capture) */
|
|
3089
|
+
ownsChild,
|
|
3090
|
+
/** Stash a live Agent seen at `subagent/start` time; claimed by `spawnResident` on resolve. */
|
|
3091
|
+
stashStartAgent,
|
|
2044
3092
|
// safety kick: drive one scheduler pass (used when an end handler errored, so an exceptional
|
|
2045
3093
|
// turn can never leave the group with no end-event and no heartbeat to continue it)
|
|
2046
3094
|
nudge:()=>scheduleNext().catch(()=>{}),
|
|
2047
|
-
|
|
3095
|
+
/**
|
|
3096
|
+
* Which resident is calling a resident-facing tool?
|
|
3097
|
+
* 1. the caller's subagent id matched against a resident's childId — the exact answer;
|
|
3098
|
+
* 2. the last-woken resident, but ONLY when no caller identity exists at all (the mock/harness
|
|
3099
|
+
* path, where `exec.agent` is undefined).
|
|
3100
|
+
* An agent that IS present but matches no resident is NOT guessed at: attributing a host call to
|
|
3101
|
+
* whichever resident happened to be woken last writes into that resident's private library with
|
|
3102
|
+
* no indication that the attribution was invented (AUDIT-CHECKLIST §0.2: identity is never
|
|
3103
|
+
* inferred from global mutable state). Such a call now fails explicitly with 'no such resident'.
|
|
3104
|
+
*/
|
|
3105
|
+
residentIdOf:(agent)=>{ const m=residentOfAgent(agent); if(m) return m
|
|
3106
|
+
let hasId=false; try { hasId=!!(agent&&agent.id) } catch(e){ hasId=false }
|
|
3107
|
+
if(hasId) return ''
|
|
3108
|
+
const c=currentResident; return (c && residents.has(c)) ? c : '' },
|
|
2048
3109
|
useResident:(id)=>{ currentResident=id },
|
|
2049
3110
|
publishProgress, recordProposition, recordMethod, recordSubproblem, listResidents, reportContext,
|
|
2050
3111
|
proposeTask, claimTask, taskDone, listTasks,
|
|
@@ -2071,6 +3132,12 @@ export function apply(ctx) {
|
|
|
2071
3132
|
// `vs` mirrors the live verification state ({targetId,targetType,targetOwner,stage,history,verdicts});
|
|
2072
3133
|
// pass one explicitly to ask "what WOULD the voters read for this object right now?".
|
|
2073
3134
|
verify:(rId,vs)=>{ const r=residents.get(String(rId)); return r?verifyPrompt(r, vs||verifyState||{targetId:'',targetType:'proposition',targetOwner:'',stage:'independent',verdicts:{}}):'' },
|
|
3135
|
+
// The meeting prompt is the ONLY group-chat / task-allocation / stop-vote surface this preset
|
|
3136
|
+
// has, so a host (or the audit corpus) must be able to READ it. Pass `st` to ask "what would
|
|
3137
|
+
// the next speaker read right now?"; the live meeting state is the default.
|
|
3138
|
+
meeting:(rId,st)=>{ const r=residents.get(String(rId)); if(!r) return ''
|
|
3139
|
+
const s=st||meetingState||{agenda:'(无进行中的会议)',type:'general',inputs:{},order:[]}
|
|
3140
|
+
return meetingPrompt(r,s) },
|
|
2074
3141
|
formalBlock:(target)=>formalPromptBlock(target),
|
|
2075
3142
|
formalWorkLine:()=>formalWorkLine(),
|
|
2076
3143
|
},
|
|
@@ -2081,9 +3148,15 @@ export function apply(ctx) {
|
|
|
2081
3148
|
if(which==='brainstorm') return brainstormPrompt(r)
|
|
2082
3149
|
if(which==='heartbeat') return heartbeatPrompt(r)
|
|
2083
3150
|
if(which==='coreRules') return coreRulesBrief()
|
|
3151
|
+
if(which==='meeting') return (arg&&arg.meeting)?meetingPrompt(r,arg.meeting):(meetingState?meetingPrompt(r,meetingState):meetingPrompt(r,{agenda:'(无进行中的会议)',type:'general',inputs:{},order:[]}))
|
|
2084
3152
|
if(which==='verify'){
|
|
2085
3153
|
const target=idSafe(String((arg&&arg.target)||''))
|
|
2086
|
-
|
|
3154
|
+
// F14 的同一条尺子:前缀(p-/m-/s-)就是对象归属的契约。此前的兜底把**任何**认不出的 id
|
|
3155
|
+
// 都当成 proposition,于是这个"读真实提示词"的工具会展示一条带着错误对象类型的投票提示词
|
|
3156
|
+
// (而真正入队的入口 maybeQueueVerify 已经拒绝这种 id)。认不出就如实拒绝,不猜。
|
|
3157
|
+
const tt=guessTargetType(target)
|
|
3158
|
+
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'
|
|
3159
|
+
return verifyPrompt(r,{targetId:target,targetType:tt,targetOwner:'',stage:String((arg&&arg.stage)||'independent'),history:{},verdicts:{}})
|
|
2087
3160
|
}
|
|
2088
3161
|
return normalPrompt(r)
|
|
2089
3162
|
},
|
|
@@ -2117,7 +3190,7 @@ export function apply(ctx) {
|
|
|
2117
3190
|
// The exact text a resident would receive. "成员读到的文字就是产品" (AUDIT-CHECKLIST §0.1): a host
|
|
2118
3191
|
// (or an audit) must be able to READ the prompt, not just the tool return values, or a prompt
|
|
2119
3192
|
// 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)=>{
|
|
3193
|
+
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
3194
|
const which=String(a.which||'normal')
|
|
2122
3195
|
if(which==='coreRules') return {ok:true,which,text:await s.promptApi.coreRules()}
|
|
2123
3196
|
const text=await s.promptFor(which,String(a.member||'r-1'),a)
|
|
@@ -2135,7 +3208,7 @@ export function apply(ctx) {
|
|
|
2135
3208
|
// MODE is dynamic — switching it changes the very next prompt), leanCommand/leanArgs select the
|
|
2136
3209
|
// executable, leanTimeoutMs bounds one run. Invalid values fall back to the defaults and an
|
|
2137
3210
|
// unknown mode degrades to 'off' (never to a STRONGER mode).
|
|
2138
|
-
registerTool('vibe_v4_set','Set V4 parameters
|
|
3211
|
+
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; finalPaper (default true) writes the run\'s final PAPER automatically once the closing meeting votes unanimously to stop, paperFormat: "both" (default) | "md" | "tex" decides which text versions are produced, paperLanguage: "zh" (default) | "en", paperCompilePdf (default true) compiles a PDF when a LaTeX engine is actually detected (xelatex|latexmk|pdflatex|lualatex|tectonic, overridable via paperLatexCommand), paperEditor: "office" (default — the session root/human side) | "resident:<id>" names who finalises the merged draft after the cross-review round. 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'},finalPaper:{type:'boolean'},paperFormat:{type:'string',enum:['both','md','tex']},paperLanguage:{type:'string',enum:['zh','en']},paperCompilePdf:{type:'boolean'},paperEditor:{type:'string'},paperLatexCommand:{type:'string'}}),(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
3212
|
// resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
|
|
2140
3213
|
// fall back to the last-woken resident when called by the host/assistant.
|
|
2141
3214
|
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 +3225,16 @@ export function apply(ctx) {
|
|
|
2152
3225
|
registerTool('vibe_v4_list_tasks','(resident) List open tasks.',objParams({}),(s)=>({ok:true,tasks:s.listTasks()}))
|
|
2153
3226
|
// context / compact (resident reports its context usage so the framework can /compact-equivalent)
|
|
2154
3227
|
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
|
-
|
|
3228
|
+
// NOT IMPLEMENTED — and the description says so. These two tools were registered with
|
|
3229
|
+
// "Reserved: shared-file write lock", which reads as "the lock exists and works": a model following
|
|
3230
|
+
// the v3 convention called `claim_write`, got `{ok:true}` and concluded it held a lock that no code
|
|
3231
|
+
// anywhere implements (this preset has no shared file a resident writes — see the write-scope rule
|
|
3232
|
+
// in `contextBrief`, which restricts every resident to its OWN Progress/Propos/Methods/Subproblems
|
|
3233
|
+
// library, while the framework alone writes Shared/ and State/). They stay registered (removing them
|
|
3234
|
+
// would change the model-visible tool surface) and keep echoing the key, but they now tell the
|
|
3235
|
+
// truth: nothing is reserved, nothing is serialized, write only your own files.
|
|
3236
|
+
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'}))
|
|
3237
|
+
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
3238
|
|
|
2158
3239
|
// ── Lean formal verification (docs/formal-verification.md §5) ─────────────
|
|
2159
3240
|
// These three tools are registered UNCONDITIONALLY. Registration is STATIC (a mode-dependent
|
|
@@ -2174,13 +3255,16 @@ export function apply(ctx) {
|
|
|
2174
3255
|
// registration belongs to this fiber and must be unwound with it.
|
|
2175
3256
|
ctx.effect(() => commands.register({
|
|
2176
3257
|
name:'v4', description:'control the Vibe Math V4 framework',
|
|
2177
|
-
input:{hint:'[configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|members|add|remove|set]'},
|
|
3258
|
+
input:{hint:'[configure|start|resume|pause|abort|status|report|message <to|all> <content>|meeting|paper [lang=zh|en] [format=md|tex|both] [editor=office|resident:<id>] [force]|members|add|remove|set]'},
|
|
2178
3259
|
handler: async function(inv){
|
|
2179
3260
|
const s=getSession(inv&&inv.agent); if(!s) return {kind:'success',text:JSON.stringify({ok:false,error:'no session'})}
|
|
2180
3261
|
const line=String(inv&&inv.rawInput?inv.rawInput:'').trim(); const parts=line.split(/\s+/); const cmd=parts[0]||''; const rest=parts.slice(1)
|
|
2181
3262
|
let r
|
|
2182
3263
|
if(cmd==='configure') r=await s.configure({project:rest[0]||'', problem:parts.slice(2).join(' ')})
|
|
2183
|
-
|
|
3264
|
+
// `/v4 start <problem>` is the natural way to try this out and the tool it mirrors accepts a
|
|
3265
|
+
// problem, so the arguments are honoured instead of silently dropped (a bare `/v4 start` keeps
|
|
3266
|
+
// using the configured problem).
|
|
3267
|
+
else if(cmd==='start') r=await s.start(rest.length?{problem:rest.join(' ')}:{})
|
|
2184
3268
|
else if(cmd==='resume') r=await s.resume()
|
|
2185
3269
|
else if(cmd==='pause') r=s.setPause()
|
|
2186
3270
|
else if(cmd==='abort') r=await s.initAbort()
|
|
@@ -2191,9 +3275,40 @@ export function apply(ctx) {
|
|
|
2191
3275
|
else if(cmd==='members') r={ok:true,residents:s.listResidents()}
|
|
2192
3276
|
else if(cmd==='add') r=await s.addMember(rest.join(' '))
|
|
2193
3277
|
else if(cmd==='remove') r=await s.removeMember(rest[0]||'')
|
|
2194
|
-
else if(cmd==='set'){
|
|
2195
|
-
|
|
2196
|
-
|
|
3278
|
+
else if(cmd==='set'){
|
|
3279
|
+
// Keys are validated HERE and the value is handed to the same `normalizeParam` the tool path
|
|
3280
|
+
// uses, so `/v4 set` cannot (a) coerce `provider=123` into a number for a string-typed key,
|
|
3281
|
+
// or (b) report success for a typo that `setParams` would silently ignore.
|
|
3282
|
+
const upd={}
|
|
3283
|
+
const known=s.status().paramsKeys||[]
|
|
3284
|
+
for(const tok of rest){
|
|
3285
|
+
const eq=tok.indexOf('='); if(eq<=0) continue
|
|
3286
|
+
const k=tok.slice(0,eq)
|
|
3287
|
+
if(known.indexOf(k)<0){ r={ok:false,message:'unknown parameter: '+k+'(用 /v4 status 查看可调参数)'}; break }
|
|
3288
|
+
upd[k]=tok.slice(eq+1)
|
|
3289
|
+
}
|
|
3290
|
+
if(r===undefined){
|
|
3291
|
+
// Apply, then answer with the SAME shape the `vibe_v4_set` TOOL answers with (the current
|
|
3292
|
+
// status + the applied/ignored bookkeeping): reporting only the raw setParams result would
|
|
3293
|
+
// make the slash command a different, less useful surface than the tool it mirrors.
|
|
3294
|
+
const res=s.setParams(upd)
|
|
3295
|
+
const st=s.status()
|
|
3296
|
+
r=(res&&res.ok===false)?Object.assign({},st,{ok:false,warning:res.message,ignored:res.ignored}):Object.assign({},st,{applied:res.applied})
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
else if(cmd==='paper'){
|
|
3300
|
+
// Manual trigger (docs/final-paper.md §2). `lang=` / `format=` / `editor=` override THIS invocation only;
|
|
3301
|
+
// a bare `force` rewrites an already-finalized paper. An unknown token becomes an unknown
|
|
3302
|
+
// OPTION and is rejected by runPaperCommand → kind:'error' (never a silent success).
|
|
3303
|
+
const o={}; let force=false
|
|
3304
|
+
for(const tok of rest){ const eq=tok.indexOf('='); if(eq>0) o[tok.slice(0,eq)]=tok.slice(eq+1); else if(tok==='force') force=true; else if(tok) o[tok]=true }
|
|
3305
|
+
r=await s.runPaperCommand(o,force)
|
|
3306
|
+
}
|
|
3307
|
+
else r={ok:false,usage:'configure|start [problem]|resume|pause|abort|status|report|message <to|all> <content>|meeting|paper [lang=zh|en] [format=md|tex|both] [editor=office|resident:<id>] [force]|members|add|remove|set <k=v>...'}
|
|
3308
|
+
// `kind:'error'` on a rejected business action, exactly as v2/v3 do: every branch above used to
|
|
3309
|
+
// report `success`, so the human's only non-tool control surface read as success on failure.
|
|
3310
|
+
const failed = !r || r.ok === false || (r.ok === undefined && !!r.error)
|
|
3311
|
+
return {kind: failed?'error':'success', text:JSON.stringify(r,null,2)}
|
|
2197
3312
|
},
|
|
2198
3313
|
}))
|
|
2199
3314
|
|
|
@@ -2202,13 +3317,36 @@ export function apply(ctx) {
|
|
|
2202
3317
|
// registry (dsh-subagent:1231 dispose -> dsh-agent:508 store.delete ->
|
|
2203
3318
|
// dsh-subagent:1241 settle/emit), so an end-time `agents.get(childId)` can never
|
|
2204
3319
|
// resolve. See `liveAgents` in the session body.
|
|
3320
|
+
//
|
|
3321
|
+
// DO NOT key this handler on `childOwner`: the host emits `subagent/start` INSIDE
|
|
3322
|
+
// `startContinuable`'s await (dsh-subagent/lib/index.js:1116 → `observer.start` → emit at `:306`,
|
|
3323
|
+
// and the direct `emit('subagent/start', …)` at `:279`), i.e. BEFORE that call resolves — and
|
|
3324
|
+
// `spawnResident` can only learn `started.childId` after the resolve. Keying on it made this
|
|
3325
|
+
// handler return for EVERY child, so `liveAgents` stayed empty for the whole run and the real
|
|
3326
|
+
// `/compact` path (`liveAgentOf` → `if(!agent || !agent.session) return`) was dead code.
|
|
3327
|
+
//
|
|
3328
|
+
// Ownership is therefore resolved the other way round: the handler finds the (usually single)
|
|
3329
|
+
// session whose spawn window is open and STASHES the Agent under the child id from the payload;
|
|
3330
|
+
// `spawnResident` claims that stash the moment its call resolves. For a host that embeds the spawn
|
|
3331
|
+
// label in the id, `ownsChild` can be decided from the payload alone — that path is used when no
|
|
3332
|
+
// spawn window is open, so a `subagent/start` for a child we did not spawn is never recorded.
|
|
2205
3333
|
ctx.on('subagent/start', function(info){
|
|
2206
3334
|
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
3335
|
let agent
|
|
2210
3336
|
try { agent = agents.get(info.id) } catch(e){ agent = undefined }
|
|
2211
|
-
if(agent)
|
|
3337
|
+
if(!agent) return
|
|
3338
|
+
let s
|
|
3339
|
+
try {
|
|
3340
|
+
for(const [,ss] of sessions){
|
|
3341
|
+
const open=ss.ownsChild && ss.ownsChild(info.id)
|
|
3342
|
+
if(open){ s=ss; break }
|
|
3343
|
+
}
|
|
3344
|
+
if(!s){ const sid=childOwner.get(info.id); if(sid!==undefined) s=sessions.get(sid) }
|
|
3345
|
+
} catch(e){ s=undefined }
|
|
3346
|
+
if(s){ s.rememberAgent(info.id, agent); return }
|
|
3347
|
+
// No owner yet — the spawn this child belongs to is still inside `startContinuable`. Stash it for
|
|
3348
|
+
// the first session whose spawn window is open, so the claim after the resolve can find it.
|
|
3349
|
+
for(const [,ss] of sessions){ if(ss.stashStartAgent){ ss.stashStartAgent(info.id, agent); return } }
|
|
2212
3350
|
})
|
|
2213
3351
|
|
|
2214
3352
|
ctx.on('subagent/end', function(info){
|