dsh-vibe-math 2.3.15 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +93 -87
- package/README.md +58 -53
- package/cordis.patch.yml +1123 -10
- package/docs/AUDIT-CHECKLIST.md +107 -43
- package/docs/COMPAT-AUDIT-ROUND2.md +3 -0
- package/docs/COMPAT-AUDIT-ROUND3.md +139 -0
- package/docs/generate_framework_diagram_v2_en.mjs +595 -0
- package/docs/generate_framework_diagram_v3_en.mjs +624 -0
- package/docs/generate_framework_diagram_v4.mjs +380 -125
- package/docs/generate_framework_diagram_v5.mjs +366 -108
- package/docs/release-notes/RELEASE-NOTES-2.3.16.md +165 -0
- package/docs/release-notes/RELEASE-NOTES-2.4.0.md +172 -0
- package/docs/test-timing.md +4 -3
- package/docs//346/236/266/346/236/204/345/233/276.md +3 -1
- package/installer.js +89 -21
- package/package.json +40 -9
- package/preset-declaration.js +62 -0
- 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/vibe-math-v2.js +6 -2
- package/vibe-math-v3/vibe-math-v3.js +6 -2
- package/vibe-math-v4/vibe-math-v4.js +191 -13
- 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 +215 -91
- 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-v2-en.svg +291 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v3-en.svg +316 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v4-en.svg +187 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5-en.svg +174 -0
- package//347/244/272/344/276/213/345/233/276//346/241/206/346/236/266/345/233/276-v5.svg +14 -14
|
@@ -25,17 +25,32 @@
|
|
|
25
25
|
// `prompt-v5-integrity.test.mjs` asserts all of that against the real prompt text and
|
|
26
26
|
// writes the full corpus to `prompt-corpus-v5/` for human review (实现方案.md §14.5).
|
|
27
27
|
//
|
|
28
|
-
// Durable state lives in
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
28
|
+
// Durable state lives in the hardened JSON file `State/<institute>.v5state.json` (see the
|
|
29
|
+
// PERSISTENCE note below): the same pure event fold is applied IN MEMORY and each commit writes the
|
|
30
|
+
// whole snapshot through a serial promise chain, so a late writer can never land a stale subset.
|
|
31
|
+
// Events never enter the model history (zero context cost), and recovery is one code path for both
|
|
32
|
+
// an in-process restart and a cross-process resume: read the file, then fold. That structurally
|
|
33
|
+
// removes v4's `State/*.json` corruption / lost-write / resume-staleness class of bugs without
|
|
34
|
+
// touching any host log. (Historically this was a HOST-ONLY session projection unit whose events were
|
|
35
|
+
// appended to the user's session; that design is gone — see the PERSISTENCE note for the exact
|
|
36
|
+
// reason, and note that the JSON file was once the fallback for hosts without that registry.)
|
|
34
37
|
//
|
|
35
38
|
// NOTE: must declare `inject` for every service read as a ctx property (the Guard
|
|
36
|
-
// rejects undeclared dependencies)
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
+
// rejects undeclared dependencies). `timer` IS injected and used (ctx.timeout) so every
|
|
40
|
+
// timer here is a fiber-owned disposer — but the global setTimeout/clearTimeout DO exist
|
|
41
|
+
// in this preset's runtime: a preset is a FILE row loaded by a plain host-realm import().
|
|
42
|
+
// The vm sandbox that traps require/setTimeout/setInterval/fetch wraps only a DYNAMIC
|
|
43
|
+
// package's host half (@deepseek-ai/dsh-cordis-host-runner/lib/types/sandbox.js, reached
|
|
44
|
+
// only from the dynamic-package start path); v2/v3 use those globals and work. An earlier
|
|
45
|
+
// version of this note claimed the globals do not exist — false for file rows, and it would
|
|
46
|
+
// only become true if this preset were ever converted to a dynamic package.
|
|
47
|
+
//
|
|
48
|
+
// PERSISTENCE (fixed after the DSH 0.2.0 audit): institute state lives ONLY in the hardened JSON file
|
|
49
|
+
// `State/<institute>.v5state.json` (see makeFileBackend/installBackend below). It is never appended
|
|
50
|
+
// to a host session log: DSH's session persistence refuses to load a session that carries an unknown
|
|
51
|
+
// event type unless the writer marked it `ignorable: true`, and `Session.append` cannot set that
|
|
52
|
+
// field — so the old projection-based primary made the user's own session unresumable.
|
|
53
|
+
export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands', 'timer']
|
|
39
54
|
|
|
40
55
|
const PROJECTION_KEY = 'vibeMathV5'
|
|
41
56
|
const PROJECTION_VERSION = 1
|
|
@@ -61,13 +76,40 @@ function v5err(code, message) {
|
|
|
61
76
|
return e
|
|
62
77
|
}
|
|
63
78
|
|
|
79
|
+
// ---- host live-child cap (DSH ≥ 0.2) ----------------------------------------
|
|
80
|
+
// The host caps the number of LIVE continuable children PER ROOT AGENT. Evidence (installed
|
|
81
|
+
// dsh-subagent 0.2.0-rc.2): `materialize` calls `ActivationPool.reserve(this.maxActiveSubagents())`
|
|
82
|
+
// and `reserve` throws `SubagentError('subagent limit reached (active child limit: <capacity>);
|
|
83
|
+
// wait for an existing child to finish or complete this work with the current agents',
|
|
84
|
+
// 'ACTIVATION_LIMIT_REACHED')` once every slot is taken. The capacity is the `subagent` row's
|
|
85
|
+
// `maxActiveSubagents` Config key (default 8; no preset sets it), the cap is shared by all
|
|
86
|
+
// descendants of that root, and a slot is released when a child settles. The `.d.ts` does NOT
|
|
87
|
+
// document the throw.
|
|
88
|
+
//
|
|
89
|
+
// The detector and the remembered limit live at MODULE scope because the cap is a property of the
|
|
90
|
+
// HOST (one number for every root of this process), not of one institute. On DSH 0.1.x
|
|
91
|
+
// `subagents.startContinuable` never throws this code, so every branch below is inert there — the
|
|
92
|
+
// error code itself is the feature test, there is no version check anywhere.
|
|
93
|
+
let hostChildLimit // undefined until the host tells us its ceiling (via a refusal)
|
|
94
|
+
function isActivationLimitReached(e) { return String((e && e.code) || '') === 'ACTIVATION_LIMIT_REACHED' }
|
|
95
|
+
function noteChildLimit(e) {
|
|
96
|
+
const m = /active child limit:\s*(\d+)/.exec(String((e && e.message) || e || ''))
|
|
97
|
+
if (m) hostChildLimit = Number(m[1])
|
|
98
|
+
return hostChildLimit
|
|
99
|
+
}
|
|
100
|
+
// One actionable sentence naming the HOST ceiling and the knob that raises it.
|
|
101
|
+
function hostChildLimitHint(limit) {
|
|
102
|
+
const n = (limit === undefined || limit === null) ? '' : ('=' + limit)
|
|
103
|
+
return '本宿主对「同时在活的续聊子代理」有上限(宿主 subagent 行的 maxActiveSubagents 参数' + n
|
|
104
|
+
+ ',写满后子代理服务抛 ACTIVATION_LIMIT_REACHED)'
|
|
105
|
+
}
|
|
106
|
+
|
|
64
107
|
export function apply(ctx) {
|
|
65
108
|
const subagents = ctx.subagents
|
|
66
109
|
const agents = ctx.agents
|
|
67
110
|
const fs = ctx.fs
|
|
68
111
|
const tools = ctx.tools
|
|
69
112
|
const commands = ctx.commands
|
|
70
|
-
const store = ctx.sessions
|
|
71
113
|
|
|
72
114
|
// Optional services are resolved LAZILY at call time, never snapshotted in apply():
|
|
73
115
|
// a `ctx.get()` snapshot taken here is order-sensitive, so a service provided later
|
|
@@ -75,7 +117,16 @@ export function apply(ctx) {
|
|
|
75
117
|
const sandboxPolicyOf = () => { try { return ctx.get('sandboxPolicy') } catch (e) { return undefined } }
|
|
76
118
|
const subprocessOf = () => { try { return ctx.get('subprocess') } catch (e) { return undefined } }
|
|
77
119
|
const compactionOf = () => { try { return ctx.get('compaction') } catch (e) { return undefined } }
|
|
78
|
-
|
|
120
|
+
// Prefer the AGENT's own context: this preset mounts its compaction row inside the preset's
|
|
121
|
+
// `compaction` isolate realm, and a realm never falls back to the host-root instance, so the root
|
|
122
|
+
// object can be a different policy. v4 resolves it this way already.
|
|
123
|
+
const compactionForAgent = (agent) => {
|
|
124
|
+
try {
|
|
125
|
+
const own = agent && agent.ctx && typeof agent.ctx.get === 'function' ? agent.ctx.get('compaction') : undefined
|
|
126
|
+
if (own !== undefined) return own
|
|
127
|
+
} catch (e) { /* fall back to the plugin plane */ }
|
|
128
|
+
return compactionOf()
|
|
129
|
+
}
|
|
79
130
|
|
|
80
131
|
// ---- utils -------------------------------------------------------------
|
|
81
132
|
const now = () => Date.now()
|
|
@@ -119,10 +170,10 @@ export function apply(ctx) {
|
|
|
119
170
|
return true
|
|
120
171
|
}
|
|
121
172
|
|
|
122
|
-
// ----
|
|
173
|
+
// ---- state fold: the one fold every entry point shares ----------------
|
|
123
174
|
// The fold is shared by BOTH persistence backends, so the state machine is
|
|
124
175
|
// defined exactly once. It must return a NEW top-level reference whenever
|
|
125
|
-
// anything changed (the
|
|
176
|
+
// anything changed (the state store compares by Object.is).
|
|
126
177
|
function emptyInstitute(key, project, institute) {
|
|
127
178
|
return {
|
|
128
179
|
key, project, institute,
|
|
@@ -137,7 +188,7 @@ export function apply(ctx) {
|
|
|
137
188
|
debates: [],
|
|
138
189
|
verdicts: {},
|
|
139
190
|
// Per-object Lean formalization record (docs/formal-verification.md). Kept in the
|
|
140
|
-
//
|
|
191
|
+
// state so it survives restore with zero context cost, exactly like verdicts.
|
|
141
192
|
formal: {},
|
|
142
193
|
todo: [],
|
|
143
194
|
queue: [],
|
|
@@ -289,38 +340,10 @@ export function apply(ctx) {
|
|
|
289
340
|
}
|
|
290
341
|
}
|
|
291
342
|
|
|
292
|
-
// Minimal structural validator standing in for a zod schema. The projection
|
|
293
|
-
// registry only ever calls `stateSchema.parse(row.val)` when it reloads a value
|
|
294
|
-
// from a CHECKPOINT row, so this is the guard against a corrupt persisted row —
|
|
295
|
-
// a plain object with `.parse` is sufficient and needs no module import (a
|
|
296
|
-
// preset-local file cannot reliably resolve `zod`).
|
|
297
|
-
const STATE_SCHEMA = {
|
|
298
|
-
parse(v) {
|
|
299
|
-
if (!v || typeof v !== 'object') throw new Error('vibe-math-v5: projection state is not an object')
|
|
300
|
-
if (v.v !== PROJECTION_VERSION) throw new Error('vibe-math-v5: projection state version mismatch')
|
|
301
|
-
if (!v.institutes || typeof v.institutes !== 'object') throw new Error('vibe-math-v5: projection state lacks institutes')
|
|
302
|
-
if (!Array.isArray(v.order)) throw new Error('vibe-math-v5: projection state lacks order')
|
|
303
|
-
return v
|
|
304
|
-
},
|
|
305
|
-
}
|
|
306
|
-
|
|
307
343
|
// ---- persistence backend ----------------------------------------------
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
kind: 'projection',
|
|
313
|
-
read() {
|
|
314
|
-
const s = proj.stateOf(session, PROJECTION_KEY)
|
|
315
|
-
return s === undefined ? initState() : s
|
|
316
|
-
},
|
|
317
|
-
async commit(type, data) {
|
|
318
|
-
session.append(type, data)
|
|
319
|
-
try { await store.flush(session) } catch (e) { /* flush is a durability hint */ }
|
|
320
|
-
return this.read()
|
|
321
|
-
},
|
|
322
|
-
}
|
|
323
|
-
}
|
|
344
|
+
// The hardened JSON file is the ONLY backend. It applies the same event fold in memory and writes
|
|
345
|
+
// the whole snapshot under a per-file serialization chain; nothing is written to a host session log
|
|
346
|
+
// (see the PERSISTENCE note at the top of this file for why the projection backend was removed).
|
|
324
347
|
// `pathOf` is a FUNCTION, not a captured string: the state path depends on the
|
|
325
348
|
// project/institute, which the first successful load syncs back into this session —
|
|
326
349
|
// a captured path would keep writing to the pre-load guess forever.
|
|
@@ -491,11 +514,7 @@ export function apply(ctx) {
|
|
|
491
514
|
async function writeTextRel(rel, content) { return await writeTextAbs(instRoot() + '/' + rel, content) }
|
|
492
515
|
|
|
493
516
|
function installBackend() {
|
|
494
|
-
|
|
495
|
-
const sess = (rootAgent && rootAgent.session) ? rootAgent.session : undefined
|
|
496
|
-
if (proj && sess && typeof proj.stateOf === 'function') backend = makeProjectionBackend(proj, sess)
|
|
497
|
-
else if (proj && sess && typeof proj.register === 'function') backend = makeProjectionBackend(proj, sess)
|
|
498
|
-
else backend = makeFileBackend(readTextAbs, writeTextAbs, () => instRoot() + '/State/' + instituteName + '.v5state.json')
|
|
517
|
+
backend = makeFileBackend(readTextAbs, writeTextAbs, () => instRoot() + '/State/' + instituteName + '.v5state.json')
|
|
499
518
|
return backend
|
|
500
519
|
}
|
|
501
520
|
// Every entry point that READS state must await this first. Without it the file
|
|
@@ -1195,6 +1214,31 @@ export function apply(ctx) {
|
|
|
1195
1214
|
} catch (e) { /* ignore */ }
|
|
1196
1215
|
return 'spawn'
|
|
1197
1216
|
}
|
|
1217
|
+
// ---- host live-child cap: a refused provisioning must be named, not opaque -----
|
|
1218
|
+
// See the module-scope note. `spawnMember` is the ONLY place a member child is created, so the
|
|
1219
|
+
// pre-check and the cap-naming error live here; the callers (founding loop, resume, hire,
|
|
1220
|
+
// addResearcher) only have to say what happened.
|
|
1221
|
+
let childLimitLoggedThisRound = false // ONE actionable line per ROUND, not one per refused member
|
|
1222
|
+
function beginSpawnRound() { childLimitLoggedThisRound = false }
|
|
1223
|
+
// LIVE continuable children of THIS institute: a member that holds a childId and has not been
|
|
1224
|
+
// stopped. A member whose spawn was refused keeps childId '' (see the rollback in spawnMember
|
|
1225
|
+
// and fire()), so it can never inflate the count it is measured against.
|
|
1226
|
+
function liveChildCount() {
|
|
1227
|
+
try { return inst().members.filter((m) => m.childId && m.phase !== 'stopped').length } catch (e) { return 0 }
|
|
1228
|
+
}
|
|
1229
|
+
// Say ONCE per round what the host refused and what raises the ceiling. The member itself is
|
|
1230
|
+
// recorded as `failed` by the caller with the same sentence, so the refusal stays auditable.
|
|
1231
|
+
function noteSpawnLimitOnce(member) {
|
|
1232
|
+
if (childLimitLoggedThisRound) return
|
|
1233
|
+
childLimitLoggedThisRound = true
|
|
1234
|
+
console.error('vibe-math-v5: ' + activationLimitText(hostChildLimit) + (member ? ('(本轮被拒:' + member.id + ')') : ''))
|
|
1235
|
+
}
|
|
1236
|
+
// The operator-facing sentence used for BOTH the typed error and the durable member record.
|
|
1237
|
+
function activationLimitText(limit) {
|
|
1238
|
+
return hostChildLimitHint(limit) + ';本所已有的在活成员已占满该上限,暂时无法再创建成员。'
|
|
1239
|
+
+ '可在宿主的 subagent 行把 maxActiveSubagents 调大后重试,或先 vibe_v5_fire 解雇不用的临时工腾出位置。'
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1198
1242
|
// Bring a member into being. ORDER IS LOAD-BEARING and is the fix for the
|
|
1199
1243
|
// "every brief describes the wrong person" bug:
|
|
1200
1244
|
// 1. commit the member as ACTIVE first, so that everything derived from
|
|
@@ -1211,6 +1255,14 @@ export function apply(ctx) {
|
|
|
1211
1255
|
// session is being rebuilt: the latter must NOT be told it "just joined the
|
|
1212
1256
|
// institute" and must not be shown the induction blurb.
|
|
1213
1257
|
async function spawnMember(member, initialTask, mode) {
|
|
1258
|
+
// Host live-child cap (DSH ≥0.2): once we KNOW the ceiling and our OWN live members fill it,
|
|
1259
|
+
// do not even call the host — it would refuse with the bare "subagent limit reached (active
|
|
1260
|
+
// child limit: N)". One line per ROUND (not per member); the caller records the member as
|
|
1261
|
+
// `failed` with the same sentence, and resume() retries cap-failed members once a slot frees.
|
|
1262
|
+
if (hostChildLimit !== undefined && liveChildCount() >= hostChildLimit) {
|
|
1263
|
+
noteSpawnLimitOnce(member)
|
|
1264
|
+
throw v5err('ACTIVATION_LIMIT_REACHED', activationLimitText(hostChildLimit))
|
|
1265
|
+
}
|
|
1214
1266
|
const provider = member.provider || pickProvider()
|
|
1215
1267
|
const ao = memberAgentOptions()
|
|
1216
1268
|
const tf = memberToolFilter(member)
|
|
@@ -1235,15 +1287,17 @@ export function apply(ctx) {
|
|
|
1235
1287
|
await writeRosterMirror()
|
|
1236
1288
|
let started
|
|
1237
1289
|
try {
|
|
1238
|
-
started = await
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1290
|
+
started = await startWithToolFilter(tf, function (f) {
|
|
1291
|
+
return {
|
|
1292
|
+
provider,
|
|
1293
|
+
label: 'vibe5 ' + member.id + ' (' + kindLabel(member.kind) + ')',
|
|
1294
|
+
request: Object.assign({
|
|
1295
|
+
prompt: [textBlock(prompt)],
|
|
1296
|
+
parent: rootAgent,
|
|
1297
|
+
persona,
|
|
1298
|
+
}, Object.keys(ao).length ? { agentOptions: ao } : {}, f ? { toolFilter: f } : {}),
|
|
1299
|
+
signal: makeSignal(params.activityTimeoutMs),
|
|
1300
|
+
}
|
|
1247
1301
|
})
|
|
1248
1302
|
} catch (e) {
|
|
1249
1303
|
// Roll the in-memory marks back so a failed provisioning leaves no phantom
|
|
@@ -1253,7 +1307,15 @@ export function apply(ctx) {
|
|
|
1253
1307
|
wakeKind.delete(member.id)
|
|
1254
1308
|
rounds.delete(member.id)
|
|
1255
1309
|
roundsSinceCompact.delete(member.id)
|
|
1256
|
-
|
|
1310
|
+
// The host's live-child cap is a HOST limit (maxActiveSubagents on the `subagent` row), not
|
|
1311
|
+
// a defect in this member: remember the ceiling, report it BY NAME instead of relaying the
|
|
1312
|
+
// opaque host string, and throw the TYPED error so the founding loop / hire / addResearcher
|
|
1313
|
+
// name it too instead of surfacing the raw host message.
|
|
1314
|
+
const limitHit = isActivationLimitReached(e)
|
|
1315
|
+
if (limitHit) { noteChildLimit(e); noteSpawnLimitOnce(member) }
|
|
1316
|
+
const message = limitHit ? activationLimitText(hostChildLimit) : String((e && e.message) || e)
|
|
1317
|
+
await putMember(Object.assign({}, memberById(member.id) || member, { phase: 'failed', error: message }))
|
|
1318
|
+
if (limitHit) throw v5err('ACTIVATION_LIMIT_REACHED', message)
|
|
1257
1319
|
throw e
|
|
1258
1320
|
}
|
|
1259
1321
|
member.childId = started.childId
|
|
@@ -1269,6 +1331,10 @@ export function apply(ctx) {
|
|
|
1269
1331
|
// Deliver one prompt to a member. MUST use `subagents.sendMessage` — the
|
|
1270
1332
|
// `subagents` SERVICE has no `followup` (that is only an Agent method); calling
|
|
1271
1333
|
// it threw a TypeError on every wake and silently stalled the whole group (v4 §25).
|
|
1334
|
+
// Unlike v4 there is no legacy fallback branch here, and none is needed: the service
|
|
1335
|
+
// method existed only up to DSH 0.1.2-alpha.*, was dropped in 0.1.2-rc.1, and every
|
|
1336
|
+
// release this preset targets exposes startContinuable/sendMessage/
|
|
1337
|
+
// drainContinuableChildren only (checked against dsh-subagent 0.2.0-rc.2).
|
|
1272
1338
|
async function wakeMember(member, promptText, kind) {
|
|
1273
1339
|
if (!member || !member.childId || member.phase !== 'active') return false
|
|
1274
1340
|
clearHeartbeat()
|
|
@@ -1889,7 +1955,7 @@ export function apply(ctx) {
|
|
|
1889
1955
|
async function writeFormalIndex() {
|
|
1890
1956
|
const recs = formalRecords()
|
|
1891
1957
|
const L = ['# Lean 形式化索引|' + instituteName + '|' + fmtTime(), '',
|
|
1892
|
-
'> 本文件由框架维护(工具调用时更新;`vibe_v5_lean_lib`
|
|
1958
|
+
'> 本文件由框架维护(工具调用时更新;`vibe_v5_lean_lib` 会重建)。权威状态在 State/<研究所>.v5state.json 里。', '',
|
|
1893
1959
|
'| 对象 | 状态 | 形式化文件 | 归档证明 | 最近运行 | 难度判断 / 阻塞原因 |', '|---|---|---|---|---|---|']
|
|
1894
1960
|
const keys = Object.keys(recs)
|
|
1895
1961
|
if (!keys.length) L.push('| (暂无) | | | | | |')
|
|
@@ -2440,7 +2506,7 @@ export function apply(ctx) {
|
|
|
2440
2506
|
async function writeTaskboardMirror() {
|
|
2441
2507
|
const ts = listTasks()
|
|
2442
2508
|
const lines = ['# 任务板(人读镜像)|' + instituteName + '|' + fmtTime(), '',
|
|
2443
|
-
'>
|
|
2509
|
+
'> 权威状态在 State/<研究所>.v5state.json 里;本文件只是给人和所办看的快照,勿手改。', '']
|
|
2444
2510
|
if (!ts.length) lines.push('(暂无任务)')
|
|
2445
2511
|
for (const t of ts) {
|
|
2446
2512
|
lines.push('- [' + t.status + '] ' + t.id + '|' + t.subject + '|owner=' + (t.owner || t.ownerName || '(未认领)') +
|
|
@@ -2452,12 +2518,12 @@ export function apply(ctx) {
|
|
|
2452
2518
|
await writeTextRel('Shared/TaskBoard.md', lines.join('\n'))
|
|
2453
2519
|
}
|
|
2454
2520
|
// The roster mirror (§8.4): a human-readable staffing table. Like the task-board
|
|
2455
|
-
// mirror it is WRITE-ONLY — the authoritative roster is
|
|
2521
|
+
// mirror it is WRITE-ONLY — the authoritative roster is State/<研究所>.v5state.json,
|
|
2456
2522
|
// so losing or hand-editing this file can never corrupt the institute.
|
|
2457
2523
|
async function writeRosterMirror() {
|
|
2458
2524
|
const s = inst()
|
|
2459
2525
|
const lines = ['# 研究所编制表(人读镜像)|' + instituteName + '|' + fmtTime(), '',
|
|
2460
|
-
'>
|
|
2526
|
+
'> 权威状态在 State/<研究所>.v5state.json 里;本文件只是快照,勿手改。', '']
|
|
2461
2527
|
lines.push('- 求真门槛:m = ' + quorumM() + '(模式 ' + params.quorumMode + ')|有表决权者 ' + voterCount() + ' 人')
|
|
2462
2528
|
lines.push('- 阶段:' + phase + '|运行中:' + running + '|已结题:' + autoDone)
|
|
2463
2529
|
lines.push('')
|
|
@@ -3171,6 +3237,10 @@ export function apply(ctx) {
|
|
|
3171
3237
|
await spawnMember(member, initialTask)
|
|
3172
3238
|
} catch (e) {
|
|
3173
3239
|
await putMember(Object.assign({}, memberById(member.id) || member, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
3240
|
+
// Name the host's live-child cap (maxActiveSubagents) instead of relaying its opaque
|
|
3241
|
+
// "subagent limit reached (active child limit: N)" string, and use the typed code so the
|
|
3242
|
+
// hirer can tell a HOST ceiling from a broken provider.
|
|
3243
|
+
if (isActivationLimitReached(e)) { noteChildLimit(e); return { ok: false, code: 'ACTIVATION_LIMIT_REACHED', message: '临时工创建失败:' + activationLimitText(hostChildLimit) } }
|
|
3174
3244
|
return { ok: false, code: 'V5_PROVISIONING_CONFLICT', message: '临时工创建失败:' + String((e && e.message) || e) }
|
|
3175
3245
|
}
|
|
3176
3246
|
await saveChatLine('【雇佣】' + (office ? '所办' : callerId) + ' 雇入临时工 ' + member.id + ',用途:' + purpose)
|
|
@@ -3630,18 +3700,63 @@ export function apply(ctx) {
|
|
|
3630
3700
|
if (member.activeMeetingId) delete member.activeMeetingId
|
|
3631
3701
|
await scheduleNext()
|
|
3632
3702
|
}
|
|
3703
|
+
// ---- toolFilter names the host may not register -------------------------
|
|
3704
|
+
// Same guard as v2/v3/v4: `tools.restrict` throws for an unregistered name and the throw escapes
|
|
3705
|
+
// child creation, so a stale `vibe_v5_set{toolAllow|toolDeny|tempToolAllow|tempToolDeny}` would
|
|
3706
|
+
// make every member spawn fail silently. The host names the registered tools in its rejection.
|
|
3707
|
+
function registeredToolsFromError(message){
|
|
3708
|
+
const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
|
|
3709
|
+
if(!m) return undefined
|
|
3710
|
+
const names = m[1].split(',').map(function(s){ return s.trim() }).filter(Boolean)
|
|
3711
|
+
return names.length > 0 ? new Set(names) : undefined
|
|
3712
|
+
}
|
|
3713
|
+
function sanitizeToolFilter(filter, known){
|
|
3714
|
+
if(!filter || !(known instanceof Set) || known.size === 0) return filter
|
|
3715
|
+
const out = {}
|
|
3716
|
+
for(const key of ['allow','deny']){
|
|
3717
|
+
const list = filter[key]
|
|
3718
|
+
if(!Array.isArray(list)) continue
|
|
3719
|
+
const kept = list.filter(function(n){ return known.has(String(n).trim()) })
|
|
3720
|
+
if(kept.length > 0) out[key] = kept
|
|
3721
|
+
}
|
|
3722
|
+
return (out.allow || out.deny) ? out : undefined
|
|
3723
|
+
}
|
|
3724
|
+
async function startWithToolFilter(toolFilter, makeSpec){
|
|
3725
|
+
try {
|
|
3726
|
+
return await subagents.startContinuable(makeSpec(toolFilter))
|
|
3727
|
+
} catch(e){
|
|
3728
|
+
const message = String((e && e.message) || e)
|
|
3729
|
+
const retry = sanitizeToolFilter(toolFilter, registeredToolsFromError(message))
|
|
3730
|
+
if(retry===undefined){
|
|
3731
|
+
console.error('vibe-math-v5: 配置的工具过滤只包含本宿主未注册的工具名,拒绝在不带过滤的情况下启动成员。filter=' + JSON.stringify(toolFilter) + ' 宿主提示:' + message)
|
|
3732
|
+
throw e
|
|
3733
|
+
}
|
|
3734
|
+
if(JSON.stringify(retry) === JSON.stringify(toolFilter)) throw e
|
|
3735
|
+
console.error('vibe-math-v5: 工具过滤里有本宿主未注册的名字,已只保留已注册的名字重试。dropped=' + JSON.stringify(toolFilter) + ' kept=' + JSON.stringify(retry))
|
|
3736
|
+
// a FRESH spec (and a fresh timeout signal) for the retry
|
|
3737
|
+
return await subagents.startContinuable(makeSpec(retry))
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3633
3741
|
async function maybeRealCompact(childId, member) {
|
|
3634
3742
|
const roundN = roundsSinceCompact.get(member.id) || 0
|
|
3635
3743
|
const pct = contextPct.get(member.id) || 0
|
|
3636
3744
|
const soft = pct >= Number(params.compactThreshold) || roundN >= Number(params.compactAfterRounds)
|
|
3637
3745
|
if (!soft) return
|
|
3638
|
-
const comp = compactionOf()
|
|
3639
3746
|
const agent = liveAgentOf(childId)
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3747
|
+
const comp = compactionForAgent(agent)
|
|
3748
|
+
if (comp && agent && agent.session) {
|
|
3749
|
+
// Prefer the FORCING verb when the host has it: `compactIfNeeded` is a policy call that may
|
|
3750
|
+
// decide not to compact and return null without saying so, while `compactNow` does what the
|
|
3751
|
+
// operator asked for. Feature-detected so older hosts keep the policy call.
|
|
3752
|
+
const force = typeof comp.compactNow === 'function'
|
|
3753
|
+
const ask = typeof comp.compactIfNeeded === 'function'
|
|
3754
|
+
if (force || ask) {
|
|
3755
|
+
try {
|
|
3756
|
+
const r = force ? await comp.compactNow(agent, makeSignal(params.activityTimeoutMs)) : await comp.compactIfNeeded(agent, 'pressure', makeSignal(params.activityTimeoutMs))
|
|
3757
|
+
if (r) { roundsSinceCompact.set(member.id, 0); needReanchor.add(member.id); return }
|
|
3758
|
+
} catch (e) { /* fall through to the soft path */ }
|
|
3759
|
+
}
|
|
3645
3760
|
}
|
|
3646
3761
|
needReanchor.add(member.id)
|
|
3647
3762
|
}
|
|
@@ -3779,11 +3894,11 @@ export function apply(ctx) {
|
|
|
3779
3894
|
await writeTextRel('State/README.md', [
|
|
3780
3895
|
'# 关于 State/',
|
|
3781
3896
|
'',
|
|
3782
|
-
'
|
|
3897
|
+
'本研究所的**权威状态就在本目录的 `<研究所>.v5state.json` 里**,不在任何宿主会话日志里。',
|
|
3783
3898
|
'本目录只存放人可读的镜像/说明,**请勿手改**;改动不会影响真正的状态。',
|
|
3784
3899
|
'要查看状态请用 `vibe_v5_status` / `vibe_v5_report`。',
|
|
3785
3900
|
'',
|
|
3786
|
-
'
|
|
3901
|
+
'研究所状态只写在加固 JSON 里(见下),不写入宿主会话日志:',
|
|
3787
3902
|
'`State/<institute>.v5state.json`(加固 JSON 后端),此时它才是权威源。',
|
|
3788
3903
|
'安装器会在启动自检里报告这一降级。',
|
|
3789
3904
|
'',
|
|
@@ -3829,6 +3944,10 @@ export function apply(ctx) {
|
|
|
3829
3944
|
'从已知的相近结论出发,看能否推广或加强得到所需结果。',
|
|
3830
3945
|
]
|
|
3831
3946
|
const spawned = []
|
|
3947
|
+
// A refused provisioning (the host's live-child cap) is remembered for this founding round so
|
|
3948
|
+
// the aggregate failure below can NAME the cap instead of blaming the provider.
|
|
3949
|
+
let limitRefused = false
|
|
3950
|
+
beginSpawnRound() // the whole founding loop is ONE round for the cap notice
|
|
3832
3951
|
// The academician is founded FIRST so it is on the roster for every later member's
|
|
3833
3952
|
// induction brief, and so it can begin overseeing the founding round. Each member
|
|
3834
3953
|
// is committed to the ACTIVE roster before its own brief is built (see
|
|
@@ -3841,6 +3960,7 @@ export function apply(ctx) {
|
|
|
3841
3960
|
+ '你打算如何组织全所(谁适合做什么、先做什么)?把你的判断写进你的 Progress/,并把关键结论在群聊里说出来。')
|
|
3842
3961
|
spawned.push(m.id)
|
|
3843
3962
|
} catch (e) {
|
|
3963
|
+
if (isActivationLimitReached(e)) limitRefused = true
|
|
3844
3964
|
await putMember(Object.assign({}, memberById(m.id) || m, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
3845
3965
|
}
|
|
3846
3966
|
}
|
|
@@ -3851,11 +3971,18 @@ export function apply(ctx) {
|
|
|
3851
3971
|
await spawnMember(m, null)
|
|
3852
3972
|
spawned.push(m.id)
|
|
3853
3973
|
} catch (e) {
|
|
3974
|
+
if (isActivationLimitReached(e)) limitRefused = true
|
|
3854
3975
|
await putMember(Object.assign({}, memberById(m.id) || m, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
3855
3976
|
}
|
|
3856
3977
|
}
|
|
3857
3978
|
if (!spawned.length) {
|
|
3858
3979
|
await patchInstitute({ phase: 'idle' })
|
|
3980
|
+
// When the HOST's per-root child cap refused every member, say so: the generic
|
|
3981
|
+
// "check the provider" message made a real host ceiling look like a broken host.
|
|
3982
|
+
if (limitRefused) {
|
|
3983
|
+
return { ok: false, code: 'ACTIVATION_LIMIT_REACHED',
|
|
3984
|
+
message: '没有任何成员创建成功:' + activationLimitText(hostChildLimit) }
|
|
3985
|
+
}
|
|
3859
3986
|
return { ok: false, code: 'V5_PROVISIONING_CONFLICT', message: '没有任何成员创建成功;请检查 subagents 提供者与会话持久化是否可用' }
|
|
3860
3987
|
}
|
|
3861
3988
|
running = true
|
|
@@ -3895,9 +4022,16 @@ export function apply(ctx) {
|
|
|
3895
4022
|
syncParamsFromState()
|
|
3896
4023
|
if (autoDone) return { ok: false, code: 'V5_INSTITUTE_STATE', message: 'this institute already concluded; configure a new one in a new session' }
|
|
3897
4024
|
const members = activeMembers()
|
|
3898
|
-
|
|
4025
|
+
// Members the host's live-child cap refused are NOT dead: they are still wanted, and a slot
|
|
4026
|
+
// may have been released since (a temp was fired, another child settled). They are the
|
|
4027
|
+
// "queued" half of the cap handling, retried in this round — gated on the RECORDED cap error
|
|
4028
|
+
// so an ordinary failed member (broken provider, rejected tool filter) is never resurrected
|
|
4029
|
+
// by a resume. `members` keeps its meaning for the return value below.
|
|
4030
|
+
const queued = inst().members.filter((m) => m.phase === 'failed' && /active child limit|maxActiveSubagents/.test(String(m.error || '')))
|
|
4031
|
+
if (!members.length && !queued.length) return { ok: false, code: 'V5_INSTITUTE_STATE', message: 'no active member to resume' }
|
|
3899
4032
|
let respawned = 0
|
|
3900
|
-
|
|
4033
|
+
beginSpawnRound() // the whole re-spawn loop is ONE round for the cap notice
|
|
4034
|
+
for (const m of members.concat(queued)) {
|
|
3901
4035
|
if (m.childId) continue
|
|
3902
4036
|
try {
|
|
3903
4037
|
const seedText = (await readTextRel('Members/' + m.id + '/Progress/progress.md')) || ''
|
|
@@ -4071,6 +4205,7 @@ export function apply(ctx) {
|
|
|
4071
4205
|
await spawnMember(m, null)
|
|
4072
4206
|
} catch (e) {
|
|
4073
4207
|
await putMember(Object.assign({}, memberById(m.id) || m, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
4208
|
+
if (isActivationLimitReached(e)) { noteChildLimit(e); return { ok: false, code: 'ACTIVATION_LIMIT_REACHED', message: activationLimitText(hostChildLimit) } }
|
|
4074
4209
|
return { ok: false, code: 'V5_PROVISIONING_CONFLICT', message: String((e && e.message) || e) }
|
|
4075
4210
|
}
|
|
4076
4211
|
await saveChatLine('【编制】所办增聘常驻研究员 ' + m.id + (direction ? '(方向:' + direction + ')' : '') +
|
|
@@ -4156,7 +4291,7 @@ export function apply(ctx) {
|
|
|
4156
4291
|
registerTool('vibe_v5_stop', 'Stop the institute: interrupt every member, clear coordination state, and release their child sessions.', objParams({}), (s) => s.initStop())
|
|
4157
4292
|
registerTool('vibe_v5_status', 'Machine-readable institute status (members, tasks, quorum, meetings, verification, mail).', objParams({}), (s) => s.status())
|
|
4158
4293
|
registerTool('vibe_v5_report', 'Human-readable institute report (staffing, tasks, consensus, meetings, file locations).', objParams({}), (s) => s.report())
|
|
4159
|
-
registerTool('vibe_v5_set', 'Tune institute parameters (persisted in
|
|
4294
|
+
registerTool('vibe_v5_set', 'Tune institute parameters (persisted in State/<institute>.v5state.json). provider/model override staff LLM routes (empty = inherit the office route). toolAllow/toolDeny restrict PERMANENT staff tools; tempToolAllow/tempToolDeny restrict temp workers. quorumCap sets m = min(quorumCap, voters); quorumMode "m-unanimous" (v5) or "all-unanimous" (v4 legacy). formalVerify: "off" (default, no extra requirement) | "encourage" (agents decide by implementation difficulty whether to formalize in Lean; a passing Lean run turns the vote into a FIDELITY review of the Lean statements) | "require" (same, plus a gate: a true/false verdict is withheld as undecided until the object is Lean-passed or has an explicit reasoned blocker record).', objParams({
|
|
4160
4295
|
academician: B, academicianLeads: B, memberMayRejectAssign: B, researcherCount: I,
|
|
4161
4296
|
quorumCap: I, quorumMode: S, verdictMaxRounds: I,
|
|
4162
4297
|
maxTempPerMember: I, maxTempTotal: I,
|
|
@@ -4315,22 +4450,11 @@ export function apply(ctx) {
|
|
|
4315
4450
|
},
|
|
4316
4451
|
}))
|
|
4317
4452
|
|
|
4318
|
-
// ──
|
|
4319
|
-
//
|
|
4320
|
-
//
|
|
4321
|
-
//
|
|
4322
|
-
|
|
4323
|
-
if (projections && typeof projections.register === 'function') {
|
|
4324
|
-
ctx.effect(() => projections.register({
|
|
4325
|
-
key: PROJECTION_KEY,
|
|
4326
|
-
stateVersion: PROJECTION_VERSION,
|
|
4327
|
-
stateSchema: STATE_SCHEMA,
|
|
4328
|
-
init: initState,
|
|
4329
|
-
apply: applyV5Event,
|
|
4330
|
-
}))
|
|
4331
|
-
} else {
|
|
4332
|
-
console.error('vibe-math-v5: sessionProjections unavailable — falling back to the hardened JSON state file (State/<institute>.v5state.json)')
|
|
4333
|
-
}
|
|
4453
|
+
// ── institute state on disk ──────────────────────────────────────────────
|
|
4454
|
+
// State/<institute>.v5state.json is written by the file backend above (installBackend); it is the
|
|
4455
|
+
// only durable authority. The host's session projections are deliberately NOT used: DSH refuses to
|
|
4456
|
+
// load a session whose log carries event types outside its known set, so writing institute events
|
|
4457
|
+
// into the user's session made that session unresumable.
|
|
4334
4458
|
|
|
4335
4459
|
// ── agent lifecycle wiring ───────────────────────────────────────────────
|
|
4336
4460
|
// Capture the live child Agent while it is STILL registered: `subagent/end` is
|