dsh-vibe-math 2.7.1 → 2.7.3
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 +682 -690
- package/README.md +19 -27
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +744 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +588 -588
- package/docs/math-computation.md +194 -163
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +49 -0
- package/docs/release-notes/README.md +48 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- package/docs/release-notes/TEMPLATE.md +126 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +157 -154
- package/installer.js +27 -17
- package/package.json +55 -3
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +305 -113
- package/prompt-corpus-v2/formal-verify-v2.md +2399 -279
- package/prompt-corpus-v3/formal-verify-v3.json +134 -110
- package/prompt-corpus-v3/formal-verify-v3.md +409 -141
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +513 -259
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +52 -4
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +107 -9
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +6 -4
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +14 -0
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +63 -22
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +50 -0
- package/tests/audit-prompt-invariants.mjs +43 -5
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +300 -13
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +15 -8
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +566 -12
- package/tests/formal-verify-v2.test.mjs +91 -5
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +85 -6
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +628 -9
- package/tests/formal-verify-v5.test.mjs +68 -9
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +396 -2
- package/tests/math-computation-v2.test.mjs +767 -0
- package/tests/math-computation-v3.test.mjs +759 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +13 -2
- package/tests/run-tests.mjs +139 -9
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +53 -1
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/v2-fix-probes.mutants.mjs +100 -0
- package/tests/v2-fix-probes.test.mjs +1039 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v3-fix-probes.mutants.mjs +95 -0
- package/tests/v3-fix-probes.test.mjs +891 -0
- package/tests/v5-institute-fixes.mutants.mjs +190 -0
- package/vibe-math-v2/agent.cordis.yml +62 -6
- package/vibe-math-v2/math-computation.js +229 -29
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/vibe-math-v2.js +458 -119
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
- package/vibe-math-v3/agent.cordis.yml +72 -6
- package/vibe-math-v3/math-computation.js +229 -29
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/vibe-math-v3.js +482 -139
- package/vibe-math-v4/agent.cordis.yml +44 -8
- package/vibe-math-v4/math-computation.js +229 -29
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/vibe-math-v4.js +245 -52
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +229 -29
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1307 -243
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1655
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -93,6 +93,11 @@ export function apply(ctx) {
|
|
|
93
93
|
// automatic parent creation). Reading on demand removes that dependency on mount order.
|
|
94
94
|
const subprocessOf = () => { try { return ctx.get('subprocess') } catch(e){ return undefined } }
|
|
95
95
|
const sandboxPolicyOf = () => { try { return ctx.get('sandboxPolicy') } catch(e){ return undefined } }
|
|
96
|
+
// G-7 invariant (class guard): AGENT-FACING compaction MUST take the agent-local instance via
|
|
97
|
+
// `compactionForAgent(agent)` - this preset mounts its compaction row inside the preset's `compaction`
|
|
98
|
+
// isolate realm, and a realm NEVER falls back to the host-root instance. Only NON-agent sites may use
|
|
99
|
+
// `compactionOf()`. Guarded by the text-level assertion in tests/formal-verify-v4.test.mjs.
|
|
100
|
+
|
|
96
101
|
const compactionOf = () => { try { return ctx.get('compaction') } catch(e){ return undefined } }
|
|
97
102
|
|
|
98
103
|
const sessions = new Map() // rootAgentId -> Session
|
|
@@ -255,6 +260,11 @@ export function apply(ctx) {
|
|
|
255
260
|
}
|
|
256
261
|
let running = false, autoDone = false, phase = 'idle'
|
|
257
262
|
let residents = new Map(), mailboxes = new Map(), taskboard = [], decisions = []
|
|
263
|
+
// round-9 (P2/D4): the participant set is frozen per verify/meeting together with this version, so
|
|
264
|
+
// every view reads the SAME roster instead of re-deriving it from the live map (mid-flight
|
|
265
|
+
// addMember/removeMember is possible: removeMember reconciles in-flight work, addMember has no
|
|
266
|
+
// verify/meeting gate).
|
|
267
|
+
let rosterVersion = 0
|
|
258
268
|
let meetings = [], reports = [], activityLog = []
|
|
259
269
|
let problemText = '', problemId = 'problem', runId = 'run-' + shortId()
|
|
260
270
|
// Lean formalization records, keyed by object id. v4 has no session-log projection, so this
|
|
@@ -334,8 +344,62 @@ export function apply(ctx) {
|
|
|
334
344
|
// escape the project tree. Keep every harmless character (incl. Chinese) and replace only
|
|
335
345
|
// separators/control chars; strip leading/trailing dots/dashes so the name is never '.'/'..'.
|
|
336
346
|
let warnedNoPolicy = false
|
|
347
|
+
let warnedDeferFormalTodo = false // F-6: name a swallowed deferForFormal failure once, WITH its consequence
|
|
348
|
+
let warnedIndexRebuild = false // F-6: name a swallowed settleLeanJob failure once, WITH its consequence
|
|
349
|
+
let warnedCompaction = false // F-6: name a swallowed realCompact failure once, WITH its consequence
|
|
350
|
+
let warnedWriteFormalTodo = false // F-6 (1/4): name a swallowed verify-conclusion write once, WITH its consequence
|
|
351
|
+
let warnedContextProbe = false // F-6 (:786): name a failed build-context comparison once (unknown != unchanged)
|
|
352
|
+
let warnedPaperWrite = false // G3: name a paper whose required artifact is missing after finalize (once)
|
|
353
|
+
let warnedMkdir = false // F-4c: name a failed mkdir ONCE (the deferred/distorted-diagnosis fix)
|
|
354
|
+
let warnedFenceDrift = false // F-5: name a resolved fence root that is NOT this session workspace (once)
|
|
355
|
+
let warnedCurrentProject = false // F-6: name a failed `.current` marker write once, WITH its consequence
|
|
337
356
|
function warnNoPolicyOnce(){ if(!warnedNoPolicy){ warnedNoPolicy=true; console.error('vibe-math-v4: sandboxPolicy unavailable; writes go out with no explicit policy') } }
|
|
338
|
-
function getPolicy(){ const sp=sandboxPolicyOf(); if(!sp){ warnNoPolicyOnce(); return undefined } try { if(rootAgent&&rootAgent.session)
|
|
357
|
+
function getPolicy(){ const sp=sandboxPolicyOf(); if(!sp){ warnNoPolicyOnce(); return undefined } try { if(rootAgent&&rootAgent.session){ const p=sp.resolve({session:rootAgent.session}); fenceRootDriftNote(p); return p } } catch(e){ warnNoPolicyOnce() } try { const p=sp.resolve({}); if(!warnedNoPolicy){ warnedNoPolicy=true; console.error('vibe-math-v4: falling back to sandboxPolicy.resolve({}) — the fence root is the host-configured workspace, not necessarily this session cwd') } return p } catch(e){ warnNoPolicyOnce() } return undefined }
|
|
358
|
+
/**
|
|
359
|
+
* F-5 (R9 ruling): the SESSION-scoped policy can resolve to a fence root that does not CONTAIN
|
|
360
|
+
* this session workspace - the host silently widened or re-homed the write fence. Shape mirrored
|
|
361
|
+
* from v3 (policyRootOf / warnFenceDriftOnce / checkFenceRoot returning p), with the conservative
|
|
362
|
+
* predicate the probe requires:
|
|
363
|
+
* - containment, never equality: an ANCESTOR root is a legitimate configuration, so equality
|
|
364
|
+
* alone would report normal setups as drift (once per session);
|
|
365
|
+
* - an array of roots (list-style fences) => accept when the workspace is inside ANY element;
|
|
366
|
+
* - no root field / non-string / an object shape we cannot interpret (e.g. {allow,deny}) =>
|
|
367
|
+
* NO-OP: today's behaviour. The live probe could not read the real host resolve() shape (no
|
|
368
|
+
* service is reachable from a plugin row in this sandbox), so we never guess - the no-op path
|
|
369
|
+
* is asserted positively in tests instead;
|
|
370
|
+
* - the `resolve({})` fallback is NEVER compared (it already has its own named warning and its
|
|
371
|
+
* root is by definition not this session cwd => comparing it could only add noise);
|
|
372
|
+
* - normalisation: separators -> "/", trailing slashes stripped, case-folded ONLY on win32 (on a
|
|
373
|
+
* case-sensitive host folding would hide a real drift = a false negative).
|
|
374
|
+
* Zero behaviour change: no refusal, and getPolicy() still returns the resolved policy.
|
|
375
|
+
*/
|
|
376
|
+
function policyRootCandidatesOf(p){
|
|
377
|
+
try {
|
|
378
|
+
if(!p || typeof p !== 'object') return []
|
|
379
|
+
let pick
|
|
380
|
+
if(p.workspaceRoot !== undefined) pick = p.workspaceRoot
|
|
381
|
+
else if(p.root !== undefined) pick = p.root
|
|
382
|
+
else if(p.cwd !== undefined) pick = p.cwd
|
|
383
|
+
else if(p.config && p.config.workspaceRoot !== undefined) pick = p.config.workspaceRoot
|
|
384
|
+
if(Array.isArray(pick)) return pick.filter(x => typeof x === 'string' && x)
|
|
385
|
+
return (typeof pick === 'string' && pick) ? [pick] : []
|
|
386
|
+
} catch(e) { return [] }
|
|
387
|
+
}
|
|
388
|
+
function fenceRootDriftNote(p){
|
|
389
|
+
try {
|
|
390
|
+
const cands = policyRootCandidatesOf(p)
|
|
391
|
+
if(!cands.length) return // no comparable root => documented NO-OP (asserted in tests)
|
|
392
|
+
const norm = (x) => { let v = String(x).replace(/\\/g, '/'); if(v.length > 1) v = v.replace(/\/+$/, ''); return process.platform === 'win32' ? v.toLowerCase() : v }
|
|
393
|
+
const session = norm(workspaceRoot())
|
|
394
|
+
if(!session || session === '.') return
|
|
395
|
+
const contained = cands.some((c) => { const r = norm(c); return !!r && (session === r || session.indexOf(r + '/') === 0) })
|
|
396
|
+
if(contained) return
|
|
397
|
+
if(!warnedFenceDrift){
|
|
398
|
+
warnedFenceDrift = true
|
|
399
|
+
console.error('vibe-math-v4: sandbox fence root does not CONTAIN this session workspace (resolved="' + cands.join('", "') + '", session="' + workspaceRoot() + '") - writes may be fenced away from the session workspace; boundary: a policy exposing no interpretable root field cannot be compared')
|
|
400
|
+
}
|
|
401
|
+
} catch(e) { /* a diagnostic must never break policy resolution */ }
|
|
402
|
+
}
|
|
339
403
|
function psQuote(p){ return "'"+String(p).replace(/'/g,"''")+"'" }
|
|
340
404
|
/** POSIX 单引号引用:把 ' 换成 '\'' 以安全嵌入任意路径。 */
|
|
341
405
|
function shQuote(p){ return "'"+String(p).replace(/'/g,"'\\''")+"'" }
|
|
@@ -435,12 +499,36 @@ export function apply(ctx) {
|
|
|
435
499
|
// project tree, NOT inside it: cross-project reuse is the whole point (spec §3). It is
|
|
436
500
|
// created here so the first `lean_archive kind='def'` never has to invent its parent.
|
|
437
501
|
const globalDirs=['Formal/Lib','Formal/Proved']
|
|
438
|
-
|
|
502
|
+
const res = await runShell(mkdirCmd([vibeRoot()+'/Projects'].concat(dirs.map(d=>base+'/'+d)).concat(globalDirs.map(d=>vibeRoot()+'/'+d))))
|
|
503
|
+
// F-4c: ensureDirs() returns the shell result and never throws, so a failed mkdir used to surface only
|
|
504
|
+
// later and as a DISTORTED diagnosis ('write failed' instead of 'mkdir failed, and where'). Name it here,
|
|
505
|
+
// once, through the same channel as warnNoPolicyOnce() (deferred/distorted diagnosis -> named warning).
|
|
506
|
+
try {
|
|
507
|
+
const _code = res && (res.exitCode !== undefined ? res.exitCode : res.code)
|
|
508
|
+
if (_code !== undefined && Number(_code) !== 0 && !warnedMkdir) {
|
|
509
|
+
warnedMkdir = true
|
|
510
|
+
console.error('vibe-math-v4: ensureDirs: mkdir failed (exit ' + _code + ') under ' + vibeRoot() + ' - later writes into a missing directory will fail; check the shell/policy')
|
|
511
|
+
}
|
|
512
|
+
} catch(e) { /* best effort: the warning must never break ensureDirs */ }
|
|
513
|
+
return res
|
|
439
514
|
}
|
|
440
515
|
async function readTextAbs(path){ try { const t=await fs.resolve(path); const s=await fs.stat(t); if(s===undefined) return undefined; return await fs.readText(t) } catch(e){ return undefined } }
|
|
441
516
|
async function writeTextAbs(path,content){ try { const t=await fs.resolve(path); await fs.writeText(t,content,undefined,undefined,getPolicy()); return true } catch(e){ return false } }
|
|
442
517
|
async function readCurrentProject(){ try { const t=await readTextAbs(vibeRoot()+'/.current'); if(t) return String(t).trim() } catch(e){} return currentProject }
|
|
443
|
-
|
|
518
|
+
// F-6 boundaries MEASURED on this revision (recorded, not fixed here - both would need a readText
|
|
519
|
+
// failure signal and therefore touch as many callers as writeTextAbs, which the lead deferred):
|
|
520
|
+
// - readCurrentProject(): a failed READ is swallowed inside readTextAbs (:514), so this function
|
|
521
|
+
// cannot tell "read failed" from "no marker"; its silent fallback stays.
|
|
522
|
+
// - assertWritable()'s outer `catch(e){}` (:491) CANNOT FIRE, because readText (:438) has already
|
|
523
|
+
// erased the failure: it swallows every error and returns undefined, so a present-but-unreadable
|
|
524
|
+
// file is treated as MISSING and the overwrite guard silently allows the write. The silent skip
|
|
525
|
+
// therefore happens ONE LAYER UP, not in the catch. Surfacing it means making readText REPORT
|
|
526
|
+
// failure - the same breadth as :515 writeTextAbs, which the lead deferred.
|
|
527
|
+
// - writeCurrentProject()'s own `catch` branch is likewise DEFENSIVE-ONLY: writeTextAbs never throws
|
|
528
|
+
// (it catches internally and returns a boolean), so the `ok===false` branch is the only reachable
|
|
529
|
+
// one; the exception path is kept as belt-and-braces and is recorded as unreachable (measured: a
|
|
530
|
+
// mutant that removes it cannot redden the N18 assertion, so it ships without a family entry).
|
|
531
|
+
async function writeCurrentProject(){ try { const ok=await writeTextAbs(vibeRoot()+'/.current', currentProject); if(ok===false && !warnedCurrentProject){ warnedCurrentProject=true; console.error('vibe-math-v4: the current-project marker was NOT persisted - the next session may load the previous project') } } catch(e){ if(!warnedCurrentProject){ warnedCurrentProject=true; console.error('vibe-math-v4: the current-project marker was NOT persisted - the next session may load the previous project') } } }
|
|
444
532
|
|
|
445
533
|
// ================= Lean formal verification ==============================
|
|
446
534
|
// Contract: docs/formal-verification.md (shared by v2/v3/v4/v5).
|
|
@@ -758,10 +846,16 @@ export function apply(ctx) {
|
|
|
758
846
|
// the job was in flight, RECORD that the current context differs (the compile itself was done
|
|
759
847
|
// with the recorded context, which is what the job id attests to).
|
|
760
848
|
let contextChanged=false
|
|
761
|
-
try {
|
|
849
|
+
try {
|
|
850
|
+
const nowCtx=await leanBuildContext()
|
|
762
851
|
contextChanged=JSON.stringify([nowCtx.engine,nowCtx.argv,nowCtx.searchPaths])!==JSON.stringify([(job.build||{}).engine,(job.build||{}).argv,(job.build||{}).searchPaths])
|
|
763
|
-
|
|
764
|
-
|
|
852
|
+
} catch(e){
|
|
853
|
+
// F-6 (:786): a comparison that could NOT run must not be recorded as "unchanged" -
|
|
854
|
+
// unknown != unchanged. Record null AND name it once, with the consequence.
|
|
855
|
+
contextChanged=null
|
|
856
|
+
if(!warnedContextProbe){ warnedContextProbe=true; console.error('vibe-math-v4: settleLeanJob: the build-context comparison could not be performed - contextChanged=false is NOT a measurement (recorded as null/unknown)') }
|
|
857
|
+
}
|
|
858
|
+
if(contextChanged===true) logActivity('formal','作业 '+job.jobId+' 的结果属于记录中的构建上下文(engine/argv/search-paths 已被改动,未影响该次编译的结论)')
|
|
765
859
|
const ok=!!(run&&run.ok)&&hashMatch
|
|
766
860
|
const timedOut=!!(run&&run.timedOut)
|
|
767
861
|
const state=ok?'settled':(timedOut?'timeout':'failed')
|
|
@@ -796,7 +890,7 @@ export function apply(ctx) {
|
|
|
796
890
|
}
|
|
797
891
|
}
|
|
798
892
|
await writeLeanJob(settled)
|
|
799
|
-
try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ }
|
|
893
|
+
try { await rebuildLeanLibIndexes() } catch(e){ if(!warnedIndexRebuild){ warnedIndexRebuild=true; console.error('vibe-math-v4: settleLeanJob: rebuildLeanLibIndexes failed - the library indexes are STALE; residents may reuse a stale index until the next successful rebuild') } /* best-effort */ }
|
|
800
894
|
pushLeanNotice(settled,Object.assign({},run,{ok,hashMatch}))
|
|
801
895
|
return settled
|
|
802
896
|
}
|
|
@@ -851,6 +945,9 @@ export function apply(ctx) {
|
|
|
851
945
|
* §2.6 crash/restart recovery: scan `<project>/Formal/Jobs/*.json` AFTER the project dirs exist.
|
|
852
946
|
* It NEVER mints a `passed` anything unless the job record itself says exitCode===0 AND the file
|
|
853
947
|
* on disk still hashes to the enqueued content.
|
|
948
|
+
* INVARIANT (F2): after redriving, the recovery DRAINS the queue itself (`await runLeanQueue()`) and only
|
|
949
|
+
* then arms one heartbeat - it must NOT rely on an already-armed heartbeat. Deleting that drain silently
|
|
950
|
+
* restores the cold-recovery stall (see the §20 tripwire in tests/formal-verify-v4.test.mjs).
|
|
854
951
|
*/
|
|
855
952
|
async function recoverLeanJobs(opts){
|
|
856
953
|
const o=opts||{}
|
|
@@ -903,7 +1000,7 @@ export function apply(ctx) {
|
|
|
903
1000
|
}
|
|
904
1001
|
}
|
|
905
1002
|
}
|
|
906
|
-
if(queued+redriven+stale+completed){ await writeLeanLibIndexesSafe() ; if(leanQueue.length) armHeartbeat() }
|
|
1003
|
+
if(queued+redriven+stale+completed){ await writeLeanLibIndexesSafe() ; if(leanQueue.length){ await runLeanQueue() ; armHeartbeat() } } // cold-recovery drain: the heartbeat alone did not drain while the run was not yet live (verified by the §20 tripwire)
|
|
907
1004
|
return {scanned:(files||[]).length,queued,redriven,stale,completed}
|
|
908
1005
|
}
|
|
909
1006
|
async function writeLeanLibIndexesSafe(){ try { await rebuildLeanLibIndexes() } catch(e){ /* best-effort */ } }
|
|
@@ -1182,15 +1279,23 @@ export function apply(ctx) {
|
|
|
1182
1279
|
return rows
|
|
1183
1280
|
}
|
|
1184
1281
|
const libRows=await scan(formalLibRoot(),'Formal/Lib','def')
|
|
1185
|
-
await writeTextAbs(formalLibRoot()+'/Index.md',['# 可复用 Lean 定义库(跨项目)|'+currentProject,'',
|
|
1282
|
+
const _idxLibOk = await writeTextAbs(formalLibRoot()+'/Index.md',['# 可复用 Lean 定义库(跨项目)|'+currentProject,'',
|
|
1186
1283
|
'> 写新定义之前先查这里:能复用就不要重新定义。复用方式:`import Formal.Lib.<名称>`(模块根 = VibeMath 根,框架已加入编译搜索路径),或用 vibe_v4_lean_read 取原文逐字复制。','',
|
|
1187
1284
|
'| 名称 | 文件 | 类别 | 依赖 | 摘要 |','|---|---|---|---|---|']
|
|
1188
1285
|
.concat(libRows.length?libRows:['| (暂无) | | | | |']).join('\n')+'\n')
|
|
1189
1286
|
const provedRows=await scan(formalProvedRoot(),'Formal/Proved','lemma')
|
|
1190
|
-
await writeTextAbs(formalProvedRoot()+'/Index.md',['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|'+currentProject,'',
|
|
1287
|
+
const _idxProvedOk = await writeTextAbs(formalProvedRoot()+'/Index.md',['# 已成立的 Lean 命题 / 引理(机器已核对,可跨项目复用)|'+currentProject,'',
|
|
1191
1288
|
'> 这些文件是通过内核检查的引理,可直接 `import Formal.Proved.<名称>` 复用(模块根 = VibeMath 根)。','',
|
|
1192
1289
|
'| 名称 | 文件 | 类别 | 依赖 | 陈述 |','|---|---|---|---|---|']
|
|
1193
1290
|
.concat(provedRows.length?provedRows:['| (暂无) | | | | |']).join('\n')+'\n')
|
|
1291
|
+
// ③ MEASURED: these two writes ignored `writeTextAbs`'s boolean (it swallows and returns
|
|
1292
|
+
// false), so a failed index write looked like success - and that silence propagated to ALL SIX
|
|
1293
|
+
// callers (settleLeanJob, writeLeanLibIndexesSafe, the three leanArchive paths, the lean_lib
|
|
1294
|
+
// tool). Naming it HERE, once, covers every caller (same shape as ensureDirs/warnedMkdir).
|
|
1295
|
+
if((_idxLibOk === false || _idxProvedOk === false) && !warnedIndexRebuild){
|
|
1296
|
+
warnedIndexRebuild = true
|
|
1297
|
+
console.error('vibe-math-v4: rebuildLeanLibIndexes: the Lean library index could NOT be written (lib=' + _idxLibOk + ' proved=' + _idxProvedOk + ') - the index is STALE until the next successful rebuild, so reuse lookups may miss entries')
|
|
1298
|
+
}
|
|
1194
1299
|
await writeFormalIndex()
|
|
1195
1300
|
await writeFormalTodo()
|
|
1196
1301
|
return {lib:libRows.length,proved:provedRows.length,objects:Object.keys(formalRecords()).length}
|
|
@@ -1550,23 +1655,24 @@ export function apply(ctx) {
|
|
|
1550
1655
|
async function inboxText(rId){ const mb=mailboxes.get(rId)||[]; if(mb.length===0) return ' (no new messages)\n'; return mb.map(m=>' ['+senderLabel(m.from)+'] '+m.content).join('\n')+'\n' }
|
|
1551
1656
|
function residentLibraries(){
|
|
1552
1657
|
const base=frameworkRoot()
|
|
1553
|
-
return '
|
|
1658
|
+
return '你的资料库根目录(**绝对前缀**,下面每条都已带库根):'+base+'/\n'
|
|
1554
1659
|
+' Progress/<你>/progress.md —— 你的研究日志(叙述,可追加。主要内容是尝试过的各方法、路线、历程、进度,当前研究进展/进度、将来的计划与打算,及各路线、过程中遇到的障碍及其原因,对各路线、方法的看法、可行性评估,自己研究过程中的一些有价值看法、感想、猜想、理解。以及其它各种你认为有价值的值得记录的事物、经验、方法/想法、创新等都可进行记录)。\n'
|
|
1555
1660
|
+' Propos/<你>/<id>.md —— 你的命题/引理。格式:\n'
|
|
1556
1661
|
+' - ID: p-<id>; - 状态: 未定论; - 概率: <0-1>; - 价值程度: <0-1>; - 动机用途计划: <为何重要/打算怎么用>\n'
|
|
1557
1662
|
+' 然后 ## 陈述 <陈述>;## 证明尝试;## 证伪尝试。\n'
|
|
1558
1663
|
+' Methods/<你>/<id>.md —— 你的理论/方法/工具。格式:- ID: m-<id>; - 状态: 经验; - 可信断言: []; - 价值程度: <0-1>; - 动机用途计划: ...;然后 ## 核心内容;## 定义与记号;## 应用记录;## 改进历史。\n'
|
|
1559
1664
|
+' Subproblems/<你>/<id>.md —— 你的子问题。格式:- ID: s-<id>; - 状态: 求解中; - 价值程度: <0-1>; - 动机用途计划: ...;然后 ## 陈述;## 进度。\n'
|
|
1665
|
+
+' ⚠ 你的文件工具(读/写)按**会话 cwd** 解析路径:上面列出的相对子路径都要先拼上**成员库根** '+base+'/ 再使用(例如 '+base+'/Propos/r-1/p-1.md);计算产物同理——请用回执里的**绝对**字段 receipt.scriptAbs,或把 receipt.cwd 与 receipt.scriptPath 拼起来,不要用相对路径。\n'
|
|
1560
1666
|
}
|
|
1561
1667
|
function toolList(){
|
|
1562
1668
|
return 'vibe_v4_send_message {to, content} —— 给某常驻发消息(to=all 广播)。\n'
|
|
1563
1669
|
+'vibe_v4_meeting {agenda} —— 发起/参与会议(框架会把各常驻的实际 input 转给其他人,让大家看到并讨论/辩论)。\n'
|
|
1564
1670
|
+'vibe_v4_propose_task/claim_task/task_done/list_tasks —— 共享任务板(提议/认领/完成/查看;任务板是你们协调分工的载体)。\n'
|
|
1565
|
-
+'vibe_v4_publish_progress/record_proposition/record_method/record_subproblem —— 便捷记录器(可选;推荐直接用 fs
|
|
1671
|
+
+'vibe_v4_publish_progress/record_proposition/record_method/record_subproblem —— 便捷记录器(可选;推荐直接用 fs 写自己的文件,路径必须带库根 )。\n'
|
|
1566
1672
|
+'vibe_v4_read_progress {id} —— 只读某常驻的进展。\n'
|
|
1567
1673
|
+'vibe_v4_list_residents / vibe_v4_list_tasks —— 查看团队组成 / 开放任务。\n'
|
|
1568
1674
|
+'vibe_v4_report_context {pct} —— 上报上下文占比(框架据此压缩你的上下文)。\n'
|
|
1569
|
-
+'fs (read/write/list) —— 读取任意文件;写入你自己的文件(推荐直接用 fs 直接写自己的 md
|
|
1675
|
+
+'fs (read/write/list) —— 读取任意文件;写入你自己的文件(推荐直接用 fs 直接写自己的 md;**路径必须带库根 ,否则会落到别处**)。\n'
|
|
1570
1676
|
}
|
|
1571
1677
|
// A shared, complete context block so a resident always knows the situation: mission,
|
|
1572
1678
|
// work model, what it can do, which files it owns (+ formats), what others' files are,
|
|
@@ -1582,14 +1688,14 @@ export function apply(ctx) {
|
|
|
1582
1688
|
s.push('### 工作模式(会发生什么)')
|
|
1583
1689
|
s.push('1. 每人有一份持久、全组可见的专属资料库(见下)。')
|
|
1584
1690
|
s.push('2. 你们自由发消息、开会;**会议会把每个人实际说的话(input)转给其他人**,让你看得到、能回复、能讨论、能辩论。')
|
|
1585
|
-
s.push('3. 你独立研究,并**直接用 fs
|
|
1691
|
+
s.push('3. 你独立研究,并**直接用 fs 写入你自己的文件**(**路径必须带库根 **,按格式),供全组阅读。')
|
|
1586
1692
|
s.push('4. 任何"已确立"的东西须**全组一致**验证(全真或全假)才作数;否则只是带概率的工作估计。')
|
|
1587
1693
|
s.push('5. 只有**全组在会议上一致认为原问题已解决**,run 才停止。')
|
|
1588
1694
|
s.push('')
|
|
1589
1695
|
s.push('### 你负责的文件(你只写自己的;但可读任何人的)')
|
|
1590
1696
|
s.push(residentLibraries())
|
|
1591
1697
|
s.push('其他人把结论/进展写进他们的目录,你就能读到。**你应主动读别人的库**,对齐事实、彼此衔接、避免重复劳动。')
|
|
1592
|
-
if(level!=='full'){ s.push('(格式见你最初的说明;直接用 fs
|
|
1698
|
+
if(level!=='full'){ s.push('(格式见你最初的说明;直接用 fs 写自己的文件即可,**路径必须带库根 **。)') }
|
|
1593
1699
|
s.push('')
|
|
1594
1700
|
s.push('### 可用工具')
|
|
1595
1701
|
s.push(toolList())
|
|
@@ -1628,7 +1734,7 @@ export function apply(ctx) {
|
|
|
1628
1734
|
+(r.direction?('\n\n你被建议的初始方向(可自行调整/细化):\n'+r.direction+'\n'):'')
|
|
1629
1735
|
+'## 这是你的第一轮:独立头脑风暴\n'
|
|
1630
1736
|
+'独立地想清楚:你对这个问题的洞察 / 解决方向 / 关键子问题 / 可能的引理 / 粗略计划。你还未见到其他人,先独立产出。\n'
|
|
1631
|
-
+'把有价值的产物**直接用 fs
|
|
1737
|
+
+'把有价值的产物**直接用 fs 写进你自己的文件**(**路径带库根 **,按上面格式),并在 summary 里概述你的切入方向与初步结论(标注哪些是猜想、哪些凭你已确证)。\n'
|
|
1632
1738
|
+'Reply with ONLY a JSON object:\n'
|
|
1633
1739
|
+'{"summary":"<your insight / direction / rough plan, one tight paragraph>","solved":false}'
|
|
1634
1740
|
}
|
|
@@ -1640,7 +1746,7 @@ export function apply(ctx) {
|
|
|
1640
1746
|
await refreshMathAvailability()
|
|
1641
1747
|
const mathAvail=mathAvailabilityBlock()
|
|
1642
1748
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
1643
|
-
+'Resident researcher '+r.rId+' — 第 '+r.rounds+' 轮。一切由你和团队讨论决定。动手前先**读别人的库**对齐事实、避免重复;把新进展/结论**直接用 fs
|
|
1749
|
+
+'Resident researcher '+r.rId+' — 第 '+r.rounds+' 轮。一切由你和团队讨论决定。动手前先**读别人的库**对齐事实、避免重复;把新进展/结论**直接用 fs 写进你自己的文件**(**路径带库根 **);想对团队说的话放 "input"(会转给其他常驻)。\n'
|
|
1644
1750
|
+'\n团队成员:\n'+banner()+'\n'
|
|
1645
1751
|
+'New items:\n'+ (await inboxText(r.rId)) +'\n'
|
|
1646
1752
|
// Settled background compiles are announced ONCE, in the next prompt constructed (spec §2.4):
|
|
@@ -1659,8 +1765,8 @@ export function apply(ctx) {
|
|
|
1659
1765
|
const prior=Object.entries(st.inputs).filter(([k])=>k!==r.rId).map(([k,iv])=>' ['+k+'] '+String(iv.input||iv.summary||'')).join('\n')
|
|
1660
1766
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
1661
1767
|
+'Resident '+r.rId+' — 团队会议进行中。 A meeting is in progress (agenda: '+st.agenda+').'
|
|
1662
|
-
+'\n这是一场真实讨论:下面已有人发言(转给你),请先看,然后**加入讨论/补充/反驳/表决**。'
|
|
1663
|
-
+(prior?('\n\n### 已有发言(他人 input,已转发给你)\n'+prior):'
|
|
1768
|
+
+(prior?'\n这是一场真实讨论:下面已有人发言(转给你),请先看,然后**加入讨论/补充/反驳/表决**。':'\n本轮是**并行独立发言**:同轮内不会有人先发言给你(唤醒是并行的),你的 input 会在**下一次唤醒/下一轮**转给其他人;请先给出你自己的判断。')
|
|
1769
|
+
+(prior?('\n\n### 已有发言(他人 input,已转发给你)\n'+prior):'')
|
|
1664
1770
|
+'\n\n你可以:提议任务(propose_task)、认领开放任务(claim_task)、提议验证对象(propose_verify)、或对"原问题是否已解决"表决(voteSolved)。请把**你的实际发言**写进 "input"。'
|
|
1665
1771
|
+'\n**停止表决必须是绝对票**:`voteSolved:true` 只表示你认为原问题**已解决**;只要有一名在册常驻没投 `true`(投 false、弃权、或漏写这个字段),run 就**不会**停止。不确定就投 false。' +'\nReply with ONLY a JSON object:\n'
|
|
1666
1772
|
+'{"input":"<your real contribution to this discussion>","propose_task":"<task title or null>","task_desc":"...","claim_task":"<task id or null>","propose_verify":"<id or null>","voteSolved":true}'
|
|
@@ -1673,7 +1779,7 @@ export function apply(ctx) {
|
|
|
1673
1779
|
const others=Object.entries(src).map(([k,v])=>'- '+k+': 正确概率 '+String(v.prob!=null?Number(v.prob).toFixed(2):0.5)+' → '+v.reason).join('\n')
|
|
1674
1780
|
const L=[]
|
|
1675
1781
|
L.push((params.residentPersona?params.residentPersona+'\n':'')
|
|
1676
|
-
+'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+targetTypeWord(vs.targetType)+',提出者 '+vs.targetOwner+')。\n'
|
|
1782
|
+
+'Resident '+r.rId+' — 团队验证。 The group is verifying object '+vs.targetId+'('+targetTypeWord(vs.targetType)+(String(vs.targetOwner||'').trim()?(',提出者 '+vs.targetOwner):'')+')。\n'
|
|
1677
1783
|
+'请给出你对「该对象为真」的**正确概率 `verdict`**,仅一个 0–1 数值:**1 = 绝对为真,0 = 绝对为假,0.5 = 完全不确定,其余为介于其间的程度**(不要给 TRUE/FALSE,就给一个数值)。\n'
|
|
1678
1784
|
// ── THE VOTE CONTRACT (why the example below is 1 and not 0.9) ──────────────────────────
|
|
1679
1785
|
// Only an EXACT 1 or 0 is a vote; anything strictly between is the model's honest
|
|
@@ -1684,7 +1790,8 @@ export function apply(ctx) {
|
|
|
1684
1790
|
+'**投票契约**:只有**恰好 1**(你认为是**绝对**为真)和**恰好 0**(你认为是**绝对**为假)算表决;**严格介于 0 与 1 之间**(例如 0.9、0.95、0.5)是**弃权**——它是你对"该对象为真"的**概率估计**,不是你的一票。\n'
|
|
1685
1791
|
+' · 有把握认为它为真就投 **1**;不要为了"留一点余地"投 0.9——那会让全组永远无法定论。\n'
|
|
1686
1792
|
+' · 弃权会被如实统计:本轮没有人全票 → 对象**不会**停止验证,而是把全组的**平均概率**写回它在库中的卡片(`- 概率:`),带概率继续留在库里。\n'
|
|
1687
|
-
+' ·
|
|
1793
|
+
+' · 弃权的合法用途:你确实不确定(用 0.5 附近的值表达)。\n'
|
|
1794
|
+
+(formalOn()?(' · 另一种弃权:已归档的机器检查证明与命题原文不一致、你**不能**用 0 表达"命题为假"(此时请用 `formal` 回执的 `decision:"defect"` 报告偏差)。\n'):'')
|
|
1688
1795
|
+'判定规则:**仅当全体在册常驻都恰好给 1(都认为是真)、或都恰好给 0(都认为是假)**,才按「真/假」写入 Verified/ 并回写来源卡的「已验证·真/假」;否则**只按概率数值(一种程度)保留在库中**,附全组平均正确概率,不写成真/假。\n'
|
|
1689
1796
|
+'请给出你**诚实独立的判断**'
|
|
1690
1797
|
+(vs.stage==='debate'?',并参考他人意见:\n':'。\n')
|
|
@@ -1898,10 +2005,23 @@ export function apply(ctx) {
|
|
|
1898
2005
|
function byChild(childId){ for(const [,r] of residents){ if(r.childId===childId) return r } return undefined }
|
|
1899
2006
|
|
|
1900
2007
|
// ---- artifact writers (resident-facing) ----
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2008
|
+
/** Cross-preset audit D1: a member-facing tool must never answer with a bare `no such resident`.
|
|
2009
|
+
* "not started yet" / "wrong id" / "already dismissed" are DIFFERENT problems, so the answer
|
|
2010
|
+
* carries a code, the LIVE roster state and an actionable next step (v5's `memberDiagnosis`). */
|
|
2011
|
+
function noSuchResident(id, tool){
|
|
2012
|
+
const ids=[...residents.keys()]
|
|
2013
|
+
const active=[...residents.values()].filter(r=>r&&r.childId).length
|
|
2014
|
+
const nextTool=tool||(running?'vibe_v4_list_members':'vibe_v4_start')
|
|
2015
|
+
return {ok:false,code:'V4_NO_SUCH_RESIDENT',
|
|
2016
|
+
message:'no such resident: '+String(id||'(none)')+(running?('(当前在册 '+residents.size+' 名,active '+active+' 名)'):'(本次运行尚未 start)'),
|
|
2017
|
+
status:{running:!!running,autoDone:!!autoDone,residentCount:residents.size,activeCount:active,ids:ids},
|
|
2018
|
+
next:{tool:nextTool,
|
|
2019
|
+
hint:running?'核对有效 id(新建成员可能仍在 spawn 中);列表用 vibe_v4_list_members':'先 vibe_v4_start 或 vibe_v4_add_member,再调用 '+nextTool}}
|
|
2020
|
+
}
|
|
2021
|
+
async function publishProgress(rId,content){ if(!rId||!residents.has(rId)) return noSuchResident(rId) ; const rel='Progress/'+rId+'/progress.md'; const prev=(await readText(rel))||''; await writeText(rel, prev+'\n### '+fmtTime()+'|'+rId+'\n'+String(content||'')+'\n'); return {ok:true} }
|
|
2022
|
+
async function recordProposition(rId,o){ if(!rId||!residents.has(rId)) return noSuchResident(rId) ; const id=o.id?idSafe(o.id):('p-'+shortId()); const lines=['# 命题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: 命题','- 状态: 未定论','- 概率: '+cl(o.prob!=null?o.prob:0.5),'- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 证明尝试','','## 证伪尝试','']; await writeText('Propos/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 命题 '+id); bumpArtifacts(); return {ok:true,id,file:'Propos/'+rId+'/'+id+'.md'} }
|
|
2023
|
+
async function recordMethod(rId,o){ if(!rId||!residents.has(rId)) return noSuchResident(rId) ; const id=o.id?idSafe(o.id):('m-'+shortId()); const lines=['# 方法|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: '+(o.type||'方法'),'- 状态: 经验','- 可信断言: []','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'','## 核心内容',String(o.content||''),'','## 定义与记号',String(o.notation||''),'','## 应用记录','## 改进历史','']; await writeText('Methods/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 方法 '+id); bumpArtifacts(); return {ok:true,id,file:'Methods/'+rId+'/'+id+'.md'} }
|
|
2024
|
+
async function recordSubproblem(rId,o){ if(!rId||!residents.has(rId)) return noSuchResident(rId) ; const id=o.id?idSafe(o.id):('s-'+shortId()); const lines=['# 子问题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 状态: 求解中','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 进度','']; await writeText('Subproblems/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 子问题 '+id); bumpArtifacts(); return {ok:true,id,file:'Subproblems/'+rId+'/'+id+'.md'} }
|
|
1905
2025
|
// ── auto-sync meeting: every `meetingKeepEvery` NEW artifacts, convene a coordination meeting ──
|
|
1906
2026
|
//
|
|
1907
2027
|
// The counting basis is the RESIDENTS' card libraries themselves (Propos/Methods/Subproblems),
|
|
@@ -2015,7 +2135,7 @@ export function apply(ctx) {
|
|
|
2015
2135
|
|
|
2016
2136
|
// ---- messaging ----
|
|
2017
2137
|
async function postMessage(from,to,content){
|
|
2018
|
-
const r=residents.get(to); if(!r) return
|
|
2138
|
+
const r=residents.get(to); if(!r) return noSuchResident(to,'vibe_v4_send_message')
|
|
2019
2139
|
if(!busy.has(to)){
|
|
2020
2140
|
currentResident=r.rId
|
|
2021
2141
|
const ok=await wakeResident(r, (await normalPrompt(r))+'\n\n[NEW MESSAGE from '+from+']\n'+content,'normal')
|
|
@@ -2075,7 +2195,7 @@ export function apply(ctx) {
|
|
|
2075
2195
|
// who sees no one else's contribution"; a real discussion lets each member lead sometimes.
|
|
2076
2196
|
const rot=Math.floor(Math.random()*Math.max(1,ids.length))
|
|
2077
2197
|
const order=ids.slice(rot).concat(ids.slice(0,rot))
|
|
2078
|
-
meetingState={id:'mt-'+shortId(),agenda,type:type||'general',targetId:targetId||null,round:0,asked:[],inputs:{},transcript:[],order,at:now(),lastInputAt:now()}
|
|
2198
|
+
meetingState={id:'mt-'+shortId(),agenda,type:type||'general',targetId:targetId||null,round:0,asked:[],inputs:{},transcript:[],order,at:now(),lastInputAt:now(),rosterSnapshot:order.slice(),rosterVersion}
|
|
2079
2199
|
markProgress();
|
|
2080
2200
|
logActivity('meeting','start: '+agenda); await saveAll(); await scheduleNext(); return {ok:true,id:meetingState.id}
|
|
2081
2201
|
}
|
|
@@ -2090,7 +2210,7 @@ export function apply(ctx) {
|
|
|
2090
2210
|
meetingState=null; wakeKind.clear(); logActivity('meeting','abandoned (stuck: no resident spoke)')
|
|
2091
2211
|
await saveAll(); await scheduleNext(); return
|
|
2092
2212
|
}
|
|
2093
|
-
const ids=Array.from(residents.keys()); const allSpoke=ids.every(id=>st.inputs[id]!==undefined)
|
|
2213
|
+
const ids=(st.rosterSnapshot||Array.from(residents.keys())).slice(); const allSpoke=ids.every(id=>st.inputs[id]!==undefined)
|
|
2094
2214
|
if(allSpoke){ await finalizeMeeting(); return }
|
|
2095
2215
|
// only wake IDLE un-spoken residents (rotated order); in-flight ones re-trigger this on end.
|
|
2096
2216
|
// NOTE: we deliberately do NOT flush mailboxes here — drafting an un-spoken resident into a normal
|
|
@@ -2109,7 +2229,7 @@ export function apply(ctx) {
|
|
|
2109
2229
|
let doSchedule=false
|
|
2110
2230
|
try {
|
|
2111
2231
|
const st=meetingState
|
|
2112
|
-
const ids=Array.from(residents.keys()); const allSpoke=ids.length>0 && ids.every(id=>st.inputs[id]!==undefined)
|
|
2232
|
+
const ids=(st.rosterSnapshot||Array.from(residents.keys())).slice(); const allSpoke=ids.length>0 && ids.every(id=>st.inputs[id]!==undefined)
|
|
2113
2233
|
const lines=['# 会议 '+st.id+'|'+fmtTime(),'','**议程**:'+st.agenda,'']
|
|
2114
2234
|
for(const [id,iv] of Object.entries(st.inputs)){ lines.push('### '+id); lines.push(iv.input||''); lines.push('') }
|
|
2115
2235
|
await writeText('Shared/meetings/'+st.id+'.md', lines.join('\n'))
|
|
@@ -2178,7 +2298,7 @@ export function apply(ctx) {
|
|
|
2178
2298
|
await saveAll(); await scheduleNext(); return
|
|
2179
2299
|
}
|
|
2180
2300
|
}
|
|
2181
|
-
verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},history:{},transcript:[],at:now(),lastVerdictAt:now()}
|
|
2301
|
+
verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},history:{},transcript:[],at:now(),lastVerdictAt:now(),rosterSnapshot:Array.from(residents.keys()),rosterVersion}
|
|
2182
2302
|
markProgress();
|
|
2183
2303
|
logActivity('verify','debate begin: '+pv.targetId+' ('+pv.targetType+')'); await saveAll(); await scheduleNext()
|
|
2184
2304
|
}
|
|
@@ -2193,7 +2313,7 @@ export function apply(ctx) {
|
|
|
2193
2313
|
verifyState=null; wakeKind.clear(); logActivity('verify',vs.targetId+' abandoned (stuck: no unanimous verdict reachable)')
|
|
2194
2314
|
await saveAll(); await scheduleNext(); return
|
|
2195
2315
|
}
|
|
2196
|
-
const ids=Array.from(residents.keys()); const allVoted=ids.every(id=>vs.verdicts[id]!==undefined)
|
|
2316
|
+
const ids=(vs.rosterSnapshot||Array.from(residents.keys())).slice(); const allVoted=ids.every(id=>vs.verdicts[id]!==undefined)
|
|
2197
2317
|
if(allVoted){ await finalizeVerify(); return }
|
|
2198
2318
|
// NOTE: we deliberately do NOT flush mailboxes here — drafting an un-voted resident into a normal
|
|
2199
2319
|
// mail round would delay its verdict and can starve the verify past its watchdog when the backlog
|
|
@@ -2209,9 +2329,14 @@ export function apply(ctx) {
|
|
|
2209
2329
|
finalizeLock='verify'
|
|
2210
2330
|
let doSchedule=false
|
|
2211
2331
|
try {
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2332
|
+
const vs=verifyState; // F1: the requirement set is the FROZEN snapshot (live map fallback) - the ONE
|
|
2333
|
+
// predicate is MEMBERSHIP, exactly like the on-end path at :2247. A count-only check could be
|
|
2334
|
+
// satisfied by a ballot from someone NOT in the frozen set (a member added after the freeze, or a
|
|
2335
|
+
// late reply from a removed one), which would promote 真/假 while a frozen voter is still silent.
|
|
2336
|
+
const ids=(vs.rosterSnapshot||Array.from(residents.keys())).slice()
|
|
2337
|
+
const expected=ids.length
|
|
2338
|
+
const allVoted = ids.length>0 && ids.every(id=>vs.verdicts[id]!==undefined)
|
|
2339
|
+
const vals=ids.filter(id=>vs.verdicts[id]!==undefined).map(id=>vs.verdicts[id])
|
|
2215
2340
|
// verdict is a PURE 0-1 probability; only ALL=1 (true) or ALL=0 (false) is a binary verdict.
|
|
2216
2341
|
const allTrue = allVoted && vals.every(x=>Number(x.prob)===1)
|
|
2217
2342
|
const allFalse = allVoted && vals.every(x=>Number(x.prob)===0)
|
|
@@ -2259,7 +2384,7 @@ export function apply(ctx) {
|
|
|
2259
2384
|
if(formalOn() && formalTodos.some(t=>t.id===target)){
|
|
2260
2385
|
formalTodos=formalTodos.filter(t=>t.id!==target)
|
|
2261
2386
|
await saveFormal()
|
|
2262
|
-
try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ /* best-effort */ }
|
|
2387
|
+
try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ if(!warnedWriteFormalTodo){ warnedWriteFormalTodo=true; console.error('vibe-math-v4: closeVerify: writeFormalTodo/writeFormalIndex failed - the verify conclusion is NOT recorded on disk (best effort continues; it is lost on reload)') } /* best-effort */ }
|
|
2263
2388
|
}
|
|
2264
2389
|
logActivity('verify',target+' → Verified ('+(isTrue?'真':'假')+') by unanimous consensus'+(formalOn()?('|形式化: '+formalStatusLine(target)):''))
|
|
2265
2390
|
verifyState=null; wakeKind.clear(); await saveAll()
|
|
@@ -2286,7 +2411,7 @@ export function apply(ctx) {
|
|
|
2286
2411
|
await rewriteSourceProb(target,mean,vs.targetOwner)
|
|
2287
2412
|
if(!formalTodos.some(t=>t.id===target)) formalTodos.push({id:target,at:now(),why,verdict:isTrue?1:0})
|
|
2288
2413
|
await putFormal(target,rec,formalTodos) // single durable write of records + todo together
|
|
2289
|
-
try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ /* best-effort */ }
|
|
2414
|
+
try { await writeFormalTodo(); await writeFormalIndex() } catch(e){ if(!warnedDeferFormalTodo){ warnedDeferFormalTodo=true; console.error('vibe-math-v4: deferForFormal: writeFormalTodo/writeFormalIndex failed - the formal-required deferral (TODO + index) is NOT persisted; the object stays unverified and this decision is lost on reload') } /* best-effort */ }
|
|
2290
2415
|
logActivity('verify',target+' 的表决结果为 '+(isTrue?'真':'假')+',但 **require 模式**要求先有 Lean 通过或显式阻塞记录,因此本轮**不定论**(已记入 Formal/TODO.md;原因 formal-required)')
|
|
2291
2416
|
verifyState=null; wakeKind.clear(); await saveAll()
|
|
2292
2417
|
// Scheduling is done by finalizeVerify (doSchedule=true) AFTER it releases finalizeLock:
|
|
@@ -2516,21 +2641,22 @@ export function apply(ctx) {
|
|
|
2516
2641
|
if(!agent || !agent.session) return
|
|
2517
2642
|
const compaction = compactionForAgent(agent)
|
|
2518
2643
|
if(compaction===undefined) return
|
|
2519
|
-
//
|
|
2520
|
-
//
|
|
2521
|
-
//
|
|
2522
|
-
|
|
2523
|
-
if(
|
|
2644
|
+
// F2 (user ruling): compaction is driven by the CONFIGURED THRESHOLD only. `compactIfNeeded` is the
|
|
2645
|
+
// POLICY verb, so a host that also offers the FORCING verb (`compactNow`) must NOT be forced - the
|
|
2646
|
+
// per-turn call is a cheap no-op below the threshold, and the core-rules re-anchor happens ONLY when
|
|
2647
|
+
// the policy call reports that it actually compacted.
|
|
2648
|
+
if(typeof compaction.compactIfNeeded !== 'function') return
|
|
2524
2649
|
try {
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2650
|
+
const signal = makeSignal(params.activityTimeoutMs||60000)
|
|
2651
|
+
const result = await compaction.compactIfNeeded(agent, 'pressure', signal)
|
|
2652
|
+
const didCompact = !!(result && ((result.shadowedSeqs||[]).length>0 || Number(result.shadowedTokenCount||0)>0))
|
|
2653
|
+
if(didCompact){
|
|
2528
2654
|
// the resident's real session was compacted → its context is now a summary.
|
|
2529
2655
|
// Flag needCompact so the NEXT wake re-anchors the core rules (they may have been blurred).
|
|
2530
2656
|
r.roundsSinceCompact=0; r.needCompact=true; r.contextPct=Math.min(r.contextPct||15,25)
|
|
2531
2657
|
logActivity('compact', r.rId+' real /compact (shadowed '+result.shadowedSeqs.length+' items, ~'+String(result.shadowedTokenCount||0)+' tokens)')
|
|
2532
2658
|
}
|
|
2533
|
-
} catch(e){ /* real compaction unavailable/failed; the soft directive already covers it */ }
|
|
2659
|
+
} catch(e){ if(!warnedCompaction){ warnedCompaction=true; console.error('vibe-math-v4: realCompact: the compaction call failed - context pressure was NOT compacted; the round/flag heuristic may desync from the real context size') } /* real compaction unavailable/failed; the soft directive already covers it */ }
|
|
2534
2660
|
}
|
|
2535
2661
|
|
|
2536
2662
|
// ---- liveness / scheduling ----
|
|
@@ -2627,7 +2753,12 @@ export function apply(ctx) {
|
|
|
2627
2753
|
// the top of the next pass — never in a tight loop here.
|
|
2628
2754
|
if(!r.childId) continue
|
|
2629
2755
|
let ok=false
|
|
2630
|
-
|
|
2756
|
+
// F4 (measured then reverted): this is the ONLY heartbeatPrompt wake site in v4 - the fairness/idle
|
|
2757
|
+
// fill. Both the soft-compaction design and the guards T13/A1 require THIS round to be a NORMAL
|
|
2758
|
+
// research round when contextPct >= compactThreshold (that is where the [CONTEXT COMPACT ...]
|
|
2759
|
+
// self-summary directive must land). Re-kinding it as its own kind therefore breaks them, and F4's
|
|
2760
|
+
// premise (a distinct non-research CHECKPOINT wake) has no separate path on this revision.
|
|
2761
|
+
try { ok = await wakeResident(r, await heartbeatPrompt(r), 'normal') } catch(e){ ok=false }
|
|
2631
2762
|
if(ok) started++
|
|
2632
2763
|
await saveAll()
|
|
2633
2764
|
if(!ok) continue // a failed wake must NOT stop the fill; try the next idle resident
|
|
@@ -2719,7 +2850,15 @@ export function apply(ctx) {
|
|
|
2719
2850
|
else if(typeof v.verdict==='string' && v.verdict.trim()!=='' && Number.isFinite(Number(v.verdict))){ p=clamp01(Number(v.verdict)) }
|
|
2720
2851
|
else { p=clamp01(Number(v.confidence)) }
|
|
2721
2852
|
// verdict is a PURE 0-1 probability (a degree); no binary TRUE/FALSE classification.
|
|
2722
|
-
|
|
2853
|
+
// F1: a ballot from a member who is NOT in the frozen participant set must NOT be recorded -
|
|
2854
|
+
// otherwise a late reply from a removed member re-creates its key and the count predicate can
|
|
2855
|
+
// reach `expected` while a frozen voter has no verdict. v5 states the same rule explicitly.
|
|
2856
|
+
const _inRoster = !Array.isArray(verifyState.rosterSnapshot) || verifyState.rosterSnapshot.indexOf(r.rId)!==-1
|
|
2857
|
+
if(!_inRoster){ logActivity('verify', r.rId+' 的表决未计入(不在本次验证的冻结名单中)') }
|
|
2858
|
+
else {
|
|
2859
|
+
verifyState.verdicts[r.rId]={prob:p,confidence:p,reason:String(v.reason||parsed.summary||'')}
|
|
2860
|
+
verifyState.lastVerdictAt=now()
|
|
2861
|
+
}
|
|
2723
2862
|
verifyState.lastVerdictAt=now()
|
|
2724
2863
|
await saveAll()
|
|
2725
2864
|
if(!running || autoDone) return // pause: freeze (resume refreshes the clocks and re-drives)
|
|
@@ -3339,6 +3478,7 @@ export function apply(ctx) {
|
|
|
3339
3478
|
if(ps.compile&&ps.compile.result==='ok') ps.files.push('paper.pdf')
|
|
3340
3479
|
ps.finalizedAt=now(); ps.status='done'
|
|
3341
3480
|
await paperWriteMeta(); await paperWriteLog()
|
|
3481
|
+
await paperWriteMeta(); await paperWriteLog()
|
|
3342
3482
|
// v2 §A1: the paper phase was entered from INSIDE the closing branch, BEFORE the completion
|
|
3343
3483
|
// flags — so the run only becomes "done" here, once the paper exists (or has degraded safely).
|
|
3344
3484
|
if(ps.deferCompletion && autoDone!==true){
|
|
@@ -3347,6 +3487,21 @@ export function apply(ctx) {
|
|
|
3347
3487
|
logActivity('stop','all residents agree: problem solved(一致性停止票;最终论文已产出)')
|
|
3348
3488
|
await saveAll()
|
|
3349
3489
|
}
|
|
3490
|
+
// G3: `done` is a CLAIM that the deliverable is complete. `final.md` is written unchecked and the
|
|
3491
|
+
// meta/log results are discarded, so VERIFY THE ARTIFACTS EXIST instead of trusting return values.
|
|
3492
|
+
// (readTextAbs returns undefined for a missing file and never throws on ENOENT.)
|
|
3493
|
+
const _paperNeed=['final.md']
|
|
3494
|
+
if(ps.cfg.format!=='tex') _paperNeed.push('paper.md')
|
|
3495
|
+
if(ps.cfg.format!=='md') _paperNeed.push('paper.tex')
|
|
3496
|
+
if(_paperNeed.indexOf('paper.md')===-1&&ps.files.indexOf('paper.md')!==-1) _paperNeed.push('paper.md')
|
|
3497
|
+
if(_paperNeed.indexOf('paper.tex')===-1&&ps.files.indexOf('paper.tex')!==-1) _paperNeed.push('paper.tex')
|
|
3498
|
+
_paperNeed.push('paper.meta.json','paper.log.md')
|
|
3499
|
+
const _paperGone=[]
|
|
3500
|
+
for(const _f of _paperNeed){ if((await readText(ps.dir+'/'+_f))===undefined) _paperGone.push(_f) }
|
|
3501
|
+
if(_paperGone.length){
|
|
3502
|
+
ps.status='failed' // terminal, like a failed compile: no complete deliverable is claimed
|
|
3503
|
+
if(!warnedPaperWrite){ warnedPaperWrite=true; console.error('vibe-math-v4: paperFinalize: required paper artifact(s) MISSING after finalize (' + _paperGone.join(', ') + ') - the paper is marked FAILED instead of done; the deliverable is incomplete and `files` never claims a missing artifact') }
|
|
3504
|
+
}
|
|
3350
3505
|
const warn=[ps.warning,ps.compile&&ps.compile.result==='failed'?ps.compile.error:''].filter(Boolean).join(';')
|
|
3351
3506
|
if(warn) logActivity('paper','最终论文完成(有警告):'+ps.dir+'|'+warn)
|
|
3352
3507
|
else logActivity('paper','最终论文完成:'+ps.dir+'('+ps.files.join('、')+(ps.compile?('|编译 '+(ps.compile.result||'')):'')+')')
|
|
@@ -3357,6 +3512,10 @@ export function apply(ctx) {
|
|
|
3357
3512
|
return {
|
|
3358
3513
|
params:{finalPaper:params.finalPaper,paperFormat:params.paperFormat,paperLanguage:params.paperLanguage,paperCompilePdf:params.paperCompilePdf,paperEditor:params.paperEditor,paperLatexCommand:params.paperLatexCommand},
|
|
3359
3514
|
id:ps?ps.id:paperIdFor(), dir:ps?ps.dir:paperDirFor(), status:ps?ps.status:'idle',
|
|
3515
|
+
// round-9 (F2): with no paper state this path is a PROJECTION (f2: paperIdFor/paperDirFor are
|
|
3516
|
+
// pure - reading `status` touches no fs and creates no directory). Say so explicitly instead of
|
|
3517
|
+
// handing callers a path that looks like it exists.
|
|
3518
|
+
dirProjected:!ps, dirSource:ps?'paper-state':'projection', readSideEffect:false,
|
|
3360
3519
|
running:!!(ps&&ps.status!=='done'&&ps.status!=='failed'), trigger:ps?ps.trigger:'',
|
|
3361
3520
|
rounds:ps?ps.round:0, maxRounds:ps?ps.maxRounds:PAPER_MAX_ROUNDS, participants:ps?ps.participants:[],
|
|
3362
3521
|
editor:ps?(ps.editorId||ps.cfg.editor):params.paperEditor, editorDowngraded:!!(ps&&ps.editorDowngraded),
|
|
@@ -3368,6 +3527,8 @@ export function apply(ctx) {
|
|
|
3368
3527
|
function paperStatusSummary(){
|
|
3369
3528
|
const v=paperStatusView()
|
|
3370
3529
|
return {ok:true, id:v.id, dir:v.dir, status:v.status, running:v.running, trigger:v.trigger, lang:v.params.paperLanguage,
|
|
3530
|
+
// round-9 (F2): carry the projection flags through the SUMMARY too (this is what `status` returns).
|
|
3531
|
+
dirProjected:v.dirProjected, dirSource:v.dirSource, readSideEffect:v.readSideEffect,
|
|
3371
3532
|
format:v.params.paperFormat, editor:v.editor, editorDowngraded:v.editorDowngraded, participants:v.participants,
|
|
3372
3533
|
parts:v.parts, reviews:v.reviews, rounds:v.rounds+'/'+v.maxRounds, finalizedAt:v.finalizedAt, files:v.files,
|
|
3373
3534
|
params:v.params,
|
|
@@ -3462,6 +3623,9 @@ export function apply(ctx) {
|
|
|
3462
3623
|
async function start({problem,residentCount,seedDirections}){
|
|
3463
3624
|
await loadSettings()
|
|
3464
3625
|
currentProject=await readCurrentProject(); if(!currentProject||currentProject==='default'){ currentProject='default'; }
|
|
3626
|
+
// F-3/F-4c verdicts: tool registration is wrapped once, in the registerTool helper (ctx.effect),
|
|
3627
|
+
// so no call site needs its own wrapper; and a failed mkdir is named ONCE inside ensureDirs itself,
|
|
3628
|
+
// which covers every caller (including this bare one) - no caller-side return check is needed.
|
|
3465
3629
|
await ensureDirs()
|
|
3466
3630
|
if(problem) problemText=String(problem)
|
|
3467
3631
|
if(!problemText) return {ok:false,message:'problem text required (pass problem, or use vibe_v4_configure first)'}
|
|
@@ -3565,7 +3729,23 @@ export function apply(ctx) {
|
|
|
3565
3729
|
if(verifyState && verifyState.lastVerdictAt) verifyState.lastVerdictAt=now()
|
|
3566
3730
|
logActivity('resume','restarted'+(crossProcess?' (cross-process: re-spawned)':needRespawn?' (re-spawned)':'')); await saveAll(); await scheduleNext(); return {ok:true,message:'resumed',project:currentProject}
|
|
3567
3731
|
}
|
|
3732
|
+
/** Round A F1/F2: the roster version must never be observable as unchanged while the frozen
|
|
3733
|
+
* participant set changed, and a frozen set must always travel WITH its own version. Every
|
|
3734
|
+
* roster change calls this AFTER all snapshot mutations (meeting + verify prunes included), so
|
|
3735
|
+
* {snapshot.rosterVersion, snapshot.rosterSnapshot} is one atomic pair and the live counter
|
|
3736
|
+
* still strictly increases on hire/fire. */
|
|
3737
|
+
function bumpRoster(){ rosterVersion++; if(meetingState&&Array.isArray(meetingState.rosterSnapshot)) meetingState.rosterVersion=rosterVersion; if(verifyState&&Array.isArray(verifyState.rosterSnapshot)) verifyState.rosterVersion=rosterVersion; return rosterVersion }
|
|
3738
|
+
/** The frozen participant set an operator should read right now (verify wins while it is live). */
|
|
3739
|
+
function activeRosterSnapshot(){ return (verifyState&&Array.isArray(verifyState.rosterSnapshot))?verifyState:((meetingState&&Array.isArray(meetingState.rosterSnapshot))?meetingState:null) }
|
|
3568
3740
|
function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
|
|
3741
|
+
// round-9 (P2): the roster VERSION travels with every status read, and an active verify/meeting
|
|
3742
|
+
// reports the participant set it froze (never a re-derivation from the live map).
|
|
3743
|
+
rosterVersion,
|
|
3744
|
+
rosterVersion, liveRosterVersion: rosterVersion,
|
|
3745
|
+
// Round A F1: the pair is ATOMIC - `frozen.version` is the version the SNAPSHOT recorded when it
|
|
3746
|
+
// was frozen (and re-stamped on every roster change), never the live counter beside a stale set.
|
|
3747
|
+
frozen: (function(sn){ return {version: sn?sn.rosterVersion:null, participants: sn?sn.rosterSnapshot:null, kind: sn?(sn===verifyState?'verify':'meeting'):null} })(activeRosterSnapshot()),
|
|
3748
|
+
frozenParticipants: (activeRosterSnapshot()||{}).rosterSnapshot||null, // deprecated: use `frozen` (set + version together)
|
|
3569
3749
|
residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
|
|
3570
3750
|
// Residents the host's live-child cap refused (maxActiveSubagents): queued, not lost. Without
|
|
3571
3751
|
// this the only trace would be the one console line, and `residentCount` alone cannot tell a
|
|
@@ -3576,8 +3756,8 @@ export function apply(ctx) {
|
|
|
3576
3756
|
// The coordination counters (`spoke k/N`, `voted k/N`, the verification round) used to live only
|
|
3577
3757
|
// in `report()`: from `status()` alone an operator could not tell "the group is progressing"
|
|
3578
3758
|
// from "the group is stuck at 2/4 votes", which is exactly how F1/F2/F7 stay invisible.
|
|
3579
|
-
consensus: meetingState?{kind:'meeting',id:meetingState.id,agenda:meetingState.agenda,round:meetingState.round,spoke:Object.keys(meetingState.inputs).length,expected:residents.
|
|
3580
|
-
:(verifyState?{kind:'verify',target:verifyState.targetId,targetType:verifyState.targetType,stage:verifyState.stage,round:verifyState.round,voted:Object.keys(verifyState.verdicts).length,expected:residents.
|
|
3759
|
+
consensus: meetingState?{kind:'meeting',id:meetingState.id,agenda:meetingState.agenda,round:meetingState.round,rosterVersion:meetingState.rosterVersion,participants:meetingState.rosterSnapshot,spoke:Object.keys(meetingState.inputs).length,expected:(meetingState.rosterSnapshot||Array.from(residents.keys())).length,solvedVotes:Object.values(meetingState.inputs).filter(iv=>iv.voteSolved===true).length,rosterVersion:meetingState.rosterVersion}
|
|
3760
|
+
:(verifyState?{kind:'verify',target:verifyState.targetId,targetType:verifyState.targetType,stage:verifyState.stage,round:verifyState.round,rosterVersion:verifyState.rosterVersion,participants:verifyState.rosterSnapshot,voted:Object.keys(verifyState.verdicts).length,expected:(verifyState.rosterSnapshot||Array.from(residents.keys())).length,rosterVersion:verifyState.rosterVersion}:null),
|
|
3581
3761
|
artifactCount: artifactCount, artifactBaseline: artifactBaseline,
|
|
3582
3762
|
paper: paperStatusSummary(),
|
|
3583
3763
|
// The Lean knobs and the per-object formal records are part of the readable status: without
|
|
@@ -3598,8 +3778,8 @@ export function apply(ctx) {
|
|
|
3598
3778
|
}
|
|
3599
3779
|
function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
|
|
3600
3780
|
residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
|
|
3601
|
-
meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+residents.
|
|
3602
|
-
verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+residents.
|
|
3781
|
+
meeting: meetingState?{id:meetingState.id, agenda:meetingState.agenda, spoke:Object.keys(meetingState.inputs).length+'/'+(meetingState.rosterSnapshot||Array.from(residents.keys())).length,rosterVersion:meetingState.rosterVersion}:null,
|
|
3782
|
+
verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage, voted:Object.keys(verifyState.verdicts).length+'/'+(verifyState.rosterSnapshot||Array.from(residents.keys())).length,rosterVersion:verifyState.rosterVersion}:null,
|
|
3603
3783
|
pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null,
|
|
3604
3784
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
3605
3785
|
formal: formalView(),
|
|
@@ -3619,10 +3799,16 @@ export function apply(ctx) {
|
|
|
3619
3799
|
// residents) can never be true for the new member (not in the snapshot order) and the meeting is
|
|
3620
3800
|
// only ever released by the stuck watchdog instead of finalizing with everyone's input.
|
|
3621
3801
|
if(meetingState){ if(!Array.isArray(meetingState.order)) meetingState.order=Array.from(residents.keys()); if(!meetingState.order.includes(r.rId)) meetingState.order.push(r.rId) }
|
|
3802
|
+
rosterVersion++ // additions never extend a frozen set: the live counter moves ahead (staleness stays visible)
|
|
3803
|
+
// P2: the roster CHANGED - bump the version so any frozen participant set is visibly stale.
|
|
3804
|
+
|
|
3622
3805
|
// Mid-verify additions are automatically asked to vote (continueVerifyRound recomputes ids from
|
|
3623
3806
|
// the live residents map), so no extra handling is needed there.
|
|
3624
3807
|
return {ok:true,id:r.rId,direction:r.direction} }
|
|
3625
|
-
async function removeMember(id){ const r=residents.get(id); if(!r) return
|
|
3808
|
+
async function removeMember(id){ const r=residents.get(id); if(!r) return noSuchResident(id,'vibe_v4_remove_member'); if(r.childId){ try{ subagents.interrupt(r.childId,{kind:'ancestor',agent:rootAgent}) }catch(e){} } residents.delete(id); busy.delete(id); mailboxes.delete(id); wakeKind.delete(id); if(currentResident===id) currentResident=''
|
|
3809
|
+
// P2: the roster CHANGED - bump the version (a frozen participant set stays readable, but is
|
|
3810
|
+
// now visibly stale; every view reports the same version it snapshotted).
|
|
3811
|
+
|
|
3626
3812
|
// Reconcile in-progress coordination so a removed member cannot hang consensus or crash a round:
|
|
3627
3813
|
// drop its meeting speech / verify verdict and prune it from the meeting's speaking order so the
|
|
3628
3814
|
// find() there never selects a ghost. Its QUEUED verify proposals are deliberately KEPT: a
|
|
@@ -3632,6 +3818,13 @@ export function apply(ctx) {
|
|
|
3632
3818
|
// only the first entry, which may belong to the removed member).
|
|
3633
3819
|
if(meetingState){ delete meetingState.inputs[id]; meetingState.order=(meetingState.order||[]).filter(x=>x!==id) }
|
|
3634
3820
|
if(verifyState){ delete verifyState.verdicts[id] }
|
|
3821
|
+
// P2 (round-9): REMOVAL prunes the FROZEN participant set as well - v4's long-standing contract is
|
|
3822
|
+
// that removing an in-flight voter RELEASES the wait (T26/T31), while the snapshot still gives
|
|
3823
|
+
// every view one identical set + version. Additions never touch a frozen set (that is the point
|
|
3824
|
+
// of freezing); the version bump above marks the set as changed.
|
|
3825
|
+
if(meetingState&&Array.isArray(meetingState.rosterSnapshot)) meetingState.rosterSnapshot=meetingState.rosterSnapshot.filter(x=>x!==id)
|
|
3826
|
+
if(verifyState&&Array.isArray(verifyState.rosterSnapshot)) verifyState.rosterSnapshot=verifyState.rosterSnapshot.filter(x=>x!==id)
|
|
3827
|
+
bumpRoster() // F2: AFTER every snapshot mutation - version and set stay one atomic pair
|
|
3635
3828
|
await saveAll()
|
|
3636
3829
|
// Re-drive the scheduler right away. If the removed member was the ONLY turn in flight (e.g. the
|
|
3637
3830
|
// last unspoken meeting speaker / the last unvoted voter, interrupted mid-turn), NO subagent/end
|