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
|
@@ -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,22 +117,25 @@ 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
|
-
const now = () => Date.now()
|
|
82
132
|
function hex(n) { let s = ''; for (let i = 0; i < n; i++) s += '0123456789abcdef'[Math.floor(Math.random() * 16)]; return s }
|
|
83
133
|
const shortId = () => hex(8)
|
|
84
|
-
function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
|
|
85
134
|
// contextPct is a PERCENT (0-100); never clamp to 0-1 or the compactThreshold
|
|
86
135
|
// comparison (e.g. 66) becomes `1.0 >= 66` and never fires (v4 §17 defect).
|
|
87
|
-
function clPct(x) { const n = Number(x); if (!Number.isFinite(n)) return 0; return Math.max(0, Math.min(100, n)) }
|
|
88
136
|
// Positive duration with a safe fallback: a NEGATIVE/NaN duration parameter must
|
|
89
137
|
// never make a watchdog fire instantly or an idle window never elapse (v4 §30-T41).
|
|
90
|
-
function posMs(v, def) { const n = Number(v); return (Number.isFinite(n) && n > 0) ? n : (def || 120000) }
|
|
91
138
|
const textBlock = (t) => ({ type: 'text', text: String(t) })
|
|
92
|
-
function blocksToText(b) { if (!b) return ''; let o = ''; for (const x of b) { if (x && x.type === 'text' && typeof x.text === 'string') o += x.text + '\n' } return o }
|
|
93
|
-
function fmtTime(ts) { try { return new Date(ts || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return String(ts || '') } }
|
|
94
139
|
function makeSignal(ms) { try { return AbortSignal.timeout(posMs(ms, 30000)) } catch (e) { return undefined } }
|
|
95
140
|
|
|
96
141
|
// Object ids (verify targets, card ids, member ids) become FILE NAMES and DIRECTORY
|
|
@@ -98,10 +143,6 @@ export function apply(ctx) {
|
|
|
98
143
|
// characters would escape the project tree. Keep every harmless character (incl.
|
|
99
144
|
// Chinese) and replace only separators/control chars; strip leading/trailing dots
|
|
100
145
|
// and dashes so the name is never '.' or '..' (v4 §30-T39).
|
|
101
|
-
function idSafe(s) {
|
|
102
|
-
const t = String(s == null ? '' : s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '-').replace(/-{2,}/g, '-').replace(/^[.\-]+|[.\-]+$/g, '')
|
|
103
|
-
return t
|
|
104
|
-
}
|
|
105
146
|
// Advisory write-scope normalisation, ported from DSH agent-teams: backslashes to
|
|
106
147
|
// '/', strip a leading './' and trailing '/', reject empty/absolute/drive-letter/
|
|
107
148
|
// '..'-segment scopes.
|
|
@@ -119,10 +160,10 @@ export function apply(ctx) {
|
|
|
119
160
|
return true
|
|
120
161
|
}
|
|
121
162
|
|
|
122
|
-
// ----
|
|
163
|
+
// ---- state fold: the one fold every entry point shares ----------------
|
|
123
164
|
// The fold is shared by BOTH persistence backends, so the state machine is
|
|
124
165
|
// defined exactly once. It must return a NEW top-level reference whenever
|
|
125
|
-
// anything changed (the
|
|
166
|
+
// anything changed (the state store compares by Object.is).
|
|
126
167
|
function emptyInstitute(key, project, institute) {
|
|
127
168
|
return {
|
|
128
169
|
key, project, institute,
|
|
@@ -137,7 +178,7 @@ export function apply(ctx) {
|
|
|
137
178
|
debates: [],
|
|
138
179
|
verdicts: {},
|
|
139
180
|
// Per-object Lean formalization record (docs/formal-verification.md). Kept in the
|
|
140
|
-
//
|
|
181
|
+
// state so it survives restore with zero context cost, exactly like verdicts.
|
|
141
182
|
formal: {},
|
|
142
183
|
todo: [],
|
|
143
184
|
queue: [],
|
|
@@ -289,38 +330,10 @@ export function apply(ctx) {
|
|
|
289
330
|
}
|
|
290
331
|
}
|
|
291
332
|
|
|
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
333
|
// ---- 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
|
-
}
|
|
334
|
+
// The hardened JSON file is the ONLY backend. It applies the same event fold in memory and writes
|
|
335
|
+
// the whole snapshot under a per-file serialization chain; nothing is written to a host session log
|
|
336
|
+
// (see the PERSISTENCE note at the top of this file for why the projection backend was removed).
|
|
324
337
|
// `pathOf` is a FUNCTION, not a captured string: the state path depends on the
|
|
325
338
|
// project/institute, which the first successful load syncs back into this session —
|
|
326
339
|
// a captured path would keep writing to the pre-load guess forever.
|
|
@@ -491,11 +504,7 @@ export function apply(ctx) {
|
|
|
491
504
|
async function writeTextRel(rel, content) { return await writeTextAbs(instRoot() + '/' + rel, content) }
|
|
492
505
|
|
|
493
506
|
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')
|
|
507
|
+
backend = makeFileBackend(readTextAbs, writeTextAbs, () => instRoot() + '/State/' + instituteName + '.v5state.json')
|
|
499
508
|
return backend
|
|
500
509
|
}
|
|
501
510
|
// Every entry point that READS state must await this first. Without it the file
|
|
@@ -1195,6 +1204,31 @@ export function apply(ctx) {
|
|
|
1195
1204
|
} catch (e) { /* ignore */ }
|
|
1196
1205
|
return 'spawn'
|
|
1197
1206
|
}
|
|
1207
|
+
// ---- host live-child cap: a refused provisioning must be named, not opaque -----
|
|
1208
|
+
// See the module-scope note. `spawnMember` is the ONLY place a member child is created, so the
|
|
1209
|
+
// pre-check and the cap-naming error live here; the callers (founding loop, resume, hire,
|
|
1210
|
+
// addResearcher) only have to say what happened.
|
|
1211
|
+
let childLimitLoggedThisRound = false // ONE actionable line per ROUND, not one per refused member
|
|
1212
|
+
function beginSpawnRound() { childLimitLoggedThisRound = false }
|
|
1213
|
+
// LIVE continuable children of THIS institute: a member that holds a childId and has not been
|
|
1214
|
+
// stopped. A member whose spawn was refused keeps childId '' (see the rollback in spawnMember
|
|
1215
|
+
// and fire()), so it can never inflate the count it is measured against.
|
|
1216
|
+
function liveChildCount() {
|
|
1217
|
+
try { return inst().members.filter((m) => m.childId && m.phase !== 'stopped').length } catch (e) { return 0 }
|
|
1218
|
+
}
|
|
1219
|
+
// Say ONCE per round what the host refused and what raises the ceiling. The member itself is
|
|
1220
|
+
// recorded as `failed` by the caller with the same sentence, so the refusal stays auditable.
|
|
1221
|
+
function noteSpawnLimitOnce(member) {
|
|
1222
|
+
if (childLimitLoggedThisRound) return
|
|
1223
|
+
childLimitLoggedThisRound = true
|
|
1224
|
+
console.error('vibe-math-v5: ' + activationLimitText(hostChildLimit) + (member ? ('(本轮被拒:' + member.id + ')') : ''))
|
|
1225
|
+
}
|
|
1226
|
+
// The operator-facing sentence used for BOTH the typed error and the durable member record.
|
|
1227
|
+
function activationLimitText(limit) {
|
|
1228
|
+
return hostChildLimitHint(limit) + ';本所已有的在活成员已占满该上限,暂时无法再创建成员。'
|
|
1229
|
+
+ '可在宿主的 subagent 行把 maxActiveSubagents 调大后重试,或先 vibe_v5_fire 解雇不用的临时工腾出位置。'
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1198
1232
|
// Bring a member into being. ORDER IS LOAD-BEARING and is the fix for the
|
|
1199
1233
|
// "every brief describes the wrong person" bug:
|
|
1200
1234
|
// 1. commit the member as ACTIVE first, so that everything derived from
|
|
@@ -1211,6 +1245,14 @@ export function apply(ctx) {
|
|
|
1211
1245
|
// session is being rebuilt: the latter must NOT be told it "just joined the
|
|
1212
1246
|
// institute" and must not be shown the induction blurb.
|
|
1213
1247
|
async function spawnMember(member, initialTask, mode) {
|
|
1248
|
+
// Host live-child cap (DSH ≥0.2): once we KNOW the ceiling and our OWN live members fill it,
|
|
1249
|
+
// do not even call the host — it would refuse with the bare "subagent limit reached (active
|
|
1250
|
+
// child limit: N)". One line per ROUND (not per member); the caller records the member as
|
|
1251
|
+
// `failed` with the same sentence, and resume() retries cap-failed members once a slot frees.
|
|
1252
|
+
if (hostChildLimit !== undefined && liveChildCount() >= hostChildLimit) {
|
|
1253
|
+
noteSpawnLimitOnce(member)
|
|
1254
|
+
throw v5err('ACTIVATION_LIMIT_REACHED', activationLimitText(hostChildLimit))
|
|
1255
|
+
}
|
|
1214
1256
|
const provider = member.provider || pickProvider()
|
|
1215
1257
|
const ao = memberAgentOptions()
|
|
1216
1258
|
const tf = memberToolFilter(member)
|
|
@@ -1235,15 +1277,17 @@ export function apply(ctx) {
|
|
|
1235
1277
|
await writeRosterMirror()
|
|
1236
1278
|
let started
|
|
1237
1279
|
try {
|
|
1238
|
-
started = await
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1280
|
+
started = await startWithToolFilter(tf, function (f) {
|
|
1281
|
+
return {
|
|
1282
|
+
provider,
|
|
1283
|
+
label: 'vibe5 ' + member.id + ' (' + kindLabel(member.kind) + ')',
|
|
1284
|
+
request: Object.assign({
|
|
1285
|
+
prompt: [textBlock(prompt)],
|
|
1286
|
+
parent: rootAgent,
|
|
1287
|
+
persona,
|
|
1288
|
+
}, Object.keys(ao).length ? { agentOptions: ao } : {}, f ? { toolFilter: f } : {}),
|
|
1289
|
+
signal: makeSignal(params.activityTimeoutMs),
|
|
1290
|
+
}
|
|
1247
1291
|
})
|
|
1248
1292
|
} catch (e) {
|
|
1249
1293
|
// Roll the in-memory marks back so a failed provisioning leaves no phantom
|
|
@@ -1253,7 +1297,15 @@ export function apply(ctx) {
|
|
|
1253
1297
|
wakeKind.delete(member.id)
|
|
1254
1298
|
rounds.delete(member.id)
|
|
1255
1299
|
roundsSinceCompact.delete(member.id)
|
|
1256
|
-
|
|
1300
|
+
// The host's live-child cap is a HOST limit (maxActiveSubagents on the `subagent` row), not
|
|
1301
|
+
// a defect in this member: remember the ceiling, report it BY NAME instead of relaying the
|
|
1302
|
+
// opaque host string, and throw the TYPED error so the founding loop / hire / addResearcher
|
|
1303
|
+
// name it too instead of surfacing the raw host message.
|
|
1304
|
+
const limitHit = isActivationLimitReached(e)
|
|
1305
|
+
if (limitHit) { noteChildLimit(e); noteSpawnLimitOnce(member) }
|
|
1306
|
+
const message = limitHit ? activationLimitText(hostChildLimit) : String((e && e.message) || e)
|
|
1307
|
+
await putMember(Object.assign({}, memberById(member.id) || member, { phase: 'failed', error: message }))
|
|
1308
|
+
if (limitHit) throw v5err('ACTIVATION_LIMIT_REACHED', message)
|
|
1257
1309
|
throw e
|
|
1258
1310
|
}
|
|
1259
1311
|
member.childId = started.childId
|
|
@@ -1269,6 +1321,10 @@ export function apply(ctx) {
|
|
|
1269
1321
|
// Deliver one prompt to a member. MUST use `subagents.sendMessage` — the
|
|
1270
1322
|
// `subagents` SERVICE has no `followup` (that is only an Agent method); calling
|
|
1271
1323
|
// it threw a TypeError on every wake and silently stalled the whole group (v4 §25).
|
|
1324
|
+
// Unlike v4 there is no legacy fallback branch here, and none is needed: the service
|
|
1325
|
+
// method existed only up to DSH 0.1.2-alpha.*, was dropped in 0.1.2-rc.1, and every
|
|
1326
|
+
// release this preset targets exposes startContinuable/sendMessage/
|
|
1327
|
+
// drainContinuableChildren only (checked against dsh-subagent 0.2.0-rc.2).
|
|
1272
1328
|
async function wakeMember(member, promptText, kind) {
|
|
1273
1329
|
if (!member || !member.childId || member.phase !== 'active') return false
|
|
1274
1330
|
clearHeartbeat()
|
|
@@ -1889,7 +1945,7 @@ export function apply(ctx) {
|
|
|
1889
1945
|
async function writeFormalIndex() {
|
|
1890
1946
|
const recs = formalRecords()
|
|
1891
1947
|
const L = ['# Lean 形式化索引|' + instituteName + '|' + fmtTime(), '',
|
|
1892
|
-
'> 本文件由框架维护(工具调用时更新;`vibe_v5_lean_lib`
|
|
1948
|
+
'> 本文件由框架维护(工具调用时更新;`vibe_v5_lean_lib` 会重建)。权威状态在 State/<研究所>.v5state.json 里。', '',
|
|
1893
1949
|
'| 对象 | 状态 | 形式化文件 | 归档证明 | 最近运行 | 难度判断 / 阻塞原因 |', '|---|---|---|---|---|---|']
|
|
1894
1950
|
const keys = Object.keys(recs)
|
|
1895
1951
|
if (!keys.length) L.push('| (暂无) | | | | | |')
|
|
@@ -2440,7 +2496,7 @@ export function apply(ctx) {
|
|
|
2440
2496
|
async function writeTaskboardMirror() {
|
|
2441
2497
|
const ts = listTasks()
|
|
2442
2498
|
const lines = ['# 任务板(人读镜像)|' + instituteName + '|' + fmtTime(), '',
|
|
2443
|
-
'>
|
|
2499
|
+
'> 权威状态在 State/<研究所>.v5state.json 里;本文件只是给人和所办看的快照,勿手改。', '']
|
|
2444
2500
|
if (!ts.length) lines.push('(暂无任务)')
|
|
2445
2501
|
for (const t of ts) {
|
|
2446
2502
|
lines.push('- [' + t.status + '] ' + t.id + '|' + t.subject + '|owner=' + (t.owner || t.ownerName || '(未认领)') +
|
|
@@ -2452,12 +2508,12 @@ export function apply(ctx) {
|
|
|
2452
2508
|
await writeTextRel('Shared/TaskBoard.md', lines.join('\n'))
|
|
2453
2509
|
}
|
|
2454
2510
|
// The roster mirror (§8.4): a human-readable staffing table. Like the task-board
|
|
2455
|
-
// mirror it is WRITE-ONLY — the authoritative roster is
|
|
2511
|
+
// mirror it is WRITE-ONLY — the authoritative roster is State/<研究所>.v5state.json,
|
|
2456
2512
|
// so losing or hand-editing this file can never corrupt the institute.
|
|
2457
2513
|
async function writeRosterMirror() {
|
|
2458
2514
|
const s = inst()
|
|
2459
2515
|
const lines = ['# 研究所编制表(人读镜像)|' + instituteName + '|' + fmtTime(), '',
|
|
2460
|
-
'>
|
|
2516
|
+
'> 权威状态在 State/<研究所>.v5state.json 里;本文件只是快照,勿手改。', '']
|
|
2461
2517
|
lines.push('- 求真门槛:m = ' + quorumM() + '(模式 ' + params.quorumMode + ')|有表决权者 ' + voterCount() + ' 人')
|
|
2462
2518
|
lines.push('- 阶段:' + phase + '|运行中:' + running + '|已结题:' + autoDone)
|
|
2463
2519
|
lines.push('')
|
|
@@ -3171,6 +3227,10 @@ export function apply(ctx) {
|
|
|
3171
3227
|
await spawnMember(member, initialTask)
|
|
3172
3228
|
} catch (e) {
|
|
3173
3229
|
await putMember(Object.assign({}, memberById(member.id) || member, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
3230
|
+
// Name the host's live-child cap (maxActiveSubagents) instead of relaying its opaque
|
|
3231
|
+
// "subagent limit reached (active child limit: N)" string, and use the typed code so the
|
|
3232
|
+
// hirer can tell a HOST ceiling from a broken provider.
|
|
3233
|
+
if (isActivationLimitReached(e)) { noteChildLimit(e); return { ok: false, code: 'ACTIVATION_LIMIT_REACHED', message: '临时工创建失败:' + activationLimitText(hostChildLimit) } }
|
|
3174
3234
|
return { ok: false, code: 'V5_PROVISIONING_CONFLICT', message: '临时工创建失败:' + String((e && e.message) || e) }
|
|
3175
3235
|
}
|
|
3176
3236
|
await saveChatLine('【雇佣】' + (office ? '所办' : callerId) + ' 雇入临时工 ' + member.id + ',用途:' + purpose)
|
|
@@ -3406,30 +3466,6 @@ export function apply(ctx) {
|
|
|
3406
3466
|
}
|
|
3407
3467
|
|
|
3408
3468
|
// ---- reply parsing -----------------------------------------------------
|
|
3409
|
-
function tryJson(s) { try { return JSON.parse(s) } catch (e) { return undefined } }
|
|
3410
|
-
function parseReply(text) {
|
|
3411
|
-
let obj
|
|
3412
|
-
const fence = /```(?:json)?[ \t]*([\s\S]*?)```/gi
|
|
3413
|
-
let m
|
|
3414
|
-
while ((m = fence.exec(text)) !== null) {
|
|
3415
|
-
const o = tryJson(String(m[1]).trim())
|
|
3416
|
-
if (o && typeof o === 'object' && !Array.isArray(o)) obj = o
|
|
3417
|
-
}
|
|
3418
|
-
if (!obj) {
|
|
3419
|
-
const w = tryJson(String(text || '').trim())
|
|
3420
|
-
if (w && typeof w === 'object' && !Array.isArray(w)) obj = w
|
|
3421
|
-
}
|
|
3422
|
-
if (!obj) {
|
|
3423
|
-
// Last resort: the outermost {...} span (models sometimes wrap prose around it).
|
|
3424
|
-
const t = String(text || '')
|
|
3425
|
-
const i = t.indexOf('{'), j = t.lastIndexOf('}')
|
|
3426
|
-
if (i !== -1 && j > i) {
|
|
3427
|
-
const o = tryJson(t.slice(i, j + 1))
|
|
3428
|
-
if (o && typeof o === 'object' && !Array.isArray(o)) obj = o
|
|
3429
|
-
}
|
|
3430
|
-
}
|
|
3431
|
-
return obj || {}
|
|
3432
|
-
}
|
|
3433
3469
|
function normVerdictNumber(v) {
|
|
3434
3470
|
// verdict is a PURE 0-1 probability. Models often send a quoted number, and a
|
|
3435
3471
|
// quoted "0.9" used to fall through to a 0.5 default and be silently recorded
|
|
@@ -3630,18 +3666,46 @@ export function apply(ctx) {
|
|
|
3630
3666
|
if (member.activeMeetingId) delete member.activeMeetingId
|
|
3631
3667
|
await scheduleNext()
|
|
3632
3668
|
}
|
|
3669
|
+
// ---- toolFilter names the host may not register -------------------------
|
|
3670
|
+
// Same guard as v2/v3/v4: `tools.restrict` throws for an unregistered name and the throw escapes
|
|
3671
|
+
// child creation, so a stale `vibe_v5_set{toolAllow|toolDeny|tempToolAllow|tempToolDeny}` would
|
|
3672
|
+
// make every member spawn fail silently. The host names the registered tools in its rejection.
|
|
3673
|
+
async function startWithToolFilter(toolFilter, makeSpec){
|
|
3674
|
+
try {
|
|
3675
|
+
return await subagents.startContinuable(makeSpec(toolFilter))
|
|
3676
|
+
} catch(e){
|
|
3677
|
+
const message = String((e && e.message) || e)
|
|
3678
|
+
const retry = sanitizeToolFilter(toolFilter, registeredToolsFromError(message))
|
|
3679
|
+
if(retry===undefined){
|
|
3680
|
+
console.error('vibe-math-v5: 配置的工具过滤只包含本宿主未注册的工具名,拒绝在不带过滤的情况下启动成员。filter=' + JSON.stringify(toolFilter) + ' 宿主提示:' + message)
|
|
3681
|
+
throw e
|
|
3682
|
+
}
|
|
3683
|
+
if(JSON.stringify(retry) === JSON.stringify(toolFilter)) throw e
|
|
3684
|
+
console.error('vibe-math-v5: 工具过滤里有本宿主未注册的名字,已只保留已注册的名字重试。dropped=' + JSON.stringify(toolFilter) + ' kept=' + JSON.stringify(retry))
|
|
3685
|
+
// a FRESH spec (and a fresh timeout signal) for the retry
|
|
3686
|
+
return await subagents.startContinuable(makeSpec(retry))
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3633
3690
|
async function maybeRealCompact(childId, member) {
|
|
3634
3691
|
const roundN = roundsSinceCompact.get(member.id) || 0
|
|
3635
3692
|
const pct = contextPct.get(member.id) || 0
|
|
3636
3693
|
const soft = pct >= Number(params.compactThreshold) || roundN >= Number(params.compactAfterRounds)
|
|
3637
3694
|
if (!soft) return
|
|
3638
|
-
const comp = compactionOf()
|
|
3639
3695
|
const agent = liveAgentOf(childId)
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3696
|
+
const comp = compactionForAgent(agent)
|
|
3697
|
+
if (comp && agent && agent.session) {
|
|
3698
|
+
// Prefer the FORCING verb when the host has it: `compactIfNeeded` is a policy call that may
|
|
3699
|
+
// decide not to compact and return null without saying so, while `compactNow` does what the
|
|
3700
|
+
// operator asked for. Feature-detected so older hosts keep the policy call.
|
|
3701
|
+
const force = typeof comp.compactNow === 'function'
|
|
3702
|
+
const ask = typeof comp.compactIfNeeded === 'function'
|
|
3703
|
+
if (force || ask) {
|
|
3704
|
+
try {
|
|
3705
|
+
const r = force ? await comp.compactNow(agent, makeSignal(params.activityTimeoutMs)) : await comp.compactIfNeeded(agent, 'pressure', makeSignal(params.activityTimeoutMs))
|
|
3706
|
+
if (r) { roundsSinceCompact.set(member.id, 0); needReanchor.add(member.id); return }
|
|
3707
|
+
} catch (e) { /* fall through to the soft path */ }
|
|
3708
|
+
}
|
|
3645
3709
|
}
|
|
3646
3710
|
needReanchor.add(member.id)
|
|
3647
3711
|
}
|
|
@@ -3779,20 +3843,16 @@ export function apply(ctx) {
|
|
|
3779
3843
|
await writeTextRel('State/README.md', [
|
|
3780
3844
|
'# 关于 State/',
|
|
3781
3845
|
'',
|
|
3782
|
-
'
|
|
3846
|
+
'本研究所的**权威状态就在本目录的 `<研究所>.v5state.json` 里**,不在任何宿主会话日志里。',
|
|
3783
3847
|
'本目录只存放人可读的镜像/说明,**请勿手改**;改动不会影响真正的状态。',
|
|
3784
3848
|
'要查看状态请用 `vibe_v5_status` / `vibe_v5_report`。',
|
|
3785
3849
|
'',
|
|
3786
|
-
'
|
|
3850
|
+
'研究所状态只写在加固 JSON 里(见下),不写入宿主会话日志:',
|
|
3787
3851
|
'`State/<institute>.v5state.json`(加固 JSON 后端),此时它才是权威源。',
|
|
3788
3852
|
'安装器会在启动自检里报告这一降级。',
|
|
3789
3853
|
'',
|
|
3790
3854
|
].join('\n'))
|
|
3791
3855
|
}
|
|
3792
|
-
function slugify(s) {
|
|
3793
|
-
const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, '')
|
|
3794
|
-
return t || ''
|
|
3795
|
-
}
|
|
3796
3856
|
async function doStart(args) {
|
|
3797
3857
|
const a = args || {}
|
|
3798
3858
|
if (running && !autoDone) return { ok: false, code: 'V5_INSTITUTE_STATE', message: 'already running' }
|
|
@@ -3829,6 +3889,10 @@ export function apply(ctx) {
|
|
|
3829
3889
|
'从已知的相近结论出发,看能否推广或加强得到所需结果。',
|
|
3830
3890
|
]
|
|
3831
3891
|
const spawned = []
|
|
3892
|
+
// A refused provisioning (the host's live-child cap) is remembered for this founding round so
|
|
3893
|
+
// the aggregate failure below can NAME the cap instead of blaming the provider.
|
|
3894
|
+
let limitRefused = false
|
|
3895
|
+
beginSpawnRound() // the whole founding loop is ONE round for the cap notice
|
|
3832
3896
|
// The academician is founded FIRST so it is on the roster for every later member's
|
|
3833
3897
|
// induction brief, and so it can begin overseeing the founding round. Each member
|
|
3834
3898
|
// is committed to the ACTIVE roster before its own brief is built (see
|
|
@@ -3841,6 +3905,7 @@ export function apply(ctx) {
|
|
|
3841
3905
|
+ '你打算如何组织全所(谁适合做什么、先做什么)?把你的判断写进你的 Progress/,并把关键结论在群聊里说出来。')
|
|
3842
3906
|
spawned.push(m.id)
|
|
3843
3907
|
} catch (e) {
|
|
3908
|
+
if (isActivationLimitReached(e)) limitRefused = true
|
|
3844
3909
|
await putMember(Object.assign({}, memberById(m.id) || m, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
3845
3910
|
}
|
|
3846
3911
|
}
|
|
@@ -3851,11 +3916,18 @@ export function apply(ctx) {
|
|
|
3851
3916
|
await spawnMember(m, null)
|
|
3852
3917
|
spawned.push(m.id)
|
|
3853
3918
|
} catch (e) {
|
|
3919
|
+
if (isActivationLimitReached(e)) limitRefused = true
|
|
3854
3920
|
await putMember(Object.assign({}, memberById(m.id) || m, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
3855
3921
|
}
|
|
3856
3922
|
}
|
|
3857
3923
|
if (!spawned.length) {
|
|
3858
3924
|
await patchInstitute({ phase: 'idle' })
|
|
3925
|
+
// When the HOST's per-root child cap refused every member, say so: the generic
|
|
3926
|
+
// "check the provider" message made a real host ceiling look like a broken host.
|
|
3927
|
+
if (limitRefused) {
|
|
3928
|
+
return { ok: false, code: 'ACTIVATION_LIMIT_REACHED',
|
|
3929
|
+
message: '没有任何成员创建成功:' + activationLimitText(hostChildLimit) }
|
|
3930
|
+
}
|
|
3859
3931
|
return { ok: false, code: 'V5_PROVISIONING_CONFLICT', message: '没有任何成员创建成功;请检查 subagents 提供者与会话持久化是否可用' }
|
|
3860
3932
|
}
|
|
3861
3933
|
running = true
|
|
@@ -3895,9 +3967,16 @@ export function apply(ctx) {
|
|
|
3895
3967
|
syncParamsFromState()
|
|
3896
3968
|
if (autoDone) return { ok: false, code: 'V5_INSTITUTE_STATE', message: 'this institute already concluded; configure a new one in a new session' }
|
|
3897
3969
|
const members = activeMembers()
|
|
3898
|
-
|
|
3970
|
+
// Members the host's live-child cap refused are NOT dead: they are still wanted, and a slot
|
|
3971
|
+
// may have been released since (a temp was fired, another child settled). They are the
|
|
3972
|
+
// "queued" half of the cap handling, retried in this round — gated on the RECORDED cap error
|
|
3973
|
+
// so an ordinary failed member (broken provider, rejected tool filter) is never resurrected
|
|
3974
|
+
// by a resume. `members` keeps its meaning for the return value below.
|
|
3975
|
+
const queued = inst().members.filter((m) => m.phase === 'failed' && /active child limit|maxActiveSubagents/.test(String(m.error || '')))
|
|
3976
|
+
if (!members.length && !queued.length) return { ok: false, code: 'V5_INSTITUTE_STATE', message: 'no active member to resume' }
|
|
3899
3977
|
let respawned = 0
|
|
3900
|
-
|
|
3978
|
+
beginSpawnRound() // the whole re-spawn loop is ONE round for the cap notice
|
|
3979
|
+
for (const m of members.concat(queued)) {
|
|
3901
3980
|
if (m.childId) continue
|
|
3902
3981
|
try {
|
|
3903
3982
|
const seedText = (await readTextRel('Members/' + m.id + '/Progress/progress.md')) || ''
|
|
@@ -4071,6 +4150,7 @@ export function apply(ctx) {
|
|
|
4071
4150
|
await spawnMember(m, null)
|
|
4072
4151
|
} catch (e) {
|
|
4073
4152
|
await putMember(Object.assign({}, memberById(m.id) || m, { phase: 'failed', error: String((e && e.message) || e) }))
|
|
4153
|
+
if (isActivationLimitReached(e)) { noteChildLimit(e); return { ok: false, code: 'ACTIVATION_LIMIT_REACHED', message: activationLimitText(hostChildLimit) } }
|
|
4074
4154
|
return { ok: false, code: 'V5_PROVISIONING_CONFLICT', message: String((e && e.message) || e) }
|
|
4075
4155
|
}
|
|
4076
4156
|
await saveChatLine('【编制】所办增聘常驻研究员 ' + m.id + (direction ? '(方向:' + direction + ')' : '') +
|
|
@@ -4156,7 +4236,7 @@ export function apply(ctx) {
|
|
|
4156
4236
|
registerTool('vibe_v5_stop', 'Stop the institute: interrupt every member, clear coordination state, and release their child sessions.', objParams({}), (s) => s.initStop())
|
|
4157
4237
|
registerTool('vibe_v5_status', 'Machine-readable institute status (members, tasks, quorum, meetings, verification, mail).', objParams({}), (s) => s.status())
|
|
4158
4238
|
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
|
|
4239
|
+
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
4240
|
academician: B, academicianLeads: B, memberMayRejectAssign: B, researcherCount: I,
|
|
4161
4241
|
quorumCap: I, quorumMode: S, verdictMaxRounds: I,
|
|
4162
4242
|
maxTempPerMember: I, maxTempTotal: I,
|
|
@@ -4315,22 +4395,11 @@ export function apply(ctx) {
|
|
|
4315
4395
|
},
|
|
4316
4396
|
}))
|
|
4317
4397
|
|
|
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
|
-
}
|
|
4398
|
+
// ── institute state on disk ──────────────────────────────────────────────
|
|
4399
|
+
// State/<institute>.v5state.json is written by the file backend above (installBackend); it is the
|
|
4400
|
+
// only durable authority. The host's session projections are deliberately NOT used: DSH refuses to
|
|
4401
|
+
// load a session whose log carries event types outside its known set, so writing institute events
|
|
4402
|
+
// into the user's session made that session unresumable.
|
|
4334
4403
|
|
|
4335
4404
|
// ── agent lifecycle wiring ───────────────────────────────────────────────
|
|
4336
4405
|
// Capture the live child Agent while it is STILL registered: `subagent/end` is
|
|
@@ -4362,3 +4431,97 @@ export function apply(ctx) {
|
|
|
4362
4431
|
.finally(() => s.forgetAgent(info.id))
|
|
4363
4432
|
})
|
|
4364
4433
|
}
|
|
4434
|
+
|
|
4435
|
+
const now = () => Date.now()
|
|
4436
|
+
|
|
4437
|
+
// ---- test seam: pure, stateless helpers --------------------------------
|
|
4438
|
+
// These helpers were declared inside `apply()` and are now declared at module scope, so
|
|
4439
|
+
// `apply()` closes over exactly the same function objects this export hands out. The audit
|
|
4440
|
+
// suites therefore exercise the REAL implementations by importing this module, instead of
|
|
4441
|
+
// extracting source text and compiling function bodies through the Function constructor
|
|
4442
|
+
// (dynamic code execution, rejected by the plugin-catalog security scan as
|
|
4443
|
+
// DANGEROUS_DYNAMIC_EXECUTION).
|
|
4444
|
+
//
|
|
4445
|
+
// Contract: no member may touch `ctx`, session state or mutable module state. Most are pure;
|
|
4446
|
+
// three are deliberately non-deterministic (`uuid`/`shortId` use Math.random, `fmtTime` falls back
|
|
4447
|
+
// to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
|
|
4448
|
+
// Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
|
|
4449
|
+
// byte-identical to the previous in-`apply` declarations.
|
|
4450
|
+
export const __testHelpers = {
|
|
4451
|
+
clamp01,
|
|
4452
|
+
clPct,
|
|
4453
|
+
posMs,
|
|
4454
|
+
blocksToText,
|
|
4455
|
+
fmtTime,
|
|
4456
|
+
idSafe,
|
|
4457
|
+
slugify,
|
|
4458
|
+
tryJson,
|
|
4459
|
+
parseReply,
|
|
4460
|
+
sanitizeToolFilter,
|
|
4461
|
+
registeredToolsFromError,
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
function clamp01(v) { const n = Number(v); if (!Number.isFinite(n)) return 0.5; return Math.max(0, Math.min(1, n)) }
|
|
4465
|
+
|
|
4466
|
+
function clPct(x) { const n = Number(x); if (!Number.isFinite(n)) return 0; return Math.max(0, Math.min(100, n)) }
|
|
4467
|
+
|
|
4468
|
+
function posMs(v, def) { const n = Number(v); return (Number.isFinite(n) && n > 0) ? n : (def || 120000) }
|
|
4469
|
+
|
|
4470
|
+
function blocksToText(b) { if (!b) return ''; let o = ''; for (const x of b) { if (x && x.type === 'text' && typeof x.text === 'string') o += x.text + '\n' } return o }
|
|
4471
|
+
|
|
4472
|
+
function fmtTime(ts) { try { return new Date(ts || now()).toISOString().replace('T', ' ').slice(0, 19) } catch (e) { return String(ts || '') } }
|
|
4473
|
+
|
|
4474
|
+
function idSafe(s) {
|
|
4475
|
+
const t = String(s == null ? '' : s).trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '-').replace(/-{2,}/g, '-').replace(/^[.\-]+|[.\-]+$/g, '')
|
|
4476
|
+
return t
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
function slugify(s) {
|
|
4480
|
+
const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, '')
|
|
4481
|
+
return t || ''
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
function tryJson(s) { try { return JSON.parse(s) } catch (e) { return undefined } }
|
|
4485
|
+
|
|
4486
|
+
function parseReply(text) {
|
|
4487
|
+
let obj
|
|
4488
|
+
const fence = /```(?:json)?[ \t]*([\s\S]*?)```/gi
|
|
4489
|
+
let m
|
|
4490
|
+
while ((m = fence.exec(text)) !== null) {
|
|
4491
|
+
const o = tryJson(String(m[1]).trim())
|
|
4492
|
+
if (o && typeof o === 'object' && !Array.isArray(o)) obj = o
|
|
4493
|
+
}
|
|
4494
|
+
if (!obj) {
|
|
4495
|
+
const w = tryJson(String(text || '').trim())
|
|
4496
|
+
if (w && typeof w === 'object' && !Array.isArray(w)) obj = w
|
|
4497
|
+
}
|
|
4498
|
+
if (!obj) {
|
|
4499
|
+
// Last resort: the outermost {...} span (models sometimes wrap prose around it).
|
|
4500
|
+
const t = String(text || '')
|
|
4501
|
+
const i = t.indexOf('{'), j = t.lastIndexOf('}')
|
|
4502
|
+
if (i !== -1 && j > i) {
|
|
4503
|
+
const o = tryJson(t.slice(i, j + 1))
|
|
4504
|
+
if (o && typeof o === 'object' && !Array.isArray(o)) obj = o
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
return obj || {}
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
function sanitizeToolFilter(filter, known){
|
|
4511
|
+
if(!filter || !(known instanceof Set) || known.size === 0) return filter
|
|
4512
|
+
const out = {}
|
|
4513
|
+
for(const key of ['allow','deny']){
|
|
4514
|
+
const list = filter[key]
|
|
4515
|
+
if(!Array.isArray(list)) continue
|
|
4516
|
+
const kept = list.filter(function(n){ return known.has(String(n).trim()) })
|
|
4517
|
+
if(kept.length > 0) out[key] = kept
|
|
4518
|
+
}
|
|
4519
|
+
return (out.allow || out.deny) ? out : undefined
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
function registeredToolsFromError(message){
|
|
4523
|
+
const m = /known global tools:\s*([^]*)$/.exec(String(message || ''))
|
|
4524
|
+
if(!m) return undefined
|
|
4525
|
+
const names = m[1].split(',').map(function(s){ return s.trim() }).filter(Boolean)
|
|
4526
|
+
return names.length > 0 ? new Set(names) : undefined
|
|
4527
|
+
}
|