dsh-vibe-math 2.3.16 → 2.4.1
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 +64 -61
- package/README.md +64 -61
- package/SECURITY.md +56 -0
- package/cordis.patch.yml +1123 -10
- package/docs/AUDIT-CHECKLIST.md +499 -384
- package/docs/COMPAT-AUDIT-ROUND2.md +3 -0
- package/docs/COMPAT-AUDIT-ROUND3.md +139 -0
- package/docs/generate_framework_diagram_v5.mjs +27 -27
- package/docs/release-notes/RELEASE-NOTES-2.4.0.md +172 -0
- package/docs/release-notes/RELEASE-NOTES-2.4.1.md +129 -0
- package/docs/test-timing.md +2 -1
- package/installer.js +89 -21
- package/package.json +36 -10
- package/preset-declaration.js +62 -0
- package/prompt-corpus-persona/persona-corpus.json +2 -2
- package/prompt-corpus-persona/persona-corpus.md +2 -2
- package/prompt-corpus-v5/prompt-corpus-v5.json +7 -7
- package/prompt-corpus-v5/prompt-corpus-v5.md +8 -129
- package/scripts/build-preset-rows.mjs +146 -0
- package/tests/audit-installer-compat.test.mjs +9 -3
- package/tests/audit-preset-rows.test.mjs +185 -0
- package/tests/audit-v5-integrity.mjs +49 -3
- package/tests/e2e-v5-round2.test.mjs +137 -67
- package/tests/formal-verify-v5.test.mjs +43 -35
- package/tests/prompt-v5-integrity.test.mjs +49 -47
- package/tests/selfdrive-v5.mjs +42 -55
- package/vibe-math-v2/agent.cordis.yml +5 -5
- package/vibe-math-v2/preset.yml +1 -1
- package/vibe-math-v2/vibe-math-v2.js +111 -64
- package/vibe-math-v3/vibe-math-v3.js +213 -150
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -2
- package/vibe-math-v4/vibe-math-v4.js +257 -34
- package/vibe-math-v5/agent.cordis.yml +8 -8
- package/vibe-math-v5/preset.yml +1 -1
- package/vibe-math-v5/vibe-math-v5.js +292 -129
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +56 -44
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +24 -22
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5-en.svg +14 -14
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +14 -14
|
@@ -4,9 +4,44 @@
|
|
|
4
4
|
// intervention). It NEVER assigns tasks: residents message & meet and decide all
|
|
5
5
|
// task allocation among themselves. Consumes HOST subagents/agents/fs/tools/commands.
|
|
6
6
|
// NOTE: must declare `inject` for every service read as a ctx property (the Guard
|
|
7
|
-
// rejects undeclared dependencies)
|
|
8
|
-
//
|
|
7
|
+
// rejects undeclared dependencies). `timer` IS injected and used (ctx.timeout) so every
|
|
8
|
+
// timer here is a fiber-owned disposer — but the global setTimeout/clearTimeout DO exist
|
|
9
|
+
// in this preset's runtime: a preset is a FILE row loaded by a plain host-realm import().
|
|
10
|
+
// The vm sandbox that traps require/setTimeout/setInterval/fetch wraps only a DYNAMIC
|
|
11
|
+
// package's host half (@deepseek-ai/dsh-cordis-host-runner/lib/types/sandbox.js, reached
|
|
12
|
+
// only from the dynamic-package start path); v2/v3 use those globals and work. An earlier
|
|
13
|
+
// version of this note claimed the globals do not exist — false for file rows, and it would
|
|
14
|
+
// only become true if this preset were ever converted to a dynamic package.
|
|
9
15
|
export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands', 'timer']
|
|
16
|
+
|
|
17
|
+
// ---- host live-child cap (DSH ≥ 0.2) ----------------------------------------
|
|
18
|
+
// The host caps the number of LIVE continuable children PER ROOT AGENT. Evidence (installed
|
|
19
|
+
// dsh-subagent 0.2.0-rc.2): `materialize` calls `ActivationPool.reserve(this.maxActiveSubagents())`
|
|
20
|
+
// and `reserve` throws `SubagentError('subagent limit reached (active child limit: <capacity>);
|
|
21
|
+
// wait for an existing child to finish or complete this work with the current agents',
|
|
22
|
+
// 'ACTIVATION_LIMIT_REACHED')` when every slot is taken. The capacity comes from the `subagent`
|
|
23
|
+
// row's `maxActiveSubagents` Config key (default 8; no preset sets it), the cap is shared by all
|
|
24
|
+
// descendants of that root, and a slot is released when a child settles. The `.d.ts` does NOT
|
|
25
|
+
// document the throw.
|
|
26
|
+
//
|
|
27
|
+
// The detector and the remembered limit live at MODULE scope because the cap is a property of the
|
|
28
|
+
// HOST (the same number for every root of this process), not of one session. On DSH 0.1.x
|
|
29
|
+
// `subagents.startContinuable` never throws this code, so every branch below is inert there — the
|
|
30
|
+
// error code itself is the feature test, there is no version check anywhere.
|
|
31
|
+
let hostChildLimit // undefined until the host tells us its ceiling (via a refusal)
|
|
32
|
+
function isActivationLimitReached(e){ return String((e && e.code) || '') === 'ACTIVATION_LIMIT_REACHED' }
|
|
33
|
+
function noteChildLimit(e){
|
|
34
|
+
const m = /active child limit:\s*(\d+)/.exec(String((e && e.message) || e || ''))
|
|
35
|
+
if(m) hostChildLimit = Number(m[1])
|
|
36
|
+
return hostChildLimit
|
|
37
|
+
}
|
|
38
|
+
// One actionable sentence naming the HOST ceiling and the knob that raises it.
|
|
39
|
+
function hostChildLimitHint(limit){
|
|
40
|
+
const n = (limit === undefined || limit === null) ? '' : ('=' + limit)
|
|
41
|
+
return '本宿主对「同时在活的续聊子代理」有上限(宿主 subagent 行的 maxActiveSubagents 参数' + n
|
|
42
|
+
+ ',写满后子代理服务抛 ACTIVATION_LIMIT_REACHED)'
|
|
43
|
+
}
|
|
44
|
+
|
|
10
45
|
export function apply(ctx) {
|
|
11
46
|
const subagents = ctx.subagents
|
|
12
47
|
const agents = ctx.agents
|
|
@@ -74,17 +109,9 @@ export function apply(ctx) {
|
|
|
74
109
|
const activityLogCap = 200
|
|
75
110
|
|
|
76
111
|
// ---- utils ----
|
|
77
|
-
function now(){ return Date.now() }
|
|
78
|
-
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 }
|
|
79
|
-
function shortId(){ const h='0123456789abcdef'; let s=''; for(let i=0;i<8;i++) s+=h[Math.floor(Math.random()*16)]; return s }
|
|
80
|
-
function clamp01(v){ const n=Number(v); if(!Number.isFinite(n)) return 0.5; return Math.max(0,Math.min(1,n)) }
|
|
81
|
-
function fmtTime(ts){ try { return new Date(ts||now()).toISOString().replace('T',' ').slice(0,19) } catch(e){ return String(ts||'') } }
|
|
82
|
-
function cl(x){ return clamp01(Number(x)) }
|
|
83
112
|
// contextPct is a PERCENT (0-100); never clamp to 0-1 or the compactThreshold
|
|
84
113
|
// comparison (e.g. 66) becomes `1.0 >= 66` and never fires.
|
|
85
|
-
function clPct(x){ const n=Number(x); if(!Number.isFinite(n)) return 0; return Math.max(0,Math.min(100,n)) }
|
|
86
114
|
function textBlock(t){ return { type:'text', text:String(t) } }
|
|
87
|
-
function blocksToText(b){ if(!b) return ''; let out=''; for(const x of b){ if(x&&x.type==='text'&&typeof x.text==='string') out+=x.text+'\n' } return out.trim() }
|
|
88
115
|
function logActivity(event,detail){ activityLog.push({at:now(),event,detail:String(detail||'')}); if(activityLog.length>activityLogCap) activityLog.shift() }
|
|
89
116
|
function logDecision(kind,detail){ decisions.push({at:now(),kind,detail:String(detail||'')}) }
|
|
90
117
|
// Record that the project made real progress (new artifact, meeting, verify, task, or a
|
|
@@ -100,7 +127,6 @@ export function apply(ctx) {
|
|
|
100
127
|
// (misconfigured via vibe_v4_set) would otherwise make recoverStallMs negative → every meeting/
|
|
101
128
|
// verify watchdog fires INSTANTLY (abandoning all consensus) and A-fill's idle window would never
|
|
102
129
|
// elapse (waking everyone every pass). Guard every duration read with this.
|
|
103
|
-
function posMs(v,def){ const n=Number(v); return (Number.isFinite(n)&&n>0)?n:(def||120000) }
|
|
104
130
|
function recoverStallMs(){ return posMs(params.activityTimeoutMs,120000) * 2 }
|
|
105
131
|
function pickProvider(){ try { const n=subagents.list?subagents.list():[]; if(n.indexOf('spawn')!==-1) return 'spawn'; if(n.indexOf('fork')!==-1) return 'fork' } catch(e){} return 'spawn' }
|
|
106
132
|
// Per-resident model/provider inheritance: when params.provider / params.model are set,
|
|
@@ -120,16 +146,11 @@ export function apply(ctx) {
|
|
|
120
146
|
function workspaceRoot(){ try { if(rootAgent&&rootAgent.session&&rootAgent.session.header&&rootAgent.session.header.cwd) return rootAgent.session.header.cwd } catch(e){} const sp=sandboxPolicyOf(); if(sp&&sp.workspaceRoot) return sp.workspaceRoot; return '.' }
|
|
121
147
|
function vibeRoot(){ return (workspaceRoot()+'/VibeMath').replace(/\\/g,'/') }
|
|
122
148
|
function frameworkRoot(){ return vibeRoot()+'/Projects/'+currentProject }
|
|
123
|
-
function slugify(s){ const t=String(s==null?'':s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g,'-').replace(/^-+|-+$/g,''); return t||'project' }
|
|
124
149
|
// Object ids (verify targets, recorded cards) become FILE NAMES and DIRECTORY PATHS
|
|
125
150
|
// (Verified/命题/<id>.md, Shared/debates/<id>.md, Propos/<r>/<id>.md, source-card scans).
|
|
126
151
|
// A hostile/sloppy id containing path separators ('../../x') or Windows-forbidden chars would
|
|
127
152
|
// escape the project tree. Keep every harmless character (incl. Chinese) and replace only
|
|
128
153
|
// separators/control chars; strip leading/trailing dots/dashes so the name is never '.'/'..'.
|
|
129
|
-
function idSafe(s){
|
|
130
|
-
const t=String(s==null?'':s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g,'-').replace(/-{2,}/g,'-').replace(/^[.\-]+|[.\-]+$/g,'')
|
|
131
|
-
return t||'id'
|
|
132
|
-
}
|
|
133
154
|
let warnedNoPolicy = false
|
|
134
155
|
function warnNoPolicyOnce(){ if(!warnedNoPolicy){ warnedNoPolicy=true; console.error('vibe-math-v4: sandboxPolicy unavailable; writes go out with no explicit policy') } }
|
|
135
156
|
function getPolicy(){ const sp=sandboxPolicyOf(); if(!sp){ warnNoPolicyOnce(); return undefined } try { if(rootAgent&&rootAgent.session) return sp.resolve({session:rootAgent.session}) } 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 }
|
|
@@ -238,13 +259,6 @@ export function apply(ctx) {
|
|
|
238
259
|
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 } }
|
|
239
260
|
async function readCurrentProject(){ try { const t=await readTextAbs(vibeRoot()+'/.current'); if(t) return String(t).trim() } catch(e){} return currentProject }
|
|
240
261
|
async function writeCurrentProject(){ try { await writeTextAbs(vibeRoot()+'/.current', currentProject) } catch(e){} }
|
|
241
|
-
function tryJson(s){ try { return JSON.parse(s) } catch(e){ return undefined } }
|
|
242
|
-
function parseReply(text){
|
|
243
|
-
let obj; const fence=/```(?:json)?[ \t]*([\s\S]*?)```/gi; let m
|
|
244
|
-
while((m=fence.exec(text))!==null){ const o=tryJson(m[1].trim()); if(o&&typeof o==='object'&&!Array.isArray(o)) obj=o }
|
|
245
|
-
if(!obj){ const w=tryJson(text.trim()); if(w&&typeof w==='object'&&!Array.isArray(w)) obj=w }
|
|
246
|
-
return obj||{}
|
|
247
|
-
}
|
|
248
262
|
|
|
249
263
|
// ================= Lean formal verification ==============================
|
|
250
264
|
// Contract: docs/formal-verification.md (shared by v2/v3/v4/v5).
|
|
@@ -384,9 +398,10 @@ export function apply(ctx) {
|
|
|
384
398
|
// contract (§7) additionally requires `handle.terminate()` on timeout, and a host that
|
|
385
399
|
// ignores/exceeds graceMs would otherwise leave the Lean process running while the framework
|
|
386
400
|
// reports LEAN_TIMEOUT. Race `done` against a cap-ms timer that terminates the handle and
|
|
387
|
-
// resolves a synthetic outcome. Note: the
|
|
388
|
-
//
|
|
389
|
-
//
|
|
401
|
+
// resolves a synthetic outcome. Note: the `timer` service (ctx.timeout) is the timer used here
|
|
402
|
+
// because it returns the disposer we clear below when `done` wins the race (a fiber-owned
|
|
403
|
+
// disposer), NOT because global timers are missing: they exist for file rows like this preset
|
|
404
|
+
// (see the header note — only a DYNAMIC package's host half is vm-sandboxed).
|
|
390
405
|
let killedByUs=false, timerDispose=null
|
|
391
406
|
let outcome
|
|
392
407
|
try {
|
|
@@ -999,15 +1014,82 @@ export function apply(ctx) {
|
|
|
999
1014
|
return L.join('\n')
|
|
1000
1015
|
}
|
|
1001
1016
|
|
|
1017
|
+
// ---- host live-child cap: a refused spawn must never kill the run --------------
|
|
1018
|
+
// A ROOT agent's live continuable children are capped by the host (see the module-scope note):
|
|
1019
|
+
// `startContinuable` throws ACTIVATION_LIMIT_REACHED once the cap is full. Without the handling
|
|
1020
|
+
// below that throw escaped `start()` mid-loop, so the run was left with `running=true`, a
|
|
1021
|
+
// half-built team and an unhandled rejection.
|
|
1022
|
+
let childLimitLoggedThisRound = false // ONE actionable line per ROUND, not one per refused child
|
|
1023
|
+
let pendingSpawns = [] // residents the cap refused: retried a round later
|
|
1024
|
+
function beginSpawnRound(){ childLimitLoggedThisRound = false }
|
|
1025
|
+
// LIVE continuable children of THIS framework: a resident that holds a childId and has not been
|
|
1026
|
+
// stopped. A refused resident carries childId '' so it can never inflate (or hide behind) the
|
|
1027
|
+
// real ceiling, and is never counted as a voter/speaker either (see noteSpawnRefused).
|
|
1028
|
+
function liveChildCount(){ let n=0; for(const [,r] of residents){ if(r.childId && r.status!=='stopped') n++ } return n }
|
|
1029
|
+
// Mark a resident whose spawn the host refused and say ONCE per round what the operator can do.
|
|
1030
|
+
// The resident is kept OUT of the live roster: a childless member must not be counted by the
|
|
1031
|
+
// meeting/verify consensus (they require EVERY resident to speak/vote, and no end event can ever
|
|
1032
|
+
// arrive for a child that was never created), and `wakeResident` could never deliver to it. It is
|
|
1033
|
+
// queued in `pendingSpawns` instead, so the work is deferred rather than lost.
|
|
1034
|
+
function noteSpawnRefused(r, limit){
|
|
1035
|
+
r.childId=''; r.status='refused'; r.lastActiveAt=now()
|
|
1036
|
+
// `start()`/`addMember` never put the resident in the roster, but a RESPAWN (resume) does:
|
|
1037
|
+
// drop it here (the queued retry re-registers it, and its own end event resumes the flow).
|
|
1038
|
+
if(residents.get(r.rId)===r) residents.delete(r.rId)
|
|
1039
|
+
busy.delete(r.rId); wakeKind.delete(r.rId)
|
|
1040
|
+
if(currentResident===r.rId) currentResident=''
|
|
1041
|
+
if(pendingSpawns.indexOf(r)<0) pendingSpawns.push(r)
|
|
1042
|
+
if(childLimitLoggedThisRound) return
|
|
1043
|
+
childLimitLoggedThisRound=true
|
|
1044
|
+
const line='vibe-math-v4: '+hostChildLimitHint(limit)+'。'+r.rId+' 等常驻本轮未能创建;'
|
|
1045
|
+
+'可在宿主的 subagent 行把 maxActiveSubagents 调大(或调小 residentCount),未创建的常驻已排队,有空位时自动重试。'
|
|
1046
|
+
console.error(line)
|
|
1047
|
+
logActivity('spawn-refused', line)
|
|
1048
|
+
}
|
|
1049
|
+
// One retry ROUND for the queued residents. Called from scheduleNext (the single choke point of
|
|
1050
|
+
// every scheduling pass) and paced by the heartbeat, so a capped team is never retried in a
|
|
1051
|
+
// tight loop; a successful retry registers the resident normally and its own end event resumes
|
|
1052
|
+
// the normal flow.
|
|
1053
|
+
async function retryPendingSpawns(){
|
|
1054
|
+
if(!pendingSpawns.length) return
|
|
1055
|
+
const queue=pendingSpawns.slice(); pendingSpawns=[]; beginSpawnRound()
|
|
1056
|
+
for(const r of queue){
|
|
1057
|
+
try { await spawnResident(r) }
|
|
1058
|
+
catch(e){
|
|
1059
|
+
// A non-cap failure has no other caller to report to here: say it once and drop this
|
|
1060
|
+
// resident (retrying a broken tool filter every round forever would be worse).
|
|
1061
|
+
console.error('vibe-math-v4: 重试创建常驻 '+r.rId+' 失败,已放弃该常驻:'+String((e&&e.message)||e))
|
|
1062
|
+
logActivity('spawn-failed', r.rId+' '+String((e&&e.message)||e))
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
await saveAll()
|
|
1066
|
+
if(pendingSpawns.length) armHeartbeat() // still capped: re-check on the next heartbeat window
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1002
1069
|
// ---- resident lifecycle ----
|
|
1003
1070
|
let residentSeq = 0
|
|
1004
1071
|
function newResident(dir){ const rId='r-'+(++residentSeq); return {rId,childId:'',direction:dir||'',status:'brainstorm',rounds:0,roundsSinceCompact:0,lastActiveAt:now(),insight:'',contextPct:0,contextSeed:'',needCompact:false} }
|
|
1005
1072
|
async function spawnResident(r){
|
|
1073
|
+
// Skip BEFORE touching the host when we already KNOW the ceiling and our own live children
|
|
1074
|
+
// fill it: the host would refuse this spawn too, and one line per ROUND (not per child) is
|
|
1075
|
+
// enough. The resident is queued and the next round retries it.
|
|
1076
|
+
if(hostChildLimit!==undefined && liveChildCount()>=hostChildLimit){ noteSpawnRefused(r, hostChildLimit); return false }
|
|
1006
1077
|
const ao=residentAgentOptions(); const tf=residentToolFilter()
|
|
1007
|
-
|
|
1078
|
+
let started
|
|
1079
|
+
try {
|
|
1080
|
+
started=await startWithToolFilter(tf, function(f){ return {provider:pickProvider(),label:r.rId,request:{prompt:[textBlock(brainstormPrompt(r))],parent:rootAgent,agentOptions:ao,...(f?{toolFilter:f}:{})},signal:makeSignal(params.activityTimeoutMs||60000)} })
|
|
1081
|
+
} catch(e){
|
|
1082
|
+
// The host's live-child cap is a HOST limit (maxActiveSubagents on the `subagent` row), not
|
|
1083
|
+
// a defect in this preset: remember the ceiling, keep the resident for a later round, and
|
|
1084
|
+
// do NOT let the throw escape (it used to abort start()'s spawn loop). Every OTHER start
|
|
1085
|
+
// failure keeps its previous behaviour and still propagates.
|
|
1086
|
+
if(!isActivationLimitReached(e)) throw e
|
|
1087
|
+
noteSpawnRefused(r, noteChildLimit(e)); return false
|
|
1088
|
+
}
|
|
1008
1089
|
r.childId=started.childId; r.status='brainstorm'; r.lastActiveAt=now()
|
|
1009
1090
|
childOwner.set(started.childId,sessionId); busy.add(r.rId); wakeKind.set(r.rId,'normal'); currentResident=r.rId
|
|
1010
1091
|
residents.set(r.rId,r); await saveAll(); logActivity('spawn',r.rId+' ('+(r.direction||'brainstorm')+')')
|
|
1092
|
+
return true
|
|
1011
1093
|
}
|
|
1012
1094
|
async function wakeResident(r, promptText, kind){
|
|
1013
1095
|
if(!r || !r.childId) return false // a removed resident must never be woken (else r.childId would crash)
|
|
@@ -1040,8 +1122,11 @@ export function apply(ctx) {
|
|
|
1040
1122
|
// The DSH continuable-wake API is subagents.sendMessage(sender, targetId, content, {signal}),
|
|
1041
1123
|
// NOT subagents.followup (which is only Agent.followup, and does NOT exist on the subagents
|
|
1042
1124
|
// service). Using a non-existent method threw TypeError and made EVERY wake fail silently →
|
|
1043
|
-
// the group went idle forever. Prefer sendMessage;
|
|
1044
|
-
//
|
|
1125
|
+
// the group went idle forever. Prefer sendMessage; the legacy `subagents.followup` branch is
|
|
1126
|
+
// kept for the earliest supported hosts only: that service method was dropped in DSH
|
|
1127
|
+
// 0.1.2-rc.1, so it is reachable only on 0.1.2-alpha.* — 0.1.2-rc.1 and every later release
|
|
1128
|
+
// this package targets expose startContinuable/sendMessage/drainContinuableChildren and
|
|
1129
|
+
// nothing else (checked against dsh-subagent 0.2.0-rc.2).
|
|
1045
1130
|
try {
|
|
1046
1131
|
if(typeof subagents.sendMessage==='function'){
|
|
1047
1132
|
await subagents.sendMessage(rootAgent, r.childId, [textBlock(prompt)], {signal: makeSignal(params.activityTimeoutMs||60000)})
|
|
@@ -1510,6 +1595,29 @@ export function apply(ctx) {
|
|
|
1510
1595
|
* plugin row where it is (nothing else moves in or out of the realm) while making
|
|
1511
1596
|
* both compaction paths agree on one instance.
|
|
1512
1597
|
*/
|
|
1598
|
+
// ---- toolFilter names the host may not register -------------------------
|
|
1599
|
+
// `tools.restrict` throws for a name outside the host's restrictable set, and that throw escapes
|
|
1600
|
+
// child creation, so one stale name in `vibe_v4_set{toolAllow|toolDeny}` would make every spawn
|
|
1601
|
+
// fail with no operator-facing message. The host's own rejection lists every registered tool, so
|
|
1602
|
+
// the retry below never guesses. FAIL CLOSED: if nothing survives the filter we rethrow instead of
|
|
1603
|
+
// spawning WITHOUT one (that would grant exactly what the operator denied).
|
|
1604
|
+
async function startWithToolFilter(toolFilter, makeSpec){
|
|
1605
|
+
try {
|
|
1606
|
+
return await subagents.startContinuable(makeSpec(toolFilter))
|
|
1607
|
+
} catch(e){
|
|
1608
|
+
const message = String((e && e.message) || e)
|
|
1609
|
+
const retry = sanitizeToolFilter(toolFilter, registeredToolsFromError(message))
|
|
1610
|
+
if(retry===undefined){
|
|
1611
|
+
console.error('vibe-math-v4: 配置的工具过滤只包含本宿主未注册的工具名,拒绝在不带过滤的情况下启动子代理。filter=' + JSON.stringify(toolFilter) + ' 宿主提示:' + message)
|
|
1612
|
+
throw e
|
|
1613
|
+
}
|
|
1614
|
+
if(JSON.stringify(retry) === JSON.stringify(toolFilter)) throw e
|
|
1615
|
+
console.error('vibe-math-v4: 工具过滤里有本宿主未注册的名字,已只保留已注册的名字重试。dropped=' + JSON.stringify(toolFilter) + ' kept=' + JSON.stringify(retry))
|
|
1616
|
+
// a FRESH spec (and a fresh timeout signal) for the retry
|
|
1617
|
+
return await subagents.startContinuable(makeSpec(retry))
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1513
1621
|
function compactionForAgent(agent){
|
|
1514
1622
|
try { const c = agent && agent.ctx ? agent.ctx.get('compaction') : undefined; if(c && c.compactIfNeeded) return c } catch(e){}
|
|
1515
1623
|
// fallback: this plugin's own plane (host root for a row outside the realm)
|
|
@@ -1520,10 +1628,15 @@ export function apply(ctx) {
|
|
|
1520
1628
|
const agent = liveAgentOf(r.childId)
|
|
1521
1629
|
if(!agent || !agent.session) return
|
|
1522
1630
|
const compaction = compactionForAgent(agent)
|
|
1523
|
-
if(compaction===undefined
|
|
1631
|
+
if(compaction===undefined) return
|
|
1632
|
+
// `compactIfNeeded` is a POLICY call on the current host: it may return null without compacting
|
|
1633
|
+
// and nothing records that, so a real /compact would silently do nothing. `compactNow` is the
|
|
1634
|
+
// forcing verb; feature-detected so older hosts keep the policy call.
|
|
1635
|
+
const force = typeof compaction.compactNow === 'function'
|
|
1636
|
+
if(!force && typeof compaction.compactIfNeeded !== 'function') return
|
|
1524
1637
|
try {
|
|
1525
1638
|
const signal = makeSignal(params.activityTimeoutMs||60000)
|
|
1526
|
-
const result = await compaction.compactIfNeeded(agent, 'pressure', signal)
|
|
1639
|
+
const result = force ? await compaction.compactNow(agent, signal) : await compaction.compactIfNeeded(agent, 'pressure', signal)
|
|
1527
1640
|
if(result && (result.shadowedSeqs||[]).length>0){
|
|
1528
1641
|
// the resident's real session was compacted → its context is now a summary.
|
|
1529
1642
|
// Flag needCompact so the NEXT wake re-anchors the core rules (they may have been blurred).
|
|
@@ -1536,6 +1649,10 @@ export function apply(ctx) {
|
|
|
1536
1649
|
// ---- liveness / scheduling ----
|
|
1537
1650
|
async function scheduleNext(){
|
|
1538
1651
|
if(!running||autoDone){ clearHeartbeat(); return }
|
|
1652
|
+
// Residents the host's live-child cap refused are retried here — the single choke point every
|
|
1653
|
+
// scheduling pass goes through — so a refused spawn is deferred by a whole round (and paced by
|
|
1654
|
+
// the heartbeat) instead of being retried inside the loop that discovered the refusal.
|
|
1655
|
+
if(pendingSpawns.length) await retryPendingSpawns()
|
|
1539
1656
|
if(phase==='brainstorm'){ await maybeFinishBrainstorm(); return }
|
|
1540
1657
|
if(meetingState){ await continueMeetingRound(); return }
|
|
1541
1658
|
if(verifyState){ await continueVerifyRound(); return }
|
|
@@ -1581,6 +1698,10 @@ export function apply(ctx) {
|
|
|
1581
1698
|
const free = mp>0 ? (mp - busy.size) : Number.MAX_SAFE_INTEGER
|
|
1582
1699
|
if(free<=0) break // concurrency cap reached → stop filling
|
|
1583
1700
|
if((now()-r.lastActiveAt)<atOs) break // the remaining are all busy-or-not-idle-enough
|
|
1701
|
+
// A resident with no childId has nothing to wake (wakeResident would refuse): that is a
|
|
1702
|
+
// resident whose spawn the host cap refused, and it is retried by retryPendingSpawns() at
|
|
1703
|
+
// the top of the next pass — never in a tight loop here.
|
|
1704
|
+
if(!r.childId) continue
|
|
1584
1705
|
let ok=false
|
|
1585
1706
|
try { ok = await wakeResident(r, await heartbeatPrompt(r), 'normal') } catch(e){ ok=false }
|
|
1586
1707
|
if(ok) started++
|
|
@@ -1724,8 +1845,20 @@ export function apply(ctx) {
|
|
|
1724
1845
|
busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; lastSyncMeetingAt=0; finalizeLock=null; verifiedRecently.clear() // fresh run must NOT inherit stale concurrency/coordination state (busy/wakeKind/currentResident/pendingMeeting) from a previous run on the same reused session
|
|
1725
1846
|
lastActivityAt=now(); lastProgressAt=now() // fresh stall/activity clock for the new run (else B could fire immediately on a reused session)
|
|
1726
1847
|
const dirs=Array.isArray(seedDirections)?seedDirections.slice(0,params.residentCount):[]
|
|
1727
|
-
|
|
1728
|
-
|
|
1848
|
+
pendingSpawns=[] // a fresh run replaces the roster: nothing is queued from before
|
|
1849
|
+
beginSpawnRound() // the whole spawn loop is ONE round (one cap notice, not N)
|
|
1850
|
+
let startedCount=0
|
|
1851
|
+
// spawnResident no longer throws for the host's live-child cap: it queues the refused resident
|
|
1852
|
+
// and returns false, so a capped host leaves a RUNNING team of the residents it accepted
|
|
1853
|
+
// instead of an aborted half-built run.
|
|
1854
|
+
for(let i=0;i<params.residentCount;i++){ const r=newResident(dirs[i]||''); if(await spawnResident(r)) startedCount++ }
|
|
1855
|
+
await saveAll()
|
|
1856
|
+
if(pendingSpawns.length) armHeartbeat() // retry the queued residents on the next round
|
|
1857
|
+
// Report the LIVE count when the host refused part of the team: "4 resident(s)" while only 2
|
|
1858
|
+
// exist would be exactly the kind of half-truth this fix exists to remove.
|
|
1859
|
+
const refused=pendingSpawns.length
|
|
1860
|
+
return {ok:true,message:'v4 started: '+(refused?(startedCount+'/'+params.residentCount):params.residentCount)+' resident(s) brainstorming'
|
|
1861
|
+
+(refused?('(宿主同时在活的子代理已达上限,'+refused+' 个常驻已排队、有空位时自动重试;'+hostChildLimitHint(hostChildLimit)+')'):''),project:currentProject}
|
|
1729
1862
|
}
|
|
1730
1863
|
async function resume(){
|
|
1731
1864
|
currentProject=await readCurrentProject(); await ensureDirs()
|
|
@@ -1761,6 +1894,7 @@ export function apply(ctx) {
|
|
|
1761
1894
|
for(const [,r] of residents){ r.childId=''; r.status='brainstorm'; r.insight=''; r.roundsSinceCompact=0 }
|
|
1762
1895
|
busy=new Set(); wakeKind=new Map(); currentResident=''; pendingMeeting=null; pendingVerify=[]; verifyState=null; meetingState=null; finalizeLock=null; verifiedRecently.clear()
|
|
1763
1896
|
}
|
|
1897
|
+
beginSpawnRound() // the whole re-spawn loop is ONE round for the cap notice
|
|
1764
1898
|
for(const [,r] of residents){ if(!r.childId){ await spawnResident(r) } }
|
|
1765
1899
|
if(!running){ running=true; autoDone=false; if(phase==='idle') phase='active' }
|
|
1766
1900
|
if(needRespawn && phase!=='brainstorm') phase='brainstorm' // let re-spawned residents re-bootstrap together
|
|
@@ -1773,6 +1907,10 @@ export function apply(ctx) {
|
|
|
1773
1907
|
}
|
|
1774
1908
|
function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
|
|
1775
1909
|
residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
|
|
1910
|
+
// Residents the host's live-child cap refused (maxActiveSubagents): queued, not lost. Without
|
|
1911
|
+
// this the only trace would be the one console line, and `residentCount` alone cannot tell a
|
|
1912
|
+
// short team from a deliberately small one.
|
|
1913
|
+
pendingSpawns: pendingSpawns.length, hostChildLimit: (hostChildLimit===undefined?null:hostChildLimit),
|
|
1776
1914
|
meetingInProgress: !!(meetingState), verifyInProgress: !!(verifyState), pendingVerify: pendingVerify.length?pendingVerify[0].targetId:null, pendingVerifyCount: pendingVerify.length,
|
|
1777
1915
|
parkedMeeting: pendingMeeting?pendingMeeting.agenda:null,
|
|
1778
1916
|
// The Lean knobs and the per-object formal records are part of the readable status: without
|
|
@@ -1803,7 +1941,9 @@ export function apply(ctx) {
|
|
|
1803
1941
|
// run is live: on a concluded (autoDone) or never-started/paused run the new member would work
|
|
1804
1942
|
// with nobody to coordinate (zombie work on a project the group already declared done).
|
|
1805
1943
|
if(!running || autoDone) return {ok:false,message:'no active run to join (start or resume first)'}
|
|
1806
|
-
const r=newResident(direction||'')
|
|
1944
|
+
const r=newResident(direction||'')
|
|
1945
|
+
beginSpawnRound()
|
|
1946
|
+
if(!await spawnResident(r)) return {ok:false,code:'ACTIVATION_LIMIT_REACHED',message:'本宿主同时在活的子代理已达上限,'+r.rId+' 未能创建(已排队、有空位时自动重试)。'+hostChildLimitHint(hostChildLimit)}
|
|
1807
1947
|
// Mid-meeting additions must join the meeting's speaking order; otherwise allSpoke (over CURRENT
|
|
1808
1948
|
// residents) can never be true for the new member (not in the snapshot order) and the meeting is
|
|
1809
1949
|
// only ever released by the stuck watchdog instead of finalizing with everyone's input.
|
|
@@ -2081,3 +2221,86 @@ export function apply(ctx) {
|
|
|
2081
2221
|
.finally(()=>{ if(s.forgetAgent) s.forgetAgent(info.id) })
|
|
2082
2222
|
})
|
|
2083
2223
|
}
|
|
2224
|
+
|
|
2225
|
+
// ---- test seam: pure, stateless helpers --------------------------------
|
|
2226
|
+
// These helpers were declared inside `apply()` and are now declared at module scope, so
|
|
2227
|
+
// `apply()` closes over exactly the same function objects this export hands out. The audit
|
|
2228
|
+
// suites therefore exercise the REAL implementations by importing this module, instead of
|
|
2229
|
+
// extracting source text and compiling function bodies through the Function constructor
|
|
2230
|
+
// (dynamic code execution, rejected by the plugin-catalog security scan as
|
|
2231
|
+
// DANGEROUS_DYNAMIC_EXECUTION).
|
|
2232
|
+
//
|
|
2233
|
+
// Contract: no member may touch `ctx`, session state or mutable module state. Most are pure;
|
|
2234
|
+
// three are deliberately non-deterministic (`uuid`/`shortId` use Math.random, `fmtTime` falls back
|
|
2235
|
+
// to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
|
|
2236
|
+
// Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
|
|
2237
|
+
// byte-identical to the previous in-`apply` declarations.
|
|
2238
|
+
export const __testHelpers = {
|
|
2239
|
+
uuid,
|
|
2240
|
+
shortId,
|
|
2241
|
+
clamp01,
|
|
2242
|
+
fmtTime,
|
|
2243
|
+
cl,
|
|
2244
|
+
clPct,
|
|
2245
|
+
blocksToText,
|
|
2246
|
+
posMs,
|
|
2247
|
+
slugify,
|
|
2248
|
+
idSafe,
|
|
2249
|
+
tryJson,
|
|
2250
|
+
parseReply,
|
|
2251
|
+
sanitizeToolFilter,
|
|
2252
|
+
registeredToolsFromError,
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function now(){ return Date.now() }
|
|
2256
|
+
|
|
2257
|
+
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 }
|
|
2258
|
+
|
|
2259
|
+
function shortId(){ const h='0123456789abcdef'; let s=''; for(let i=0;i<8;i++) s+=h[Math.floor(Math.random()*16)]; return s }
|
|
2260
|
+
|
|
2261
|
+
function clamp01(v){ const n=Number(v); if(!Number.isFinite(n)) return 0.5; return Math.max(0,Math.min(1,n)) }
|
|
2262
|
+
|
|
2263
|
+
function fmtTime(ts){ try { return new Date(ts||now()).toISOString().replace('T',' ').slice(0,19) } catch(e){ return String(ts||'') } }
|
|
2264
|
+
|
|
2265
|
+
function cl(x){ return clamp01(Number(x)) }
|
|
2266
|
+
|
|
2267
|
+
function clPct(x){ const n=Number(x); if(!Number.isFinite(n)) return 0; return Math.max(0,Math.min(100,n)) }
|
|
2268
|
+
|
|
2269
|
+
function blocksToText(b){ if(!b) return ''; let out=''; for(const x of b){ if(x&&x.type==='text'&&typeof x.text==='string') out+=x.text+'\n' } return out.trim() }
|
|
2270
|
+
|
|
2271
|
+
function posMs(v,def){ const n=Number(v); return (Number.isFinite(n)&&n>0)?n:(def||120000) }
|
|
2272
|
+
|
|
2273
|
+
function slugify(s){ const t=String(s==null?'':s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g,'-').replace(/^-+|-+$/g,''); return t||'project' }
|
|
2274
|
+
|
|
2275
|
+
function idSafe(s){
|
|
2276
|
+
const t=String(s==null?'':s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g,'-').replace(/-{2,}/g,'-').replace(/^[.\-]+|[.\-]+$/g,'')
|
|
2277
|
+
return t||'id'
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function tryJson(s){ try { return JSON.parse(s) } catch(e){ return undefined } }
|
|
2281
|
+
|
|
2282
|
+
function parseReply(text){
|
|
2283
|
+
let obj; const fence=/```(?:json)?[ \t]*([\s\S]*?)```/gi; let m
|
|
2284
|
+
while((m=fence.exec(text))!==null){ const o=tryJson(m[1].trim()); if(o&&typeof o==='object'&&!Array.isArray(o)) obj=o }
|
|
2285
|
+
if(!obj){ const w=tryJson(text.trim()); if(w&&typeof w==='object'&&!Array.isArray(w)) obj=w }
|
|
2286
|
+
return obj||{}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function sanitizeToolFilter(filter, known){
|
|
2290
|
+
if(!filter || !(known instanceof Set) || known.size === 0) return filter
|
|
2291
|
+
const out = {}
|
|
2292
|
+
for(const key of ['allow','deny']){
|
|
2293
|
+
const list = filter[key]
|
|
2294
|
+
if(!Array.isArray(list)) continue
|
|
2295
|
+
const kept = list.filter(function(n){ return known.has(String(n).trim()) })
|
|
2296
|
+
if(kept.length > 0) out[key] = kept
|
|
2297
|
+
}
|
|
2298
|
+
return (out.allow || out.deny) ? out : undefined
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function registeredToolsFromError(message){
|
|
2302
|
+
const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
|
|
2303
|
+
if(!m) return undefined
|
|
2304
|
+
const names = m[1].split(',').map(function(s){ return s.trim() }).filter(Boolean)
|
|
2305
|
+
return names.length > 0 ? new Set(names) : undefined
|
|
2306
|
+
}
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
# tasks — the academician does that, as a member subject to the same m-vote rule.
|
|
20
20
|
#
|
|
21
21
|
# The vibe-math-v5 plugin row consumes HOST subagents/agents/fs/tools/commands/
|
|
22
|
-
# timer
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# timer services and publishes nothing, so it needs no isolate realm. Institute
|
|
23
|
+
# state lives in its own hardened JSON file (State/<institute>.v5state.json):
|
|
24
|
+
# the plugin appends NOTHING to any host session log.
|
|
25
25
|
|
|
26
26
|
# ── identity ────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
@@ -373,10 +373,10 @@
|
|
|
373
373
|
# organization & task assignment / m-vote boolean consensus verification /
|
|
374
374
|
# hire & fire / context compaction / resume / human intervention).
|
|
375
375
|
#
|
|
376
|
-
# It consumes HOST subagents/agents/fs/tools/commands/timer services
|
|
377
|
-
#
|
|
378
|
-
#
|
|
379
|
-
#
|
|
380
|
-
#
|
|
376
|
+
# It consumes HOST subagents/agents/fs/tools/commands/timer services; it provides
|
|
377
|
+
# no service of its own, so it sits loose (no isolate realm). Institute state is
|
|
378
|
+
# persisted by the plugin itself in State/<institute>.v5state.json — the host
|
|
379
|
+
# session log is never written (DSH refuses to load a session that carries an
|
|
380
|
+
# unknown event type, and Session.append cannot mark one ignorable).
|
|
381
381
|
- id: vibe-math-v5
|
|
382
382
|
name: './vibe-math-v5.js'
|
package/vibe-math-v5/preset.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
name: Vibe Math V5
|
|
2
|
-
description: 研究所体系(第五代架构):设院士(领头人/组织协调中心)、常驻研究员、临时工三类职员;成员可群聊/开会、自主分工与雇佣解雇临时工;院士统筹全所、拆解并分派任务、设定优先级、主持会议、督导进度;每位成员有自己的 Progress/命题/方法/子问题库(progress 有完整定义与用途说明);任何结论须至少 m 名有表决权者一致给出布尔概率 1 或 0 才写入 Verified
|
|
2
|
+
description: 研究所体系(第五代架构):设院士(领头人/组织协调中心)、常驻研究员、临时工三类职员;成员可群聊/开会、自主分工与雇佣解雇临时工;院士统筹全所、拆解并分派任务、设定优先级、主持会议、督导进度;每位成员有自己的 Progress/命题/方法/子问题库(progress 有完整定义与用途说明);任何结论须至少 m 名有表决权者一致给出布尔概率 1 或 0 才写入 Verified/,否则留库附全组平均概率;上下文达阈值自动压缩;仅当全体有表决权者一致认为原问题已解决才停止;状态存于加固 JSON(State/<研究所>.v5state.json),支持断点续跑与多会话隔离。
|