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
|
@@ -64,6 +64,25 @@ export function apply(ctx) {
|
|
|
64
64
|
// ================= per-session registry =================
|
|
65
65
|
const sessions = new Map() // rootAgentId -> Session
|
|
66
66
|
const childOwner = new Map() // childId -> rootAgentId (route subagent/end back to its session)
|
|
67
|
+
// childOwner 的**有界化**(审计 L1)。这条映射只在"这个 child 还可能再发 subagent/end"时有价值,
|
|
68
|
+
// 但(与 v2 同一结论)不能在事件回调里立即回收:辩论/续轮会在同一个 child 上再次 end,丢了映射这次
|
|
69
|
+
// 事件就没人路由,verdict 收口随之失效。所以改为**引用 + 宽限期**:仍被任务或 agentRegistry 引用的
|
|
70
|
+
// 一律保留,没有引用且距最近一次 end 已超过宽限期才回收。
|
|
71
|
+
const CHILD_OWNER_GRACE_MS = 5 * 60 * 1000
|
|
72
|
+
const lastChildEndAt = new Map() // childId -> 最近一次 subagent/end 的时间
|
|
73
|
+
function pruneChildOwner() {
|
|
74
|
+
const keep = new Set()
|
|
75
|
+
for (const s of sessions.values()) {
|
|
76
|
+
try { for (const id of s.referencedChildIds()) keep.add(id) } catch (e) { /* 尽力而为 */ }
|
|
77
|
+
}
|
|
78
|
+
const cutoff = now() - CHILD_OWNER_GRACE_MS
|
|
79
|
+
for (const cid of Array.from(childOwner.keys())) {
|
|
80
|
+
if (keep.has(cid)) continue
|
|
81
|
+
const ended = lastChildEndAt.get(cid)
|
|
82
|
+
if (ended === undefined || ended > cutoff) continue
|
|
83
|
+
childOwner.delete(cid); lastChildEndAt.delete(cid)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
67
86
|
const fileOwner = {} // 进程级写锁:fileKey -> { childId, sessionId, at } —— 防任何代理(跨会话)并发写同一 md 文件
|
|
68
87
|
// Process epoch: PROCESS-level (one per apply, shared by every session), written to
|
|
69
88
|
// State/process_epoch.json at init; a DIFFERENT persisted epoch means a previous DSH
|
|
@@ -71,6 +90,11 @@ export function apply(ctx) {
|
|
|
71
90
|
// same-process pause→resume (children may still be alive). Kept at apply level so two
|
|
72
91
|
// sessions in one process never treat each other as a stale previous process.
|
|
73
92
|
const processEpoch = String(Date.now()) + '-' + Math.random().toString(36).slice(2, 8)
|
|
93
|
+
// 项目锁 LEASE 的续租轮询间隔(apply 级定时器用,见 apply() 末尾):它必须**明显快于**续租判据
|
|
94
|
+
// (projectLockTimeoutMs/4),否则"多久检查一次"会成为租约年龄的下界——实测 2500ms 轮询配 625ms
|
|
95
|
+
// 判据时最坏年龄仍有 2467ms,几乎顶到 2500ms 的租约上限。250ms 轮询下最坏租约年龄 ≈
|
|
96
|
+
// max(250ms, projectLockTimeoutMs/4),即上限的 1/4 再加一次轮询的抖动。
|
|
97
|
+
const LOCK_POLL_MS = 250
|
|
74
98
|
|
|
75
99
|
function sessionIdOf(agent) { try { return (agent && agent.id) ? String(agent.id) : undefined } catch (e) { return undefined } }
|
|
76
100
|
// Walk up the durable session lineage to the top-level (root) agent of this session,
|
|
@@ -111,7 +135,7 @@ export function apply(ctx) {
|
|
|
111
135
|
directionsPerSolver: 1, // directions shown per solver prompt (1 = own only)
|
|
112
136
|
verifierCount: 3, // independent reviewers per verification
|
|
113
137
|
debateMaxRounds: 5, // debate round cap
|
|
114
|
-
verdictMode: 'forced', // flat = 均衡(0.5) | forced =
|
|
138
|
+
verdictMode: 'forced', // flat = 均衡(0.5) | forced = 对每票**等权**取均值(每票按自己报出的概率贡献;严格 1/0 是绝对投票,其影响通过数值本身拉向端点)。**不再**按"历史准确率"加权:那个统计量统计的是"与本批裁决的一致度",没有后续真值可纠正它。两者都先做近共识判定(同侧且均值≥0.85/≤0.15取均值)
|
|
115
139
|
provider: '',
|
|
116
140
|
model: '',
|
|
117
141
|
solverPersona: '',
|
|
@@ -143,7 +167,7 @@ export function apply(ctx) {
|
|
|
143
167
|
plannerEnabled: true, // false = 完全走内置启发式(规划代理禁用)
|
|
144
168
|
plannerProvider: '', // 规划代理模型 provider(空 = 继承)
|
|
145
169
|
plannerModel: '', // 规划代理模型 id(空 = 继承)
|
|
146
|
-
planMinIntervalMs: 30000, //
|
|
170
|
+
planMinIntervalMs: 30000, // 每一次规划调用的最小间隔(含空计划与「空闲但有工作」)
|
|
147
171
|
plannerMaxFails: 3, // 规划代理连续失败达此值 → 自动降级启发式
|
|
148
172
|
methodKeepIntervalMs: 0, // Method Keeper 定时整理间隔(0 = 事件驱动)
|
|
149
173
|
methodKeepEvery: 5, // 每积累 N 个待沉淀发明/新命题触发一次整理
|
|
@@ -155,12 +179,26 @@ export function apply(ctx) {
|
|
|
155
179
|
leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
|
|
156
180
|
leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配 leanCommand='lake')
|
|
157
181
|
leanTimeoutMs: 120000, // 单次 Lean 运行超时上限(毫秒,正整数)
|
|
182
|
+
// ---- 最终论文(规格:docs/final-paper.md;v2/v3 为单作者变体)----
|
|
183
|
+
finalPaper: true, // 收口时是否自动撰写最终论文(false 只关自动触发;/vibe paper 仍可用)
|
|
184
|
+
paperFormat: 'both', // both = md + tex | md = 只写 markdown | tex = 只写 latex
|
|
185
|
+
paperLanguage: 'zh', // zh = 中文(ctexart/xelatex 优先)| en = 英文(article/pdflatex 优先)
|
|
186
|
+
paperCompilePdf: true, // 检测到 LaTeX 时是否编译 paper.pdf
|
|
187
|
+
paperLatexCommand: '', // 指定 LaTeX 引擎(空 = 按语言探测 xelatex/latexmk/pdflatex/lualatex/tectonic)
|
|
158
188
|
}
|
|
159
189
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
160
190
|
let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导(防漂移,同 v2)
|
|
161
191
|
let agentRegistry = {}
|
|
162
192
|
let decisionQueue = []
|
|
163
193
|
let verifierAccuracy = {}
|
|
194
|
+
// 验证者票数统计的保留上限(审计 L1):它按**一次性的 childId** 记,每次验证都新增键,
|
|
195
|
+
// 长跑进程里无界增长并整表落盘。它已经不再参与任何加权(见 finalVerdict 的 M8 说明),
|
|
196
|
+
// 只是审计轨迹,所以按插入顺序保留最近 N 条即可。
|
|
197
|
+
const VERIFIER_ACC_KEEP = 200
|
|
198
|
+
function pruneVerifierAccuracy() {
|
|
199
|
+
const keys = Object.keys(verifierAccuracy)
|
|
200
|
+
for (let i = 0; i < keys.length - VERIFIER_ACC_KEEP; i++) delete verifierAccuracy[keys[i]]
|
|
201
|
+
}
|
|
164
202
|
let tasks = {} // verify tasks keyed by 'verify:<rId>'
|
|
165
203
|
let activityLog = []
|
|
166
204
|
let lastReportWrite = 0
|
|
@@ -223,9 +261,9 @@ export function apply(ctx) {
|
|
|
223
261
|
{ name: 'maxParallelThreshold', type: 'integer', description: '全局最大并发子代理轮数(新派发前须满足 active < 阈值)', suggestion: 4 },
|
|
224
262
|
{ name: 'solverMaxRounds', type: 'integer', description: '每个求解方向的最大迭代轮数', suggestion: 3 },
|
|
225
263
|
{ name: 'directionsPerSolver', type: 'integer', description: '每个 solver 提示词附带的其他活跃方向摘要数量:1 = 只看自己方向', suggestion: 1 },
|
|
226
|
-
{ name: 'verifierCount', type: 'integer', description: '
|
|
264
|
+
{ name: 'verifierCount', type: 'integer', description: '每个验证对象的独立验证器数量(实际下界为 2:少于 2 份独立评审一律不定论,见 minVotes)', suggestion: 3 },
|
|
227
265
|
{ name: 'debateMaxRounds', type: 'integer', description: '验证辩论(交流群)最大轮数', suggestion: 5 },
|
|
228
|
-
{ name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: '裁决模式:flat =
|
|
266
|
+
{ name: 'verdictMode', type: 'enum', options: ['flat', 'forced'], description: '裁决模式:flat = 均衡机制(分歧时 0.5);forced = 强制裁决(对每票等权取均值;严格 1/0 是绝对投票,其影响通过数值本身拉向端点。**不按"历史准确率"加权**——该统计量统计的是与本批裁决的一致度,没有后续真值可纠正);两者都先做近共识判定(同侧且均值≥0.85/≤0.15取均值,修复 v2 flat 误判)', suggestion: 'forced' },
|
|
229
267
|
{ name: 'provider', type: 'string', description: '子代理模型 provider(空 = 继承根代理)', suggestion: '' },
|
|
230
268
|
{ name: 'model', type: 'string', description: '子代理模型 id(空 = 继承根代理)', suggestion: '' },
|
|
231
269
|
{ name: 'solverPersona', type: 'string', description: '注入每个求解器提示词开头的人格/要求', suggestion: '' },
|
|
@@ -256,7 +294,7 @@ export function apply(ctx) {
|
|
|
256
294
|
{ name: 'plannerEnabled', type: 'boolean', description: 'false = 完全走内置启发式调度(规划代理禁用)', suggestion: true },
|
|
257
295
|
{ name: 'plannerProvider', type: 'string', description: '规划代理模型 provider(空 = 继承根代理)', suggestion: '' },
|
|
258
296
|
{ name: 'plannerModel', type: 'string', description: '规划代理模型 id(空 = 继承根代理)', suggestion: '' },
|
|
259
|
-
{ name: 'planMinIntervalMs', type: 'integer', description: '
|
|
297
|
+
{ name: 'planMinIntervalMs', type: 'integer', description: '每一次规划调用的最小间隔(毫秒);对**每一次**规划调用都生效(含空计划、含「空闲但仍有工作」,不再有空闲绕过)', suggestion: 30000 },
|
|
260
298
|
{ name: 'plannerMaxFails', type: 'integer', description: '规划代理连续失败达此值 → 自动降级启发式', suggestion: 3 },
|
|
261
299
|
{ name: 'methodKeepIntervalMs', type: 'integer', description: 'Method Keeper 定时整理间隔(0 = 事件驱动)', suggestion: 0 },
|
|
262
300
|
{ name: 'methodKeepEvery', type: 'integer', description: '每积累 N 个待沉淀发明/新命题触发一次整理', suggestion: 5 },
|
|
@@ -267,6 +305,11 @@ export function apply(ctx) {
|
|
|
267
305
|
{ name: 'leanCommand', type: 'string', description: 'Lean 可执行文件(例:lean / lake;配合 leanArgs=[env,lean] 用 lake)', suggestion: 'lean' },
|
|
268
306
|
{ name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加参数', suggestion: [] },
|
|
269
307
|
{ name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,非正数回退默认)', suggestion: 120000 },
|
|
308
|
+
{ name: 'finalPaper', type: 'boolean', description: '收口(checkTermination 的完整收口分支:无未解决问题、无待验证对象、无任务/计划/门)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已定论命题/解法/方法整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
|
|
309
|
+
{ name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md(跳过编译);tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
|
|
310
|
+
{ name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
|
|
311
|
+
{ name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志(不阻塞定稿)', suggestion: true },
|
|
312
|
+
{ name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。解析不到时按"未检测到"降级', suggestion: '' },
|
|
270
313
|
]
|
|
271
314
|
|
|
272
315
|
// ================= fs (adapted to DSH 0.1.1: resolve returns {targetKey, displayPath}) =================
|
|
@@ -345,7 +388,10 @@ export function apply(ctx) {
|
|
|
345
388
|
* 与 `Verified/Lean/`(归档证明,与定论卡片同处 Verified/,一眼可见"这条结论的证明在哪"),
|
|
346
389
|
* 以及**项目树之外**的全局可复用库 `<VibeMath 根>/Formal/{Lib,Proved}`(跨项目复用是核心收益)。
|
|
347
390
|
*/
|
|
348
|
-
|
|
391
|
+
// Progress_Logs/ 必须和别的骨架目录**一起**建出来(审计 L8):vibe_math_report 会写
|
|
392
|
+
// `Progress_Logs/report.json`,README 也把它列为布局的一部分,但此前它只靠 writeText 的隐式
|
|
393
|
+
// mkdir 兜底——在宿主不支持删除/创建的路径上(runShell 失败)报告目录就时有时无。
|
|
394
|
+
async function ensureDirs() { const base = frameworkRoot(); const dirs = ['Problems', 'Progress', 'Progress_Logs', 'Propos', 'Methods', 'Verified/命题', 'Verified/问题', 'Verified/Lean', 'Formal', 'Reliable', 'Notes', 'Logs/Verification', 'Logs/Plans', 'State']; const paths = [vibeRoot() + '/Projects', vibeRoot() + '/Methods', vibeRoot() + '/Formal/Lib', vibeRoot() + '/Formal/Proved'].concat(dirs.map(function (d) { return base + '/' + d })); return await runShell(mkdirCmd(paths)) }
|
|
349
395
|
async function removeFile(rel) { const base = frameworkRoot(); return await runShell(rmCmd(base + '/' + rel)) }
|
|
350
396
|
|
|
351
397
|
// ================= settings =================
|
|
@@ -354,7 +400,7 @@ export function apply(ctx) {
|
|
|
354
400
|
const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'planningHorizon', 'planMinIntervalMs', 'plannerMaxFails', 'methodKeepIntervalMs', 'methodKeepEvery', 'projectLockTimeoutMs']
|
|
355
401
|
const numFields = ['promoteValueThreshold']
|
|
356
402
|
const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'leanArgs']
|
|
357
|
-
const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild']
|
|
403
|
+
const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild', 'finalPaper', 'paperCompilePdf']
|
|
358
404
|
for (const k of Object.keys(DEFAULT_PARAMS)) {
|
|
359
405
|
if (!(k in obj)) continue
|
|
360
406
|
const v = obj[k]
|
|
@@ -373,6 +419,10 @@ export function apply(ctx) {
|
|
|
373
419
|
else if (k === 'leanCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s || 'lean' }
|
|
374
420
|
else if (k === 'leanTimeoutMs') { const n = Number(v); out[k] = (Number.isFinite(n) && n > 0) ? Math.floor(n) : DEFAULT_PARAMS[k] }
|
|
375
421
|
else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
|
|
422
|
+
// 最终论文(spec v2 §B):布尔/枚举必须**显式归一化**,不允许未知键直通('false' 会保持真值)。
|
|
423
|
+
else if (k === 'paperFormat') { out[k] = (v === 'both' || v === 'md' || v === 'tex') ? v : DEFAULT_PARAMS[k] }
|
|
424
|
+
else if (k === 'paperLanguage') { out[k] = (v === 'zh' || v === 'en') ? v : DEFAULT_PARAMS[k] }
|
|
425
|
+
else if (k === 'paperLatexCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s }
|
|
376
426
|
else { out[k] = v }
|
|
377
427
|
}
|
|
378
428
|
return out
|
|
@@ -424,7 +474,11 @@ export function apply(ctx) {
|
|
|
424
474
|
// ================= md soft-spec helpers =================
|
|
425
475
|
// 软规范:对象 md 头部锚点行(唯一强制部分)+ 正文自由叙述。调度器只解析
|
|
426
476
|
// 头部锚点与条目标题行(### 解法/证明/证伪 N|标题|概率X|状态Y),从不解析正文散文。
|
|
427
|
-
function anchorLine(k, v) {
|
|
477
|
+
function anchorLine(k, v) {
|
|
478
|
+
// 锚点行是**逐行**格式,parseAnchors 的 `.*$` 不跨行(无 `s` 标志):值里含换行会让这一行被
|
|
479
|
+
// 劈成两行,第二行既不是锚点也会污染下一个锚点/段落的解析。compose 侧统一折成空格(审计 L6)。
|
|
480
|
+
return '- ' + k + ': ' + String(v == null ? '' : v).replace(/[\r\n]+/g, ' ')
|
|
481
|
+
}
|
|
428
482
|
// 解析条目标题行 + 其后正文,直到下一个 ### / ## 标题。返回 [{heading, text}]
|
|
429
483
|
/**
|
|
430
484
|
* 按顺序切出正文里的所有 `## ` 顶层段。用于"无损往返":compose 只重新生成自己管理的
|
|
@@ -460,6 +514,12 @@ export function apply(ctx) {
|
|
|
460
514
|
lines.push(anchorLine('依赖', JSON.stringify(p.依赖 || [])))
|
|
461
515
|
lines.push(anchorLine('被依赖', JSON.stringify(p.被依赖 || [])))
|
|
462
516
|
lines.push(anchorLine('来源', p.来源 || '原始'))
|
|
517
|
+
// 关联锚点(H4):这两行此前**只读不写** ⇒ 任何一次重载(resume / vibe_math_index /
|
|
518
|
+
// set_project / 进程重启都会按文件名重新解析卡片)之后,判断问题与它的临时假设命题
|
|
519
|
+
// 全部失联:processStatusUpdates 的"收口僵尸判断问题"分支、settleVerdict 的"回写源命题"
|
|
520
|
+
// 分支、Verified 卡的来源字段都只在同进程内有效。写出来才是真正的持久关联。
|
|
521
|
+
if (p.判断命题) lines.push(anchorLine('判断命题', p.判断命题))
|
|
522
|
+
if (p.来源命题) lines.push(anchorLine('来源命题', p.来源命题))
|
|
463
523
|
lines.push(anchorLine('计划', p.计划 || ''))
|
|
464
524
|
lines.push('')
|
|
465
525
|
lines.push('## 陈述')
|
|
@@ -471,7 +531,7 @@ export function apply(ctx) {
|
|
|
471
531
|
if (sols.length === 0) lines.push('(暂无解法候选)')
|
|
472
532
|
else for (let i = 0; i < sols.length; i++) {
|
|
473
533
|
const s = sols[i]
|
|
474
|
-
lines.push('### 解法 ' + (i + 1) + '|' + (s.title || '解法' + (i + 1)) + '|概率' + (s.prob != null ? s.prob : 0.5) + '|状态' + (s.status || '未定论'))
|
|
534
|
+
lines.push('### 解法 ' + (i + 1) + '|' + escField(s.title || '解法' + (i + 1)) + '|概率' + probText(s.prob != null ? s.prob : 0.5) + '|状态' + escField(s.status || '未定论'))
|
|
475
535
|
lines.push(s.text || '')
|
|
476
536
|
lines.push('')
|
|
477
537
|
}
|
|
@@ -480,7 +540,7 @@ export function apply(ctx) {
|
|
|
480
540
|
function parseProblemMd(id, text) {
|
|
481
541
|
const { head, body } = splitHeader(text)
|
|
482
542
|
const a = parseAnchors(head)
|
|
483
|
-
const sols = parseEntries(body,
|
|
543
|
+
const sols = parseEntries(body, entryRe('解法')).map(function (e) { return { title: unescField(e.title), prob: clamp01(e.prob), status: unescField(e.status), text: e.text } })
|
|
484
544
|
return {
|
|
485
545
|
id: id, 标题: a['标题'] || id, 状态: a['状态'] || '求解中',
|
|
486
546
|
优先级: (a['优先级'] === 'never' ? 'never' : (Number(a['优先级']) || 1)),
|
|
@@ -505,6 +565,10 @@ export function apply(ctx) {
|
|
|
505
565
|
if (formalAnchorLine(p.id)) lines.push(anchorLine('形式化', formalAnchorLine(p.id)))
|
|
506
566
|
lines.push(anchorLine('优先级', p.优先级 == null ? 1 : p.优先级))
|
|
507
567
|
lines.push(anchorLine('依赖', JSON.stringify(p.依赖 || [])))
|
|
568
|
+
// 来源锚点(H4):与问题卡的 判断命题/来源命题 对称——不写盘的话,重载后
|
|
569
|
+
// 「引理来自哪个问题/方向」丢失,Verified 命题卡的 `- 来源:` 也会永久为空。
|
|
570
|
+
if (p.来源问题) lines.push(anchorLine('来源问题', p.来源问题))
|
|
571
|
+
if (p.来源方向) lines.push(anchorLine('来源方向', p.来源方向))
|
|
508
572
|
if (p.价值关键性 != null) lines.push(anchorLine('价值/关键性', p.价值关键性))
|
|
509
573
|
lines.push('')
|
|
510
574
|
lines.push('## 陈述')
|
|
@@ -515,7 +579,7 @@ export function apply(ctx) {
|
|
|
515
579
|
if (proofs.length === 0) lines.push('(暂无证明尝试)')
|
|
516
580
|
else for (let i = 0; i < proofs.length; i++) {
|
|
517
581
|
const s = proofs[i]
|
|
518
|
-
lines.push('### 证明 ' + (i + 1) + '|' + (s.title || '证明' + (i + 1)) + '|概率' + (s.prob != null ? s.prob : 0.5) + '|状态' + (s.status || '未定论'))
|
|
582
|
+
lines.push('### 证明 ' + (i + 1) + '|' + escField(s.title || '证明' + (i + 1)) + '|概率' + probText(s.prob != null ? s.prob : 0.5) + '|状态' + escField(s.status || '未定论'))
|
|
519
583
|
lines.push(s.text || '')
|
|
520
584
|
lines.push('')
|
|
521
585
|
}
|
|
@@ -524,7 +588,7 @@ export function apply(ctx) {
|
|
|
524
588
|
if (refutes.length === 0) lines.push('(暂无证伪尝试)')
|
|
525
589
|
else for (let i = 0; i < refutes.length; i++) {
|
|
526
590
|
const s = refutes[i]
|
|
527
|
-
lines.push('### 证伪 ' + (i + 1) + '|' + (s.title || '证伪' + (i + 1)) + '|概率' + (s.prob != null ? s.prob : 0.5) + '|状态' + (s.status || '未定论'))
|
|
591
|
+
lines.push('### 证伪 ' + (i + 1) + '|' + escField(s.title || '证伪' + (i + 1)) + '|概率' + probText(s.prob != null ? s.prob : 0.5) + '|状态' + escField(s.status || '未定论'))
|
|
528
592
|
lines.push(s.text || '')
|
|
529
593
|
lines.push('')
|
|
530
594
|
}
|
|
@@ -533,8 +597,8 @@ export function apply(ctx) {
|
|
|
533
597
|
function parsePropositionMd(id, text) {
|
|
534
598
|
const { head, body } = splitHeader(text)
|
|
535
599
|
const a = parseAnchors(head)
|
|
536
|
-
const proofs = parseEntries(body,
|
|
537
|
-
const refutes = parseEntries(body,
|
|
600
|
+
const proofs = parseEntries(body, entryRe('证明')).map(function (e) { return { title: unescField(e.title), prob: clamp01(e.prob), status: unescField(e.status), text: e.text } })
|
|
601
|
+
const refutes = parseEntries(body, entryRe('证伪')).map(function (e) { return { title: unescField(e.title), prob: clamp01(e.prob), status: unescField(e.status), text: e.text } })
|
|
538
602
|
return {
|
|
539
603
|
id: id, 标题: a['标题'] || id, 状态: a['状态'] || '未定论',
|
|
540
604
|
概率: clamp01(a['概率'] != null ? Number(a['概率']) : 0.5),
|
|
@@ -768,6 +832,8 @@ export function apply(ctx) {
|
|
|
768
832
|
function allProblems() { return Array.from(problems.values()) }
|
|
769
833
|
function allPropos() { return Array.from(propos.values()) }
|
|
770
834
|
function depResolved(id) {
|
|
835
|
+
// M9:对象键一律是 idSafe(id)(见各写入点),依赖是模型写的原始 id ⇒ 查表前必须走同一把归一化。
|
|
836
|
+
id = idSafe(id)
|
|
771
837
|
if (problems.has(id)) { const q = problems.get(id); return q.状态 === '已解决' || q.优先级 === 'never' }
|
|
772
838
|
if (propos.has(id)) { const p = propos.get(id); return p.概率 === 1 || p.概率 === 0 || p.优先级 === 'never' } // never = 主动弃权,视为依赖已满足,避免等待依赖死锁
|
|
773
839
|
return true // unknown dependency: treat as resolved (conservative)
|
|
@@ -804,7 +870,14 @@ export function apply(ctx) {
|
|
|
804
870
|
const va = await readJson('State/verifier_accuracy.json'); if (va) verifierAccuracy = va
|
|
805
871
|
const tk = await readJson('State/tasks.json'); if (tk) tasks = tk
|
|
806
872
|
const er = await readJson('State/explorer_retries.json'); if (er) explorerRetries = er
|
|
807
|
-
const pq = await readJson('State/plans.json')
|
|
873
|
+
const pq = await readJson('State/plans.json')
|
|
874
|
+
// 计划队列的**跨进程**语义(审计 L9):planQueue 是"本进程内跨 tick"的队列,里面的动作指的是
|
|
875
|
+
// 当时那个进程/那次运行的对象与状态。上一进程留下的队列在恢复时重放,轻则对已不存在的 target
|
|
876
|
+
// 空转,重则把过期计划的动作施加到新状态上。进程 epoch 是权威判据:只有本进程写下的队列才恢复。
|
|
877
|
+
if (pq && Array.isArray(pq.queued)) {
|
|
878
|
+
if (pq.epoch === processEpoch) planQueue = pq.queued
|
|
879
|
+
else if (pq.queued.length > 0) { planQueue = []; logActivity('plan', '丢弃上一进程留下的 ' + pq.queued.length + ' 条计划动作(跨进程重放会施加过期计划)') }
|
|
880
|
+
}
|
|
808
881
|
const ml = await readJson('State/method_log.json'); if (ml) methodLog = Object.assign({ pendingInventions: [], keepCount: 0, lastKeepAt: 0 }, ml)
|
|
809
882
|
const pl = await readJson('State/project_lock.json'); if (pl) projectLock = Object.assign({ sessionId: '', at: 0 }, pl)
|
|
810
883
|
const lp = await readJson('State/last_plan.json'); if (lp) lastPlanSummary = lp
|
|
@@ -825,7 +898,7 @@ export function apply(ctx) {
|
|
|
825
898
|
await writeJson('State/verifier_accuracy.json', verifierAccuracy)
|
|
826
899
|
await writeJson('State/tasks.json', tasks)
|
|
827
900
|
await writeJson('State/explorer_retries.json', explorerRetries)
|
|
828
|
-
await writeJson('State/plans.json', { queued: planQueue })
|
|
901
|
+
await writeJson('State/plans.json', { queued: planQueue, epoch: processEpoch })
|
|
829
902
|
await writeJson('State/method_log.json', methodLog)
|
|
830
903
|
await writeJson('State/project_lock.json', projectLock)
|
|
831
904
|
await writeJson('State/archived_journals.json', archivedJ)
|
|
@@ -927,7 +1000,10 @@ export function apply(ctx) {
|
|
|
927
1000
|
',问题 ' + report.problems.solved + '/' + report.problems.total + ' 已解决,命题 ' + report.propositions.resolved + '/' + report.propositions.total + ' 已定论,' +
|
|
928
1001
|
'活跃代理轮数=' + report.activeCount + ',待人工决策=' + report.pendingDecisions.length + ',待执行计划=' + report.queuedPlanActions + '。' +
|
|
929
1002
|
'请调用 vibe_math_report 汇总当前进展及各代理状态,并用人话简要汇报(不打断用户,简短即可)。'
|
|
930
|
-
|
|
1003
|
+
// 来源 kind 必须是**已声明**的:`MessageSourceMap` 是 merge-extensible 联合,但没有共享的
|
|
1004
|
+
// catch-all `plugin` kind(审计 L5/dsh-llm message.d.ts),{kind:'plugin'} 是契约外形状。
|
|
1005
|
+
// role 本来就是 'user',正文自带 "[Vibe Math V3] 进度更新" 的真署名,故用核心声明的 {kind:'user'}。
|
|
1006
|
+
rootAgent.followup({ id: uuid(), role: 'user', content: [textBlock(text)], source: { kind: 'user' } })
|
|
931
1007
|
lastPushReport = now()
|
|
932
1008
|
} catch (e) {
|
|
933
1009
|
console.error('vibe-math-v3: push report failed: ' + String((e && e.message) || e))
|
|
@@ -1053,7 +1129,8 @@ export function apply(ctx) {
|
|
|
1053
1129
|
function kcObjectModels() {
|
|
1054
1130
|
return '\n2) OBJECT MODELS(md 卡片,软规范:头部锚点行 + 正文自由叙述):\n' +
|
|
1055
1131
|
'- 问题卡 Problems/<id>.md:{ 标题, ID, 类型:问题, 状态:原始|求解中|等待依赖|已解决|死路, 优先级, 依赖:[], 被依赖:[], 来源:原始|后生, 计划(由调度器按规划代理的计划自动更新:一句话说明下一轮安排), ## 陈述(完整问题陈述,每个记号/对象都要完整定义), ## 来源与动机(后生问题:产生流程/动机/如何回填主线), ## 解法候选(### 解法 N|标题|概率X|状态Y + 叙述式完整解法)}。\n' +
|
|
1056
|
-
'- 命题卡 Propos/<分类>/<id>.md:{ 标题, ID, 类型:命题, 状态:未定论|已验证·真|已验证·假, 概率, 优先级, 依赖:[], ## 陈述(完整), ## 证明尝试(### 证明 N|…|概率X|状态Y), ## 证伪尝试(### 证伪 N|…|概率X|状态Y)}。\n' +
|
|
1132
|
+
'- 命题卡 Propos/<分类>/<id>.md:{ 标题, ID, 类型:命题, 状态:未定论|已验证·真|已验证·假, 概率, 优先级, 依赖:[], 价值/关键性, 来源问题, 来源方向, ## 陈述(完整), ## 证明尝试(### 证明 N|…|概率X|状态Y), ## 证伪尝试(### 证伪 N|…|概率X|状态Y)}。\n' +
|
|
1133
|
+
'- `价值/关键性 ∈ [0,1]`(M1):这条命题对**整个项目主线**有多关键——若它成立/被证伪,能改变多少后续方向。≥ promoteValueThreshold(默认 0.7)且仍未定论(概率∈(0,1))的命题会被自动晋升成「判断下述命题是否成立:…」问题,由求解器专门证明/证伪并把结果回写源命题。你不填就默认 0.5(永不晋升)。**只有你(写卡的代理)能设这个值**——请在你认为"这条引理/猜想值得单独立项"时显式写上。\n' +
|
|
1057
1134
|
'- 证明/证伪尝试语义:`## 证明尝试`=为证实而写的论证;`## 证伪尝试`=专门反驳/反例的论证。**失败的"找反例未果"/sanity check 是支持性证据,不属于证伪尝试**;不要写入 `## 证伪尝试`(否则系统会当作待验证的反驳去验证)。对仍未完成的证明/证伪,明确标注缺口而非伪装完成。\n' +
|
|
1058
1135
|
'- 方法卡 Methods/<id>.md:{ 标题, ID, 类型:方法, 状态:经验|应用验证|含已验证断言, 可信断言:[](只允许已进 Verified/ 的 ID), 上级体系/子方法/相关, 适用场景, ## 核心内容, ## 定义与记号, ## 应用记录, ## 改进历史 }。\n' +
|
|
1059
1136
|
'- 收口规则:某个解法/证明/证伪 概率=1 → 问题已解决 / 命题已验证(状态/概率锚点由调度器改写)。\n'
|
|
@@ -1079,7 +1156,7 @@ export function apply(ctx) {
|
|
|
1079
1156
|
function kcSolverFiles() {
|
|
1080
1157
|
return '你的归属文件:\n' +
|
|
1081
1158
|
'- 求解器:把该方向的完整叙述(本轮进展/子路线/可行性信号/教训/完整解法文本)写进 `Progress/<问题id>/<方向id>.md`;聚合索引 `Progress/<问题id>.md` 由调度器维护,不要动它。\n' +
|
|
1082
|
-
'- 新引理:写一张完整命题卡到 `Propos/<分类>/<p-id>.md`,含锚点 `- 标题:`、`- ID
|
|
1159
|
+
'- 新引理:写一张完整命题卡到 `Propos/<分类>/<p-id>.md`,含锚点 `- 标题:`、`- ID/类型/状态/概率/优先级/价值关键性` 与 `## 陈述`;证明写进 `### 证明 1|标题|概率X|状态Y` 段落(完整证明文本是验证必需,否则验证器只能验裸命题)。`- 价值/关键性: <0..1>` 表示这条引理对项目主线的关键程度(≥ 阈值会被自动晋升为独立问题),别省。\n'
|
|
1083
1160
|
}
|
|
1084
1161
|
// 方法整理代理专属:方法卡
|
|
1085
1162
|
function kcKeeperFiles() {
|
|
@@ -1207,7 +1284,7 @@ export function apply(ctx) {
|
|
|
1207
1284
|
head += '\nLEMMA RULES: every lemma you register MUST carry a complete proof in `lemmas[].proof` (and in the card\'s `## 证明尝试`). If a claim is only partly argued, do NOT register it as a finished lemma — either prove it fully or record it as an explicit gap/conjecture stating the missing step, so the verifier knows exactly what is (and is not) being claimed. Incomplete "lemmas" waste verification and can mislead.\n'
|
|
1208
1285
|
head += '\nOUTPUT CONTRACT — pick ONE channel. Write content into Markdown; only lightweight scheduling metadata (and verification-required proofs) cross the machine reply.\n' +
|
|
1209
1286
|
'CHANNEL A (recommended, you can write files): write the full round narrative into `Progress/' + q.id + '/' + dir.id + '.md` and each new lemma card into `Propos/<分类>/<id>.md`, then reply ONLY this metadata object:\n' +
|
|
1210
|
-
'{"meta":{"kind":"solver","qid":"' + q.id + '","dirId":"' + dir.id + '","round":' + round + ',"survival":0.5,"status":"continue|success|dead-end","dead_end_reason":"... or null","lemmas":[{"id":"p-...","title":"...","statement":"...","proof":"<完整证明文本,供验证器核验>","prob":0.6,"分类":"<引理卡目录名,必须与你要写入的 Propos/<分类>/ 目录严格一致>","优先级":1}],"methods_used":[{"id":"m-...","效果":"...","建议":"..."}],"new_inventions":[{"类型":"...","标题":"...","内容描述":"...","是否已入库":false}],"solution_prob":0.85,"solution_text":"<完整解法文本,或 null>","sub_questions":[{"q_sub_title":"...","q_sub_statement":"完整问题陈述(含所有对象/定义)","assumption_title":"p_{q-tmp} 标题","assumption_statement":"完整假设陈述(含所有定义)"}]}}\n' +
|
|
1287
|
+
'{"meta":{"kind":"solver","qid":"' + q.id + '","dirId":"' + dir.id + '","round":' + round + ',"survival":0.5,"status":"continue|success|dead-end","dead_end_reason":"... or null","lemmas":[{"id":"p-...","title":"...","statement":"...","proof":"<完整证明文本,供验证器核验>","prob":0.6,"价值/关键性":0.5,"分类":"<引理卡目录名,必须与你要写入的 Propos/<分类>/ 目录严格一致>","优先级":1}],"methods_used":[{"id":"m-...","效果":"...","建议":"..."}],"new_inventions":[{"类型":"...","标题":"...","内容描述":"...","是否已入库":false}],"solution_prob":0.85,"solution_text":"<完整解法文本,或 null>","sub_questions":[{"q_sub_title":"...","q_sub_statement":"完整问题陈述(含所有对象/定义)","assumption_title":"p_{q-tmp} 标题","assumption_statement":"完整假设陈述(含所有定义)"}]}}\n' +
|
|
1211
1288
|
'CHANNEL B (your file tools are unavailable): put the content you would have written into __writes and carry the same meta:\n' +
|
|
1212
1289
|
'{"__writes":[{"path":"Progress/' + q.id + '/' + dir.id + '.md","content":"<完整本轮叙述>"}],"meta":{"kind":"solver","qid":"' + q.id + '","dirId":"' + dir.id + '",...同上 meta 字段...}}\n' +
|
|
1213
1290
|
'区分规则:methods_used 只能填**已存在的方法卡 ID**(m-…,来自 AVAILABLE METHODS 列表)——引用你自己刚想出的新方法/新技巧不属于 methods_used,请如实填入 new_inventions(它会由 Method Keeper 蒸馏建卡);不要把方法名/标题当 id 填进 methods_used。'
|
|
@@ -1281,7 +1358,24 @@ export function apply(ctx) {
|
|
|
1281
1358
|
}
|
|
1282
1359
|
|
|
1283
1360
|
// ================= decisions (manual/auto) =================
|
|
1284
|
-
|
|
1361
|
+
// 已结清决策的保留条数(审计 L1):decisionQueue 以前只把 status 置 resolved、永不裁剪,
|
|
1362
|
+
// 且整表落盘到 State/decision_queue.json,长跑进程里它是第二处无界增长。保留最近 N 条已结清记录
|
|
1363
|
+
// 作为审计轨迹(list_decisions 只列 pending,所以裁剪不影响任何读取路径),pending 一律不裁。
|
|
1364
|
+
const RESOLVED_DECISION_KEEP = 50
|
|
1365
|
+
function pruneDecisionQueue() {
|
|
1366
|
+
let resolved = 0
|
|
1367
|
+
for (let i = 0; i < decisionQueue.length; i++) if (decisionQueue[i] && decisionQueue[i].status !== 'pending') resolved++
|
|
1368
|
+
let drop = resolved - RESOLVED_DECISION_KEEP
|
|
1369
|
+
if (drop <= 0) return
|
|
1370
|
+
const kept = []
|
|
1371
|
+
for (let i = 0; i < decisionQueue.length; i++) {
|
|
1372
|
+
const d = decisionQueue[i]
|
|
1373
|
+
if (d && d.status !== 'pending' && drop > 0) { drop--; continue }
|
|
1374
|
+
kept.push(d)
|
|
1375
|
+
}
|
|
1376
|
+
decisionQueue = kept
|
|
1377
|
+
}
|
|
1378
|
+
function enqueueDecision(node, contextText, data) { const d = { id: uuid(), node: node, context: contextText, data: data, status: 'pending', resolution: null, createdAt: now() }; decisionQueue.push(d); pruneDecisionQueue(); return d }
|
|
1285
1379
|
async function maybeGate(node, contextText, data, autoFn) { if (params.mode === 'auto') return await autoFn(data); const d = enqueueDecision(node, contextText, data); setGate(d, node); logActivity('gate', node + ': ' + contextText); await saveAll(); return { gated: true, decisionId: d.id } }
|
|
1286
1380
|
/**
|
|
1287
1381
|
* 设置唯一闸门,并把被它取代的旧决策**立即结清**。
|
|
@@ -1372,6 +1466,8 @@ export function apply(ctx) {
|
|
|
1372
1466
|
await reconcileVerify()
|
|
1373
1467
|
await executePlanQueue()
|
|
1374
1468
|
await maybePlan()
|
|
1469
|
+
// 方法整理排在终止判定之前:到期的方法整理是**真实工作**,先把它派出去,
|
|
1470
|
+
// 再让 checkTermination 看到"有在途的 method-keeper"从而不误判停机(H1 修复的配套)。
|
|
1375
1471
|
await maybeMethodKeepFallback()
|
|
1376
1472
|
await maybePushReport(false)
|
|
1377
1473
|
await maybeWriteReport(false)
|
|
@@ -1570,7 +1666,9 @@ export function apply(ctx) {
|
|
|
1570
1666
|
}
|
|
1571
1667
|
function verifyTaskBusy(rId) { if (tasks['verify:' + rId]) return true; return Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.role === 'verifier' && m.rId === rId }) }
|
|
1572
1668
|
async function createVerifyTask(c) {
|
|
1573
|
-
|
|
1669
|
+
// M9:验证 id 由对象 id 派生(`r-<对象id>[-sN]`),对象键已归一化 ⇒ rId 也归一化,
|
|
1670
|
+
// 否则原始 id 里的路径分隔符会被拼进 Logs/Verification/<rId>_<ts>.json 造出子目录。
|
|
1671
|
+
const rId = idSafe(c.rId)
|
|
1574
1672
|
if (verifyTaskBusy(rId)) return false
|
|
1575
1673
|
// require 门禁的**防空转**(契约 §2「对象留在原库,可形式化后再次提议」):已经被记为
|
|
1576
1674
|
// 「形式化待办」、而形式化又还没补齐的对象不再重复表决——重复表决只会一次又一次被同一道
|
|
@@ -1657,15 +1755,18 @@ export function apply(ctx) {
|
|
|
1657
1755
|
// 规划代理在途时不再重复调用
|
|
1658
1756
|
const plannerInFlight = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.role === 'planner' })
|
|
1659
1757
|
if (plannerInFlight) return
|
|
1660
|
-
// 有"可调度工作"= 有待解问题方向可推进 或 有验证候选 或
|
|
1661
|
-
//
|
|
1662
|
-
//
|
|
1758
|
+
// 有"可调度工作"= 有待解问题方向可推进 或 有验证候选 或 有**到期**的方法整理 或 有待执行计划。
|
|
1759
|
+
// H1 修复:这里必须用真正的到期判定 methodKeepDue(),而不是"待沉淀发明非空"。
|
|
1760
|
+
// 旧条件在 pendingInventions ∈ [1, methodKeepEvery-1] 时恒真,于是**没有任何工作**的每一
|
|
1761
|
+
// 个 tick 都派生一个 planner:空计划不留痕(lastPlanSummary 不写)、冷却又被"无在途工作"
|
|
1762
|
+
// 绕过、终止判定又被刚派出的 planner 挡掉 ⇒ 按 tick 计费的 planner 热循环永不终止。
|
|
1763
|
+
// 待沉淀发明到期时由 maybeMethodKeepFallback() 处理,不需要(也不应该)叫 planner。
|
|
1663
1764
|
const verifyWork = (await buildVerifyCandidates()).length > 0
|
|
1664
|
-
if (!hasSchedulableWork() && !verifyWork &&
|
|
1665
|
-
//
|
|
1765
|
+
if (!hasSchedulableWork() && !verifyWork && !methodKeepDue() && planQueue.length === 0) return
|
|
1766
|
+
// 冷却对**每一次**规划调用生效(含空计划):空计划同样是一次真实的模型调用,必须退避。
|
|
1767
|
+
// 旧代码用 hasInflight 跳过冷却,本意是"空闲且有工作时不空转",但那恰好是 H1 的放大器。
|
|
1666
1768
|
const cooldown = Number(params.planMinIntervalMs) || 0
|
|
1667
|
-
|
|
1668
|
-
if (cooldown > 0 && hasInflight && (now() - lastPlanAt) < cooldown) return
|
|
1769
|
+
if (cooldown > 0 && (now() - lastPlanAt) < cooldown) return
|
|
1669
1770
|
await callPlanner()
|
|
1670
1771
|
}
|
|
1671
1772
|
async function callPlanner() {
|
|
@@ -1742,7 +1843,7 @@ export function apply(ctx) {
|
|
|
1742
1843
|
if (act === 'spawn') {
|
|
1743
1844
|
const role = String(a.role || '')
|
|
1744
1845
|
if (role === 'explorer') {
|
|
1745
|
-
const q = problems.get(String(a.target || ''))
|
|
1846
|
+
const q = problems.get(idSafe(String(a.target || ''))) // M9:问题键已归一化
|
|
1746
1847
|
if (!q || q.状态 === '已解决' || q.优先级 === 'never') continue
|
|
1747
1848
|
const dirs = getDirState(q.id)
|
|
1748
1849
|
const allExhausted = dirs.length > 0 && dirs.every(function (d) { return d.status === 'dead-end' || d.status === 'success' })
|
|
@@ -1751,7 +1852,7 @@ export function apply(ctx) {
|
|
|
1751
1852
|
if (busy) continue
|
|
1752
1853
|
if ((explorerRetries[q.id] || 0) >= (Number(params.maxExplorerRetries) || 3)) continue
|
|
1753
1854
|
} else if (role === 'solver') {
|
|
1754
|
-
const q = problems.get(String(a.target || ''))
|
|
1855
|
+
const q = problems.get(idSafe(String(a.target || ''))) // M9:问题键已归一化
|
|
1755
1856
|
if (!q || q.状态 === '已解决' || q.优先级 === 'never' || q.状态 === '等待依赖') continue
|
|
1756
1857
|
const dir = (getDirState(q.id) || []).find(function (d) { return d.id === String(a.direction || '') })
|
|
1757
1858
|
if (!dir || dir.status !== 'active') continue
|
|
@@ -1759,11 +1860,13 @@ export function apply(ctx) {
|
|
|
1759
1860
|
const running = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.qid === q.id && m.direction === dir.id && m.role === 'solver' })
|
|
1760
1861
|
if (running) continue
|
|
1761
1862
|
} else if (role === 'verifier') {
|
|
1762
|
-
const rId = String(a.target || '')
|
|
1863
|
+
const rId = idSafe(String(a.target || '')) // M9:与 createVerifyTask 同一把归一化
|
|
1763
1864
|
if (!rId) continue
|
|
1764
1865
|
if (verifyTaskBusy(rId)) continue
|
|
1765
1866
|
const cands = await buildVerifyCandidates()
|
|
1766
|
-
if (!cands.some(function (c) { return c.rId === rId })) continue
|
|
1867
|
+
if (!cands.some(function (c) { return idSafe(c.rId) === rId })) continue
|
|
1868
|
+
out.push({ action: 'spawn', role: role, target: rId, direction: a.direction || '', reason: String(a.reason || '') })
|
|
1869
|
+
continue
|
|
1767
1870
|
} else if (role === 'method-keeper') {
|
|
1768
1871
|
// 无目标;允许(由代码兜底去重:一次只允许一个 method-keeper)
|
|
1769
1872
|
const running = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.role === 'method-keeper' })
|
|
@@ -1775,10 +1878,10 @@ export function apply(ctx) {
|
|
|
1775
1878
|
if (!agentRegistry[cid]) continue
|
|
1776
1879
|
out.push({ action: 'interrupt', childId: cid, reason: String(a.reason || '') })
|
|
1777
1880
|
} else if (act === 'promote') {
|
|
1778
|
-
const p = propos.get(String(a.target || ''))
|
|
1881
|
+
const p = propos.get(idSafe(String(a.target || ''))) // M9:命题键已归一化
|
|
1779
1882
|
if (!p || p.概率 === 1 || p.概率 === 0 || p.优先级 === 'never' || p.在问题清单) continue
|
|
1780
1883
|
if (Number(p.价值关键性) < Number(params.promoteValueThreshold)) continue
|
|
1781
|
-
out.push({ action: 'promote', target:
|
|
1884
|
+
out.push({ action: 'promote', target: p.id, reason: String(a.reason || '') })
|
|
1782
1885
|
} else if (act === 'wait' || act === 'continue' || act === 'stop') {
|
|
1783
1886
|
out.push(Object.assign({ action: act }, a.childId ? { childId: String(a.childId) } : {}, a.target ? { target: String(a.target) } : {}, { reason: String(a.reason || '') }))
|
|
1784
1887
|
}
|
|
@@ -1974,7 +2077,9 @@ export function apply(ctx) {
|
|
|
1974
2077
|
if (parsed.lessons) dir.lessons = (dir.lessons || []).concat(parsed.lessons)
|
|
1975
2078
|
if (parsed.blockers) dir.blockers = (dir.blockers || []).concat(parsed.blockers)
|
|
1976
2079
|
if (parsed.dead_end_reason) dir.dead_end_reason = parsed.dead_end_reason
|
|
2080
|
+
// M11:`survival_probability`(遗留扁平名)与 `survival`(提示词名)都接受。
|
|
1977
2081
|
if (typeof parsed.survival_probability === 'number') dir.survival = clamp01(parsed.survival_probability)
|
|
2082
|
+
else if (typeof parsed.survival === 'number') dir.survival = clamp01(parsed.survival)
|
|
1978
2083
|
if (parsed.lemmas && parsed.lemmas.length) { for (let i = 0; i < parsed.lemmas.length; i++) { const lid = await addLemmaAsProposition(qid, parsed.lemmas[i]); if (lid) { dir.lemmas = dir.lemmas || []; dir.lemmas.push({ id: lid, title: parsed.lemmas[i].title || '' }) } } }
|
|
1979
2084
|
if (parsed.sub_questions && parsed.sub_questions.length) { for (let i = 0; i < parsed.sub_questions.length; i++) { const sq = parsed.sub_questions[i]; if (sq && sq.q_sub_statement && dir.sub_questions && dir.sub_questions.some(function (x) { return x.statement === sq.q_sub_statement })) continue; const rec = await addSubQuestion(qid, dirId, sq); if (rec) { dir.sub_questions = dir.sub_questions || []; dir.sub_questions.push(rec) } } }
|
|
1980
2085
|
// journal narrative (论文式续写:把本轮叙述追加进研究日志)
|
|
@@ -1988,11 +2093,16 @@ export function apply(ctx) {
|
|
|
1988
2093
|
await consumeMethodFeedback(parsed, { qid: qid, dirId: dirId })
|
|
1989
2094
|
}
|
|
1990
2095
|
if (status === 'success') {
|
|
1991
|
-
|
|
2096
|
+
// M11:遗留扁平通道的字段名(solution / solution_probability)与提示词教的名字
|
|
2097
|
+
// (solution_text / solution_prob)不一致 ⇒ 严格按提示词回写的 solver 会掉进 else 分支,
|
|
2098
|
+
// 在轮次上限处被判成"claimed success without solution"并把方向写死。两套名字都接受。
|
|
2099
|
+
const flatSolution = (parsed && parsed.solution != null) ? parsed.solution : (parsed ? parsed.solution_text : undefined)
|
|
2100
|
+
const flatSolutionProb = (parsed && parsed.solution_probability != null) ? parsed.solution_probability : (parsed ? parsed.solution_prob : undefined)
|
|
2101
|
+
if (parsed && flatSolution) {
|
|
1992
2102
|
dir.status = 'success'
|
|
1993
2103
|
delete agentRegistry[childId]
|
|
1994
2104
|
logActivity('solver', qid + '/' + dirId + ' success at round ' + meta.round)
|
|
1995
|
-
await addSolution(qid,
|
|
2105
|
+
await addSolution(qid, flatSolution, flatSolutionProb)
|
|
1996
2106
|
} else {
|
|
1997
2107
|
if (meta.round >= params.solverMaxRounds) {
|
|
1998
2108
|
dir.status = 'dead-end'; dir.dead_end_reason = dir.dead_end_reason || 'claimed success without solution at iteration cap'
|
|
@@ -2031,12 +2141,14 @@ export function apply(ctx) {
|
|
|
2031
2141
|
function statusFromStop(stopReason) { return (stopReason === 'completed' || stopReason === 'max-tokens') ? 'continue' : 'dead-end' }
|
|
2032
2142
|
async function addLemmaAsProposition(qid, lemma) {
|
|
2033
2143
|
if (!lemma || !lemma.title) return
|
|
2034
|
-
|
|
2144
|
+
// M11:`prob`(提示词名)与 `布尔估计`(遗留扁平名)都接受;`分类` 与 `细类型` 同理。
|
|
2145
|
+
const rawProb = lemmaProb(lemma)
|
|
2146
|
+
let be = clamp01(rawProb != null ? rawProb : 0.6)
|
|
2035
2147
|
if (be >= 1) be = 0.99; else if (be <= 0) be = 0.01
|
|
2036
2148
|
const p = {
|
|
2037
2149
|
id: 'p-' + shortId(), 标题: lemma.title, 状态: '未定论', 概率: be,
|
|
2038
2150
|
优先级: (lemma.优先级 != null) ? lemma.优先级 : 1, 依赖: [], 价值关键性: clamp01(lemma['价值/关键性'] != null ? lemma['价值/关键性'] : 0.5),
|
|
2039
|
-
分类: categoryOf({ 分类:
|
|
2151
|
+
分类: categoryOf({ 分类: lemmaCategory(lemma) || '未分类' }),
|
|
2040
2152
|
陈述: lemma.statement || lemma.title,
|
|
2041
2153
|
proofs: [{ title: lemma.title + '(证明)', prob: clamp01(0.7), status: '未定论', text: lemma.proof || '' }],
|
|
2042
2154
|
refutes: [], 来源问题: qid, 在问题清单: false,
|
|
@@ -2101,21 +2213,23 @@ export function apply(ctx) {
|
|
|
2101
2213
|
if (Array.isArray(parsed.methods_used)) {
|
|
2102
2214
|
for (const mu of parsed.methods_used) {
|
|
2103
2215
|
if (!mu || !mu.id) continue
|
|
2104
|
-
|
|
2216
|
+
// M9:方法键一律是 idSafe(id),代理给的是原始 id ⇒ 查表前归一化。
|
|
2217
|
+
const muId = idSafe(mu.id)
|
|
2218
|
+
const m = methods.get(muId) || globalMethods.get(muId)
|
|
2105
2219
|
if (!m) {
|
|
2106
2220
|
// 未知 id:solver 引用了一个尚未入卡的方法/技巧 → 作为待沉淀发明记录,防引用丢失(Method Keeper 将据此建卡)
|
|
2107
|
-
methodLog.pendingInventions.push({ at: now(), 来源: ctx.qid ? ('问题 ' + ctx.qid + (ctx.dirId ? ' 方向 ' + ctx.dirId : '')) : '', 类型: '方法', 标题: String(
|
|
2108
|
-
logActivity('method', 'methods_used referenced unknown method ' +
|
|
2221
|
+
methodLog.pendingInventions.push({ at: now(), 来源: ctx.qid ? ('问题 ' + ctx.qid + (ctx.dirId ? ' 方向 ' + ctx.dirId : '')) : '', 类型: '方法', 标题: String(muId), 内容描述: (mu.效果 || '') + (mu.建议 ? ';建议:' + mu.建议 : '') })
|
|
2222
|
+
logActivity('method', 'methods_used referenced unknown method ' + muId + ' → queued as pending invention')
|
|
2109
2223
|
continue
|
|
2110
2224
|
}
|
|
2111
|
-
if (methods.has(
|
|
2225
|
+
if (methods.has(muId)) {
|
|
2112
2226
|
// 只记录"有实际问题/方向上下文"的引用;method-keeper 纯整理时的引用(qid/dirId 皆空)不当作应用,
|
|
2113
2227
|
// 避免把"整理时引用该方法"误记为"实际应用",从而污染应用计数并触发错误的全局晋升。
|
|
2114
2228
|
if (ctx.dirId || ctx.qid) {
|
|
2115
2229
|
m.applications = m.applications || []
|
|
2116
2230
|
m.applications.push({ at: fmtTime(), 问题: ctx.qid || '', 方向: ctx.dirId || '', text: (mu.效果 || '') + (mu.建议 ? ';建议:' + mu.建议 : '') })
|
|
2117
2231
|
await saveMethod(m, false)
|
|
2118
|
-
logActivity('method', 'application record appended to ' +
|
|
2232
|
+
logActivity('method', 'application record appended to ' + muId + ' (问题 ' + ctx.qid + ' 方向 ' + ctx.dirId + ')')
|
|
2119
2233
|
}
|
|
2120
2234
|
}
|
|
2121
2235
|
}
|
|
@@ -2871,7 +2985,15 @@ export function apply(ctx) {
|
|
|
2871
2985
|
const id = formalId(target)
|
|
2872
2986
|
if (!id) return { ok: false, code: 'V3_INVALID_ARGUMENT', message: 'defect 记录必须写明 target' }
|
|
2873
2987
|
const prev = formalOf(id)
|
|
2874
|
-
|
|
2988
|
+
// 撤回目标(审计 L2):只在**确实有过归档证明**时才撤回。此前 prev.proof 为空时也会拿
|
|
2989
|
+
// 'Verified/Lean/<id>.lean' 去撤,而 withdrawArchivedProof 在文件删不掉/不存在时会**就地覆盖写**
|
|
2990
|
+
// ——于是一个从未有过证明的对象会在"所有人都来这里找证明"的目录里凭空多出一份撤回声明文件。
|
|
2991
|
+
let archived = String(prev.proof || '').trim()
|
|
2992
|
+
if (!archived) {
|
|
2993
|
+
const fallback = 'Verified/Lean/' + id + '.lean'
|
|
2994
|
+
const abs = leanAbsPath(fallback)
|
|
2995
|
+
if (abs !== null && (await readTextAbs(abs)) !== undefined) archived = fallback
|
|
2996
|
+
}
|
|
2875
2997
|
await putFormal(id, Object.assign({}, prev, {
|
|
2876
2998
|
status: 'attempted',
|
|
2877
2999
|
decision: 'defect',
|
|
@@ -2880,7 +3002,7 @@ export function apply(ctx) {
|
|
|
2880
3002
|
updatedAt: now(),
|
|
2881
3003
|
}))
|
|
2882
3004
|
// 归档证明必须消失,否则 Verified/Lean/ 里会留下一份"看起来已通过"的不忠实代码。
|
|
2883
|
-
const withdrawn = await withdrawArchivedProof(archived)
|
|
3005
|
+
const withdrawn = archived ? await withdrawArchivedProof(archived) : 'none'
|
|
2884
3006
|
// 待办条目按 id 去重、就地刷新:note 进入 TODO.md 的 why 列(require 档据此搁置定论)。
|
|
2885
3007
|
const list = formalTodo()
|
|
2886
3008
|
const i = list.findIndex(function (x) { return x && x.id === id })
|
|
@@ -2892,12 +3014,13 @@ export function apply(ctx) {
|
|
|
2892
3014
|
await rebuildLeanLibIndexes()
|
|
2893
3015
|
await saveAll()
|
|
2894
3016
|
// 如实说出**哪一种**撤回发生了:删除成功 / 就地覆盖 / 两者都失败(后者必须让人手动处理,
|
|
2895
|
-
// 否则一份不忠实的代码会静静留在"证明"的路径上而无人知晓)。
|
|
3017
|
+
// 否则一份不忠实的代码会静静留在"证明"的路径上而无人知晓)。'none' = 本来就没有归档证明。
|
|
2896
3018
|
await formalAnnounce('【形式化】' + who + ' 认定 ' + id + ' 的 Lean 形式化存在**忠实性缺陷**:' + note
|
|
2897
3019
|
+ '。这不是"命题为假",而是**形式化不合格**:已撤回其「已通过」状态(降级为 attempted)、'
|
|
2898
|
-
+ (withdrawn === '
|
|
2899
|
-
: withdrawn === '
|
|
2900
|
-
: '归档证明 ' + archived + '
|
|
3020
|
+
+ (withdrawn === 'none' ? '该对象此前没有任何归档证明,无需撤回'
|
|
3021
|
+
: withdrawn === 'deleted' ? '删除归档证明 ' + archived
|
|
3022
|
+
: withdrawn === 'overwritten' ? '归档证明 ' + archived + ' 无法删除(宿主不支持删除),已**就地覆盖为撤回声明**'
|
|
3023
|
+
: '归档证明 ' + archived + ' **未能撤回**(宿主删除与覆盖均失败,请手动删除,不要把它当作该对象的证明)')
|
|
2901
3024
|
+ '、写入 Formal/TODO.md;'
|
|
2902
3025
|
+ (formalMode() === 'require'
|
|
2903
3026
|
? '本次裁定**不定论**,修正形式化并重新跑通(vibe_math_lean_archive kind=\'proof\')后再投票。'
|
|
@@ -2952,6 +3075,26 @@ export function apply(ctx) {
|
|
|
2952
3075
|
}
|
|
2953
3076
|
|
|
2954
3077
|
// ================= verification (验证器) =================
|
|
3078
|
+
/**
|
|
3079
|
+
* 一个判决对象**至少**需要几票。
|
|
3080
|
+
*
|
|
3081
|
+
* H5:此前没有任何下限——`maxParallelThreshold=1` 时 backfillVerifiers 只派出 1 个验证器,
|
|
3082
|
+
* 它一结束 `allReported`(只看"已存在的孩子是否都报了")即为真,`consensus` 对单元素数组
|
|
3083
|
+
* 恒真,于是跳过全部辩论直接 `finalVerdict → 1 → settleVerdict`,写出 `概率:1/已验证·真`
|
|
3084
|
+
* 并落 `Verified/`。`Verified/` 是所有提示词的"绝对可信"层,一票定论 = 一个验证器的偏见
|
|
3085
|
+
* 直接变成下游全部代理的"已知事实"。
|
|
3086
|
+
*
|
|
3087
|
+
* 下限取 max(2, expectedCount):verifierCount=1 不是"允许单票",只是"至少 2 个独立验证器"
|
|
3088
|
+
* (与 createVerifyTask 的 `Math.max(2, ...)` 一致)。
|
|
3089
|
+
*/
|
|
3090
|
+
function minVotes(t) { return Math.max(2, Number(t && t.expectedCount) || 0, Number(params.verifierCount) || 0) }
|
|
3091
|
+
function haveEnoughVotes(t) { return Object.keys(t.childResults).length >= minVotes(t) }
|
|
3092
|
+
/** 所有已派出的验证器都报了本轮的票,**且**票数达到下限——否则不得推进/裁决。 */
|
|
3093
|
+
function allReportedWithQuorum(t, round) {
|
|
3094
|
+
if (!t || !Array.isArray(t.children) || t.children.length === 0) return false
|
|
3095
|
+
if (t.children.length < Math.max(2, Number(t.expectedCount) || 0)) return false
|
|
3096
|
+
return t.children.every(function (cid) { const r = t.childResults[cid]; return r && r.round === round })
|
|
3097
|
+
}
|
|
2955
3098
|
function consensus(t) { const vs = Object.keys(t.childResults).map(function (cid) { return t.childResults[cid].Result }); if (vs.length === 0) return false; return vs.every(function (v) { return v === 1 }) || vs.every(function (v) { return v === 0 }) }
|
|
2956
3099
|
function buildTranscript(t) { const parts = []; const cids = Object.keys(t.childResults); for (let i = 0; i < cids.length; i++) { const r = t.childResults[cids[i]]; parts.push('Reviewer ' + i + ': Result=' + r.Result + ' Reason=' + r.Reason) } return parts.join('\n') }
|
|
2957
3100
|
async function handleVerifier(childId, meta, output, stopReason) {
|
|
@@ -2974,12 +3117,16 @@ export function apply(ctx) {
|
|
|
2974
3117
|
if (t.children.indexOf(childId) === -1) t.children.push(childId)
|
|
2975
3118
|
t.childResults[childId] = { Result: Result, Reason: Reason, round: meta.round }
|
|
2976
3119
|
delete agentRegistry[childId]
|
|
2977
|
-
|
|
2978
|
-
|
|
3120
|
+
// H5:推进/裁决的前置条件是"已凑齐下限票数且本轮都已回报",不是"目前存在的那几个孩子都报了"。
|
|
3121
|
+
// 未凑齐时保持 spawning,由 reconcileVerify 在后续 tick 里补派验证器(不裁决、不定论)。
|
|
3122
|
+
if (!allReportedWithQuorum(t, meta.round)) { await saveAll(); return }
|
|
2979
3123
|
await advanceVerification(t, meta.round)
|
|
2980
3124
|
await saveAll()
|
|
2981
3125
|
}
|
|
2982
3126
|
async function advanceVerification(t, round) {
|
|
3127
|
+
// H5 第二道闸门:票数不足下限时**不辩论、不裁决**,回到 spawning 等 reconcileVerify 补派。
|
|
3128
|
+
// 放在最前面:即便 handleVerifier 因为"孩子被删除"等边缘情形推进到这里,也绝不定论。
|
|
3129
|
+
if (!haveEnoughVotes(t)) { t.status = 'spawning'; return }
|
|
2983
3130
|
if (round < params.debateMaxRounds && !consensus(t) && t.children.length > 0) {
|
|
2984
3131
|
if (!scheduler.running) { t.status = 'paused'; return }
|
|
2985
3132
|
if (activeCount() >= params.maxParallelThreshold) { t.status = 'paused'; return }
|
|
@@ -3008,6 +3155,8 @@ export function apply(ctx) {
|
|
|
3008
3155
|
}
|
|
3009
3156
|
}
|
|
3010
3157
|
async function finalizeVerification(t) {
|
|
3158
|
+
// H5 最后一道闸门:票数不足下限时**决不**给出裁决(单票绝不结论)。
|
|
3159
|
+
if (!haveEnoughVotes(t)) { t.status = 'spawning'; return }
|
|
3011
3160
|
const verdict = finalVerdict(t)
|
|
3012
3161
|
if (params.mode === 'manual') {
|
|
3013
3162
|
const d = enqueueDecision('verdict', 'verdict for ' + t.rId + ' (debate finished) = ' + verdict, { rId: t.rId, verdict: verdict, task: JSON.parse(JSON.stringify(t)) })
|
|
@@ -3033,16 +3182,14 @@ export function apply(ctx) {
|
|
|
3033
3182
|
if (mean >= 0.85 || mean <= 0.15) return Math.max(0.01, Math.min(0.99, mean))
|
|
3034
3183
|
}
|
|
3035
3184
|
if (params.verdictMode === 'forced') {
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
}
|
|
3045
|
-
return den > 0 ? Math.max(0.01, Math.min(0.99, num / den)) : 0.5
|
|
3185
|
+
// M8:这里**不再**按 verifierAccuracy 加权。那份统计量统计的是"这一票与该次裁决是否一致",
|
|
3186
|
+
// 而裁决本身就是同一批票的聚合——即"与自己一致"(注:v 为加权均值时 `Result === v` 几乎恒假,
|
|
3187
|
+
// 于是它其实把"从众度"记成 0),且没有任何后续真值(Lean 通过 / 被 defect 撤回 / 命题被反证)
|
|
3188
|
+
// 会纠正它。用这样一个量去加权,放大而非抑制系统性偏见,与"按历史准确率加权"的宣称相反。
|
|
3189
|
+
// 改为**等权**取均值:每票按自己报出的概率贡献,本身就是该验证器的贝叶斯式估计;
|
|
3190
|
+
// 严格的 1/0 是绝对投票,其影响已通过数值本身(拉向端点)自然体现,无需人为加权。
|
|
3191
|
+
let sum = 0; for (let i = 0; i < rs.length; i++) sum += rs[i].Result
|
|
3192
|
+
return Math.max(0.01, Math.min(0.99, sum / rs.length))
|
|
3046
3193
|
}
|
|
3047
3194
|
return 0.5
|
|
3048
3195
|
}
|
|
@@ -3050,12 +3197,17 @@ export function apply(ctx) {
|
|
|
3050
3197
|
const v = clamp01(verdict)
|
|
3051
3198
|
const r = t.r
|
|
3052
3199
|
const cids = Object.keys(t.childResults)
|
|
3200
|
+
// 票数记录保留(Logs/Verification 与 State/verifier_accuracy.json 是审计轨迹),但该统计量
|
|
3201
|
+
// **不再**参与任何加权(见 finalVerdict 的 M8 说明)。这里记的是"该票的实际概率"而非布尔值:
|
|
3202
|
+
// 原来的 `Result === v` 记法在 v 为加权均值时几乎不可能相等,等于把"从众度"记成 0,
|
|
3203
|
+
// 与字段名 correct/total 的含义相去更远。
|
|
3053
3204
|
for (let i = 0; i < cids.length; i++) {
|
|
3054
3205
|
const acc = verifierAccuracy[cids[i]] || { correct: 0, total: 0 }
|
|
3055
3206
|
acc.total += 1
|
|
3056
|
-
if (t.childResults[cids[i]].Result === v) acc.correct += 1
|
|
3207
|
+
if (Number(t.childResults[cids[i]].Result) === v) acc.correct += 1
|
|
3057
3208
|
verifierAccuracy[cids[i]] = acc
|
|
3058
3209
|
}
|
|
3210
|
+
pruneVerifierAccuracy()
|
|
3059
3211
|
await writeJson('Logs/Verification/' + t.rId + '_' + Date.now() + '.json', { r: r, verdict: v, results: t.childResults, transcript: buildTranscript(t), history: t.history || [], at: now() })
|
|
3060
3212
|
// ── require 门禁(契约 §8)────────────────────────────────────────────────
|
|
3061
3213
|
// 判定为真(严格证明)或假(严格反驳)之前必须先有 `passed` 或 `blocked`。门禁放在**改
|
|
@@ -3152,13 +3304,493 @@ export function apply(ctx) {
|
|
|
3152
3304
|
return best
|
|
3153
3305
|
}
|
|
3154
3306
|
|
|
3307
|
+
// ==========================================================================================
|
|
3308
|
+
// 最终论文(规格:docs/final-paper.md)—— 会话侧(v3 数据来源:md 知识库)。
|
|
3309
|
+
//
|
|
3310
|
+
// 时序(修订 §A2/§A3):v3 的收口信号是 checkTermination() 的完整收口分支(无未解决问题、
|
|
3311
|
+
// `leftoverVerify` 为空——**这才是真实完整性判据**(§A4)、无真实在途代理、无任务/计划/挂起门)。
|
|
3312
|
+
// 撰写子代理必须在 `scheduler.running=false` 与 `releaseProjectLock()` **之前**派遣
|
|
3313
|
+
// (停止后 scheduleTick() 是空操作;提前释放项目锁后另一会话可能写同一棵树)。派不出去
|
|
3314
|
+
// (宿主激活上限 ACTIVATION_LIMIT_REACHED)→ 排队 + apply 级心跳重试 + 可见告警。
|
|
3315
|
+
// 落盘(§C/§D):md/tex 是文本;**paper.pdf 只能由编译器子进程生成**、插件只 stat 且**永不删除/覆盖**;
|
|
3316
|
+
// paper.meta.json 最后写(原子提交点);幂等 = run id + finalizedAt + 逐产物存在性(不用稳定哈希)。
|
|
3317
|
+
// ==========================================================================================
|
|
3318
|
+
let paperInFlight = '' // 正在撰写的 childId
|
|
3319
|
+
let paperInFlightAt = 0 // 派遣时刻(判定卡死窗口;force 与心跳都据它降级)
|
|
3320
|
+
let paperReaps = 0 // 本 run 内已回收卡死撰写者的次数(自动重派上限)
|
|
3321
|
+
const paperAbandoned = {} // childId -> true:已放弃的撰写者;它事后返回时输出被丢弃并留痕
|
|
3322
|
+
let paperPending = null // 激活上限排队重试:{ tries, retryAt, reason, limit, trigger, opts }
|
|
3323
|
+
const PAPER_RETRY_MS = 5000
|
|
3324
|
+
const PAPER_MAX_RETRIES = 12
|
|
3325
|
+
const PAPER_STALE_MS = 10 * 60 * 1000 // 撰写者卡死窗口:超过它,force 与心跳都会回收并重派(10 分钟)
|
|
3326
|
+
const PAPER_MAX_AUTO_REAPS = 2 // 心跳自动回收+重派的次数上限,之后交给 /vibe paper force
|
|
3327
|
+
const PAPER_LOCK_STALE_MS = 120000
|
|
3328
|
+
function activationLimitFrom(message) { const m = /active child limit:\s*(\d+)/.exec(String(message == null ? '' : message)); return m ? Number(m[1]) : undefined }
|
|
3329
|
+
function paperId(opts) { return paperDirId((opts && opts.id) || currentProject) }
|
|
3330
|
+
function paperDir(id) { return 'Paper/' + paperDirId(id) }
|
|
3331
|
+
function paperAbsDir(id) { return frameworkRoot() + '/' + paperDir(id) }
|
|
3332
|
+
function paperAuthorLine() { return 'Vibe Math V3(单作者:论文撰写子代理)· 项目 ' + currentProject }
|
|
3333
|
+
function paperOpts(opts) {
|
|
3334
|
+
const o = opts || {}
|
|
3335
|
+
const fmt = (o.format === 'md' || o.format === 'tex' || o.format === 'both') ? o.format : ((params.paperFormat === 'md' || params.paperFormat === 'tex') ? params.paperFormat : 'both')
|
|
3336
|
+
const lang = (o.lang === 'en' || o.lang === 'zh') ? o.lang : (params.paperLanguage === 'en' ? 'en' : 'zh')
|
|
3337
|
+
const compilePdf = (o.compilePdf === true || o.compilePdf === false) ? o.compilePdf : (params.paperCompilePdf !== false)
|
|
3338
|
+
return { format: fmt, lang: lang, compilePdf: compilePdf, force: o.force === true, id: o.id }
|
|
3339
|
+
}
|
|
3340
|
+
async function paperReadMeta(id) { const m = await readJson(paperDir(id) + '/paper.meta.json'); return (m && typeof m === 'object' && !Array.isArray(m)) ? m : undefined }
|
|
3341
|
+
async function paperPathExists(rel) { try { const t = await fsTarget(rel); return (await fs.stat(t)) !== undefined } catch (e) { return false } }
|
|
3342
|
+
async function paperAppendLog(id, tag, text) {
|
|
3343
|
+
const rel = paperDir(id) + '/paper.log.md'
|
|
3344
|
+
const prev = await readText(rel)
|
|
3345
|
+
const head = (prev === undefined || !String(prev).trim()) ? (paperLogHead(id).join('\n') + '\n') : String(prev)
|
|
3346
|
+
await writeText(rel, head + paperLogLine(now(), tag, text) + '\n')
|
|
3347
|
+
}
|
|
3348
|
+
/** paper 作用域锁(修订 §A3):v3 在收口时已释放项目锁,用它防止两个会话同时写同一棵 Paper/ 树。 */
|
|
3349
|
+
async function paperLockAcquire(id) {
|
|
3350
|
+
const rel = paperDir(id) + '/paper.lock.json'
|
|
3351
|
+
const cur = await readJson(rel)
|
|
3352
|
+
if (cur && cur.sessionId && cur.sessionId !== sessionId && (now() - Number(cur.at || 0)) < PAPER_LOCK_STALE_MS) return { ok: false, holder: String(cur.sessionId), at: Number(cur.at || 0) }
|
|
3353
|
+
await writeJson(rel, { sessionId: sessionId, at: now(), project: currentProject })
|
|
3354
|
+
return { ok: true }
|
|
3355
|
+
}
|
|
3356
|
+
async function paperLockRelease(id) {
|
|
3357
|
+
const rel = paperDir(id) + '/paper.lock.json'
|
|
3358
|
+
const cur = await readJson(rel)
|
|
3359
|
+
if (cur && cur.sessionId === sessionId) await writeJson(rel, { sessionId: '', at: now(), released: true })
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* 回收卡死的撰写者(修订 §1:死掉的撰写者绝不能把论文永久卡住)。清状态 + 尽力中断 +
|
|
3363
|
+
* 从 agentRegistry 摘除(否则一直被算作活跃子代理)+ 记进 paperAbandoned(事后返回的输出被明确
|
|
3364
|
+
* 丢弃并留痕)+ 可见告警。
|
|
3365
|
+
*/
|
|
3366
|
+
async function paperReapWriter(id, why) {
|
|
3367
|
+
const cid = paperInFlight
|
|
3368
|
+
paperInFlight = ''; paperInFlightAt = 0
|
|
3369
|
+
if (cid) {
|
|
3370
|
+
try { await interruptChild(cid) } catch (e) { /* best effort */ }
|
|
3371
|
+
delete agentRegistry[cid]
|
|
3372
|
+
paperAbandoned[cid] = true
|
|
3373
|
+
}
|
|
3374
|
+
await paperAppendLog(id, 'reap', '回收卡死的撰写者 ' + (cid || '(未知)') + ':' + why + '(论文不会被永久卡住;force/心跳会继续)')
|
|
3375
|
+
logActivity('paper', '回收卡死的论文撰写子代理 ' + (cid || '(未知)') + ':' + why)
|
|
3376
|
+
console.error('vibe-math-v3: reaped a stalled paper writer (' + (cid || 'unknown') + '): ' + why)
|
|
3377
|
+
reportDirty = true
|
|
3378
|
+
return cid
|
|
3379
|
+
}
|
|
3380
|
+
function paperWriterVerdictNow(force) {
|
|
3381
|
+
return paperWriterVerdict({ inFlight: !!paperInFlight, force: !!force, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, childInRegistry: paperInFlight ? (agentRegistry[paperInFlight] !== undefined) : true })
|
|
3382
|
+
}
|
|
3383
|
+
function paperOneLine(s, cap) { return String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, cap || 400) }
|
|
3384
|
+
/** 证据索引(spec §3.9):只列**确实存在**的文件;排除 Paper/ 自己(避免自我污染)。
|
|
3385
|
+
* formalVerify=off 必须真的是无操作(formal-verify-v3 的语料守卫断言"off 的提示词里零 Lean 文本"),
|
|
3386
|
+
* 所以 off 档不把形式化路径写进材料。 */
|
|
3387
|
+
async function paperEvidenceIndex() {
|
|
3388
|
+
const formalNow = formalOn()
|
|
3389
|
+
const out = []
|
|
3390
|
+
if (formalNow && await paperPathExists('State/formal.json')) out.push('State/formal.json')
|
|
3391
|
+
if (await paperPathExists('State/index.json')) out.push('State/index.json')
|
|
3392
|
+
for (const p of allProblems()) out.push(problemRel(p))
|
|
3393
|
+
for (const p of allPropos()) out.push(propositionRel(p))
|
|
3394
|
+
for (const m of methods.values()) out.push(methodRel(m, false))
|
|
3395
|
+
const dirs = ['Verified/命题', 'Verified/问题', 'Logs/Verification', 'Logs/Plans', 'Progress'].concat(formalNow ? ['Verified/Lean', 'Formal'] : [])
|
|
3396
|
+
for (let i = 0; i < dirs.length; i++) { const files = await listFiles(dirs[i]); for (let j = 0; j < files.length; j++) out.push(dirs[i] + '/' + files[j]) }
|
|
3397
|
+
const seen = {}, uniq = []
|
|
3398
|
+
for (let i = 0; i < out.length; i++) { const x = String(out[i]); if (seen[x] || x.indexOf('Paper/') === 0) continue; seen[x] = true; uniq.push(x) }
|
|
3399
|
+
return uniq.sort()
|
|
3400
|
+
}
|
|
3401
|
+
/** 汇总材料(spec §4 + 修订 §A4):只含既有证据;未决项(含 leftoverVerify 的待验证对象)显式标注。 */
|
|
3402
|
+
async function buildPaperDigest() {
|
|
3403
|
+
const L = []
|
|
3404
|
+
L.push('PRESET: vibe-math-v3 (single-author)')
|
|
3405
|
+
L.push('PROJECT: ' + currentProject)
|
|
3406
|
+
const qs = allProblems()
|
|
3407
|
+
L.push('')
|
|
3408
|
+
L.push('[ORIGINAL PROBLEMS] (Problems/*.md)')
|
|
3409
|
+
if (qs.length === 0) L.push('- (none)')
|
|
3410
|
+
for (let i = 0; i < qs.length; i++) {
|
|
3411
|
+
const q = qs[i]
|
|
3412
|
+
L.push('- id=' + q.id + ' | 状态=' + q.状态 + ' | 优先级=' + q.优先级 + ' | 依赖=' + JSON.stringify(q.依赖 || []) + ' | 来源=' + (q.来源 || '原始') + ' | 陈述=' + paperOneLine(q.陈述, 800))
|
|
3413
|
+
const dirs = getDirState(q.id)
|
|
3414
|
+
if (dirs.length) L.push(' · 方向:' + dirs.map(function (d) { return d.id + ':' + d.status }).join(', '))
|
|
3415
|
+
const sols = q.solutions || []
|
|
3416
|
+
if (sols.length === 0) L.push(' · 解法:无')
|
|
3417
|
+
for (let j = 0; j < sols.length; j++) L.push(' · 解法#' + j + ' 概率=' + sols[j].prob + ' 状态=' + (sols[j].status || '') + ' 证据=Verified/问题/ 与 Logs/Verification/ | ' + paperOneLine(sols[j].text, 1200))
|
|
3418
|
+
}
|
|
3419
|
+
const ps = allPropos()
|
|
3420
|
+
L.push('')
|
|
3421
|
+
L.push('[PROPOSITIONS] (Propos/<分类>/<id>.md)')
|
|
3422
|
+
if (ps.length === 0) L.push('- (none)')
|
|
3423
|
+
for (let i = 0; i < ps.length; i++) {
|
|
3424
|
+
const p = ps[i]
|
|
3425
|
+
const rec = formalOn() ? formalOf(p.id) : null // off 档不把形式化字段写进材料(off 是真无操作)
|
|
3426
|
+
L.push('- id=' + p.id + ' | 概率=' + p.概率 + ' | 状态=' + p.状态 + ' | 优先级=' + p.优先级 + ' | 价值/关键性=' + p.价值关键性 + ' | 来源问题=' + (p.来源问题 || '') + (rec ? (' | 形式化=' + ((rec && rec.status) || 'none') + (rec && rec.proof ? ('(' + rec.proof + ')') : '')) : '') + ' | 陈述=' + paperOneLine(p.陈述, 800))
|
|
3427
|
+
const sides = [['证明', p.proofs || []], ['证伪', p.refutes || []]]
|
|
3428
|
+
for (let k = 0; k < sides.length; k++) { const arr = sides[k][1]; for (let j = 0; j < arr.length; j++) if (arr[j] && arr[j].prob === 1) L.push(' · 已检验通过:' + sides[k][0] + '#' + j + ' | ' + paperOneLine(arr[j].text, 900)) }
|
|
3429
|
+
}
|
|
3430
|
+
L.push('')
|
|
3431
|
+
L.push('[METHODS / ARTEFACTS] (Methods/ + 全局 VibeMath/Methods/)')
|
|
3432
|
+
const ms = Array.from(methods.values()).concat(Array.from(globalMethods.values()))
|
|
3433
|
+
if (ms.length === 0) L.push('- (none)')
|
|
3434
|
+
for (let i = 0; i < ms.length; i++) {
|
|
3435
|
+
const m = ms[i]
|
|
3436
|
+
L.push('- id=' + m.id + ' | 标题=' + m.标题 + ' | 类型=' + m.类型 + ' | 状态=' + m.状态 + ' | 可信断言=' + JSON.stringify(m.可信断言 || []) + ' | 应用次数=' + ((m.applications || []).length) + ' | 核心内容=' + paperOneLine(m.核心内容, 900))
|
|
3437
|
+
}
|
|
3438
|
+
// §A4:v3 的真实完整性判据是 leftoverVerify(buildVerifyCandidates)——把仍未验证的对象列出来。
|
|
3439
|
+
let cands = []
|
|
3440
|
+
try { cands = await buildVerifyCandidates() } catch (e) { /* 尽力而为 */ }
|
|
3441
|
+
L.push('')
|
|
3442
|
+
L.push('[STILL UNVERIFIED — v3 的完整性判据 leftoverVerify(buildVerifyCandidates);必须标注为未决]')
|
|
3443
|
+
if (cands.length === 0) L.push('- (none — 没有待验证对象)')
|
|
3444
|
+
for (let i = 0; i < cands.length; i++) L.push('- 未决:' + cands[i].rId + '(' + cands[i].kind + ',' + paperOneLine(cands[i].概述, 200) + (cands[i].prob === undefined ? '' : (',prob=' + cands[i].prob)) + ')')
|
|
3445
|
+
L.push('')
|
|
3446
|
+
L.push('[UNRESOLVED / REFUTED — 论文里必须显式标注,不得当成已成立的结论]')
|
|
3447
|
+
let any = false
|
|
3448
|
+
for (let i = 0; i < ps.length; i++) {
|
|
3449
|
+
if (!(ps[i].概率 === 1 || ps[i].概率 === 0)) { any = true; L.push('- 未定论:命题 ' + ps[i].id + '(概率=' + ps[i].概率 + ')') }
|
|
3450
|
+
else if (ps[i].概率 === 0) L.push('- 已被否证:命题 ' + ps[i].id)
|
|
3451
|
+
}
|
|
3452
|
+
for (let i = 0; i < qs.length; i++) {
|
|
3453
|
+
const sols = qs[i].solutions || []
|
|
3454
|
+
for (let j = 0; j < sols.length; j++) if (!(sols[j].prob === 1 || sols[j].prob === 0)) { any = true; L.push('- 未定论:问题 ' + qs[i].id + ' 的解法#' + j + '(概率=' + sols[j].prob + ')') }
|
|
3455
|
+
}
|
|
3456
|
+
if (!any && cands.length === 0) L.push('- (none — 所有对象均已定论)')
|
|
3457
|
+
L.push('')
|
|
3458
|
+
L.push('[EVIDENCE INDEX] (only files that exist)')
|
|
3459
|
+
const ev = await paperEvidenceIndex()
|
|
3460
|
+
for (let i = 0; i < ev.length; i++) L.push('- ' + ev[i])
|
|
3461
|
+
return L.join('\n')
|
|
3462
|
+
}
|
|
3463
|
+
/** 「论文撰写」子代理提示词(单作者;与 v2 同一形状/同一骨架)。 */
|
|
3464
|
+
function paperWriterPrompt(digest, o) {
|
|
3465
|
+
const langName = o.lang === 'en' ? 'English' : '中文(Chinese)'
|
|
3466
|
+
return 'You are the DEDICATED PAPER WRITER (single-author mode) of a math research run that has just CONVERGED.\n' +
|
|
3467
|
+
'Write its final paper in ' + langName + ', using ONLY the evidence in the MATERIAL section below.\n\n' +
|
|
3468
|
+
'HARD RULES:\n' +
|
|
3469
|
+
'- NEVER invent content: no new proposition, no new computation, no citation that is not in the MATERIAL.\n' +
|
|
3470
|
+
'- Unresolved or refuted items MUST be explicitly labelled (「未定论」/「已被否证」, or "unresolved"/"refuted" in English); never present them as established.\n' +
|
|
3471
|
+
'- Fixed 9-section skeleton — provide bodies for these EXACT `## ` headings (the framework writes the headings, author/date and the evidence index itself):\n' +
|
|
3472
|
+
PAPER_SKELETON.map(function (s, i) { return ' ' + (i + 1) + '. ' + s.key + ' — ' + s.spec }).join('\n') + '\n' +
|
|
3473
|
+
'- A section with no evidence must be exactly 「' + PAPER_NO_EVIDENCE + '」 (do not pad it).\n' +
|
|
3474
|
+
'- Markdown subset only: `#`/`##`/`###`, `- ` lists, `**bold**`, `*em*`, `` `code` ``, and inline math as `$...$`. No tables, images, footnotes or raw HTML.\n\n' +
|
|
3475
|
+
'OUTPUT CONTRACT — respond with ONLY one ```json code fence, no prose:\n' +
|
|
3476
|
+
'{"title":"<paper title>","abstract":"<original problem + main results>","sections":[{"name":"<one of the 9 headings>","body":"<markdown>"}, ...]}\n\n' +
|
|
3477
|
+
'MATERIAL (evidence only — do not add anything beyond it):\n' + digest
|
|
3478
|
+
}
|
|
3479
|
+
/** LaTeX 检测(spec §5 + 修订 §D):paperLatexCommand 非空时只用它;否则按语言顺序探测。 */
|
|
3480
|
+
async function paperDetectLatex(lang) {
|
|
3481
|
+
const sub = subprocessOf()
|
|
3482
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return { available: [], reason: 'no-subprocess' }
|
|
3483
|
+
if (typeof sub.resolveExecutable !== 'function') return { available: [], reason: 'no-resolveExecutable' }
|
|
3484
|
+
const forced = String(params.paperLatexCommand || '').trim()
|
|
3485
|
+
if (forced) {
|
|
3486
|
+
try { const p = await sub.resolveExecutable(forced); return p ? { available: [{ name: forced, path: String(p) }], reason: 'ok', forced: true } : { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
|
|
3487
|
+
catch (e) { return { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
|
|
3488
|
+
}
|
|
3489
|
+
const order = lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH
|
|
3490
|
+
const available = []
|
|
3491
|
+
for (let i = 0; i < order.length; i++) {
|
|
3492
|
+
try { const p = await sub.resolveExecutable(order[i]); if (p) available.push({ name: order[i], path: String(p) }) } catch (e) { /* 未安装 */ }
|
|
3493
|
+
}
|
|
3494
|
+
return { available: available, reason: available.length ? 'ok' : 'none' }
|
|
3495
|
+
}
|
|
3496
|
+
function paperArgvFor(engine) {
|
|
3497
|
+
if (engine.name === 'latexmk') return [engine.path, '-pdf', '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
|
|
3498
|
+
if (engine.name === 'tectonic') return [engine.path, '--keep-logs', 'paper.tex']
|
|
3499
|
+
return [engine.path, '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
|
|
3500
|
+
}
|
|
3501
|
+
/** 一次编译(nonstopmode),带主动超时 terminate。 */
|
|
3502
|
+
async function paperRunOnce(engine, dirAbs, capMs) {
|
|
3503
|
+
const sub = subprocessOf()
|
|
3504
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return { ok: false, exitCode: null, code: 'NO_SUBPROCESS' }
|
|
3505
|
+
let handle
|
|
3506
|
+
try { handle = sub.spawn({ argv: paperArgvFor(engine), cwd: dirAbs, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: capMs }) }
|
|
3507
|
+
catch (e) { return { ok: false, exitCode: null, code: 'SPAWN_FAILED', message: String((e && e.message) || e) } }
|
|
3508
|
+
let timedOut = false, timer = null, outcome
|
|
3509
|
+
try {
|
|
3510
|
+
outcome = await Promise.race([
|
|
3511
|
+
handle.done,
|
|
3512
|
+
new Promise(function (resolve) { timer = setTimeout(function () { timedOut = true; try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ } resolve({ exitCode: null }) }, capMs) }),
|
|
3513
|
+
])
|
|
3514
|
+
} catch (e) { if (timer !== null) clearTimeout(timer); return { ok: false, exitCode: null, code: 'RUN_FAILED', message: String((e && e.message) || e) } }
|
|
3515
|
+
if (timer !== null) clearTimeout(timer)
|
|
3516
|
+
let err = ''
|
|
3517
|
+
try { if (handle.collected && handle.collected.stderr) err = String(handle.collected.stderr.readFrom(0).text || '') } catch (e) { /* best effort */ }
|
|
3518
|
+
return { ok: !timedOut && !!outcome && outcome.exitCode === 0, exitCode: outcome ? outcome.exitCode : null, timedOut: timedOut, stderr: err.slice(-1200) }
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* 编译(spec §5 + 修订 §D/§E):主引擎(nonstopmode,两遍)→ 换引擎 → 去不支持宏包 → 最小模板再试一次;
|
|
3522
|
+
* 然后报告并降级。**永不删除/覆盖已有 pdf**:成功判据 = 本次运行 exit 0 **且** pdf 存在。
|
|
3523
|
+
*/
|
|
3524
|
+
async function paperCompile(id, o, texPrimary, bodyTex) {
|
|
3525
|
+
const relDir = paperDir(id), absDir = paperAbsDir(id)
|
|
3526
|
+
const attempts = []
|
|
3527
|
+
if (!o.compilePdf) return { compile: 'skipped', reason: 'paperCompilePdf=false', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
3528
|
+
if (o.format === 'md') return { compile: 'skipped', reason: 'paperFormat=md(不产出 tex,跳过编译)', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
3529
|
+
const det = await paperDetectLatex(o.lang)
|
|
3530
|
+
if (det.available.length === 0) {
|
|
3531
|
+
await paperAppendLog(id, 'latex', '未检测到任何 LaTeX 引擎(' + det.reason + ')→ 只保留 paper.tex + paper.md,不编译 pdf(spec §5:不阻塞定稿)')
|
|
3532
|
+
return { compile: 'not-detected', reason: det.reason, engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
3533
|
+
}
|
|
3534
|
+
await paperAppendLog(id, 'latex', '检测到引擎 ' + det.available.map(function (x) { return x.name }).join(', ') + (det.forced ? '(paperLatexCommand 指定)' : ('(顺序 ' + (o.lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH).join(' > ') + ')')))
|
|
3535
|
+
const plan = [{ engine: det.available[0], tex: texPrimary, mode: 'primary' }]
|
|
3536
|
+
if (det.available[1]) plan.push({ engine: det.available[1], tex: texPrimary, mode: 'engine-fallback' })
|
|
3537
|
+
plan.push({ engine: det.available[0], tex: paperSanitizeTex(texPrimary), mode: 'drop-unsupported-packages' })
|
|
3538
|
+
plan.push({ engine: det.available[0], tex: paperMinimalTexDoc({ lang: o.lang, bodyTex: bodyTex }), mode: 'minimal-template' })
|
|
3539
|
+
const cap = 120000
|
|
3540
|
+
for (let i = 0; i < plan.length; i++) {
|
|
3541
|
+
const step = plan[i]
|
|
3542
|
+
await writeText(relDir + '/paper.tex', step.tex)
|
|
3543
|
+
const runs = []
|
|
3544
|
+
for (let pass = 0; pass < 2; pass++) runs.push(await paperRunOnce(step.engine, absDir, cap))
|
|
3545
|
+
const last = runs[runs.length - 1]
|
|
3546
|
+
const pdf = !!last.ok && await paperPathExists(relDir + '/paper.pdf')
|
|
3547
|
+
attempts.push({ engine: step.engine.name, mode: step.mode, ok: pdf, exitCode: last.exitCode, timedOut: !!last.timedOut, stderr: last.stderr || '' })
|
|
3548
|
+
await paperAppendLog(id, 'compile', step.mode + ' engine=' + step.engine.name + ' → ' + (pdf ? 'ok(paper.pdf 已生成)' : ('failed(exit=' + String(last.exitCode) + (last.timedOut ? ', timeout' : '') + ')')))
|
|
3549
|
+
if (pdf) return { compile: (i === 0) ? 'ok' : 'repaired', engine: step.engine.name, mode: step.mode, attempts: attempts, pdfPreserved: false }
|
|
3550
|
+
}
|
|
3551
|
+
await writeText(relDir + '/paper.tex', texPrimary)
|
|
3552
|
+
const preserved = await paperPathExists(relDir + '/paper.pdf')
|
|
3553
|
+
await paperAppendLog(id, 'compile', '所有尝试均失败:保留 paper.tex + paper.md' + (preserved ? '(paper.pdf 是**上一次成功编译**留下的,未被覆盖)' : ',未生成 paper.pdf') + '(不阻塞定稿)')
|
|
3554
|
+
logActivity('paper', '论文 pdf 编译失败(已尝试换引擎/去不支持宏包/最小模板),已保留 tex+md:' + relDir)
|
|
3555
|
+
reportDirty = true
|
|
3556
|
+
return { compile: 'failed', engine: det.available[0].name, attempts: attempts, pdfPreserved: preserved }
|
|
3557
|
+
}
|
|
3558
|
+
/** 落盘 md/tex/pdf/meta(meta **最后**写 = 原子提交点;spec §2/§5)。 */
|
|
3559
|
+
async function paperEmitArtifacts(id, o, info) {
|
|
3560
|
+
const relDir = paperDir(id)
|
|
3561
|
+
const dateStr = fmtTime(now()).slice(0, 10)
|
|
3562
|
+
const author = paperAuthorLine()
|
|
3563
|
+
const evidence = info.evidence || (await paperEvidenceIndex())
|
|
3564
|
+
const title = String(info.title || '').trim() || ('研究报告:' + currentProject)
|
|
3565
|
+
const md = paperBuildMarkdown({ title: title, author: author, date: dateStr, abstract: info.abstract, sections: info.sections, evidence: evidence })
|
|
3566
|
+
const artifacts = { md: false, tex: false, pdf: false }
|
|
3567
|
+
let compile = 'skipped', engine = null, attempts = [], pdfPreserved = false
|
|
3568
|
+
if (o.format === 'md' || o.format === 'both') { await writeText(relDir + '/paper.md', md); artifacts.md = true }
|
|
3569
|
+
if (o.format === 'tex' || o.format === 'both') {
|
|
3570
|
+
const bodyTex = paperMdToTexBody(md)
|
|
3571
|
+
const tex = paperTexDoc({ title: title, author: author, date: dateStr, lang: o.lang, bodyTex: bodyTex })
|
|
3572
|
+
await writeText(relDir + '/paper.tex', tex)
|
|
3573
|
+
artifacts.tex = true
|
|
3574
|
+
const c = await paperCompile(id, o, tex, bodyTex)
|
|
3575
|
+
compile = c.compile; engine = c.engine || null; attempts = c.attempts || []
|
|
3576
|
+
pdfPreserved = !!c.pdfPreserved
|
|
3577
|
+
if (compile === 'ok' || compile === 'repaired') artifacts.pdf = await paperPathExists(relDir + '/paper.pdf')
|
|
3578
|
+
}
|
|
3579
|
+
const filled = PAPER_SKELETON.map(function (s) { return s.key }).filter(function (k) { return !!(info.sections && String(info.sections[k] || '').trim()) })
|
|
3580
|
+
const meta = {
|
|
3581
|
+
id: id, dir: relDir, preset: 'vibe-math-v3', singleAuthor: true, project: currentProject,
|
|
3582
|
+
title: title, author: author, date: dateStr, abstract: String(info.abstract || ''),
|
|
3583
|
+
sectionBodies: info.sections || {}, sections: PAPER_SKELETON.map(function (s) { return s.key }), filledSections: filled,
|
|
3584
|
+
raw: String(info.raw == null ? '' : info.raw), notes: Array.isArray(info.notes) ? info.notes : [],
|
|
3585
|
+
trigger: info.trigger || 'manual', runStartedAt: Number(scheduler.startedAt) || 0,
|
|
3586
|
+
params: { finalPaper: params.finalPaper !== false, paperFormat: o.format, paperLanguage: o.lang, paperCompilePdf: !!o.compilePdf, paperLatexCommand: String(params.paperLatexCommand || '') },
|
|
3587
|
+
finalizedAt: now(), finalizedAtText: fmtTime(now()),
|
|
3588
|
+
artifacts: artifacts, compile: compile, compileEngine: engine, compileAttempts: attempts, pdfPreserved: pdfPreserved,
|
|
3589
|
+
material: info.material || null, evidence: evidence,
|
|
3590
|
+
}
|
|
3591
|
+
await writeJson(relDir + '/paper.meta.json', meta)
|
|
3592
|
+
return meta
|
|
3593
|
+
}
|
|
3594
|
+
async function paperMissingArtifacts(id, o) {
|
|
3595
|
+
const relDir = paperDir(id)
|
|
3596
|
+
const need = []
|
|
3597
|
+
if (o.format === 'md' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.md'))) need.push('paper.md') }
|
|
3598
|
+
if (o.format === 'tex' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.tex'))) need.push('paper.tex') }
|
|
3599
|
+
return need
|
|
3600
|
+
}
|
|
3601
|
+
/** 派遣(内部):激活上限拒绝 → 排队重试(修订 §A2)。 */
|
|
3602
|
+
async function paperDispatch(trigger, o, id, prevTries) {
|
|
3603
|
+
const relDir = paperDir(id)
|
|
3604
|
+
const digest = await buildPaperDigest()
|
|
3605
|
+
let childId
|
|
3606
|
+
try {
|
|
3607
|
+
childId = await spawnChild('paper-writer:' + id, paperWriterPrompt(digest, o), { role: 'paper', paperId: id, trigger: trigger, lang: o.lang, format: o.format, compilePdf: o.compilePdf })
|
|
3608
|
+
} catch (e) {
|
|
3609
|
+
const message = String((e && e.message) || e)
|
|
3610
|
+
const limit = activationLimitFrom(message)
|
|
3611
|
+
if (limit !== undefined || /ACTIVATION_LIMIT_REACHED|active child limit/i.test(message)) {
|
|
3612
|
+
const tries = Number(prevTries || 0) + 1
|
|
3613
|
+
if (tries > PAPER_MAX_RETRIES) {
|
|
3614
|
+
paperPending = null
|
|
3615
|
+
await paperAppendLog(id, 'dispatch-failed', '宿主激活上限连续拒绝 ' + PAPER_MAX_RETRIES + ' 次,已放弃自动派遣:' + message)
|
|
3616
|
+
logActivity('paper', '论文撰写子代理被宿主激活上限连续拒绝(' + PAPER_MAX_RETRIES + ' 次),已放弃自动派遣(稍后可 /vibe paper force 重试):' + message)
|
|
3617
|
+
console.error('vibe-math-v3: paper writer refused by the host activation limit ' + PAPER_MAX_RETRIES + ' times; giving up (retry later with /vibe paper force) — ' + message)
|
|
3618
|
+
await paperLockRelease(id)
|
|
3619
|
+
return { ok: false, message: 'paper writer refused by the activation limit ' + PAPER_MAX_RETRIES + ' times: ' + message, id: id, dir: relDir, limit: limit }
|
|
3620
|
+
}
|
|
3621
|
+
paperPending = { tries: tries, retryAt: now() + PAPER_RETRY_MS, reason: message, limit: limit, trigger: trigger, opts: { lang: o.lang, format: o.format, compilePdf: o.compilePdf } }
|
|
3622
|
+
if (tries === 1 || tries % 3 === 0) {
|
|
3623
|
+
await paperAppendLog(id, 'dispatch-queued', '宿主激活上限(' + (limit === undefined ? '?' : limit) + ')拒绝派遣(第 ' + tries + ' 次):' + (PAPER_RETRY_MS / 1000) + 's 后重试')
|
|
3624
|
+
logActivity('paper', '论文撰写子代理被宿主激活上限拒绝(第 ' + tries + ' 次),已排队重试:' + message)
|
|
3625
|
+
console.error('vibe-math-v3: paper writer queued after an activation-limit refusal (try ' + tries + '): ' + message)
|
|
3626
|
+
}
|
|
3627
|
+
reportDirty = true
|
|
3628
|
+
return { ok: true, queued: true, reason: 'activation-limit-reached', tries: tries, retryAt: paperPending.retryAt, limit: limit, id: id, dir: relDir }
|
|
3629
|
+
}
|
|
3630
|
+
await paperAppendLog(id, 'dispatch-failed', message)
|
|
3631
|
+
logActivity('paper', '「论文撰写」子代理派遣失败:' + message)
|
|
3632
|
+
await paperLockRelease(id)
|
|
3633
|
+
return { ok: false, message: 'paper writer dispatch failed: ' + message, id: id, dir: relDir }
|
|
3634
|
+
}
|
|
3635
|
+
paperInFlight = childId
|
|
3636
|
+
paperInFlightAt = now()
|
|
3637
|
+
// 只有"非自动重试"的派遣才重置回收计数:auto-retry 必须保留计数,否则卡死重派会无限循环。
|
|
3638
|
+
if (trigger !== 'auto-retry') paperReaps = 0
|
|
3639
|
+
paperPending = null
|
|
3640
|
+
const material = paperMaterialSummary(digest)
|
|
3641
|
+
await paperAppendLog(id, 'dispatch', 'trigger=' + trigger + ' child=' + childId + ' format=' + o.format + ' lang=' + o.lang + ' compilePdf=' + o.compilePdf + (o.force ? ' force=true' : '') + ' run=' + (Number(scheduler.startedAt) || 0) + ' material=' + material.chars + ' chars/' + material.lines + ' lines')
|
|
3642
|
+
logActivity('paper', '已派遣「论文撰写」子代理(' + trigger + ',' + o.format + '/' + o.lang + ')→ ' + relDir)
|
|
3643
|
+
reportDirty = true
|
|
3644
|
+
return { ok: true, dispatched: true, childId: childId, id: id, dir: relDir, trigger: trigger, format: o.format, lang: o.lang, compilePdf: o.compilePdf }
|
|
3645
|
+
}
|
|
3646
|
+
/** 触发入口(spec §2/§3 + 修订 §A2/§A3):自动(收口)与手动命令共用。 */
|
|
3647
|
+
async function maybeWritePaper(trigger, opts) {
|
|
3648
|
+
const o = paperOpts(opts)
|
|
3649
|
+
const id = paperId(opts)
|
|
3650
|
+
const relDir = paperDir(id)
|
|
3651
|
+
const meta = await paperReadMeta(id)
|
|
3652
|
+
const sameRun = !!meta && Number(meta.runStartedAt || 0) === (Number(scheduler.startedAt) || 0)
|
|
3653
|
+
if (!o.force && meta && meta.finalizedAt && sameRun) {
|
|
3654
|
+
const missing = await paperMissingArtifacts(id, o)
|
|
3655
|
+
if (missing.length === 0) {
|
|
3656
|
+
await paperAppendLog(id, 'skip', '幂等:同一 run(' + (Number(scheduler.startedAt) || 0) + ')已有定稿且产物齐全 → 不重复撰写(/vibe paper force 可重写)')
|
|
3657
|
+
return { ok: true, skipped: true, reason: 'already-finalized-this-run', id: id, dir: relDir, finalizedAt: meta.finalizedAt, artifacts: meta.artifacts || {}, compile: meta.compile, note: '同一 run 只写一次;重复触发只补写缺失产物(spec §2)' }
|
|
3658
|
+
}
|
|
3659
|
+
await paperAppendLog(id, 'fill', '幂等:已有定稿但缺少 ' + missing.join(', ') + ' → 用已存的定稿内容补写(不重新派遣作者)')
|
|
3660
|
+
const filled = await paperEmitArtifacts(id, o, { title: meta.title, abstract: meta.abstract, sections: meta.sectionBodies || {}, evidence: meta.evidence, raw: meta.raw, notes: (meta.notes || []).concat(['补写缺失产物:' + missing.join(', ')]), trigger: meta.trigger || trigger, material: meta.material })
|
|
3661
|
+
return { ok: true, skipped: true, reason: 'filled-missing-artifacts', filled: missing, id: id, dir: relDir, finalizedAt: filled.finalizedAt, artifacts: filled.artifacts, compile: filled.compile }
|
|
3662
|
+
}
|
|
3663
|
+
if (paperInFlight) {
|
|
3664
|
+
const verdict = paperWriterVerdictNow(o.force)
|
|
3665
|
+
if (verdict === 'reap') {
|
|
3666
|
+
const gone = agentRegistry[paperInFlight] === undefined
|
|
3667
|
+
await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
|
|
3668
|
+
} else if (verdict === 'refuse') {
|
|
3669
|
+
const ageMs = now() - Number(paperInFlightAt || 0)
|
|
3670
|
+
await paperAppendLog(id, 'force-refused', '仍有在途撰写者 ' + paperInFlight + '(' + Math.round(ageMs / 1000) + 's):超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟才会被 force 放弃')
|
|
3671
|
+
return { ok: false, reason: 'writer-in-flight', childId: paperInFlight, ageMs: ageMs, staleMs: PAPER_STALE_MS, id: id, dir: relDir, message: '已有在途的论文撰写子代理 ' + paperInFlight + '(已 ' + Math.round(ageMs / 1000) + 's 未返回)。等它返回,或等超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟后 force 会自动回收并重派(心跳也会自动回收)。' }
|
|
3672
|
+
} else {
|
|
3673
|
+
return { ok: true, skipped: true, reason: 'writer-in-flight', childId: paperInFlight, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, id: id, dir: relDir }
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
if (paperPending) return { ok: true, queued: true, reason: 'retry-queued', retryAt: paperPending.retryAt, tries: paperPending.tries, id: id, dir: relDir }
|
|
3677
|
+
const lock = await paperLockAcquire(id)
|
|
3678
|
+
if (!lock.ok) {
|
|
3679
|
+
await paperAppendLog(id, 'skip', 'paper 锁由会话 ' + lock.holder + ' 持有(未过期)→ 本次不派遣(修订 §A3 的 paper 作用域锁)')
|
|
3680
|
+
return { ok: true, skipped: true, reason: 'locked-by-another-session', holder: lock.holder, id: id, dir: relDir }
|
|
3681
|
+
}
|
|
3682
|
+
return await paperDispatch(trigger, o, id, 0)
|
|
3683
|
+
}
|
|
3684
|
+
function paperRetryDue() { return !!(paperPending && now() >= Number(paperPending.retryAt || 0)) }
|
|
3685
|
+
/**
|
|
3686
|
+
* 心跳(修订 §1):在途 → 续租 paper 锁;**卡死则自动回收**(child 不在注册表,或超过卡死窗口),
|
|
3687
|
+
* 并自动重派(上限 PAPER_MAX_AUTO_REAPS,之后给出可执行提示交给 /vibe paper force);有排队则重试派遣。
|
|
3688
|
+
*/
|
|
3689
|
+
async function runPaperRetry() {
|
|
3690
|
+
const id = paperId()
|
|
3691
|
+
if (paperInFlight) {
|
|
3692
|
+
if (paperWriterVerdictNow(false) === 'reap') {
|
|
3693
|
+
const gone = agentRegistry[paperInFlight] === undefined
|
|
3694
|
+
await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
|
|
3695
|
+
const meta = await paperReadMeta(id)
|
|
3696
|
+
if (params.finalPaper !== false && !(meta && meta.finalizedAt) && Number(paperReaps || 0) < PAPER_MAX_AUTO_REAPS) {
|
|
3697
|
+
paperReaps = Number(paperReaps || 0) + 1
|
|
3698
|
+
await paperAppendLog(id, 'retry-after-reap', '第 ' + paperReaps + ' 次自动重派(上限 ' + PAPER_MAX_AUTO_REAPS + ')')
|
|
3699
|
+
await paperDispatch('auto-retry', paperOpts({}), id, 0)
|
|
3700
|
+
} else {
|
|
3701
|
+
await paperAppendLog(id, 'reap-giveup', '自动重派已达上限 ' + PAPER_MAX_AUTO_REAPS + '(或已有定稿):不再自动派遣,请用 /vibe paper force 手动重写')
|
|
3702
|
+
logActivity('paper', '论文撰写子代理连续卡死/已放弃:不再自动重派,可用 /vibe paper force 手动重写')
|
|
3703
|
+
}
|
|
3704
|
+
return
|
|
3705
|
+
}
|
|
3706
|
+
try { await paperLockAcquire(id) } catch (e) { /* 续租失败不致命 */ }
|
|
3707
|
+
return
|
|
3708
|
+
}
|
|
3709
|
+
if (!paperRetryDue()) return
|
|
3710
|
+
const p = paperPending
|
|
3711
|
+
paperPending = null
|
|
3712
|
+
const o = paperOpts(p.opts)
|
|
3713
|
+
await paperDispatch(p.trigger || 'auto-retry', o, id, Number(p.tries || 0))
|
|
3714
|
+
}
|
|
3715
|
+
/** `/vibe paper [lang=zh|en] [format=both|md|tex] [force]`(spec §2/§6)。 */
|
|
3716
|
+
async function paperCommand(args) {
|
|
3717
|
+
const rest = Array.isArray(args) ? args : []
|
|
3718
|
+
const opts = {}
|
|
3719
|
+
for (let i = 0; i < rest.length; i++) {
|
|
3720
|
+
const a = String(rest[i] || '')
|
|
3721
|
+
if (a === 'force') { opts.force = true; continue }
|
|
3722
|
+
const eq = a.indexOf('=')
|
|
3723
|
+
const k = eq > 0 ? a.slice(0, eq).toLowerCase() : ''
|
|
3724
|
+
const v = eq > 0 ? a.slice(eq + 1).toLowerCase() : ''
|
|
3725
|
+
if (k === 'lang') { if (v !== 'zh' && v !== 'en') return { ok: false, message: '/vibe paper lang= 只接受 zh|en(收到 ' + JSON.stringify(a) + ')' }; opts.lang = v; continue }
|
|
3726
|
+
if (k === 'format') { if (v !== 'both' && v !== 'md' && v !== 'tex') return { ok: false, message: '/vibe paper format= 只接受 both|md|tex(收到 ' + JSON.stringify(a) + ')' }; opts.format = v; continue }
|
|
3727
|
+
return { ok: false, message: 'unknown /vibe paper option: ' + a + '(可用 lang=zh|en、format=both|md|tex、force)' }
|
|
3728
|
+
}
|
|
3729
|
+
const r = await maybeWritePaper('manual', opts)
|
|
3730
|
+
if (r.ok === false) return r
|
|
3731
|
+
const autoNote = params.finalPaper === false ? '(自动触发已关闭 finalPaper=false;手动命令仍可用)' : ''
|
|
3732
|
+
const msg = r.dispatched ? ('已派遣「论文撰写」子代理,产物将写入 ' + r.dir + '/')
|
|
3733
|
+
: r.queued ? ('宿主激活上限已满:论文撰写已排队,' + (PAPER_RETRY_MS / 1000) + 's 后自动重试(第 ' + r.tries + ' 次)')
|
|
3734
|
+
: (r.reason === 'already-finalized-this-run' ? '同一 run 已有定稿且产物齐全:不重复撰写(需要重写请用 /vibe paper force)'
|
|
3735
|
+
: (r.reason === 'filled-missing-artifacts' ? ('已用既有定稿补写缺失产物:' + (r.filled || []).join(', ')) : ('未重新派遣:' + (r.reason || ''))))
|
|
3736
|
+
return Object.assign({}, r, { message: msg + autoNote, autoDisabled: params.finalPaper === false })
|
|
3737
|
+
}
|
|
3738
|
+
/** 论文撰写子代理结束(spec §4):解析回复 → 组装 9 节 → 落盘 → 编译 → 记日志 → 释放 paper 锁。 */
|
|
3739
|
+
async function handlePaperWriter(childId, meta, output) {
|
|
3740
|
+
const id = meta.paperId || paperId()
|
|
3741
|
+
paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0
|
|
3742
|
+
delete paperAbandoned[childId]
|
|
3743
|
+
const o = paperOpts({ lang: meta.lang, format: meta.format, compilePdf: meta.compilePdf })
|
|
3744
|
+
const parsed = parseJson(output)
|
|
3745
|
+
const info = paperSkeletonFromReply(parsed, output)
|
|
3746
|
+
if (!info.title) info.title = paperTitleFromMd(String(output || '')) || ('研究报告:' + currentProject)
|
|
3747
|
+
for (let i = 0; i < info.notes.length; i++) await paperAppendLog(id, 'note', info.notes[i])
|
|
3748
|
+
if (!String(output || '').trim()) await paperAppendLog(id, 'reply', '作者回复为空:只写骨架 + 占位说明(绝不编造内容)')
|
|
3749
|
+
let written
|
|
3750
|
+
try {
|
|
3751
|
+
written = await paperEmitArtifacts(id, o, { title: info.title, abstract: info.abstract, sections: info.sections, raw: String(output || ''), notes: info.notes, trigger: meta.trigger || 'auto', material: paperMaterialSummary(await buildPaperDigest()) })
|
|
3752
|
+
} catch (e) {
|
|
3753
|
+
await paperAppendLog(id, 'finalize-failed', String((e && e.message) || e))
|
|
3754
|
+
logActivity('paper', '论文落盘失败:' + String((e && e.message) || e))
|
|
3755
|
+
await paperLockRelease(id)
|
|
3756
|
+
return
|
|
3757
|
+
}
|
|
3758
|
+
await paperAppendLog(id, 'finalize', '产物 ' + JSON.stringify(written.artifacts) + ' compile=' + written.compile + (written.compileEngine ? (' engine=' + written.compileEngine) : '') + ' run=' + (Number(scheduler.startedAt) || 0))
|
|
3759
|
+
logActivity('paper', '论文(最终稿)已落盘:' + paperDir(id) + '/(' + Object.keys(written.artifacts).filter(function (k) { return written.artifacts[k] }).join('/') + ',编译=' + written.compile + ')')
|
|
3760
|
+
await paperLockRelease(id)
|
|
3761
|
+
reportDirty = true
|
|
3762
|
+
await saveAll()
|
|
3763
|
+
}
|
|
3764
|
+
/** status 里的论文视图(spec §2/§6)。 */
|
|
3765
|
+
async function paperStatusView() {
|
|
3766
|
+
const id = paperId()
|
|
3767
|
+
const meta = await paperReadMeta(id)
|
|
3768
|
+
return {
|
|
3769
|
+
id: id, dir: paperDir(id), inFlight: paperInFlight || null,
|
|
3770
|
+
inFlightSince: paperInFlight ? Number(paperInFlightAt || 0) : null,
|
|
3771
|
+
inFlightAgeMs: paperInFlight ? (now() - Number(paperInFlightAt || 0)) : null,
|
|
3772
|
+
staleMs: PAPER_STALE_MS, reapedThisRun: Number(paperReaps || 0),
|
|
3773
|
+
queued: paperPending ? { tries: paperPending.tries, retryAt: paperPending.retryAt, reason: paperPending.reason, limit: paperPending.limit === undefined ? null : paperPending.limit } : null,
|
|
3774
|
+
autoFinalPaper: params.finalPaper !== false, format: params.paperFormat, language: params.paperLanguage, compilePdf: params.paperCompilePdf !== false,
|
|
3775
|
+
finalizedAt: meta ? meta.finalizedAt : null, artifacts: meta ? (meta.artifacts || {}) : {}, compile: meta ? meta.compile : null,
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3155
3779
|
// ================= child result dispatch =================
|
|
3156
3780
|
async function onChildEnd(info) {
|
|
3157
3781
|
// 子代理结束/中断:自动释放它持有的所有写锁(防锁残留导致文件被永久锁住)
|
|
3158
3782
|
const endedId = String(info.id)
|
|
3159
3783
|
for (const k of Object.keys(fileOwner)) { if (String(fileOwner[k].childId) === endedId) delete fileOwner[k] }
|
|
3160
3784
|
const meta = agentRegistry[info.id]
|
|
3161
|
-
if (meta === undefined)
|
|
3785
|
+
if (meta === undefined) {
|
|
3786
|
+
// 已回收的撰写者事后才返回:输出被丢弃,但必须**留痕**,不能静默消失(修订 §1)。
|
|
3787
|
+
if (paperAbandoned[info.id]) {
|
|
3788
|
+
delete paperAbandoned[info.id]
|
|
3789
|
+
logActivity('paper', '已回收的论文撰写子代理 ' + info.id + ' 事后返回:其输出被丢弃(已重派新的撰写者)')
|
|
3790
|
+
reportDirty = true
|
|
3791
|
+
}
|
|
3792
|
+
return
|
|
3793
|
+
}
|
|
3162
3794
|
const output = blocksToText(info.lastAssistantMessage)
|
|
3163
3795
|
try {
|
|
3164
3796
|
if (meta.role === 'explorer') await handleExplorer(info.id, meta, output)
|
|
@@ -3166,6 +3798,9 @@ export function apply(ctx) {
|
|
|
3166
3798
|
else if (meta.role === 'verifier') await handleVerifier(info.id, meta, output, info.stopReason)
|
|
3167
3799
|
else if (meta.role === 'planner') await handlePlanner(info.id, meta, output)
|
|
3168
3800
|
else if (meta.role === 'method-keeper') await handleMethodKeeper(info.id, meta, output)
|
|
3801
|
+
// 修订 §A2:论文撰写子代理必须有**自己的分支**——未知 role 的产出会被静默丢弃。
|
|
3802
|
+
else if (meta.role === 'paper') await handlePaperWriter(info.id, meta, output)
|
|
3803
|
+
else console.error('vibe-math-v3: subagent ' + info.id + ' ended with an unknown role "' + String(meta.role) + '" — its output is dropped')
|
|
3169
3804
|
} catch (e) { console.error('vibe-math-v3 onChildEnd error: ' + String((e && e.stack) || e)) }
|
|
3170
3805
|
await saveAll()
|
|
3171
3806
|
await maybePromoteMethods()
|
|
@@ -3184,6 +3819,9 @@ export function apply(ctx) {
|
|
|
3184
3819
|
if (Object.keys(agentRegistry).length > 0 || Object.keys(tasks).length > 0) {
|
|
3185
3820
|
logActivity(fresh ? 'start' : 'resume', 'cleared ' + Object.keys(agentRegistry).length + ' agent(s) and ' + Object.keys(tasks).length + ' task(s) (' + (fresh ? 'restart' : 'stale from previous process') + ')')
|
|
3186
3821
|
agentRegistry = {}; tasks = {}
|
|
3822
|
+
// 论文派遣态是本进程内存态:子代理已被中断/丢弃,排队与在途标记必须一起清掉(修订 §A2)。
|
|
3823
|
+
if (paperInFlight || paperPending) logActivity('paper', '重启/跨进程恢复:清空论文撰写态(inFlight=' + (paperInFlight || '-') + (paperPending ? ', queued' : '') + ')')
|
|
3824
|
+
paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
|
|
3187
3825
|
}
|
|
3188
3826
|
}
|
|
3189
3827
|
await writeJson('State/process_epoch.json', processEpoch)
|
|
@@ -3191,10 +3829,43 @@ export function apply(ctx) {
|
|
|
3191
3829
|
if (params.indexAutoRebuild) await rebuildIndex()
|
|
3192
3830
|
return { ok: true }
|
|
3193
3831
|
}
|
|
3194
|
-
|
|
3832
|
+
/**
|
|
3833
|
+
* 项目锁 = 租约(lease),不是一次性写入的时间戳。
|
|
3834
|
+
*
|
|
3835
|
+
* 旧实现只在获取时写一次 `at`,全仓没有任何续租点:运行满 projectLockTimeoutMs(默认 60s)后
|
|
3836
|
+
* 第二个会话就会把锁判为"过期"并直接夺锁,两个进程于是并发写同一棵 md 树——而写锁 fileOwner
|
|
3837
|
+
* 是**进程级**的,跨进程不共享,挡不住这一路。
|
|
3838
|
+
*
|
|
3839
|
+
* 现在:(1) 独立的续租定时器按 LOCK_POLL_MS 轮询、在租约用掉 1/4 时写盘——最坏租约年龄因此远低于
|
|
3840
|
+
* projectLockTimeoutMs,且与 tick 时长完全无关(Round C 修正:此前 renewProjectLock 放在 tick()
|
|
3841
|
+
* 顶部,单个 tick 超过 projectLockTimeoutMs 就会让租约过期);
|
|
3842
|
+
* (2) 夺取**其他会话**持有的锁需要显式 override(`{override:true}` / `/vibe resume override`)。
|
|
3843
|
+
* 唯一保留的隐式接管是"锁租约已明确过期"(持有者进程崩溃、不再续租),以免崩溃后项目永久锁死;
|
|
3844
|
+
* 正常运行中的会话不会被接管。
|
|
3845
|
+
*/
|
|
3846
|
+
// 节流判据取**落盘的那份租约自己的年龄**(`projectLock.at`),不再维护影子时间戳
|
|
3847
|
+
// `projectLockRenewedAt`:实测它连续两次轮询都读到"距上次续租约 5s"(因而每次都判定"不必写"),
|
|
3848
|
+
// 而文件里的 `at` 始终停在会话启动那一刻——影子时间戳与真正的租约可以分叉。
|
|
3849
|
+
// `projectLock.at` 正是 acquireProjectLock() 判过期、也是崩溃恢复所依据的唯一状态。
|
|
3850
|
+
// 因此"多久写一次"由租约年龄自己决定,二者不可能再分叉。
|
|
3851
|
+
async function renewProjectLock() {
|
|
3852
|
+
if (!scheduler.running) return
|
|
3853
|
+
if (projectLock.sessionId !== sessionId) return
|
|
3854
|
+
const timeout = Number(params.projectLockTimeoutMs) || 60000
|
|
3855
|
+
if ((now() - (Number(projectLock.at) || 0)) < Math.max(1000, timeout / 4)) return
|
|
3856
|
+
projectLock.at = now()
|
|
3857
|
+
await saveAll()
|
|
3858
|
+
}
|
|
3859
|
+
async function acquireProjectLock(override) {
|
|
3195
3860
|
const timeout = Number(params.projectLockTimeoutMs) || 60000
|
|
3196
|
-
|
|
3197
|
-
|
|
3861
|
+
const held = projectLock.sessionId && projectLock.sessionId !== sessionId
|
|
3862
|
+
if (held) {
|
|
3863
|
+
const age = now() - (Number(projectLock.at) || 0)
|
|
3864
|
+
// 过期租约 = 持有者进程已不在续租(崩溃) → 允许接管,否则崩溃一次项目就永久锁死。
|
|
3865
|
+
if (age < timeout && !override) {
|
|
3866
|
+
return { ok: false, code: 'PROJECT_LOCKED', message: '项目 "' + currentProject + '" 正被会话 ' + projectLock.sessionId + ' 占用(租约剩余 ' + Math.max(0, timeout - age) + 'ms;确认对方已停止后可显式接管:override=true)' }
|
|
3867
|
+
}
|
|
3868
|
+
logActivity('lock', 'project lock taken over from session ' + projectLock.sessionId + (override ? ' (explicit override)' : ' (lease expired after ' + age + 'ms)'))
|
|
3198
3869
|
}
|
|
3199
3870
|
projectLock = { sessionId: sessionId, at: now() }
|
|
3200
3871
|
await saveAll()
|
|
@@ -3203,10 +3874,10 @@ export function apply(ctx) {
|
|
|
3203
3874
|
async function releaseProjectLock() {
|
|
3204
3875
|
if (projectLock.sessionId === sessionId) { projectLock = { sessionId: '', at: 0 }; await saveAll() }
|
|
3205
3876
|
}
|
|
3206
|
-
async function startScheduler() { const r = await init(true); if (!r.ok) return r; const lock = await acquireProjectLock(); if (!lock.ok) return lock; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject + '(v3:md 知识库 + 规划代理调度 + 方法库)'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3207
|
-
async function resumeScheduler() { const r = await init(false); if (!r.ok) return r; const lock = await acquireProjectLock(); if (!lock.ok) return lock; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3877
|
+
async function startScheduler(override) { const r = await init(true); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject + '(v3:md 知识库 + 规划代理调度 + 方法库)'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3878
|
+
async function resumeScheduler(override) { const r = await init(false); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3208
3879
|
async function pauseScheduler() { scheduler.running = false; await releaseProjectLock(); logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
|
|
3209
|
-
async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
3880
|
+
async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
3210
3881
|
async function autoResolvePending() {
|
|
3211
3882
|
const pending = decisionQueue.filter(function (d) { return d.status === 'pending' })
|
|
3212
3883
|
for (let i = 0; i < pending.length; i++) {
|
|
@@ -3243,6 +3914,8 @@ export function apply(ctx) {
|
|
|
3243
3914
|
plannerEnabled: params.plannerEnabled, plannerFails: plannerFails,
|
|
3244
3915
|
formal: formalSummary(),
|
|
3245
3916
|
recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params,
|
|
3917
|
+
// 最终论文的可观测面(spec §2/§6)。
|
|
3918
|
+
paper: await paperStatusView(),
|
|
3246
3919
|
}
|
|
3247
3920
|
}
|
|
3248
3921
|
async function checkTermination() {
|
|
@@ -3258,12 +3931,22 @@ export function apply(ctx) {
|
|
|
3258
3931
|
// 注意:待沉淀发明(pendingInventions)不应阻塞终止——它是"批量蒸馏"(积够 methodKeepEvery 才触发),
|
|
3259
3932
|
// 少量残留不会再有 method-keeper 触发,若纳入会令 scheduler 永不终止(闲置空转)。
|
|
3260
3933
|
const leftoverVerify = (await buildVerifyCandidates()).length > 0
|
|
3261
|
-
|
|
3934
|
+
// H1:在途的 **planner 不算"活跃工作"**。planner 不产出知识、不持有验证任务,只是"问下一步做什么";
|
|
3935
|
+
// 旧判定把刚派出的 planner 也算成活跃代理,于是本 tick 的停机判定永远被自己派出的 planner 挡掉,
|
|
3936
|
+
// 空闲时形成"每 tick 一个 planner"的活锁。排除它之后,空闲的那一 tick 就能正常收口停机。
|
|
3937
|
+
const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'planner' && m.role !== 'paper' }).length
|
|
3938
|
+
if (unsolved.length === 0 && !leftoverVerify && realAgents === 0 && Object.keys(tasks).length === 0 && planQueue.length === 0) {
|
|
3939
|
+
// ★ 最终论文(修订 §A2/§A3):必须在 running=false 与 releaseProjectLock() **之前**派遣——
|
|
3940
|
+
// 停止之后 scheduleTick() 是空操作;提前放锁后另一会话可能开始写同一棵树(paper 锁兜底)。
|
|
3941
|
+
// 触发点就是这个分支本身:`leftoverVerify` 为空才是 v3 的真实完整性判据(§A4)。
|
|
3942
|
+
if (params.finalPaper !== false) {
|
|
3943
|
+
try { await maybeWritePaper('auto') } catch (e) { logActivity('paper', '自动撰写最终论文失败:' + String((e && e.message) || e)) }
|
|
3944
|
+
}
|
|
3262
3945
|
scheduler.running = false
|
|
3263
3946
|
await releaseProjectLock()
|
|
3264
3947
|
logActivity('stop', 'all active problems solved (never-priority excluded) and no active agents/tasks/plans — scheduler stopped (strict termination)')
|
|
3265
3948
|
await saveAll(); await maybeWriteReport(true); await maybePushReport(true)
|
|
3266
|
-
} else if (!hasPendingGate &&
|
|
3949
|
+
} else if (!hasPendingGate && realAgents === 0 && Object.keys(tasks).length === 0 && planQueue.length === 0 && unsolved.length > 0) {
|
|
3267
3950
|
let allBlocked = true
|
|
3268
3951
|
for (let i = 0; i < unsolved.length; i++) {
|
|
3269
3952
|
const q = unsolved[i]
|
|
@@ -3290,7 +3973,7 @@ export function apply(ctx) {
|
|
|
3290
3973
|
if (!create && !exists) return { ok: false, message: 'project not found: ' + slug }
|
|
3291
3974
|
if (scheduler.running) await abortScheduler()
|
|
3292
3975
|
currentProject = slug; await writeCurrentProject(); await ensureDirs()
|
|
3293
|
-
params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }
|
|
3976
|
+
params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
|
|
3294
3977
|
await loadSettings(); await migrateLegacyParams(); await loadState(); await loadKnowledgeBase(); await saveAll()
|
|
3295
3978
|
if (params.indexAutoRebuild) await rebuildIndex()
|
|
3296
3979
|
return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
|
|
@@ -3302,47 +3985,50 @@ export function apply(ctx) {
|
|
|
3302
3985
|
function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
|
|
3303
3986
|
const handlers = {}
|
|
3304
3987
|
function registerTool(name, description, parameters, executeFn) { handlers[name] = executeFn }
|
|
3305
|
-
registerTool('vibe_math_start',
|
|
3306
|
-
registerTool('vibe_math_resume',
|
|
3307
|
-
registerTool('vibe_math_pause',
|
|
3308
|
-
registerTool('vibe_math_abort',
|
|
3309
|
-
registerTool('vibe_math_status',
|
|
3310
|
-
registerTool('vibe_math_report',
|
|
3311
|
-
registerTool('vibe_math_set_mode',
|
|
3312
|
-
registerTool('vibe_math_set_params',
|
|
3313
|
-
registerTool('vibe_math_setup',
|
|
3314
|
-
registerTool('vibe_math_save_settings',
|
|
3315
|
-
registerTool('vibe_math_template',
|
|
3316
|
-
registerTool('vibe_math_add_problem',
|
|
3317
|
-
registerTool('vibe_math_add_proposition',
|
|
3318
|
-
|
|
3319
|
-
|
|
3988
|
+
registerTool('vibe_math_start', TOOL_DESC.vibe_math_start, objParams({ override: { type: 'boolean' } }), async function (args) { return await startScheduler(args && args.override) })
|
|
3989
|
+
registerTool('vibe_math_resume', TOOL_DESC.vibe_math_resume, objParams({ override: { type: 'boolean' } }), async function (args) { return await resumeScheduler(args && args.override) })
|
|
3990
|
+
registerTool('vibe_math_pause', TOOL_DESC.vibe_math_pause, objParams({}), async function () { return await pauseScheduler() })
|
|
3991
|
+
registerTool('vibe_math_abort', TOOL_DESC.vibe_math_abort, objParams({}), async function () { return await abortScheduler() })
|
|
3992
|
+
registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), async function () { await refreshParams(); return await getStatus() })
|
|
3993
|
+
registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
|
|
3994
|
+
registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), async function (args) { params.mode = args.mode; await saveAll(); await saveSettings(); if (params.mode === 'auto') await autoResolvePending(); return { ok: true, mode: params.mode } })
|
|
3995
|
+
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, 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' }, paperLatexCommand: { type: 'string' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
|
|
3996
|
+
registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), async function () { await refreshParams(); const list = PARAM_SCHEMA.map(function (p) { const out = Object.assign({}, p); out.current = params[p.name]; out.default = DEFAULT_PARAMS[p.name]; return out }); return { ok: true, parameters: list, saveTo: frameworkRoot() + '/vibe_math_setting.json' } })
|
|
3997
|
+
registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), async function () { return await saveSettings() })
|
|
3998
|
+
registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
|
|
3999
|
+
registerTool('vibe_math_add_problem', TOOL_DESC.vibe_math_add_problem, objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' }, dependencies: { type: 'array', items: { type: 'string' } } }, ['id', 'description']), async function (args) { const id = idSafe(args.id); if (problems.has(id)) return { ok: false, message: 'problem id already exists' }; problems.set(id, { id: id, 标题: id, 状态: '求解中', 优先级: args.priority || 0, 依赖: Array.isArray(args.dependencies) ? args.dependencies : [], 被依赖: [], 来源: '原始', 计划: '待调度', 陈述: String(args.description == null ? '' : args.description), 来源与动机: '', solutions: [], 判断命题: '', 来源命题: '' }); await saveProblem(problems.get(id)); await syncDependencies(); await rebuildIndex(); scheduleTick(); return { ok: true, message: 'problem added', file: problemRel(problems.get(id)) } })
|
|
4000
|
+
registerTool('vibe_math_add_proposition', TOOL_DESC.vibe_math_add_proposition, objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 概率: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 分类: { type: 'string' }, 来源问题: { type: 'string' }, 来源方向: { type: 'string' } }, ['id', '概述']), async function (args) {
|
|
4001
|
+
// M9:内存键、对象 id、文件名必须是**同一个**归一化 id。此前内存键用原始 id、文件名用
|
|
4002
|
+
// idSafe(id)、重载键又来自文件名 ⇒ 同一个对象分裂成两个身份(`p-1/2` 与 `p-1-2`)。
|
|
4003
|
+
const id = idSafe(args.id)
|
|
4004
|
+
if (propos.has(id)) return { ok: false, message: 'proposition id already exists' }
|
|
4005
|
+
const p = { id: id, 标题: id, 状态: '未定论', 概率: clamp01(args.概率 != null ? args.概率 : 0.5), 优先级: (args.优先级 != null) ? args.优先级 : 1, 依赖: [], 价值关键性: clamp01(args['价值/关键性'] != null ? args['价值/关键性'] : 0.5), 分类: args.分类 || '未分类', 陈述: String(args.概述 == null ? '' : args.概述), proofs: [], refutes: [], 来源问题: String(args.来源问题 || ''), 来源方向: String(args.来源方向 || ''), 在问题清单: false }
|
|
3320
4006
|
propos.set(p.id, p); await saveProposition(p); await rebuildIndex(); scheduleTick(); return { ok: true, proposition: p, file: propositionRel(p) }
|
|
3321
4007
|
})
|
|
3322
|
-
registerTool('vibe_math_list_propositions',
|
|
3323
|
-
registerTool('vibe_math_new_project',
|
|
3324
|
-
registerTool('vibe_math_set_project',
|
|
3325
|
-
registerTool('vibe_math_list_projects',
|
|
3326
|
-
registerTool('vibe_math_list_decisions',
|
|
3327
|
-
registerTool('vibe_math_decide',
|
|
3328
|
-
registerTool('vibe_math_list_agents',
|
|
3329
|
-
registerTool('vibe_math_message_agent',
|
|
3330
|
-
registerTool('vibe_math_interrupt_agent',
|
|
3331
|
-
registerTool('vibe_math_plan',
|
|
3332
|
-
registerTool('vibe_math_index',
|
|
3333
|
-
registerTool('vibe_math_method_add',
|
|
3334
|
-
registerTool('vibe_math_method_list',
|
|
3335
|
-
registerTool('vibe_math_lock_status',
|
|
3336
|
-
registerTool('vibe_math_claim_write',
|
|
3337
|
-
registerTool('vibe_math_release_write',
|
|
3338
|
-
registerTool('vibe_math_sync_meta',
|
|
4008
|
+
registerTool('vibe_math_list_propositions', TOOL_DESC.vibe_math_list_propositions, objParams({}), async function () { const all = allPropos(); return { ok: true, count: all.length, propositions: all.map(function (p) { return { id: p.id, 标题: p.标题, 概率: p.概率, 状态: p.状态, 优先级: p.优先级, 分类: categoryOf(p) } }) } })
|
|
4009
|
+
registerTool('vibe_math_new_project', TOOL_DESC.vibe_math_new_project, objParams({ name: { type: 'string' } }, ['name']), async function (args) { const slug = slugify(args.name); return await setProject(slug, true) })
|
|
4010
|
+
registerTool('vibe_math_set_project', TOOL_DESC.vibe_math_set_project, objParams({ name: { type: 'string' } }, ['name']), async function (args) { const slug = slugify(args.name); return await setProject(slug, false) })
|
|
4011
|
+
registerTool('vibe_math_list_projects', TOOL_DESC.vibe_math_list_projects, objParams({}), async function () { return { ok: true, current: currentProject, projects: await listDirsAt(vibeRoot(), 'Projects') } })
|
|
4012
|
+
registerTool('vibe_math_list_decisions', TOOL_DESC.vibe_math_list_decisions, objParams({}), async function () { return { ok: true, decisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }) } })
|
|
4013
|
+
registerTool('vibe_math_decide', TOOL_DESC.vibe_math_decide, objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), async function (args) { const d = decisionQueue.find(function (x) { return x.id === args.id }); if (!d) return { ok: false, message: 'decision not found' }; if (d.status !== 'pending') return { ok: false, message: 'decision already resolved' }; const resolution = { action: args.action, verdict: args.verdict }; const applied = await applyDecision(d.node, d.data, resolution); const r = await resolveDecision(args.id, resolution); return Object.assign({ ok: true, applied: applied }, r) })
|
|
4014
|
+
registerTool('vibe_math_list_agents', TOOL_DESC.vibe_math_list_agents, objParams({}), async function () { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round, rId: m.rId }) } return { ok: true, agents: out, count: out.length } })
|
|
4015
|
+
registerTool('vibe_math_message_agent', TOOL_DESC.vibe_math_message_agent, objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), async function (args) { if (!agentRegistry[args.childId]) return { ok: false, message: 'unknown childId' }; await followupChild(args.childId, args.message); return { ok: true, message: 'message delivered' } })
|
|
4016
|
+
registerTool('vibe_math_interrupt_agent', TOOL_DESC.vibe_math_interrupt_agent, objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { await interruptChild(args.childId); return { ok: true, message: 'interrupt requested' } })
|
|
4017
|
+
registerTool('vibe_math_plan', TOOL_DESC.vibe_math_plan, objParams({ force: { type: 'boolean' } }), async function (args) { if (args && args.force && scheduler.running && !scheduler.gate) { await callPlanner(); return { ok: true, message: 'planning triggered', queued: planQueue.length } } return { ok: true, queued: planQueue, lastPlan: lastPlanSummary } })
|
|
4018
|
+
registerTool('vibe_math_index', TOOL_DESC.vibe_math_index, objParams({}), async function () { await loadKnowledgeBase(); const r = await rebuildIndex(); return { ok: true, index: r, project: currentProject } })
|
|
4019
|
+
registerTool('vibe_math_method_add', TOOL_DESC.vibe_math_method_add, objParams({ id: { type: 'string' }, 标题: { type: 'string' }, 类型: { type: 'string' }, 核心内容: { type: 'string' }, 适用场景: { type: 'string' } }, ['id', '标题']), async function (args) { const id = idSafe(args.id); if (methods.has(id)) return { ok: false, message: 'method id already exists' }; const m = { id: id, 标题: args.标题, 类型: args.类型 || '方法', 状态: '经验', 可信断言: [], 上级体系: [], 子方法: [], 相关: [], 适用场景: args.适用场景 || '', 核心内容: args.核心内容 || '', 定义与记号: '', applications: [], improvements: [], 来源: 'user' }; methods.set(m.id, m); await saveMethod(m, false); await rebuildIndex(); return { ok: true, method: m, file: methodRel(m, false) } })
|
|
4020
|
+
registerTool('vibe_math_method_list', TOOL_DESC.vibe_math_method_list, objParams({}), async function () { const all = Array.from(methods.values()); const g = Array.from(globalMethods.values()); return { ok: true, count: all.length, globalCount: g.length, methods: all.map(function (m) { return { id: m.id, 标题: m.标题, 类型: m.类型, 状态: m.状态, 可信断言: m.可信断言 || [], applications: (m.applications || []).length, global: false } }).concat(g.map(function (m) { return { id: m.id, 标题: m.标题, 类型: m.类型, 状态: m.状态, 可信断言: m.可信断言 || [], applications: (m.applications || []).length, global: true } })) } })
|
|
4021
|
+
registerTool('vibe_math_lock_status', TOOL_DESC.vibe_math_lock_status, objParams({}), async function () { return { ok: true, project: currentProject, lock: projectLock } })
|
|
4022
|
+
registerTool('vibe_math_claim_write', TOOL_DESC.vibe_math_claim_write, objParams({ target: { type: 'string' } }, ['target']), async function (args, agent) { return await claimWrite(String(args.target || ''), agent) })
|
|
4023
|
+
registerTool('vibe_math_release_write', TOOL_DESC.vibe_math_release_write, objParams({ target: { type: 'string' } }, ['target']), async function (args, agent) { return await releaseWrite(String(args.target || ''), agent) })
|
|
4024
|
+
registerTool('vibe_math_sync_meta', TOOL_DESC.vibe_math_sync_meta, objParams({ meta: { type: 'object' } }, ['meta']), async function (args, agent) { return await syncMeta(args.meta || {}, agent) })
|
|
3339
4025
|
|
|
3340
4026
|
// ---- Lean 形式化验证工具(契约 docs/formal-verification.md §5)----
|
|
3341
4027
|
// **无条件注册**:注册是静态的(与既有 ctx.effect 纪律一致),模式只决定框架是否主动告诉
|
|
3342
4028
|
// 代理它们存在;off 模式下人/代理主动调用时照常工作。
|
|
3343
|
-
registerTool('vibe_math_lean_run',
|
|
3344
|
-
registerTool('vibe_math_lean_archive',
|
|
3345
|
-
registerTool('vibe_math_lean_lib',
|
|
4029
|
+
registerTool('vibe_math_lean_run', TOOL_DESC.vibe_math_lean_run, objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), async function (args, agent) { return await leanRunTool(agent && agent.id, args) })
|
|
4030
|
+
registerTool('vibe_math_lean_archive', TOOL_DESC.vibe_math_lean_archive, objParams({ kind: { type: 'string', enum: ['def', 'lemma', 'proof', 'blocked'] }, name: { type: 'string' }, target: { type: 'string' }, content: { type: 'string' }, from: { type: 'string' }, note: { type: 'string' }, run: { type: 'boolean' } }, ['kind']), async function (args, agent) { return await leanArchive(agent && agent.id, args) })
|
|
4031
|
+
registerTool('vibe_math_lean_lib', TOOL_DESC.vibe_math_lean_lib, objParams({ refresh: { type: 'boolean' } }), async function (args) {
|
|
3346
4032
|
const a = args || {}
|
|
3347
4033
|
const r = a.refresh === false
|
|
3348
4034
|
? { lib: null, proved: null, objects: Object.keys(formalRecords()).length }
|
|
@@ -3455,11 +4141,13 @@ export function apply(ctx) {
|
|
|
3455
4141
|
if (Array.isArray(meta.lemmas)) {
|
|
3456
4142
|
for (const l of meta.lemmas) {
|
|
3457
4143
|
if (!l || (!l.id && !l.title)) continue
|
|
3458
|
-
|
|
4144
|
+
// M9:代理给的 id 可能与文件名归一化结果不同(`p-1/2` → `p-1-2`)⇒ 键与 id 一并归一化。
|
|
4145
|
+
const pid = l.id ? idSafe(l.id) : ('p-' + shortId())
|
|
3459
4146
|
if (!propos.has(pid)) {
|
|
3460
|
-
const
|
|
4147
|
+
const lProb = lemmaProb(l)
|
|
4148
|
+
const pn = { id: pid, 标题: l.title || pid, 状态: '未定论', 概率: clamp01(lProb != null ? lProb : 0.6), 优先级: l.优先级 != null ? l.优先级 : 1, 依赖: [], 价值关键性: clamp01(l['价值/关键性'] != null ? l['价值/关键性'] : 0.5), 分类: lemmaCategory(l) || '未分类', 陈述: l.statement || l.title || '', proofs: [], refutes: [], 来源问题: qid, 在问题清单: false }
|
|
3461
4149
|
// 引理证明文本(验证必需)由代理在 sync_meta 的 l.proof 上报(结构化,非长叙述);无则验证器只能验裸命题
|
|
3462
|
-
if (l.proof) { pn.proofs = [{ title: (l.title || pid) + '(证明)', prob: clamp01(
|
|
4150
|
+
if (l.proof) { pn.proofs = [{ title: (l.title || pid) + '(证明)', prob: clamp01(lProb != null ? lProb : 0.7), status: '未定论', text: String(l.proof) }] }
|
|
3463
4151
|
propos.set(pid, pn)
|
|
3464
4152
|
// 若代理已直接写了该命题卡,保留其内容(不覆盖);否则写一张标准卡兜底(保证可被索引/验证)
|
|
3465
4153
|
const rel = propositionRel(pn) // 与 saveProposition 同一路径
|
|
@@ -3468,11 +4156,16 @@ export function apply(ctx) {
|
|
|
3468
4156
|
if (!(dir.lemmas || []).some(function (x) { return x.id === pid })) { dir.lemmas = dir.lemmas || []; dir.lemmas.push({ id: pid, title: l.title || pid }) }
|
|
3469
4157
|
}
|
|
3470
4158
|
}
|
|
3471
|
-
// 解法上报(prob 由代理写进 Problems/<qid>.md
|
|
3472
|
-
|
|
4159
|
+
// 解法上报(prob 由代理写进 Problems/<qid>.md;这里只登记)。
|
|
4160
|
+
// M11:提示词教的是 `solution_text` / `solution_prob`,但遗留扁平通道用的是
|
|
4161
|
+
// `parsed.solution` / `parsed.solution_probability`。**两套拼写都接受**,否则一个
|
|
4162
|
+
// 严格按提示词回 `solution_text` 的 solver 会在这里被判成"声称成功却没有解法",方向被写死。
|
|
4163
|
+
const solText = (meta.solution_text != null) ? meta.solution_text : meta.solution
|
|
4164
|
+
const solProb = (meta.solution_prob != null) ? meta.solution_prob : meta.solution_probability
|
|
4165
|
+
if (solProb != null && solText) {
|
|
3473
4166
|
q.solutions = q.solutions || []
|
|
3474
|
-
const p = clamp01(
|
|
3475
|
-
q.solutions.push({ title: '解法 ' + (q.solutions.length + 1), prob: p >= 1 ? 0.99 : (p <= 0 ? 0.01 : p), status: '未定论', text: String(
|
|
4167
|
+
const p = clamp01(solProb)
|
|
4168
|
+
q.solutions.push({ title: '解法 ' + (q.solutions.length + 1), prob: p >= 1 ? 0.99 : (p <= 0 ? 0.01 : p), status: '未定论', text: String(solText).slice(0, 2000) })
|
|
3476
4169
|
}
|
|
3477
4170
|
// 子问题/临时假设(与旧 JSON 路径一致):注册 q_sub 问题 + 判断问题 + p-tmp 假设
|
|
3478
4171
|
if (Array.isArray(meta.sub_questions)) {
|
|
@@ -3491,7 +4184,8 @@ export function apply(ctx) {
|
|
|
3491
4184
|
}
|
|
3492
4185
|
if (kind === 'methods') {
|
|
3493
4186
|
if (Array.isArray(meta.used)) for (const mu of meta.used) await consumeMethodFeedback({ methods_used: mu ? [mu] : [] }, { qid: '', dirId: '' })
|
|
3494
|
-
if (Array.isArray(meta.created)) for (const
|
|
4187
|
+
if (Array.isArray(meta.created)) for (const rawMid of meta.created) {
|
|
4188
|
+
const mid = idSafe(rawMid) // M9:键、id 与文件名同一把归一化
|
|
3495
4189
|
if (!methods.has(mid)) {
|
|
3496
4190
|
const mm = { id: mid, 标题: mid, 类型: '方法', 状态: '经验', 可信断言: [], 上级体系: [], 子方法: [], 相关: [], 适用场景: '', 核心内容: '', 定义与记号: '', applications: [], improvements: [], 来源: 'agent-written' }
|
|
3497
4191
|
methods.set(mid, mm)
|
|
@@ -3503,12 +4197,13 @@ export function apply(ctx) {
|
|
|
3503
4197
|
if (Array.isArray(meta.improvements)) {
|
|
3504
4198
|
for (const imp of meta.improvements) {
|
|
3505
4199
|
if (!imp || !imp.id) continue
|
|
3506
|
-
const
|
|
3507
|
-
|
|
4200
|
+
const impId = idSafe(imp.id) // M9:方法键已归一化,改进目标必须查同一把键
|
|
4201
|
+
const m = methods.get(impId)
|
|
4202
|
+
if (!m) { logActivity('method', 'improvement referenced unknown method ' + impId); continue }
|
|
3508
4203
|
m.improvements = m.improvements || []
|
|
3509
4204
|
m.improvements.push({ v: m.improvements.length + 1, 原因: imp.原因 || '', text: imp.改进内容 || '' })
|
|
3510
4205
|
await saveMethod(m, false)
|
|
3511
|
-
logActivity('method', 'method ' +
|
|
4206
|
+
logActivity('method', 'method ' + impId + ' improved (v' + m.improvements.length + ')')
|
|
3512
4207
|
}
|
|
3513
4208
|
}
|
|
3514
4209
|
await saveAll()
|
|
@@ -3517,10 +4212,26 @@ export function apply(ctx) {
|
|
|
3517
4212
|
return { ok: false, message: 'unknown meta kind: ' + kind }
|
|
3518
4213
|
}
|
|
3519
4214
|
|
|
4215
|
+
// ---- 会话级 handler 表的工具描述(唯一来源)----
|
|
4216
|
+
//
|
|
4217
|
+
// 提示词审计 M3(v3):同一批工具在**两条注册路径**上各写了一份 description——会话 handler 表
|
|
4218
|
+
// (registerTool(name, description, …) 只存 handler)与文件末尾真正的 `tools.register`
|
|
4219
|
+
// (模型实际看到的那一份)。两边的**参数 schema 一致**(I13/I14 守),但描述会漂移:
|
|
4220
|
+
// `vibe_math_list_propositions` / `vibe_math_method_list` / `claim_write` / `release_write` /
|
|
4221
|
+
// `sync_meta` 五处曾对不上,模型看到哪一版取决于哪条路径生效。
|
|
4222
|
+
//
|
|
4223
|
+
// 现在这两条路径共用**模块级**常量 TOOL_DESC(定义在文件末尾的 shared helpers 区),
|
|
4224
|
+
// 任何一侧被改都会同时生效,不可能再漂移。`tests/audit-v3-registration-parity.mjs` 守这条不变式。
|
|
4225
|
+
|
|
3520
4226
|
// ================= slash command /vibe =================
|
|
4227
|
+
// 注意:签名**必须**保持 `dispatchVibeCommand(cmd, args)`——`audit-persona-surface` 的
|
|
4228
|
+
// `commandRegion()` 靠这个字面量把 /vibe 的分支表接进提示词面一致性检查(hint/usage/handler
|
|
4229
|
+
// 三处必须一致)。多一个形参会让那段源码不再被扫到,等于把这条守卫静默关掉。
|
|
4230
|
+
// `override` 通过 args 本身传递(见下面 commands.register 的处理)。
|
|
3521
4231
|
async function dispatchVibeCommand(cmd, args) {
|
|
3522
|
-
|
|
3523
|
-
if (cmd === '
|
|
4232
|
+
const override = args.indexOf('override') !== -1
|
|
4233
|
+
if (cmd === 'start') return await startScheduler(override)
|
|
4234
|
+
if (cmd === 'resume') return await resumeScheduler(override)
|
|
3524
4235
|
if (cmd === 'pause') return await pauseScheduler()
|
|
3525
4236
|
if (cmd === 'abort') return await abortScheduler()
|
|
3526
4237
|
if (cmd === 'status') { await refreshParams(); return await getStatus() }
|
|
@@ -3529,8 +4240,8 @@ export function apply(ctx) {
|
|
|
3529
4240
|
if (cmd === 'setup') { await refreshParams(); const list = PARAM_SCHEMA.map(function (p) { const out = Object.assign({}, p); out.current = params[p.name]; out.default = DEFAULT_PARAMS[p.name]; return out }); return { ok: true, parameters: list, saveTo: frameworkRoot() + '/vibe_math_setting.json' } }
|
|
3530
4241
|
if (cmd === 'save') return await saveSettings()
|
|
3531
4242
|
if (cmd === 'template') return await createTemplate(args[0] === 'project' ? 'project' : 'global')
|
|
3532
|
-
if (cmd === 'add') { const
|
|
3533
|
-
if (cmd === 'add-proposition') { const
|
|
4243
|
+
if (cmd === 'add') { const raw = args[0]; const desc = args.slice(1).join(' '); if (!raw || !desc) return { ok: false, message: 'usage: /vibe add <id> <description>' }; const id = idSafe(raw); if (problems.has(id)) return { ok: false, message: 'problem id already exists' }; problems.set(id, { id: id, 标题: id, 状态: '求解中', 优先级: 0, 依赖: [], 被依赖: [], 来源: '原始', 计划: '待调度', 陈述: desc, 来源与动机: '', solutions: [], 判断命题: '', 来源命题: '' }); await saveProblem(problems.get(id)); await rebuildIndex(); scheduleTick(); return { ok: true, message: 'problem added', file: problemRel(problems.get(id)) } }
|
|
4244
|
+
if (cmd === 'add-proposition') { const raw = args[0]; const desc = args.slice(1).join(' '); if (!raw || !desc) return { ok: false, message: 'usage: /vibe add-proposition <id> <概述>' }; const id = idSafe(raw); const p = { id: id, 标题: id, 状态: '未定论', 概率: 0.5, 优先级: 1, 依赖: [], 价值关键性: 0.5, 分类: '未分类', 陈述: desc, proofs: [], refutes: [], 来源问题: '', 在问题清单: false }; propos.set(p.id, p); await saveProposition(p); await rebuildIndex(); scheduleTick(); return { ok: true, proposition: p, file: propositionRel(p) } }
|
|
3534
4245
|
if (cmd === 'list-propositions') { const all = allPropos(); return { ok: true, count: all.length, propositions: all.map(function (p) { return { id: p.id, 标题: p.标题, 概率: p.概率, 状态: p.状态, 优先级: p.优先级, 分类: categoryOf(p) } }) } }
|
|
3535
4246
|
if (cmd === 'methods') { const all = Array.from(methods.values()); return { ok: true, count: all.length, methods: all.map(function (m) { return { id: m.id, 标题: m.标题, 类型: m.类型, 状态: m.状态 } }) } }
|
|
3536
4247
|
if (cmd === 'index') { await loadKnowledgeBase(); return await rebuildIndex() }
|
|
@@ -3543,7 +4254,8 @@ export function apply(ctx) {
|
|
|
3543
4254
|
}
|
|
3544
4255
|
if (cmd === 'decisions') return { ok: true, decisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }) }
|
|
3545
4256
|
if (cmd === 'agents') { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round }) } return { ok: true, agents: out } }
|
|
3546
|
-
|
|
4257
|
+
if (cmd === 'paper') return await paperCommand(args)
|
|
4258
|
+
return { ok: false, usage: 'start [override] | resume [override] | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <desc> | add-proposition <id> <概述> | list-propositions | methods | index | plan | lock | project [list|new <name>|<name>] | decisions | agents | paper [lang=zh|en] [format=both|md|tex] [force]', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
|
|
3547
4259
|
}
|
|
3548
4260
|
|
|
3549
4261
|
// ================= session surface =================
|
|
@@ -3556,11 +4268,29 @@ export function apply(ctx) {
|
|
|
3556
4268
|
get scheduler() { return scheduler },
|
|
3557
4269
|
get tickInFlight() { return tickInFlight },
|
|
3558
4270
|
scheduleTick: scheduleTick,
|
|
4271
|
+
// Lease renewal for the apply-scope interval (see the ctx.effect below). Kept on the session so
|
|
4272
|
+
// the timer needs no per-session context, and so a mock host WITHOUT ctx.timeout still renews.
|
|
4273
|
+
renewLockIfDue: renewProjectLock,
|
|
3559
4274
|
onChildEnd: onChildEnd,
|
|
3560
4275
|
dispatchVibeCommand: dispatchVibeCommand,
|
|
3561
4276
|
handlers: handlers,
|
|
3562
4277
|
refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
|
|
3563
4278
|
getRunning: function () { return scheduler.running },
|
|
4279
|
+
// apply 级心跳用(修订 §A2):running=false 之后 scheduleTick() 是空操作,论文的排队重试
|
|
4280
|
+
// (激活上限)与 paper 锁续租必须由**独立于调度器**的心跳驱动。
|
|
4281
|
+
paperRetryDue: paperRetryDue,
|
|
4282
|
+
runPaperRetry: runPaperRetry,
|
|
4283
|
+
// childOwner 裁剪用(审计 L1):这个会话当前仍"可能再发 subagent/end"的 child
|
|
4284
|
+
// = 在册子代理 + 任何任务正在等的那几个。
|
|
4285
|
+
referencedChildIds: function () {
|
|
4286
|
+
const out = Object.keys(agentRegistry)
|
|
4287
|
+
const ids = Object.keys(tasks)
|
|
4288
|
+
for (let i = 0; i < ids.length; i++) {
|
|
4289
|
+
const t = tasks[ids[i]]
|
|
4290
|
+
if (t && Array.isArray(t.children)) for (let j = 0; j < t.children.length; j++) out.push(t.children[j])
|
|
4291
|
+
}
|
|
4292
|
+
return out
|
|
4293
|
+
},
|
|
3564
4294
|
tickDue: function () { const iv = Math.max(200, Number(params.tickIntervalMs) || 2000); return (now() - lastTickAt) >= iv },
|
|
3565
4295
|
}
|
|
3566
4296
|
}
|
|
@@ -3581,47 +4311,47 @@ export function apply(ctx) {
|
|
|
3581
4311
|
},
|
|
3582
4312
|
}))
|
|
3583
4313
|
}
|
|
3584
|
-
registerTool('vibe_math_start',
|
|
3585
|
-
registerTool('vibe_math_resume',
|
|
3586
|
-
registerTool('vibe_math_pause',
|
|
3587
|
-
registerTool('vibe_math_abort',
|
|
3588
|
-
registerTool('vibe_math_status',
|
|
3589
|
-
registerTool('vibe_math_report',
|
|
3590
|
-
registerTool('vibe_math_set_mode',
|
|
3591
|
-
registerTool('vibe_math_set_params',
|
|
3592
|
-
registerTool('vibe_math_setup',
|
|
3593
|
-
registerTool('vibe_math_save_settings',
|
|
3594
|
-
registerTool('vibe_math_template',
|
|
3595
|
-
registerTool('vibe_math_add_problem',
|
|
3596
|
-
registerTool('vibe_math_add_proposition',
|
|
3597
|
-
registerTool('vibe_math_list_propositions',
|
|
3598
|
-
registerTool('vibe_math_new_project',
|
|
3599
|
-
registerTool('vibe_math_set_project',
|
|
3600
|
-
registerTool('vibe_math_list_projects',
|
|
3601
|
-
registerTool('vibe_math_list_decisions',
|
|
3602
|
-
registerTool('vibe_math_decide',
|
|
3603
|
-
registerTool('vibe_math_list_agents',
|
|
3604
|
-
registerTool('vibe_math_message_agent',
|
|
3605
|
-
registerTool('vibe_math_interrupt_agent',
|
|
3606
|
-
registerTool('vibe_math_plan',
|
|
3607
|
-
registerTool('vibe_math_index',
|
|
3608
|
-
registerTool('vibe_math_method_add',
|
|
3609
|
-
registerTool('vibe_math_method_list',
|
|
3610
|
-
registerTool('vibe_math_lock_status',
|
|
3611
|
-
registerTool('vibe_math_claim_write',
|
|
3612
|
-
registerTool('vibe_math_release_write',
|
|
3613
|
-
registerTool('vibe_math_sync_meta',
|
|
4314
|
+
registerTool('vibe_math_start', TOOL_DESC.vibe_math_start, objParams({ override: { type: 'boolean' } }), 'vibe_math_start')
|
|
4315
|
+
registerTool('vibe_math_resume', TOOL_DESC.vibe_math_resume, objParams({ override: { type: 'boolean' } }), 'vibe_math_resume')
|
|
4316
|
+
registerTool('vibe_math_pause', TOOL_DESC.vibe_math_pause, objParams({}), 'vibe_math_pause')
|
|
4317
|
+
registerTool('vibe_math_abort', TOOL_DESC.vibe_math_abort, objParams({}), 'vibe_math_abort')
|
|
4318
|
+
registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), 'vibe_math_status')
|
|
4319
|
+
registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), 'vibe_math_report')
|
|
4320
|
+
registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
|
|
4321
|
+
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, 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' }, paperLatexCommand: { type: 'string' } }), 'vibe_math_set_params')
|
|
4322
|
+
registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), 'vibe_math_setup')
|
|
4323
|
+
registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), 'vibe_math_save_settings')
|
|
4324
|
+
registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
|
|
4325
|
+
registerTool('vibe_math_add_problem', TOOL_DESC.vibe_math_add_problem, objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' }, dependencies: { type: 'array', items: { type: 'string' } } }, ['id', 'description']), 'vibe_math_add_problem')
|
|
4326
|
+
registerTool('vibe_math_add_proposition', TOOL_DESC.vibe_math_add_proposition, objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 概率: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 分类: { type: 'string' }, 来源问题: { type: 'string' }, 来源方向: { type: 'string' } }, ['id', '概述']), 'vibe_math_add_proposition')
|
|
4327
|
+
registerTool('vibe_math_list_propositions', TOOL_DESC.vibe_math_list_propositions, objParams({}), 'vibe_math_list_propositions')
|
|
4328
|
+
registerTool('vibe_math_new_project', TOOL_DESC.vibe_math_new_project, objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_new_project')
|
|
4329
|
+
registerTool('vibe_math_set_project', TOOL_DESC.vibe_math_set_project, objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_set_project')
|
|
4330
|
+
registerTool('vibe_math_list_projects', TOOL_DESC.vibe_math_list_projects, objParams({}), 'vibe_math_list_projects')
|
|
4331
|
+
registerTool('vibe_math_list_decisions', TOOL_DESC.vibe_math_list_decisions, objParams({}), 'vibe_math_list_decisions')
|
|
4332
|
+
registerTool('vibe_math_decide', TOOL_DESC.vibe_math_decide, objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), 'vibe_math_decide')
|
|
4333
|
+
registerTool('vibe_math_list_agents', TOOL_DESC.vibe_math_list_agents, objParams({}), 'vibe_math_list_agents')
|
|
4334
|
+
registerTool('vibe_math_message_agent', TOOL_DESC.vibe_math_message_agent, objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), 'vibe_math_message_agent')
|
|
4335
|
+
registerTool('vibe_math_interrupt_agent', TOOL_DESC.vibe_math_interrupt_agent, objParams({ childId: { type: 'string' } }, ['childId']), 'vibe_math_interrupt_agent')
|
|
4336
|
+
registerTool('vibe_math_plan', TOOL_DESC.vibe_math_plan, objParams({ force: { type: 'boolean' } }), 'vibe_math_plan')
|
|
4337
|
+
registerTool('vibe_math_index', TOOL_DESC.vibe_math_index, objParams({}), 'vibe_math_index')
|
|
4338
|
+
registerTool('vibe_math_method_add', TOOL_DESC.vibe_math_method_add, objParams({ id: { type: 'string' }, 标题: { type: 'string' }, 类型: { type: 'string' }, 核心内容: { type: 'string' }, 适用场景: { type: 'string' } }, ['id', '标题']), 'vibe_math_method_add')
|
|
4339
|
+
registerTool('vibe_math_method_list', TOOL_DESC.vibe_math_method_list, objParams({}), 'vibe_math_method_list')
|
|
4340
|
+
registerTool('vibe_math_lock_status', TOOL_DESC.vibe_math_lock_status, objParams({}), 'vibe_math_lock_status')
|
|
4341
|
+
registerTool('vibe_math_claim_write', TOOL_DESC.vibe_math_claim_write, objParams({ target: { type: 'string' } }, ['target']), 'vibe_math_claim_write')
|
|
4342
|
+
registerTool('vibe_math_release_write', TOOL_DESC.vibe_math_release_write, objParams({ target: { type: 'string' } }, ['target']), 'vibe_math_release_write')
|
|
4343
|
+
registerTool('vibe_math_sync_meta', TOOL_DESC.vibe_math_sync_meta, objParams({ meta: { type: 'object' } }, ['meta']), 'vibe_math_sync_meta')
|
|
3614
4344
|
// Lean 形式化验证(docs/formal-verification.md):三个工具**无条件注册**——注册是静态的,
|
|
3615
4345
|
// 模式只决定框架是否主动告诉代理它们存在。off 模式下人/代理主动调用时照常工作。
|
|
3616
|
-
registerTool('vibe_math_lean_run',
|
|
3617
|
-
registerTool('vibe_math_lean_archive',
|
|
3618
|
-
registerTool('vibe_math_lean_lib',
|
|
4346
|
+
registerTool('vibe_math_lean_run', TOOL_DESC.vibe_math_lean_run, objParams({ file: { type: 'string' }, target: { type: 'string' }, timeout_ms: { type: 'integer' } }, ['file']), 'vibe_math_lean_run')
|
|
4347
|
+
registerTool('vibe_math_lean_archive', TOOL_DESC.vibe_math_lean_archive, objParams({ kind: { type: 'string', enum: ['def', 'lemma', 'proof', 'blocked'] }, name: { type: 'string' }, target: { type: 'string' }, content: { type: 'string' }, from: { type: 'string' }, note: { type: 'string' }, run: { type: 'boolean' } }, ['kind']), 'vibe_math_lean_archive')
|
|
4348
|
+
registerTool('vibe_math_lean_lib', TOOL_DESC.vibe_math_lean_lib, objParams({ refresh: { type: 'boolean' } }), 'vibe_math_lean_lib')
|
|
3619
4349
|
|
|
3620
4350
|
// /vibe slash command (registered once; routed per session)
|
|
3621
4351
|
ctx.effect(() => commands.register({
|
|
3622
4352
|
name: 'vibe',
|
|
3623
4353
|
description: 'control the Vibe Math V3 solver (start/pause/projects/setup/save/decisions/agents/methods/index/plan/lock)',
|
|
3624
|
-
input: { hint: '[start|resume|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents]' },
|
|
4354
|
+
input: { hint: '[start [override]|resume [override]|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents|paper [lang=zh|en] [format=both|md|tex] [force]]' },
|
|
3625
4355
|
handler: async function (invocation) {
|
|
3626
4356
|
const s = getSession(invocation && invocation.agent)
|
|
3627
4357
|
if (!s) return { kind: 'error', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
|
|
@@ -3640,16 +4370,61 @@ export function apply(ctx) {
|
|
|
3640
4370
|
|
|
3641
4371
|
// subagent/end (registered once; routed to the owning session via childOwner)
|
|
3642
4372
|
ctx.on('subagent/end', function (info) {
|
|
4373
|
+
// 记下最近一次 end 的时间:childOwner 的宽限期从这里算(见 pruneChildOwner)。
|
|
4374
|
+
lastChildEndAt.set(info.id, now())
|
|
3643
4375
|
const sid = childOwner.get(info.id)
|
|
3644
4376
|
const s = sid !== undefined ? sessions.get(sid) : undefined
|
|
3645
4377
|
if (s) s.onChildEnd(info).catch(function (e) { console.error('vibe-math-v3 onChildEnd reject: ' + String((e && e.stack) || e)) })
|
|
3646
|
-
//
|
|
4378
|
+
// 注意:这里**不要**立即回收 childOwner 条目(与 v2 同一结论)。该映射在子代理 end 之后仍会
|
|
3647
4379
|
// 被后续事件路由用到;在 v2 上实测过"事件回调里回收"与"onChildEnd 末尾回收"两种写法,
|
|
3648
|
-
// 都会让 verdict
|
|
4380
|
+
// 都会让 verdict 收口失效。回收交给引用 + 宽限期的 pruneChildOwner(审计 L1)。
|
|
3649
4381
|
})
|
|
3650
4382
|
|
|
4383
|
+
// Project-lock LEASE renewal timer (Round C, HIGH): renewal must NOT live in tick().
|
|
4384
|
+
//
|
|
4385
|
+
// Round A put `await renewProjectLock()` at the top of tick(), which left the lease starvable: a
|
|
4386
|
+
// single tick whose awaited work outlives projectLockTimeoutMs (one slow shell/Lean call is enough)
|
|
4387
|
+
// keeps the on-disk `at` stale, and the `if (tickInFlight) return` early-return (:1374) renews
|
|
4388
|
+
// nothing either, so a second session's acquireProjectLock() saw `age >= timeout` and took the
|
|
4389
|
+
// project lock WITHOUT `override` while this session was alive and mid-tick. Measured: 5803 ms of
|
|
4390
|
+
// stale lease against a 2500 ms timeout (probe _oneoff/rb-probe-v3-locklease.mjs).
|
|
4391
|
+
//
|
|
4392
|
+
// An independent interval cannot be delayed by tick duration, tick overlap or a held gate. It is
|
|
4393
|
+
// registered once per preset via ctx.effect (disposed with the plugin) — NOT per startScheduler —
|
|
4394
|
+
// so an `await` inside its callback cannot race plugin disposal, and the scheduler stays mockable
|
|
4395
|
+
// (tests/selfdrive-v3.mjs has no ctx.timeout). The poll runs many times inside one lease window,
|
|
4396
|
+
// so the lease can never approach expiry however long a single tick runs; renewProjectLock() itself
|
|
4397
|
+
// is still the throttled writer and keeps its `scheduler.running` / ownership guards, which preserves
|
|
4398
|
+
// the crash-recovery path exactly: a dead process stops renewing, its lease ages out, and only then
|
|
4399
|
+
// may a new session take it over.
|
|
4400
|
+
// 心跳定时器(每个宿主一次,随插件 fiber 释放)。
|
|
4401
|
+
//
|
|
4402
|
+
// L7:优先用**宿主的 timer 服务**(`ctx.interval`,随宿主暂停/记账、随 fiber 释放),
|
|
4403
|
+
// 只有在宿主没有该服务时(测试 mock / 旧线)才回落到裸 setInterval——回落是必需的:
|
|
4404
|
+
// `inject` 里没有 'timer'(加进去会让缺该服务的主机整个不激活),而 mock 宿主没有 ctx.interval。
|
|
4405
|
+
// 两种实现都返回 disposer,ctx.effect 的清理语义不变。
|
|
4406
|
+
function everyMs(ms, fn) {
|
|
4407
|
+
try { if (typeof ctx.interval === 'function') return ctx.interval(fn, ms) } catch (e) { /* 回落到 setInterval */ }
|
|
4408
|
+
const t = setInterval(fn, ms)
|
|
4409
|
+
return () => clearInterval(t)
|
|
4410
|
+
}
|
|
4411
|
+
ctx.effect(() => everyMs(LOCK_POLL_MS, function () { for (const s of sessions.values()) { s.renewLockIfDue().catch(function (e) { console.error('vibe-math-v3 lock renew error: ' + String((e && e.message) || e)) }) } }))
|
|
3651
4412
|
// tick timer (registered once; ticks every running session at its own pace)
|
|
3652
|
-
ctx.effect(() => {
|
|
4413
|
+
ctx.effect(() => {
|
|
4414
|
+
let beat = 0
|
|
4415
|
+
return everyMs(1000, function () {
|
|
4416
|
+
for (const s of sessions.values()) {
|
|
4417
|
+
if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick()
|
|
4418
|
+
// 论文心跳(修订 §A2):**独立于 scheduler.running** —— 收口后 running=false,但被激活上限
|
|
4419
|
+
// 拒绝的撰写派遣仍要重试,在途时还要续租 paper 锁。runPaperRetry() 无事可做时立即返回。
|
|
4420
|
+
if (typeof s.runPaperRetry === 'function') s.runPaperRetry().catch(function (e) { console.error('vibe-math-v3: paper retry failed: ' + String((e && e.message) || e)) })
|
|
4421
|
+
}
|
|
4422
|
+
// childOwner 裁剪(审计 L1):每 30 拍(约 30s)一次,成本是"会话数 × 映射数"的一次扫描。
|
|
4423
|
+
if ((++beat % 30) === 0) {
|
|
4424
|
+
try { pruneChildOwner() } catch (e) { console.error('vibe-math-v3: pruneChildOwner failed: ' + String((e && e.message) || e)) }
|
|
4425
|
+
}
|
|
4426
|
+
})
|
|
4427
|
+
})
|
|
3653
4428
|
}
|
|
3654
4429
|
|
|
3655
4430
|
// ---- test seam: pure, stateless helpers --------------------------------
|
|
@@ -3665,6 +4440,255 @@ export function apply(ctx) {
|
|
|
3665
4440
|
// to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
|
|
3666
4441
|
// Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
|
|
3667
4442
|
// byte-identical to the previous in-`apply` declarations.
|
|
4443
|
+
// ============================================================================================
|
|
4444
|
+
// 最终论文(规格:docs/final-paper.md)—— **纯函数部分**(module scope;
|
|
4445
|
+
// 与 v2 逐字同构,只有数据来源不同)。会话相关的落盘/派遣/编译在 makeSession 里。
|
|
4446
|
+
// ============================================================================================
|
|
4447
|
+
/** 固定 9 节骨架(spec §3)。key 同时是 md 的 `## ` 标题与 tex 的 `\section{}`。 */
|
|
4448
|
+
const PAPER_SKELETON = [
|
|
4449
|
+
{ key: '摘要', spec: '1 标题、作者、日期、摘要(原问题 + 主要结论)' },
|
|
4450
|
+
{ key: '引言与问题背景', spec: '2 原问题的完整陈述' },
|
|
4451
|
+
{ key: '原问题的完整解法', spec: '3 最终答案 + 完整推理链' },
|
|
4452
|
+
{ key: '已检验通过的命题', spec: '4 逐条列出,含判定为真的估计值与证据来源' },
|
|
4453
|
+
{ key: '已解决的子问题与中间成果', spec: '5' },
|
|
4454
|
+
{ key: '创造或发现的有价值之物', spec: '6 方法、理论、思想、有价值经验、数学理解' },
|
|
4455
|
+
{ key: '规律总结', spec: '7 从上述条目归纳出的可复用规律' },
|
|
4456
|
+
{ key: '讨论、局限与展望', spec: '8' },
|
|
4457
|
+
{ key: '附录:证据与文件索引', spec: '9 Verified/、Logs/、关键卡片路径' },
|
|
4458
|
+
]
|
|
4459
|
+
const PAPER_NO_EVIDENCE = '(本节暂无证据支持的内容——不编造。)'
|
|
4460
|
+
const PAPER_EVIDENCE_HEADING = '证据与文件索引'
|
|
4461
|
+
/** LaTeX 引擎检测顺序(spec §5):中文优先 xelatex,英文优先 pdflatex/latexmk。 */
|
|
4462
|
+
const PAPER_ENGINE_ORDER_ZH = ['xelatex', 'latexmk', 'pdflatex', 'lualatex', 'tectonic']
|
|
4463
|
+
const PAPER_ENGINE_ORDER_EN = ['pdflatex', 'latexmk', 'xelatex', 'lualatex', 'tectonic']
|
|
4464
|
+
/** 只用常见宏包(spec §5);修复阶段会丢弃不在这个名单里的 \usepackage。 */
|
|
4465
|
+
const PAPER_ALLOWED_PACKAGES = ['amsmath', 'amssymb', 'amsthm', 'geometry', 'hyperref', 'longtable', 'booktabs']
|
|
4466
|
+
/** tex 必须转义的 10 个字符(spec §5):\ _ % & # $ { } ~ ^ ——一次替换,避免二次转义。 */
|
|
4467
|
+
const PAPER_TEX_MAP = { '\\': '\\textbackslash{}', '_': '\\_', '%': '\\%', '&': '\\&', '#': '\\#', '$': '\\$', '{': '\\{', '}': '\\}', '~': '\\textasciitilde{}', '^': '\\textasciicircum{}' }
|
|
4468
|
+
function texEscape(s) { return String(s == null ? '' : s).replace(/[\\_%&#${}~^]/g, function (c) { return PAPER_TEX_MAP[c] }) }
|
|
4469
|
+
/** 材料摘要(只作记录;幂等判定用 run id + finalizedAt + 逐产物存在性,修订 §C)。 */
|
|
4470
|
+
function paperMaterialSummary(digest) {
|
|
4471
|
+
const t = String(digest == null ? '' : digest)
|
|
4472
|
+
return { chars: t.length, lines: t.split('\n').length }
|
|
4473
|
+
}
|
|
4474
|
+
/**
|
|
4475
|
+
* 撰写者卡死判定(纯函数,便于守卫覆盖两条分支):
|
|
4476
|
+
* 'none' 正常在途(还没到卡死窗口,且 force 未要求放弃)
|
|
4477
|
+
* 'reap' 必须回收:child 已不在 agentRegistry(end 事件丢失 / 宿主丢弃,它永远不会再回来),
|
|
4478
|
+
* 或已超过卡死窗口——force 与心跳都据此自动降级,论文不能被一个死掉的撰写者永久卡住
|
|
4479
|
+
* 'refuse' force 但还没到窗口:调用方必须给出**可执行**的报错(告诉调用者还要等多久)
|
|
4480
|
+
*/
|
|
4481
|
+
function paperWriterVerdict(o) {
|
|
4482
|
+
if (!(o && o.inFlight)) return 'none'
|
|
4483
|
+
if (o.childInRegistry === false) return 'reap'
|
|
4484
|
+
const age = Number((o && o.ageMs) || 0)
|
|
4485
|
+
const stale = Math.max(1, Number((o && o.staleMs) || 0))
|
|
4486
|
+
if (age >= stale) return 'reap'
|
|
4487
|
+
return (o && o.force) ? 'refuse' : 'none'
|
|
4488
|
+
}
|
|
4489
|
+
/** 行内 md → tex:``code`` / **bold** / *em* 转成命令,其余按 spec §5 转义;`$...$` 原样保留。 */
|
|
4490
|
+
function paperInlineToTex(s) {
|
|
4491
|
+
const t = String(s == null ? '' : s)
|
|
4492
|
+
let out = ''
|
|
4493
|
+
let i = 0
|
|
4494
|
+
while (i < t.length) {
|
|
4495
|
+
const c = t[i]
|
|
4496
|
+
if (c === '$') { const j = t.indexOf('$', i + 1); if (j > i) { out += t.slice(i, j + 1); i = j + 1; continue } }
|
|
4497
|
+
if (c === '`') { const j = t.indexOf('`', i + 1); if (j > i) { out += '\\texttt{' + texEscape(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
|
|
4498
|
+
if (c === '*' && t[i + 1] === '*') { const j = t.indexOf('**', i + 2); if (j > i) { out += '\\textbf{' + paperInlineToTex(t.slice(i + 2, j)) + '}'; i = j + 2; continue } }
|
|
4499
|
+
if (c === '*') { const j = t.indexOf('*', i + 1); if (j > i) { out += '\\emph{' + paperInlineToTex(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
|
|
4500
|
+
out += PAPER_TEX_MAP[c] !== undefined ? PAPER_TEX_MAP[c] : c
|
|
4501
|
+
i++
|
|
4502
|
+
}
|
|
4503
|
+
return out
|
|
4504
|
+
}
|
|
4505
|
+
/** md 正文 → tex 正文(标题/列表/段落;只认 compose 侧约定的最小语法)。 */
|
|
4506
|
+
function paperMdToTexBody(md) {
|
|
4507
|
+
const out = []
|
|
4508
|
+
let inList = false
|
|
4509
|
+
let buf = []
|
|
4510
|
+
const flush = function () { if (buf.length) { out.push(paperInlineToTex(buf.join(' '))); buf = [] } }
|
|
4511
|
+
const closeList = function () { if (inList) { out.push('\\end{itemize}'); inList = false } }
|
|
4512
|
+
const lines = String(md == null ? '' : md).split(/\r?\n/)
|
|
4513
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4514
|
+
const line = lines[i].replace(/\s+$/, '')
|
|
4515
|
+
const h = /^(#{1,4})\s+(.*)$/.exec(line)
|
|
4516
|
+
if (h) {
|
|
4517
|
+
flush(); closeList()
|
|
4518
|
+
const txt = paperInlineToTex(h[2])
|
|
4519
|
+
const lv = h[1].length
|
|
4520
|
+
out.push(lv <= 1 ? ('\\section*{' + txt + '}') : lv === 2 ? ('\\section{' + txt + '}') : lv === 3 ? ('\\subsection{' + txt + '}') : ('\\subsubsection{' + txt + '}'))
|
|
4521
|
+
continue
|
|
4522
|
+
}
|
|
4523
|
+
const li = /^\s*[-*]\s+(.*)$/.exec(line)
|
|
4524
|
+
if (li) { flush(); if (!inList) { out.push('\\begin{itemize}'); inList = true } out.push(' \\item ' + paperInlineToTex(li[1])); continue }
|
|
4525
|
+
if (!line.trim()) { flush(); closeList(); out.push(''); continue }
|
|
4526
|
+
buf.push(line.trim())
|
|
4527
|
+
}
|
|
4528
|
+
flush(); closeList()
|
|
4529
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
4530
|
+
}
|
|
4531
|
+
/** 主模板(spec §5):中文 ctexart / 英文 article,只加常见宏包。 */
|
|
4532
|
+
function paperTexDoc(o) {
|
|
4533
|
+
const lang = (o && o.lang === 'en') ? 'en' : 'zh'
|
|
4534
|
+
return [
|
|
4535
|
+
'\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
|
|
4536
|
+
'\\usepackage[margin=2.5cm]{geometry}',
|
|
4537
|
+
'\\usepackage{amsmath,amssymb,amsthm}',
|
|
4538
|
+
'\\usepackage{hyperref}',
|
|
4539
|
+
'\\usepackage{longtable,booktabs}',
|
|
4540
|
+
'\\title{' + texEscape(o && o.title) + '}',
|
|
4541
|
+
'\\author{' + texEscape(o && o.author) + '}',
|
|
4542
|
+
'\\date{' + texEscape(o && o.date) + '}',
|
|
4543
|
+
'\\begin{document}',
|
|
4544
|
+
'\\maketitle',
|
|
4545
|
+
String((o && o.bodyTex) || ''),
|
|
4546
|
+
'\\end{document}',
|
|
4547
|
+
].join('\n') + '\n'
|
|
4548
|
+
}
|
|
4549
|
+
/** 修复阶段的**最小模板**(spec §5):只留 documentclass + 正文。 */
|
|
4550
|
+
function paperMinimalTexDoc(o) {
|
|
4551
|
+
const lang = (o && o.lang === 'en') ? 'en' : 'zh'
|
|
4552
|
+
return [
|
|
4553
|
+
'\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
|
|
4554
|
+
'\\begin{document}',
|
|
4555
|
+
String((o && o.bodyTex) || ''),
|
|
4556
|
+
'\\end{document}',
|
|
4557
|
+
].join('\n') + '\n'
|
|
4558
|
+
}
|
|
4559
|
+
/** 修复阶段:丢弃不在常见宏包名单里的 \usepackage(其余原样保留)。 */
|
|
4560
|
+
function paperSanitizeTex(tex) {
|
|
4561
|
+
const keep = []
|
|
4562
|
+
const lines = String(tex == null ? '' : tex).split('\n')
|
|
4563
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4564
|
+
const m = /^\s*\\usepackage(\[[^\]]*\])?\{([^}]*)\}\s*$/.exec(lines[i])
|
|
4565
|
+
if (m) {
|
|
4566
|
+
const pkgs = String(m[2]).split(',').map(function (x) { return x.trim() }).filter(Boolean)
|
|
4567
|
+
const ok = pkgs.filter(function (p) { return PAPER_ALLOWED_PACKAGES.indexOf(p) >= 0 })
|
|
4568
|
+
if (ok.length === 0) continue
|
|
4569
|
+
keep.push('\\usepackage' + (m[1] || '') + '{' + ok.join(',') + '}')
|
|
4570
|
+
continue
|
|
4571
|
+
}
|
|
4572
|
+
keep.push(lines[i])
|
|
4573
|
+
}
|
|
4574
|
+
return keep.join('\n')
|
|
4575
|
+
}
|
|
4576
|
+
/** 骨架名匹配:允许带序号/前缀,或只写节名的一部分(例如「附录」)。 */
|
|
4577
|
+
function paperSkeletonKey(name) {
|
|
4578
|
+
const n = String(name == null ? '' : name).replace(/^#+\s*/, '').trim()
|
|
4579
|
+
if (!n) return undefined
|
|
4580
|
+
for (const s of PAPER_SKELETON) if (n === s.key) return s.key
|
|
4581
|
+
for (const s of PAPER_SKELETON) if (n.indexOf(s.key) !== -1 || s.key.indexOf(n) !== -1) return s.key
|
|
4582
|
+
return undefined
|
|
4583
|
+
}
|
|
4584
|
+
/** 按 `## ` 把任意 markdown 切成 [{name,text}](无标题时 name='')。 */
|
|
4585
|
+
function paperSplitByHeadings(md) {
|
|
4586
|
+
const out = []
|
|
4587
|
+
let cur = { name: '', text: [] }
|
|
4588
|
+
const lines = String(md == null ? '' : md).split(/\r?\n/)
|
|
4589
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4590
|
+
const h = /^##\s+(.*?)\s*$/.exec(lines[i])
|
|
4591
|
+
if (h) { if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() }); cur = { name: h[1], text: [] }; continue }
|
|
4592
|
+
cur.text.push(lines[i])
|
|
4593
|
+
}
|
|
4594
|
+
if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() })
|
|
4595
|
+
return out
|
|
4596
|
+
}
|
|
4597
|
+
/** 把「论文撰写」子代理的回复映射到 9 节骨架上(**绝不编造**:缺的节由调用方填占位说明)。 */
|
|
4598
|
+
function paperSkeletonFromReply(reply, fallbackText) {
|
|
4599
|
+
const sections = {}
|
|
4600
|
+
const notes = []
|
|
4601
|
+
let title = ''
|
|
4602
|
+
let abstract = ''
|
|
4603
|
+
const put = function (key, body) {
|
|
4604
|
+
const b = String(body == null ? '' : body).trim()
|
|
4605
|
+
if (!b) return
|
|
4606
|
+
sections[key] = sections[key] ? (sections[key] + '\n\n' + b) : b
|
|
4607
|
+
}
|
|
4608
|
+
const absorbUnknown = function (name, text) {
|
|
4609
|
+
if (!name && !String(text || '').trim()) return
|
|
4610
|
+
notes.push('作者写了一节「' + (name || '(无名)') + '」,不属于固定 9 节骨架,已并入「讨论、局限与展望」')
|
|
4611
|
+
put('讨论、局限与展望', (name ? ('**' + name + '**\n\n') : '') + text)
|
|
4612
|
+
}
|
|
4613
|
+
const obj = (reply && typeof reply === 'object' && !Array.isArray(reply)) ? reply : null
|
|
4614
|
+
if (obj) {
|
|
4615
|
+
title = String(obj.title || obj.标题 || '').trim()
|
|
4616
|
+
abstract = String(obj.abstract || obj.摘要 || '').trim()
|
|
4617
|
+
const list = Array.isArray(obj.sections) ? obj.sections : (Array.isArray(obj.章节) ? obj.章节 : [])
|
|
4618
|
+
for (let i = 0; i < list.length; i++) {
|
|
4619
|
+
const s = list[i]
|
|
4620
|
+
if (!s) continue
|
|
4621
|
+
const name = String(s.name || s.key || s.heading || s.标题 || '')
|
|
4622
|
+
const body = String(s.body != null ? s.body : (s.content != null ? s.content : (s.内容 != null ? s.内容 : '')))
|
|
4623
|
+
const key = paperSkeletonKey(name)
|
|
4624
|
+
if (key === undefined) absorbUnknown(name.trim(), body)
|
|
4625
|
+
else put(key, body)
|
|
4626
|
+
}
|
|
4627
|
+
const bodyField = String(obj.body || obj.content || obj.md || obj.正文 || '')
|
|
4628
|
+
if (bodyField.trim()) {
|
|
4629
|
+
const parts = paperSplitByHeadings(bodyField)
|
|
4630
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4631
|
+
const key = paperSkeletonKey(parts[i].name)
|
|
4632
|
+
if (key === undefined) { if (!parts[i].name && !abstract && !sections['摘要']) abstract = parts[i].text; else if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
|
|
4633
|
+
else put(key, parts[i].text)
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
} else {
|
|
4637
|
+
const parts = paperSplitByHeadings(String(fallbackText == null ? '' : fallbackText))
|
|
4638
|
+
let matched = 0
|
|
4639
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4640
|
+
const key = paperSkeletonKey(parts[i].name)
|
|
4641
|
+
if (key === undefined) { if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
|
|
4642
|
+
else { put(key, parts[i].text); matched++ }
|
|
4643
|
+
}
|
|
4644
|
+
if (matched === 0) {
|
|
4645
|
+
const raw = String(fallbackText == null ? '' : fallbackText).trim()
|
|
4646
|
+
if (raw) { notes.push('作者回复不是契约 JSON,也没有可识别的 `## ` 节标题;其原样正文已记入「讨论、局限与展望」'); put('讨论、局限与展望', raw) }
|
|
4647
|
+
else notes.push('作者回复为空:没有任何内容可写(不得编造)')
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
return { title: title, abstract: abstract, sections: sections, notes: notes }
|
|
4651
|
+
}
|
|
4652
|
+
/** 组装 md(spec §3):9 节固定骨架 + 确定性证据索引;缺证据的节写占位说明。 */
|
|
4653
|
+
function paperBuildMarkdown(o) {
|
|
4654
|
+
const sec = (o && o.sections) || {}
|
|
4655
|
+
const ev = ((o && o.evidence) || []).slice()
|
|
4656
|
+
const L = []
|
|
4657
|
+
const ttl = String((o && o.title) || '').trim() || '研究报告'
|
|
4658
|
+
L.push('# ' + ttl)
|
|
4659
|
+
L.push('')
|
|
4660
|
+
L.push('- 作者:' + String((o && o.author) || ''))
|
|
4661
|
+
L.push('- 日期:' + String((o && o.date) || ''))
|
|
4662
|
+
L.push('')
|
|
4663
|
+
for (let i = 0; i < PAPER_SKELETON.length; i++) {
|
|
4664
|
+
const key = PAPER_SKELETON[i].key
|
|
4665
|
+
L.push('## ' + key)
|
|
4666
|
+
L.push('')
|
|
4667
|
+
let body = String(sec[key] || '').trim()
|
|
4668
|
+
if (key === '摘要') { const ab = String((o && o.abstract) || '').trim(); if (ab) body = body ? (ab + '\n\n' + body) : ab }
|
|
4669
|
+
if (key === '附录:证据与文件索引' && ev.length) {
|
|
4670
|
+
const idx = '### ' + PAPER_EVIDENCE_HEADING + '\n\n' + ev.map(function (x) { return '- `' + String(x) + '`' }).join('\n')
|
|
4671
|
+
body = body ? (body + '\n\n' + idx) : idx
|
|
4672
|
+
}
|
|
4673
|
+
L.push(body || PAPER_NO_EVIDENCE)
|
|
4674
|
+
L.push('')
|
|
4675
|
+
}
|
|
4676
|
+
return L.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
|
|
4677
|
+
}
|
|
4678
|
+
/** paper.log.md 的**唯一格式**(v2/v3 逐字一致,spec §4)。 */
|
|
4679
|
+
function paperLogHead(id) {
|
|
4680
|
+
return ['# 论文写作日志|' + String(id), '',
|
|
4681
|
+
'> 单作者流程(v2/v3 同一格式):收口触发 → 派遣「论文撰写」子代理 → 落盘 md/tex → 编译 pdf。', '']
|
|
4682
|
+
}
|
|
4683
|
+
function paperLogLine(at, tag, text) { return '- ' + fmtTime(at) + ' [' + String(tag) + '] ' + String(text == null ? '' : text) }
|
|
4684
|
+
/** 论文目录 id:只允许安全字符(同一把归一化 ⇒ 不可能写出 Paper/ 之外,spec §6 越权用例)。 */
|
|
4685
|
+
function paperDirId(raw) { return (idSafe(String(raw == null ? '' : raw).trim()) || 'run') }
|
|
4686
|
+
/** 从回复里抽出可能存在的标题行(md 非契约路径的兜底)。 */
|
|
4687
|
+
function paperTitleFromMd(md) {
|
|
4688
|
+
const m = /^#\s+(.*?)\s*$/m.exec(String(md == null ? '' : md))
|
|
4689
|
+
return m ? m[1].trim() : ''
|
|
4690
|
+
}
|
|
4691
|
+
|
|
3668
4692
|
export const __testHelpers = {
|
|
3669
4693
|
uuid,
|
|
3670
4694
|
shortId,
|
|
@@ -3682,21 +4706,109 @@ export const __testHelpers = {
|
|
|
3682
4706
|
parseEntries,
|
|
3683
4707
|
section,
|
|
3684
4708
|
parseBodySections,
|
|
4709
|
+
findSectionHeads,
|
|
4710
|
+
escField,
|
|
4711
|
+
unescField,
|
|
4712
|
+
entryRe,
|
|
4713
|
+
probText,
|
|
3685
4714
|
extraBodySections,
|
|
3686
4715
|
parseAppTitle,
|
|
3687
4716
|
parseMethodMd,
|
|
3688
4717
|
sanitizeToolFilter,
|
|
3689
4718
|
registeredToolsFromError,
|
|
4719
|
+
// final paper(spec §6 的纯函数守卫面;与 v2 同构)
|
|
4720
|
+
PAPER_SKELETON,
|
|
4721
|
+
PAPER_NO_EVIDENCE,
|
|
4722
|
+
PAPER_ALLOWED_PACKAGES,
|
|
4723
|
+
PAPER_ENGINE_ORDER_ZH,
|
|
4724
|
+
PAPER_ENGINE_ORDER_EN,
|
|
4725
|
+
texEscape,
|
|
4726
|
+
paperMaterialSummary,
|
|
4727
|
+
paperWriterVerdict,
|
|
4728
|
+
paperInlineToTex,
|
|
4729
|
+
paperMdToTexBody,
|
|
4730
|
+
paperTexDoc,
|
|
4731
|
+
paperMinimalTexDoc,
|
|
4732
|
+
paperSanitizeTex,
|
|
4733
|
+
paperSkeletonKey,
|
|
4734
|
+
paperSkeletonFromReply,
|
|
4735
|
+
paperBuildMarkdown,
|
|
4736
|
+
paperLogHead,
|
|
4737
|
+
paperLogLine,
|
|
4738
|
+
paperDirId,
|
|
4739
|
+
paperTitleFromMd,
|
|
3690
4740
|
}
|
|
3691
4741
|
|
|
3692
4742
|
function now() { return Date.now() }
|
|
3693
4743
|
|
|
4744
|
+
/**
|
|
4745
|
+
* 工具描述的唯一来源(提示词审计 M3):v3 的每个工具都在**两条路径**上注册——会话 handler 表
|
|
4746
|
+
* (makeSession 内只存 handler)与文件末尾真正生效的 `tools.register`。两条路径必须逐字一致,
|
|
4747
|
+
* 否则模型看到的描述取决于哪条路径生效,任何一侧被改都会静默改变提示词面。
|
|
4748
|
+
* 全部 33 个工具都从此表取描述;`tests/audit-v3-registration-parity.mjs` 逐字比对两条路径。
|
|
4749
|
+
*/
|
|
4750
|
+
const TOOL_DESC = {
|
|
4751
|
+
vibe_math_start: 'Start (or restart) the Vibe Math V3 scheduler for the current project. override=true forcibly takes over the project lock held by another session (only after you verified that session is gone).',
|
|
4752
|
+
vibe_math_resume: 'Resume the Vibe Math V3 scheduler after a checkpoint/restart. override=true forcibly takes over the project lock held by another session (only after you verified that session is gone).',
|
|
4753
|
+
vibe_math_pause: 'Pause the scheduler (in-flight children finish their current turn).',
|
|
4754
|
+
vibe_math_abort: 'Abort the scheduler and interrupt all active children.',
|
|
4755
|
+
vibe_math_status: 'Show scheduler status, params, active agents, projects, and recent activity.',
|
|
4756
|
+
vibe_math_report: 'Return the full progress report and write it to Progress_Logs/report.json + Logs/报告.md.',
|
|
4757
|
+
vibe_math_set_mode: 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.',
|
|
4758
|
+
// set_params 的 schema 现在也收 mode(M10):同一能力既有专用工具 vibe_math_set_mode,也可用这个键;
|
|
4759
|
+
// 两条注册路径共用这一份描述,`--self-probe` 会证明漂移能被抓到。
|
|
4760
|
+
vibe_math_set_params: 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测 xelatex→latexmk→pdflatex→lualatex→tectonic),产物在 Paper/<项目>/。',
|
|
4761
|
+
vibe_math_setup: 'Return the interactive parameter schema for guided configuration.',
|
|
4762
|
+
vibe_math_save_settings: 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.',
|
|
4763
|
+
vibe_math_template: 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.',
|
|
4764
|
+
vibe_math_add_problem: 'Add a problem to the current project (creates Problems/<id>.md).',
|
|
4765
|
+
vibe_math_add_proposition: 'Add a proposition to Propos/ (creates Propos/<分类>/<id>.md).',
|
|
4766
|
+
vibe_math_list_propositions: 'List propositions from Propos/ (summary index: id, 标题, 概率, 状态, 优先级, 分类).',
|
|
4767
|
+
vibe_math_new_project: 'Create a new math project folder and switch to it.',
|
|
4768
|
+
vibe_math_set_project: 'Switch the current math project.',
|
|
4769
|
+
vibe_math_list_projects: 'List math projects.',
|
|
4770
|
+
vibe_math_list_decisions: 'List pending manual decisions.',
|
|
4771
|
+
vibe_math_decide: 'Resolve a pending manual decision (plan: approve|reject; verdict: override with verdict 1|0; spawn: approve|reject; method-promote: approve|reject).',
|
|
4772
|
+
vibe_math_list_agents: 'List tracked sub-agents (child sessions).',
|
|
4773
|
+
vibe_math_message_agent: 'Send a message to a tracked child agent (next turn).',
|
|
4774
|
+
vibe_math_interrupt_agent: 'Interrupt a tracked child agent.',
|
|
4775
|
+
vibe_math_plan: 'Show the queued plan / last plan result, or force a planning round.',
|
|
4776
|
+
vibe_math_index: 'Rebuild the machine index (State/index.json) from the Markdown knowledge base.',
|
|
4777
|
+
vibe_math_method_add: 'Manually add a method card to Methods/ (creates Methods/<id>.md).',
|
|
4778
|
+
vibe_math_method_list: 'List methods from Methods/ (+ global VibeMath/Methods/): id, 标题, 类型, 状态, 可信断言, applications count.',
|
|
4779
|
+
vibe_math_lock_status: 'Show the project lock occupancy.',
|
|
4780
|
+
vibe_math_claim_write: '(member) Acquire the write lock for one target file (relative to the project root). Call before writing a Markdown file directly; a file may only be written by ONE agent at a time. Returns the display path you may write (VibeMath/Projects/<project>/<target>) and a hint.',
|
|
4781
|
+
vibe_math_release_write: '(member) Release the write lock for one target file (relative to the project root). Call after you finished writing it.',
|
|
4782
|
+
vibe_math_sync_meta: '(member) After you write content into Markdown files, report ONLY lightweight scheduling metadata to keep the scheduler state in sync (content stays in the md files). meta.kind must be one of:\n- "directions": {qid, directions:[{id,title,method,core_assumption,feasibility}], methods_used:[{id,效果,建议}], new_inventions:[{类型,标题,内容描述,是否已入库}]}\n- "solver": {qid, dirId, round, survival, status:"continue|success|dead-end", dead_end_reason, lemmas:[{id,title,statement,proof,prob,价值/关键性,分类,优先级}], methods_used, new_inventions, solution_prob, solution_text, sub_questions:[{q_sub_title,q_sub_statement,assumption_title,assumption_statement}]}\n- "methods": {used:[{id,效果,建议}], created:[ids], improvements:[{id,改进内容,原因}]}',
|
|
4783
|
+
vibe_math_lean_run: "(member) Execute the Lean toolchain on one .lean file inside the workspace and report the result. Never throws: a missing toolchain returns LEAN_NOT_FOUND, a non-zero exit returns the compiler output. Pass target=<object id> to also record the run against that object.",
|
|
4784
|
+
vibe_math_lean_archive: '(member) Archive Lean code. kind="def": a REUSABLE definition/object/assumption → the global cross-project library (VibeMath/Formal/Lib). kind="lemma": a machine-checked lemma → VibeMath/Formal/Proved. kind="proof": the formal proof of a project object → Formal/<target>.lean, and (when the run passes) also Verified/Lean/<target>.lean, marking the object Lean-passed. kind="blocked": record an explicit, reasoned "cannot/not worth formalizing" decision (note required).',
|
|
4785
|
+
vibe_math_lean_lib: "(member) List (and by default rebuild) the Lean reuse library: this project's Formal/Index.md, plus the global cross-project Formal/Lib and Formal/Proved indexes. Look here BEFORE writing a new definition so you reuse instead of redefining.",
|
|
4786
|
+
}
|
|
4787
|
+
|
|
3694
4788
|
function uuid() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 36; i++) { if (i === 8 || i === 13 || i === 18 || i === 23) s += '-'; else s += h[Math.floor(Math.random() * 16)] } return s }
|
|
3695
4789
|
|
|
3696
4790
|
function shortId() { const h = '0123456789abcdef'; let s = ''; for (let i = 0; i < 8; i++) s += h[Math.floor(Math.random() * 16)]; return s }
|
|
3697
4791
|
|
|
3698
4792
|
function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
|
|
3699
4793
|
|
|
4794
|
+
/**
|
|
4795
|
+
* M11:同一个语义两套字段名。提示词教的是 `prob`(`lemmas[].prob`),而遗留扁平 JSON 通道
|
|
4796
|
+
* 用的是 `布尔估计`(`细类型` 同理)。这里统一取值顺序,两套拼写都接受,避免"按提示词回写的
|
|
4797
|
+
* 引理"被当成没有估计值。
|
|
4798
|
+
*/
|
|
4799
|
+
function lemmaProb(l) {
|
|
4800
|
+
if (l == null) return undefined
|
|
4801
|
+
if (l.prob != null) return l.prob
|
|
4802
|
+
if (l['布尔估计'] != null) return l['布尔估计']
|
|
4803
|
+
return undefined
|
|
4804
|
+
}
|
|
4805
|
+
function lemmaCategory(l) {
|
|
4806
|
+
if (l == null) return ''
|
|
4807
|
+
if (l['分类'] != null) return String(l['分类'])
|
|
4808
|
+
if (l['细类型'] != null) return String(Object.keys(l['细类型'] || { 未分类: {} })[0] || '未分类')
|
|
4809
|
+
return ''
|
|
4810
|
+
}
|
|
4811
|
+
|
|
3700
4812
|
function fmtTime(ts) { try { return new Date(ts || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return String(ts || '') } }
|
|
3701
4813
|
|
|
3702
4814
|
function slugify(s) { const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, ''); return t || 'project' }
|
|
@@ -3744,12 +4856,37 @@ function safeJson(v, fb) { if (v == null || v === '') return fb; try { return JS
|
|
|
3744
4856
|
function stripJsonComments(text) { let out = ''; let inStr = false; let inLine = false; let inBlock = false; let esc = false; for (let i = 0; i < text.length; i++) { const c = text[i]; const n = text[i + 1]; if (inLine) { if (c === '\n') { inLine = false; out += c } continue } if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++ } continue } if (inStr) { out += c; if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') inStr = false; continue } if (c === '"') { inStr = true; out += c; continue } if (c === '/' && n === '/') { inLine = true; i++; continue } if (c === '/' && n === '*') { inBlock = true; i++; continue } out += c } return out }
|
|
3745
4857
|
|
|
3746
4858
|
function splitHeader(text) {
|
|
3747
|
-
const
|
|
3748
|
-
const
|
|
3749
|
-
const
|
|
4859
|
+
const heads = findSectionHeads(text)
|
|
4860
|
+
const first = (heads.length > 0 && heads[0].from > 0) ? heads[0].from : -1
|
|
4861
|
+
const head = first === -1 ? String(text) : String(text).slice(0, first - 1)
|
|
4862
|
+
const body = first === -1 ? '' : String(text).slice(first)
|
|
3750
4863
|
return { head: head, body: body }
|
|
3751
4864
|
}
|
|
3752
4865
|
|
|
4866
|
+
/**
|
|
4867
|
+
* 顶层 `## ` 段落边界,**跳过围栏代码块**(审计 L4)。
|
|
4868
|
+
*
|
|
4869
|
+
* 此前 splitHeader / section / parseBodySections 都把正文里**任意** `## ` 行当段落边界。代理把
|
|
4870
|
+
* 引用或代码粘进「陈述」段时,围栏里的 `## ` 行会被当成新段落:那半段代码变成"额外的段",
|
|
4871
|
+
* 下一次 compose 还会给它补一个 `## ` 标题(正文被改写、代码块被劈开)。围栏内的 `## ` 不是边界。
|
|
4872
|
+
*/
|
|
4873
|
+
function findSectionHeads(text) {
|
|
4874
|
+
const out = []
|
|
4875
|
+
const src = String(text == null ? '' : text)
|
|
4876
|
+
const lines = src.split('\n')
|
|
4877
|
+
let pos = 0, fence = false
|
|
4878
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4879
|
+
const line = lines[i]
|
|
4880
|
+
if (/^\s*(```|~~~)/.test(line)) { fence = !fence; pos += line.length + 1; continue }
|
|
4881
|
+
if (!fence) {
|
|
4882
|
+
const m = /^##\s+(.+?)\s*$/.exec(line)
|
|
4883
|
+
if (m) out.push({ name: m[1].trim(), from: pos, len: line.length })
|
|
4884
|
+
}
|
|
4885
|
+
pos += line.length + 1
|
|
4886
|
+
}
|
|
4887
|
+
return out
|
|
4888
|
+
}
|
|
4889
|
+
|
|
3753
4890
|
function parseAnchors(head) {
|
|
3754
4891
|
const anchors = {}
|
|
3755
4892
|
const re = /^-\s*([A-Za-z\u4e00-\u9fa5/]+)\s*:\s*(.*)$/gm
|
|
@@ -3758,6 +4895,28 @@ function parseAnchors(head) {
|
|
|
3758
4895
|
return anchors
|
|
3759
4896
|
}
|
|
3760
4897
|
|
|
4898
|
+
/**
|
|
4899
|
+
* md 卡片是**逐行**格式:字段里出现换行会把标题行劈成两行(`parseAnchors` 的 `.*$` 与
|
|
4900
|
+
* `parseEntries` 的逐行扫描都不跨行),出现全角竖线 `|` 则会让
|
|
4901
|
+
* `### 解法 N|标题|概率X|状态Y` 的惰性匹配在**标题内部**提前切段(审计 L6)。
|
|
4902
|
+
* 约定:compose 侧转义 `\`、`|` 与换行(`\n`),parse 侧还原 ⇒ 往返无损,且惰性匹配不可能切进字段。
|
|
4903
|
+
*/
|
|
4904
|
+
function escField(v) { return String(v == null ? '' : v).replace(/\\/g, '\\\\').replace(/|/g, '\\|').replace(/\r?\n/g, '\\n') }
|
|
4905
|
+
function unescField(v) {
|
|
4906
|
+
const s = String(v == null ? '' : v)
|
|
4907
|
+
if (s.indexOf('\\') === -1) return s
|
|
4908
|
+
return s.replace(/\\(.)/g, function (_m, c) { return c === 'n' ? '\n' : c })
|
|
4909
|
+
}
|
|
4910
|
+
// 字段体:接受 `\\.` 转义对,但不接受裸 `|`/裸换行 ⇒ 惰性匹配只在真正的分隔符处停。
|
|
4911
|
+
const ENTRY_FIELD = '(?:[^|\\r\\n\\\\]|\\\\.)*?'
|
|
4912
|
+
/**
|
|
4913
|
+
* 条目标题行正则。概率段用 `[-+0-9.eE]+` 而非 `[0-9.]+`(审计 L3):指数记法(如 `概率5e-7`,
|
|
4914
|
+
* 来自 meta.solution_prob / l.prob 这类 ∈(0,1) 的数值)此前整条匹配失败,条目在重载后被静默丢弃。
|
|
4915
|
+
*/
|
|
4916
|
+
function entryRe(kind) { return new RegExp('^###\\s*' + kind + '\\s*\\d+|(' + ENTRY_FIELD + ')|概率([-+0-9.eE]+)|状态(' + ENTRY_FIELD + ')$') }
|
|
4917
|
+
/** 写出条目标题时的概率文本:非有限值(NaN/Infinity)绝不能写出去,否则那一行永远解析不回来。 */
|
|
4918
|
+
function probText(v) { const n = Number(v); return Number.isFinite(n) ? String(n) : '0.5' }
|
|
4919
|
+
|
|
3761
4920
|
function parseEntries(body, kindRe) {
|
|
3762
4921
|
const out = []
|
|
3763
4922
|
const lines = String(body).split('\n')
|
|
@@ -3783,23 +4942,24 @@ function parseEntries(body, kindRe) {
|
|
|
3783
4942
|
}
|
|
3784
4943
|
|
|
3785
4944
|
function section(body, name) {
|
|
3786
|
-
const
|
|
3787
|
-
const
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
4945
|
+
const text = String(body)
|
|
4946
|
+
const heads = findSectionHeads(text)
|
|
4947
|
+
let start = -1, end = -1
|
|
4948
|
+
for (let i = 0; i < heads.length; i++) {
|
|
4949
|
+
if (start === -1) { if (heads[i].name === name) start = heads[i].from + heads[i].len }
|
|
4950
|
+
else { end = heads[i].from; break }
|
|
4951
|
+
}
|
|
4952
|
+
if (start === -1) return ''
|
|
4953
|
+
return (end === -1 ? text.slice(start) : text.slice(start, end)).trim()
|
|
3792
4954
|
}
|
|
3793
4955
|
|
|
3794
4956
|
function parseBodySections(body) {
|
|
3795
4957
|
const text = String(body || '')
|
|
3796
|
-
const
|
|
4958
|
+
const heads = findSectionHeads(text)
|
|
3797
4959
|
const found = []
|
|
3798
|
-
let
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
const to = i + 1 < found.length ? found[i + 1].from : text.length
|
|
3802
|
-
found[i].text = text.slice(found[i].bodyFrom, to).trim()
|
|
4960
|
+
for (let i = 0; i < heads.length; i++) {
|
|
4961
|
+
const to = i + 1 < heads.length ? heads[i + 1].from : text.length
|
|
4962
|
+
found.push({ name: heads[i].name, from: heads[i].from, bodyFrom: heads[i].from + heads[i].len, text: text.slice(heads[i].from + heads[i].len, to).trim() })
|
|
3803
4963
|
}
|
|
3804
4964
|
return found
|
|
3805
4965
|
}
|
|
@@ -3812,8 +4972,12 @@ function extraBodySections(body, managedNames) {
|
|
|
3812
4972
|
|
|
3813
4973
|
function parseAppTitle(title) {
|
|
3814
4974
|
const t = String(title || '')
|
|
3815
|
-
|
|
3816
|
-
|
|
4975
|
+
// M7:字段名后**必须**有空白才取值为字段,且值里不许含分隔符 `|`。
|
|
4976
|
+
// 旧写法 `/问题\s*(\S+)/` 在"问题为空、方向非空"时把分隔符也吃进去:
|
|
4977
|
+
// `应用 1|<时间>|问题 |方向 d1` → 问题='|方向'(composeMethodMd 恰好会写出这种标题),
|
|
4978
|
+
// 下一次回写就把这个脏值固化进方法卡。
|
|
4979
|
+
const mQ = /(?:^|[\s|])问题[ \t]+([^|\s]+)/.exec(t)
|
|
4980
|
+
const mD = /(?:^|[\s|])方向[ \t]+([^|\s]+)/.exec(t)
|
|
3817
4981
|
// `at` = 第一段(时间戳)。取第一个 '|' 之前的部分。
|
|
3818
4982
|
const at = t.split('|')[0].trim()
|
|
3819
4983
|
return { at: at, 问题: mQ ? mQ[1] : '', 方向: mD ? mD[1] : '' }
|
|
@@ -3828,8 +4992,13 @@ function parseMethodMd(id, text) {
|
|
|
3828
4992
|
})
|
|
3829
4993
|
// 改进历史必须真正解析回来:此前硬编码 [] 导致每次 compose 都写成占位符,
|
|
3830
4994
|
// 于是"下一次用到该方法"就把代理沉淀的改进历史静默销毁(违反「正文只追加,不覆盖」)。
|
|
4995
|
+
//
|
|
4996
|
+
// H3:原因必须从**标题行独立解析**,不能借道 parseEntries 的槽位。约定里 m[1]=标题段、
|
|
4997
|
+
// m[2]=概率段、m[3]=状态段,而改进标题 `### v3(<原因>)` 只有两个捕获组:m[2] 是原因、
|
|
4998
|
+
// m[3] 是 undefined ⇒ `e.status` 恒为 '',每次回写都写成 `### v1()`(原因永久丢失)。
|
|
3831
4999
|
const improvements = parseEntries(body, /^###\s*v(\d+)\s*((.*?))\s*$/).map(function (e) {
|
|
3832
|
-
|
|
5000
|
+
const m = /^###\s*v(\d+)\s*((.*?))\s*$/.exec(e.heading)
|
|
5001
|
+
return { v: m ? Number(m[1]) : 0, 原因: m ? m[2] : '', text: e.text }
|
|
3833
5002
|
})
|
|
3834
5003
|
return {
|
|
3835
5004
|
id: id, 标题: a['标题'] || id, 类型: a['类型'] || '方法', 状态: a['状态'] || '经验',
|