dsh-vibe-math 0.3.17 → 0.3.18

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.md CHANGED
@@ -41,6 +41,7 @@
41
41
  - **断点续跑**:调度状态、任务栈、代理注册表、决策队列、验证器历史准确率等全部落盘;重启后 `resume` 即可恢复(v2 用进程纪元区分"同进程暂停→恢复"与"跨进程重启")。
42
42
  - **中途人工干预(并继续)**:`auto / manual` 模式随时切换;manual 在关键节点挂起决策等你 approve/reject/override;可对任意子代理发消息 / 中断。
43
43
  - **按项目隔离**:每个数学问题一个独立项目文件夹,互不干扰,可随时切换。
44
+ - **多会话并行隔离**:DSH 的 agent preset 是 standing mount(同一 preset 的所有会话共享一个插件实例),插件内部按**根会话 id** 隔离全部运行状态——两个会话可以同时各跑一个项目(如 A 会话跑项目 A、B 会话跑项目 B),各自的子代理会正确挂在自己会话名下,调度器 / 参数 / 决策队列 / 当前项目互不干扰。当前项目按会话分别持久化(`VibeMath/current.<会话id>.json`)。
44
45
  - **子代理权限可调控**:可限制子代理允许/禁止的工具、每轮外部工具调用上限,并明确告知其可读 `Verified/`、`Propos/`、`Reliable/` 与进度日志。
45
46
  - **可配置**:`vibe_math_setting.json`(含注释)自定义默认参数;`/vibe setup` 交互式问答配置。
46
47
  - **自然语言控制**:主代理充当「助手 + 汇报者」,你把需求说成人话,它自己调用工具、汇报进度、配置参数。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-vibe-math",
3
3
  "description": "Multi-agent mathematical problem-solving & verification frameworks for DeepSeek Harness — TWO agent presets in one install: vibe-math-v1 (classic pipeline: brainstorm → solver iteration → multi-verifier debate → Verified) and vibe-math-v2 (new probability-driven architecture: qs.json + Propos knowledge base + explorer→solver→review/debate verdict). Installing this bundle auto-installs both presets into the DSH preset root.",
4
- "version": "0.3.17",
4
+ "version": "0.3.18",
5
5
  "type": "module",
6
6
  "main": "installer.js",
7
7
  "exports": {
@@ -40,6 +40,13 @@ function ensurePresetInstalled(logger) {
40
40
  export const name = 'vibe-math'
41
41
  export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands']
42
42
 
43
+ // Standing mount: DSH mounts each agent preset ONCE per preset and joins every
44
+ // session that names it to that SAME plugin instance (see @deepseek-ai/dsh-agent-presets).
45
+ // This plugin must therefore isolate ALL per-session state itself, keyed by the
46
+ // root agent (session) id — otherwise two sessions running the preset at the same
47
+ // time (e.g. project A and project B) would share one rootAgent/scheduler/registry
48
+ // and spawn children under the wrong parent session. Each session gets its own
49
+ // Session instance below via makeSession(rootAgent, sessionId).
43
50
  export function apply(ctx) {
44
51
  ensurePresetInstalled(ctx.logger)
45
52
  const subagents = ctx.subagents
@@ -50,7 +57,41 @@ export function apply(ctx) {
50
57
  const subprocess = ctx.get('subprocess')
51
58
  const sandboxPolicy = ctx.get('sandboxPolicy')
52
59
 
53
- let rootAgent = undefined
60
+ // ================= per-session registry =================
61
+ const sessions = new Map() // rootAgentId -> Session
62
+ const childOwner = new Map() // childId -> rootAgentId (route subagent/end back to its session)
63
+
64
+ function sessionIdOf(agent) { try { return (agent && agent.id) ? String(agent.id) : undefined } catch (e) { return undefined } }
65
+ // Walk up the durable session lineage to the top-level (root) agent of this session,
66
+ // so calls from a child agent (which inherits this preset) still route to its session.
67
+ function rootOf(agent) {
68
+ try {
69
+ let cur = agent
70
+ const seen = new Set()
71
+ while (cur) {
72
+ const id = cur.id
73
+ if (seen.has(id)) return cur
74
+ seen.add(id)
75
+ const parentId = (cur.session && cur.session.header) ? cur.session.header.parentSession : undefined
76
+ if (parentId === undefined) return cur
77
+ const parent = agents.get(parentId)
78
+ if (!parent) return cur
79
+ cur = parent
80
+ }
81
+ } catch (e) { /* fall through */ }
82
+ return agent
83
+ }
84
+ function getSession(agent) {
85
+ const root = rootOf(agent)
86
+ const sid = sessionIdOf(root)
87
+ if (sid === undefined) return undefined
88
+ let s = sessions.get(sid)
89
+ if (!s) { s = makeSession(root, sid); sessions.set(sid, s) }
90
+ return s
91
+ }
92
+
93
+ // ================= per-session plugin body =================
94
+ function makeSession(rootAgent, sessionId) {
54
95
  let currentProject = 'default'
55
96
  const DEFAULT_PARAMS = {
56
97
  mode: 'auto',
@@ -80,6 +121,7 @@ export function apply(ctx) {
80
121
  let decisionQueue = []
81
122
  let tasks = {}
82
123
  let tickInFlight = false
124
+ let lastTickAt = 0
83
125
  let brainstormRetries = {}
84
126
  let deriveRetries = {}
85
127
  let solvedByVerified = {}
@@ -99,6 +141,7 @@ export function apply(ctx) {
99
141
  function projectRoot(slug) { return vibeRoot() + '/Projects/' + slug }
100
142
  function frameworkRoot() { return projectRoot(currentProject) }
101
143
  function slugify(s) { const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, ''); return t || 'project' }
144
+ function safeId(s) { return String(s == null ? 'anon' : s).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80) || 'anon' }
102
145
  function getPolicy() { try { if (sandboxPolicy && rootAgent && rootAgent.session) return sandboxPolicy.resolve({ session: rootAgent.session }) } catch (e) {} try { if (sandboxPolicy) return sandboxPolicy.resolve({}) } catch (e) {} return undefined }
103
146
  function makeSignal(ms) { return AbortSignal.timeout(ms || 30000) }
104
147
  function blocksToText(blocks) { if (!blocks) return ''; let out = ''; for (let i = 0; i < blocks.length; i++) { const b = blocks[i]; if (b && b.type === 'text' && typeof b.text === 'string') out += b.text + '\n' } return out.trim() }
@@ -169,8 +212,14 @@ export function apply(ctx) {
169
212
  async function writeJson(rel, obj) { return await writeText(rel, JSON.stringify(obj, null, 2)) }
170
213
  async function listFiles(rel) { try { const t = await fsTarget(rel); const s = await fs.stat(t); if (s === undefined) return []; const entries = await fs.listDir(t); return entries.filter(function (e) { return e && e.type === 'file' }).map(function (e) { return e.name }) } catch (e) { return [] } }
171
214
  async function listDirsAt(base, rel) { try { const t = await fs.resolve(rel, { cwd: base }); const s = await fs.stat(t); if (s === undefined) return []; const entries = await fs.listDir(t); return entries.filter(function (e) { return e && e.type === 'directory' }).map(function (e) { return e.name }) } catch (e) { return [] } }
172
- async function readCurrentProject() { try { const t = await fs.resolve('current.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s === undefined) return 'default'; const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } catch (e) { return 'default' } }
173
- async function writeCurrentProject() { try { const t = await fs.resolve('current.json', { cwd: vibeRoot() }); await fs.writeText(t, JSON.stringify({ project: currentProject }), undefined, undefined, getPolicy()) } catch (e) {} }
215
+ async function readCurrentProject() {
216
+ // 按会话隔离的 current 文件(多会话并行时互不覆盖);无则回退旧共享文件
217
+ try { const t = await fs.resolve('current.' + safeId(sessionId) + '.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s !== undefined) { const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } } catch (e) {}
218
+ try { const t = await fs.resolve('current.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s === undefined) return 'default'; const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } catch (e) { return 'default' }
219
+ }
220
+ async function writeCurrentProject() {
221
+ try { const t = await fs.resolve('current.' + safeId(sessionId) + '.json', { cwd: vibeRoot() }); await fs.writeText(t, JSON.stringify({ project: currentProject }), undefined, undefined, getPolicy()) } catch (e) {}
222
+ }
174
223
 
175
224
  // ================= subprocess =================
176
225
  function psQuote(p) { return "'" + String(p).replace(/'/g, "''") + "'" }
@@ -292,7 +341,7 @@ export function apply(ctx) {
292
341
  function pickProvider() { try { const names = subagents.list ? subagents.list() : []; if (names.indexOf('spawn') !== -1) return 'spawn'; if (names.indexOf('fork') !== -1) return 'fork' } catch (e) {} return 'spawn' }
293
342
  function childAgentOptions() { const o = {}; try { if (rootAgent && rootAgent.options) { if (rootAgent.options.provider) o.provider = rootAgent.options.provider; if (rootAgent.options.model) o.model = rootAgent.options.model } } catch (e) {} if (params.provider) o.provider = params.provider; if (params.model) o.model = params.model; return o }
294
343
  function buildToolFilter(role) { const allow = role === 'solver' ? params.solverToolAllow : role === 'verifier' ? params.verifierToolAllow : undefined; const deny = role === 'solver' ? params.solverToolDeny : role === 'verifier' ? params.verifierToolDeny : undefined; const f = {}; if (Array.isArray(allow) && allow.length > 0) f.allow = allow.slice(); if (Array.isArray(deny) && deny.length > 0) f.deny = deny.slice(); return (f.allow || f.deny) ? f : undefined }
295
- async function spawnChild(label, promptText, meta) { const request = { prompt: [textBlock(promptText)], parent: rootAgent, agentOptions: childAgentOptions() }; const tf = buildToolFilter(meta && meta.role); if (tf) request.toolFilter = tf; let started; try { started = await subagents.startContinuable({ provider: pickProvider(), label: label, request: request, signal: makeSignal(30000) }) } catch (e) { if (request.toolFilter) { delete request.toolFilter; console.error('vibe-math: startContinuable with toolFilter failed, retrying without it: ' + String((e && e.message) || e)); started = await subagents.startContinuable({ provider: pickProvider(), label: label, request: request, signal: makeSignal(30000) }) } else { throw e } } agentRegistry[started.childId] = Object.assign({ createdAt: now() }, meta || {}); scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1; await saveAll(); return started.childId }
344
+ async function spawnChild(label, promptText, meta) { const request = { prompt: [textBlock(promptText)], parent: rootAgent, agentOptions: childAgentOptions() }; const tf = buildToolFilter(meta && meta.role); if (tf) request.toolFilter = tf; let started; try { started = await subagents.startContinuable({ provider: pickProvider(), label: label, request: request, signal: makeSignal(30000) }) } catch (e) { if (request.toolFilter) { delete request.toolFilter; console.error('vibe-math: startContinuable with toolFilter failed, retrying without it: ' + String((e && e.message) || e)); started = await subagents.startContinuable({ provider: pickProvider(), label: label, request: request, signal: makeSignal(30000) }) } else { throw e } } agentRegistry[started.childId] = Object.assign({ createdAt: now() }, meta || {}); childOwner.set(started.childId, sessionId); scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1; await saveAll(); return started.childId }
296
345
  async function followupChild(childId, promptText) { await subagents.followup(rootAgent, childId, [textBlock(promptText)], { source: { kind: 'user' }, signal: makeSignal(30000) }); scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1; await saveAll() }
297
346
  async function interruptChild(childId) { try { subagents.interrupt(childId, { kind: 'ancestor', agent: rootAgent }) } catch (e) {} }
298
347
 
@@ -324,10 +373,10 @@ export function apply(ctx) {
324
373
  function statusFromStop(stopReason) { return (stopReason === 'completed' || stopReason === 'max-tokens') ? 'continue' : 'dead-end' }
325
374
 
326
375
  // ================= init / control =================
327
- async function resolveRootAgent(agent) { if (rootAgent) return rootAgent; if (agent) { rootAgent = agent; return rootAgent } try { const roots = agents.roots ? agents.roots() : []; if (roots && roots.length > 0) { rootAgent = roots[0]; return rootAgent } } catch (e) {} return rootAgent }
328
- async function init(agent) { await resolveRootAgent(agent); if (!rootAgent) return { ok: false, message: 'no root agent available' }; currentProject = await readCurrentProject(); await ensureDirs(); if ((await readText('qs/qs.csv')) === undefined) await writeText('qs/qs.csv', 'id,description,priority,status,deps\n'); params = Object.assign({}, DEFAULT_PARAMS); await loadSettings(); await migrateLegacyParams(); await loadState(); scheduler.activeCount = 0; await saveAll(); return { ok: true } }
329
- async function startScheduler(agent) { const r = await init(agent); if (!r.ok) return r; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
330
- async function resumeScheduler(agent) { const r = await init(agent); if (!r.ok) return r; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
376
+ async function init() {
377
+ if (!rootAgent) return { ok: false, message: 'no root agent available' }; currentProject = await readCurrentProject(); await ensureDirs(); if ((await readText('qs/qs.csv')) === undefined) await writeText('qs/qs.csv', 'id,description,priority,status,deps\n'); params = Object.assign({}, DEFAULT_PARAMS); await loadSettings(); await migrateLegacyParams(); await loadState(); scheduler.activeCount = 0; await saveAll(); return { ok: true } }
378
+ async function startScheduler() { const r = await init(); if (!r.ok) return r; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
379
+ async function resumeScheduler() { const r = await init(); if (!r.ok) return r; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
331
380
  async function pauseScheduler() { scheduler.running = false; logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
332
381
  async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); scheduler.activeCount = 0; logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
333
382
  // auto 模式语义 = 无人值守自动通过关键节点:切回 auto 时把仍挂起的人工决策按自动策略放行
@@ -368,7 +417,7 @@ export function apply(ctx) {
368
417
 
369
418
  // ================= scheduler =================
370
419
  function scheduleTick() { tick().catch(function (e) { console.error('vibe-math tick error: ' + String((e && e.stack) || e)) }) }
371
- async function tick() { if (tickInFlight) return; if (!rootAgent) return; if (!scheduler.running) return; if (scheduler.gate) return; tickInFlight = true; try { await processVerification(); await reconcileTasks(); await processPromotion(); await processDecider(); await processSolve(); await maybeWriteReport(false); const qs = await getQs(); const unsolved = qs.filter(function (q) { return q.status !== 'solved' && !solvedByVerified[q.id] }); if (unsolved.length === 0 && Object.keys(agentRegistry).length === 0 && Object.keys(tasks).length === 0) { scheduler.running = false; logActivity('stop', 'no unsolved problems, no active agents/tasks — scheduler stopped'); await saveAll(); await maybeWriteReport(true) } } finally { tickInFlight = false } }
420
+ async function tick() { if (tickInFlight) return; if (!rootAgent) return; if (!scheduler.running) return; if (scheduler.gate) return; tickInFlight = true; lastTickAt = now(); try { await processVerification(); await reconcileTasks(); await processPromotion(); await processDecider(); await processSolve(); await maybeWriteReport(false); const qs = await getQs(); const unsolved = qs.filter(function (q) { return q.status !== 'solved' && !solvedByVerified[q.id] }); if (unsolved.length === 0 && Object.keys(agentRegistry).length === 0 && Object.keys(tasks).length === 0) { scheduler.running = false; logActivity('stop', 'no unsolved problems, no active agents/tasks — scheduler stopped'); await saveAll(); await maybeWriteReport(true) } } finally { tickInFlight = false } }
372
421
  async function processSolve() { if (scheduler.activeCount >= params.maxParallelThreshold) return; const qs = await getQs(); const unsolved = qs.filter(function (q) { return q.status !== 'solved' && !solvedByVerified[q.id] }).sort(function (a, b) { return a.priority - b.priority }); for (let i = 0; i < unsolved.length; i++) { if (scheduler.activeCount >= params.maxParallelThreshold) break; const q = unsolved[i]; const busy = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.qid === q.id && (m.role === 'brainstorm' || m.role === 'solver' || m.role === 'derive') }); if (busy) continue; const prog = await readProgress(q.id); if (prog.length === 0) { if ((brainstormRetries[q.id] || 0) >= 3) { await writeProgress(q.id, [{ direction_id: 'd_' + shortId(), title: 'brainstorm failed', method: '', core_assumption: '', round: 0, status: 'dead-end', survival_probability: 0, dead_end_reason: 'brainstorm produced no directions after 3 attempts', lemmas: [], sub_routes: [], aux_hypotheses: [], updated_at: String(now()) }]); continue } brainstormRetries[q.id] = (brainstormRetries[q.id] || 0) + 1; const label = 'brainstorm:' + q.id; const promptText = brainstormPrompt(q); const r = await maybeGate('spawn', 'brainstorm for problem ' + q.id, { label: label, promptText: promptText, meta: { role: 'brainstorm', qid: q.id } }, async function (d) { await spawnChild(d.label, d.promptText, d.meta); return { spawned: true } }); if (r && r.gated) return } else { for (let j = 0; j < prog.length; j++) { if (scheduler.activeCount >= params.maxParallelThreshold) break; const dir = prog[j]; if (dir.status === 'success' || dir.status === 'dead-end') continue; const running = Object.keys(agentRegistry).some(function (cid) { const m = agentRegistry[cid]; return m && m.qid === q.id && m.direction === dir.direction_id && m.role === 'solver' }); if (running) continue; const label = 'solver:' + q.id + ':' + dir.direction_id; const promptText = solverPrompt(q, dir, Math.max(1, dir.round + 1)); const r = await maybeGate('spawn', 'solver for problem ' + q.id + ' direction ' + dir.direction_id, { label: label, promptText: promptText, meta: { role: 'solver', qid: q.id, description: q.description, direction: dir.direction_id, round: Math.max(1, dir.round + 1) } }, async function (d) { await spawnChild(d.label, d.promptText, d.meta); return { spawned: true } }); if (r && r.gated) return } if (scheduler.activeCount < params.maxParallelThreshold) { const activeDirs = prog.filter(function (d) { return d.status !== 'success' && d.status !== 'dead-end' }); if (activeDirs.length === 0 && (deriveRetries[q.id] || 0) < 3) { deriveRetries[q.id] = (deriveRetries[q.id] || 0) + 1; const dlabel = 'derive:' + q.id; const dprompt = deriveDirectionsPrompt(q, prog); const dr = await maybeGate('spawn', 'derive new directions for problem ' + q.id, { label: dlabel, promptText: dprompt, meta: { role: 'derive', qid: q.id, description: q.description } }, async function (d) { await spawnChild(d.label, d.promptText, d.meta); return { spawned: true } }); if (dr && dr.gated) return } } } } }
373
422
 
374
423
  // ================= child result handling =================
@@ -507,21 +556,17 @@ export function apply(ctx) {
507
556
  async function handleDecider(childId, meta, output) { delete agentRegistry[childId]; const parsed = parseJson(output); const qid = parsed && parsed.solves_qid; const processed = (await readJson('VibeMath_State/decided_verified.json')) || []; if (processed.indexOf(meta.verifiedFile) === -1) processed.push(meta.verifiedFile); if (qid && qid !== 'null') { const qs = await getQs(); const q = qs.find(function (x) { return x.id === qid }); if (q && q.status !== 'solved') { q.status = 'solved'; solvedByVerified[qid] = true; await writeQs(qs); const newName = qid + '-的解法_' + shortId() + '.csv'; await atomicMove('Verified/' + meta.verifiedFile, 'Verified/' + newName); processed.push(newName); logActivity('decider', 'problem ' + qid + ' solved; Verified file renamed to ' + newName) } } await writeJson('VibeMath_State/decided_verified.json', processed) }
508
557
 
509
558
  // ================= events / timer =================
510
- ctx.on('subagent/end', function (info) { onChildEnd(info).catch(function (e) { console.error('vibe-math onChildEnd reject: ' + String((e && e.stack) || e)) }) })
511
- ctx.effect(() => { const t = setInterval(function () { scheduleTick() }, Math.max(200, Number(params.tickIntervalMs) || 2000)); return () => clearInterval(t) })
559
+ // NOTE: subagent/end listener and the tick timer are registered ONCE at the
560
+ // apply level (below), routing through childOwner/sessions NOT here, because
561
+ // the standing-mount plugin instance is shared by every session.
512
562
 
513
563
  // ================= tools =================
514
564
  function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
515
- function registerTool(name, description, parameters, executeFn) {
516
- ctx.effect(() => tools.register({
517
- name: name, description: description, parameters: parameters,
518
- output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
519
- execute: async function (args, exec) { try { const agent = (exec && exec.agent) || undefined; await resolveRootAgent(agent); if (rootAgent) currentProject = await readCurrentProject(); return JSON.stringify(await executeFn(args || {}, agent)) } catch (e) { return JSON.stringify({ ok: false, error: String((e && e.message) || e) }) } },
520
- }))
521
- }
565
+ const handlers = {}
566
+ function registerTool(name, description, parameters, executeFn) { handlers[name] = executeFn }
522
567
 
523
- registerTool('vibe_math_start', 'Start (or restart) the Vibe Math scheduler for the current project.', objParams({}), async function (args, agent) { return await startScheduler(agent) })
524
- registerTool('vibe_math_resume', 'Resume the scheduler after a checkpoint/restart.', objParams({}), async function (args, agent) { return await resumeScheduler(agent) })
568
+ registerTool('vibe_math_start', 'Start (or restart) the Vibe Math scheduler for the current project.', objParams({}), async function () { return await startScheduler() })
569
+ registerTool('vibe_math_resume', 'Resume the scheduler after a checkpoint/restart.', objParams({}), async function () { return await resumeScheduler() })
525
570
  registerTool('vibe_math_pause', 'Pause the scheduler (in-flight children finish their current turn).', objParams({}), async function () { return await pauseScheduler() })
526
571
  registerTool('vibe_math_abort', 'Abort the scheduler and interrupt all active children.', objParams({}), async function () { return await abortScheduler() })
527
572
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), async function () { await refreshParams(); return await getStatus() })
@@ -542,9 +587,9 @@ export function apply(ctx) {
542
587
  registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { await interruptChild(args.childId); return { ok: true, message: 'interrupt requested' } })
543
588
 
544
589
  // ================= slash command /vibe =================
545
- async function dispatchVibeCommand(cmd, args, agent) {
546
- if (cmd === 'start') return await startScheduler(agent)
547
- if (cmd === 'resume') return await resumeScheduler(agent)
590
+ async function dispatchVibeCommand(cmd, args) {
591
+ if (cmd === 'start') return await startScheduler()
592
+ if (cmd === 'resume') return await resumeScheduler()
548
593
  if (cmd === 'pause') return await pauseScheduler()
549
594
  if (cmd === 'abort') return await abortScheduler()
550
595
  if (cmd === 'status') { await refreshParams(); return await getStatus() }
@@ -563,17 +608,85 @@ export function apply(ctx) {
563
608
  if (cmd === 'agents') { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round }) } return { ok: true, agents: out } }
564
609
  return { ok: false, usage: 'start | resume | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <description> | project [list | new <name> | <name>] | decisions | agents', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
565
610
  }
611
+
612
+ // ================= session surface =================
613
+ return {
614
+ sessionId: sessionId,
615
+ scheduler: scheduler,
616
+ tickInFlight: tickInFlight,
617
+ scheduleTick: scheduleTick,
618
+ onChildEnd: onChildEnd,
619
+ dispatchVibeCommand: dispatchVibeCommand,
620
+ handlers: handlers,
621
+ // 每次工具调用前同步当前项目(按会话读 current.json;多会话互不干扰)
622
+ refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
623
+ getRunning: function () { return scheduler.running },
624
+ // 会话自己的心跳节流:timer 每 1s 询问是否到点;tick 执行时刷新 lastTickAt
625
+ tickDue: function () { const iv = Math.max(200, Number(params.tickIntervalMs) || 2000); return (now() - lastTickAt) >= iv },
626
+ }
627
+ }
628
+
629
+ // ================= apply-level registrations (ONCE per preset) =================
630
+ function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
631
+ function registerTool(name, description, parameters, handlerName) {
632
+ ctx.effect(() => tools.register({
633
+ name: name, description: description, parameters: parameters,
634
+ output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
635
+ execute: async function (args, exec) {
636
+ try {
637
+ const s = getSession(exec && exec.agent)
638
+ if (!s) return JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' })
639
+ await s.refreshProject()
640
+ return JSON.stringify(await s.handlers[handlerName](args || {}, exec && exec.agent))
641
+ } catch (e) { return JSON.stringify({ ok: false, error: String((e && e.message) || e) }) }
642
+ },
643
+ }))
644
+ }
645
+ registerTool('vibe_math_start', 'Start (or restart) the Vibe Math scheduler for the current project.', objParams({}), 'vibe_math_start')
646
+ registerTool('vibe_math_resume', 'Resume the scheduler after a checkpoint/restart.', objParams({}), 'vibe_math_resume')
647
+ registerTool('vibe_math_pause', 'Pause the scheduler (in-flight children finish their current turn).', objParams({}), 'vibe_math_pause')
648
+ registerTool('vibe_math_abort', 'Abort the scheduler and interrupt all active children.', objParams({}), 'vibe_math_abort')
649
+ registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), 'vibe_math_status')
650
+ registerTool('vibe_math_report', 'Return the full progress report (status + recent activity + params) and write it to Progress_Logs/report.json.', objParams({}), 'vibe_math_report')
651
+ registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
652
+ registerTool('vibe_math_set_params', 'Update scheduler parameters (partial).', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['direct-veto', 'weighted-vote'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' } }), 'vibe_math_set_params')
653
+ registerTool('vibe_math_setup', 'Return the interactive parameter schema (each param: name, type, current, default, description, options, suggestion) for guided configuration.', objParams({}), 'vibe_math_setup')
654
+ registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), 'vibe_math_save_settings')
655
+ registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
656
+ registerTool('vibe_math_add_problem', 'Add a problem to the current project qs.csv.', objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' } }, ['id', 'description']), 'vibe_math_add_problem')
657
+ registerTool('vibe_math_new_project', 'Create a new math project folder and switch to it.', objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_new_project')
658
+ registerTool('vibe_math_set_project', 'Switch the current math project.', objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_set_project')
659
+ registerTool('vibe_math_list_projects', 'List math projects.', objParams({}), 'vibe_math_list_projects')
660
+ registerTool('vibe_math_list_decisions', 'List pending manual decisions.', objParams({}), 'vibe_math_list_decisions')
661
+ registerTool('vibe_math_decide', 'Resolve a pending manual decision.', objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'string', enum: ['true', 'false'] } }, ['id', 'action']), 'vibe_math_decide')
662
+ registerTool('vibe_math_list_agents', 'List tracked sub-agents (child sessions).', objParams({}), 'vibe_math_list_agents')
663
+ registerTool('vibe_math_message_agent', 'Send a message to a tracked child agent (next turn).', objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), 'vibe_math_message_agent')
664
+ registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), 'vibe_math_interrupt_agent')
665
+
666
+ // /vibe slash command (registered once; routed per session)
566
667
  ctx.effect(() => commands.register({
567
668
  name: 'vibe',
568
669
  description: 'control the Vibe Math solver (start/pause/projects/setup/save/decisions/agents)',
569
670
  input: { hint: '[start|resume|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|project [list|new <name>|<name>]|decisions|agents]' },
570
671
  handler: async function (invocation) {
672
+ const s = getSession(invocation && invocation.agent)
673
+ if (!s) return { kind: 'success', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
571
674
  const line = String(invocation && invocation.rawInput ? invocation.rawInput : '').trim()
572
675
  const parts = line.length > 0 ? line.split(/\s+/) : []
573
676
  const cmd = parts[0] || ''
574
677
  const rest = parts.slice(1)
575
- const result = await dispatchVibeCommand(cmd, rest, invocation.agent)
678
+ const result = await s.dispatchVibeCommand(cmd, rest)
576
679
  return { kind: 'success', text: JSON.stringify(result, null, 2) }
577
680
  },
578
681
  }))
682
+
683
+ // subagent/end (registered once; routed to the owning session via childOwner)
684
+ ctx.on('subagent/end', function (info) {
685
+ const sid = childOwner.get(info.id)
686
+ const s = sid !== undefined ? sessions.get(sid) : undefined
687
+ if (s) s.onChildEnd(info).catch(function (e) { console.error('vibe-math onChildEnd reject: ' + String((e && e.stack) || e)) })
688
+ })
689
+
690
+ // tick timer (registered once; ticks every running session at its own pace)
691
+ ctx.effect(() => { const t = setInterval(function () { for (const s of sessions.values()) { if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick() } }, 1000); return () => clearInterval(t) })
579
692
  }
@@ -8,6 +8,8 @@
8
8
 
9
9
  每隔一段时间(如:当有变动、出现新进展或发生新调用,或者当有代理的会话结束时),委托主代理总结汇告当前进展、各代理情况/进度等信息。
10
10
 
11
+ - 3. 多会话并行隔离:DSH 的 agent preset 是 standing mount——同一 preset 的所有会话共享同一个插件实例,因此插件必须自行按根会话 id 隔离内部状态(rootAgent、当前项目、调度器、代理注册表、决策队列、参数、任务栈等全部 per-session)。两个会话可同时各跑一个项目,各自的子代理挂在各自会话名下、互不干扰;当前项目按会话持久化(`VibeMath/current.<会话id>.json`,兼容读取旧 `current.json`)。
12
+
11
13
 
12
14
  # Vibe Mathematics —— 多代理数学问题求解与验证框架
13
15
 
@@ -19,6 +19,13 @@
19
19
  export const name = 'vibe-math-v2'
20
20
  export const inject = ['subagents', 'agents', 'fs', 'tools', 'commands']
21
21
 
22
+ // Standing mount: DSH mounts each agent preset ONCE per preset and joins every
23
+ // session that names it to that SAME plugin instance (see @deepseek-ai/dsh-agent-presets).
24
+ // This plugin must therefore isolate ALL per-session state itself, keyed by the
25
+ // root agent (session) id — otherwise two sessions running the preset at the same
26
+ // time (e.g. project A and project B) would share one rootAgent/scheduler/registry
27
+ // and spawn children under the wrong parent session. Each session gets its own
28
+ // Session instance below via makeSession(rootAgent, sessionId).
22
29
  export function apply(ctx) {
23
30
  const subagents = ctx.subagents
24
31
  const agents = ctx.agents
@@ -28,7 +35,41 @@ export function apply(ctx) {
28
35
  const subprocess = ctx.get('subprocess')
29
36
  const sandboxPolicy = ctx.get('sandboxPolicy')
30
37
 
31
- let rootAgent = undefined
38
+ // ================= per-session registry =================
39
+ const sessions = new Map() // rootAgentId -> Session
40
+ const childOwner = new Map() // childId -> rootAgentId (route subagent/end back to its session)
41
+
42
+ function sessionIdOf(agent) { try { return (agent && agent.id) ? String(agent.id) : undefined } catch (e) { return undefined } }
43
+ // Walk up the durable session lineage to the top-level (root) agent of this session,
44
+ // so calls from a child agent (which inherits this preset) still route to its session.
45
+ function rootOf(agent) {
46
+ try {
47
+ let cur = agent
48
+ const seen = new Set()
49
+ while (cur) {
50
+ const id = cur.id
51
+ if (seen.has(id)) return cur
52
+ seen.add(id)
53
+ const parentId = (cur.session && cur.session.header) ? cur.session.header.parentSession : undefined
54
+ if (parentId === undefined) return cur
55
+ const parent = agents.get(parentId)
56
+ if (!parent) return cur
57
+ cur = parent
58
+ }
59
+ } catch (e) { /* fall through */ }
60
+ return agent
61
+ }
62
+ function getSession(agent) {
63
+ const root = rootOf(agent)
64
+ const sid = sessionIdOf(root)
65
+ if (sid === undefined) return undefined
66
+ let s = sessions.get(sid)
67
+ if (!s) { s = makeSession(root, sid); sessions.set(sid, s) }
68
+ return s
69
+ }
70
+
71
+ // ================= per-session plugin body =================
72
+ function makeSession(rootAgent, sessionId) {
32
73
  let currentProject = 'default'
33
74
  const DEFAULT_PARAMS = {
34
75
  mode: 'auto', // auto | manual
@@ -74,6 +115,7 @@ export function apply(ctx) {
74
115
  let lastPushReport = 0
75
116
  let reportDirty = false
76
117
  let tickInFlight = false
118
+ let lastTickAt = 0
77
119
  let explorerRetries = {}
78
120
  // Process epoch: written to state at init; a DIFFERENT persisted epoch means a
79
121
  // previous DSH process wrote this state (in-flight children are gone), while an
@@ -91,6 +133,7 @@ export function apply(ctx) {
91
133
  function projectRoot(slug) { return vibeRoot() + '/Projects/' + slug }
92
134
  function frameworkRoot() { return projectRoot(currentProject) }
93
135
  function slugify(s) { const t = String(s == null ? '' : s).trim().toLowerCase().replace(/[^a-z0-9_\-\u4e00-\u9fa5]+/g, '-').replace(/^-+|-+$/g, ''); return t || 'project' }
136
+ function safeId(s) { return String(s == null ? 'anon' : s).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80) || 'anon' }
94
137
  function getPolicy() { try { if (sandboxPolicy && rootAgent && rootAgent.session) return sandboxPolicy.resolve({ session: rootAgent.session }) } catch (e) {} try { if (sandboxPolicy) return sandboxPolicy.resolve({}) } catch (e) {} return undefined }
95
138
  function makeSignal(ms) { return AbortSignal.timeout(ms || 30000) }
96
139
  function blocksToText(blocks) { if (!blocks) return ''; let out = ''; for (let i = 0; i < blocks.length; i++) { const b = blocks[i]; if (b && b.type === 'text' && typeof b.text === 'string') out += b.text + '\n' } return out.trim() }
@@ -166,8 +209,14 @@ export function apply(ctx) {
166
209
  async function listDirsAt(base, rel) { try { const t = await fs.resolve(rel, { cwd: base }); const s = await fs.stat(t); if (s === undefined) return []; const entries = await fs.listDir(t); return entries.filter(function (e) { return e && e.type === 'directory' }).map(function (e) { return e.name }) } catch (e) { return [] } }
167
210
  async function readTextAbs(path) { try { const t = await fs.resolve(path); const s = await fs.stat(t); if (s === undefined) return undefined; return await fs.readText(t) } catch (e) { return undefined } }
168
211
  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 } }
169
- async function readCurrentProject() { try { const t = await fs.resolve('current.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s === undefined) return 'default'; const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } catch (e) { return 'default' } }
170
- async function writeCurrentProject() { try { const t = await fs.resolve('current.json', { cwd: vibeRoot() }); await fs.writeText(t, JSON.stringify({ project: currentProject }), undefined, undefined, getPolicy()) } catch (e) {} }
212
+ async function readCurrentProject() {
213
+ // 按会话隔离的 current 文件(多会话并行时互不覆盖);无则回退旧共享文件
214
+ try { const t = await fs.resolve('current.' + safeId(sessionId) + '.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s !== undefined) { const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } } catch (e) {}
215
+ try { const t = await fs.resolve('current.json', { cwd: vibeRoot() }); const s = await fs.stat(t); if (s === undefined) return 'default'; const txt = await fs.readText(t); const j = safeJson(txt, null); const p = (j && j.project) ? String(j.project) : 'default'; return slugify(p) } catch (e) { return 'default' }
216
+ }
217
+ async function writeCurrentProject() {
218
+ try { const t = await fs.resolve('current.' + safeId(sessionId) + '.json', { cwd: vibeRoot() }); await fs.writeText(t, JSON.stringify({ project: currentProject }), undefined, undefined, getPolicy()) } catch (e) {}
219
+ }
171
220
 
172
221
  // ================= subprocess =================
173
222
  function psQuote(p) { return "'" + String(p).replace(/'/g, "''") + "'" }
@@ -388,6 +437,7 @@ export function apply(ctx) {
388
437
  if (request.toolFilter) { delete request.toolFilter; console.error('vibe-math-v2: startContinuable with toolFilter failed, retrying without it: ' + String((e && e.message) || e)); started = await subagents.startContinuable({ provider: pickProvider(), label: label, request: request, signal: makeSignal(30000) }) } else { throw e }
389
438
  }
390
439
  agentRegistry[started.childId] = Object.assign({ createdAt: now() }, meta || {})
440
+ childOwner.set(started.childId, sessionId)
391
441
  scheduler.activeCount = Math.max(0, scheduler.activeCount) + 1
392
442
  await saveAll(); return started.childId
393
443
  }
@@ -568,6 +618,7 @@ export function apply(ctx) {
568
618
  async function tick() {
569
619
  if (tickInFlight) return; if (!rootAgent) return; if (!scheduler.running) return; if (scheduler.gate) return
570
620
  tickInFlight = true
621
+ lastTickAt = now()
571
622
  try {
572
623
  await processStatusUpdates()
573
624
  await processPriorityAdjust()
@@ -1187,9 +1238,8 @@ export function apply(ctx) {
1187
1238
  }
1188
1239
 
1189
1240
  // ================= init / control =================
1190
- async function resolveRootAgent(agent) { if (rootAgent) return rootAgent; if (agent) { rootAgent = agent; return rootAgent } try { const roots = agents.roots ? agents.roots() : []; if (roots && roots.length > 0) { rootAgent = roots[0]; return rootAgent } } catch (e) {} return rootAgent }
1191
- async function init(agent, fresh) {
1192
- await resolveRootAgent(agent); if (!rootAgent) return { ok: false, message: 'no root agent available' }
1241
+ async function init(fresh) {
1242
+ if (!rootAgent) return { ok: false, message: 'no root agent available' }
1193
1243
  currentProject = await readCurrentProject(); await ensureDirs()
1194
1244
  if ((await readJson('qs/qs.json')) === undefined) await writeJson('qs/qs.json', [])
1195
1245
  params = Object.assign({}, DEFAULT_PARAMS); await loadSettings(); await migrateLegacyParams(); await loadState()
@@ -1210,8 +1260,8 @@ export function apply(ctx) {
1210
1260
  await saveAll()
1211
1261
  return { ok: true }
1212
1262
  }
1213
- async function startScheduler(agent) { const r = await init(agent, true); if (!r.ok) return r; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
1214
- async function resumeScheduler(agent) { const r = await init(agent, false); if (!r.ok) return r; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
1263
+ async function startScheduler() { const r = await init(true); if (!r.ok) return r; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
1264
+ async function resumeScheduler() { const r = await init(false); if (!r.ok) return r; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
1215
1265
  async function pauseScheduler() { scheduler.running = false; logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
1216
1266
  async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); scheduler.activeCount = 0; logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
1217
1267
  // auto 模式语义 = 无人值守自动通过关键节点:切回 auto 时把仍挂起的人工决策按自动策略放行
@@ -1255,20 +1305,16 @@ export function apply(ctx) {
1255
1305
  }
1256
1306
 
1257
1307
  // ================= events / timer =================
1258
- ctx.on('subagent/end', function (info) { onChildEnd(info).catch(function (e) { console.error('vibe-math-v2 onChildEnd reject: ' + String((e && e.stack) || e)) }) })
1259
- ctx.effect(() => { const t = setInterval(function () { scheduleTick() }, Math.max(200, Number(params.tickIntervalMs) || 2000)); return () => clearInterval(t) })
1308
+ // NOTE: subagent/end listener and the tick timer are registered ONCE at the
1309
+ // apply level (below), routing through childOwner/sessions NOT here, because
1310
+ // the standing-mount plugin instance is shared by every session.
1260
1311
 
1261
1312
  // ================= tools =================
1262
1313
  function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
1263
- function registerTool(name, description, parameters, executeFn) {
1264
- ctx.effect(() => tools.register({
1265
- name: name, description: description, parameters: parameters,
1266
- output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
1267
- execute: async function (args, exec) { try { const agent = (exec && exec.agent) || undefined; await resolveRootAgent(agent); if (rootAgent) currentProject = await readCurrentProject(); return JSON.stringify(await executeFn(args || {}, agent)) } catch (e) { return JSON.stringify({ ok: false, error: String((e && e.message) || e) }) } },
1268
- }))
1269
- }
1270
- registerTool('vibe_math_start', 'Start (or restart) the Vibe Math V2 scheduler for the current project.', objParams({}), async function (args, agent) { return await startScheduler(agent) })
1271
- registerTool('vibe_math_resume', 'Resume the Vibe Math V2 scheduler after a checkpoint/restart.', objParams({}), async function (args, agent) { return await resumeScheduler(agent) })
1314
+ const handlers = {}
1315
+ function registerTool(name, description, parameters, executeFn) { handlers[name] = executeFn }
1316
+ registerTool('vibe_math_start', 'Start (or restart) the Vibe Math V2 scheduler for the current project.', objParams({}), async function () { return await startScheduler() })
1317
+ registerTool('vibe_math_resume', 'Resume the Vibe Math V2 scheduler after a checkpoint/restart.', objParams({}), async function () { return await resumeScheduler() })
1272
1318
  registerTool('vibe_math_pause', 'Pause the scheduler (in-flight children finish their current turn).', objParams({}), async function () { return await pauseScheduler() })
1273
1319
  registerTool('vibe_math_abort', 'Abort the scheduler and interrupt all active children.', objParams({}), async function () { return await abortScheduler() })
1274
1320
  registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), async function () { await refreshParams(); return await getStatus() })
@@ -1294,9 +1340,9 @@ export function apply(ctx) {
1294
1340
  registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), async function (args) { await interruptChild(args.childId); return { ok: true, message: 'interrupt requested' } })
1295
1341
 
1296
1342
  // ================= slash command /vibe =================
1297
- async function dispatchVibeCommand(cmd, args, agent) {
1298
- if (cmd === 'start') return await startScheduler(agent)
1299
- if (cmd === 'resume') return await resumeScheduler(agent)
1343
+ async function dispatchVibeCommand(cmd, args) {
1344
+ if (cmd === 'start') return await startScheduler()
1345
+ if (cmd === 'resume') return await resumeScheduler()
1300
1346
  if (cmd === 'pause') return await pauseScheduler()
1301
1347
  if (cmd === 'abort') return await abortScheduler()
1302
1348
  if (cmd === 'status') { await refreshParams(); return await getStatus() }
@@ -1317,17 +1363,87 @@ export function apply(ctx) {
1317
1363
  if (cmd === 'agents') { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round }) } return { ok: true, agents: out } }
1318
1364
  return { ok: false, usage: 'start | resume | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <desc> | add-proposition <id> <概述> | list-propositions | project [list|new <name>|<name>] | decisions | agents', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
1319
1365
  }
1366
+
1367
+ // ================= session surface =================
1368
+ return {
1369
+ sessionId: sessionId,
1370
+ scheduler: scheduler,
1371
+ tickInFlight: tickInFlight,
1372
+ scheduleTick: scheduleTick,
1373
+ onChildEnd: onChildEnd,
1374
+ dispatchVibeCommand: dispatchVibeCommand,
1375
+ handlers: handlers,
1376
+ // 每次工具调用前同步当前项目(按会话读 current.json;多会话互不干扰)
1377
+ refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
1378
+ getRunning: function () { return scheduler.running },
1379
+ // 会话自己的心跳节流:timer 每 1s 询问是否到点;tick 执行时刷新 lastTickAt
1380
+ tickDue: function () { const iv = Math.max(200, Number(params.tickIntervalMs) || 2000); return (now() - lastTickAt) >= iv },
1381
+ }
1382
+ }
1383
+
1384
+ // ================= apply-level registrations (ONCE per preset) =================
1385
+ function objParams(props, required) { return { type: 'object', properties: props, additionalProperties: false, required: required || [] } }
1386
+ function registerTool(name, description, parameters, handlerName) {
1387
+ ctx.effect(() => tools.register({
1388
+ name: name, description: description, parameters: parameters,
1389
+ output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] },
1390
+ execute: async function (args, exec) {
1391
+ try {
1392
+ const s = getSession(exec && exec.agent)
1393
+ if (!s) return JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' })
1394
+ await s.refreshProject()
1395
+ return JSON.stringify(await s.handlers[handlerName](args || {}, exec && exec.agent))
1396
+ } catch (e) { return JSON.stringify({ ok: false, error: String((e && e.message) || e) }) }
1397
+ },
1398
+ }))
1399
+ }
1400
+ registerTool('vibe_math_start', 'Start (or restart) the Vibe Math V2 scheduler for the current project.', objParams({}), 'vibe_math_start')
1401
+ registerTool('vibe_math_resume', 'Resume the Vibe Math V2 scheduler after a checkpoint/restart.', objParams({}), 'vibe_math_resume')
1402
+ registerTool('vibe_math_pause', 'Pause the scheduler (in-flight children finish their current turn).', objParams({}), 'vibe_math_pause')
1403
+ registerTool('vibe_math_abort', 'Abort the scheduler and interrupt all active children.', objParams({}), 'vibe_math_abort')
1404
+ registerTool('vibe_math_status', 'Show scheduler status, params, active agents, projects, and recent activity.', objParams({}), 'vibe_math_status')
1405
+ registerTool('vibe_math_report', 'Return the full progress report and write it to Progress_Logs/report.json.', objParams({}), 'vibe_math_report')
1406
+ registerTool('vibe_math_set_mode', 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.', objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
1407
+ registerTool('vibe_math_set_params', 'Update scheduler parameters (partial).', objParams({ maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' } }), 'vibe_math_set_params')
1408
+ registerTool('vibe_math_setup', 'Return the interactive parameter schema for guided configuration.', objParams({}), 'vibe_math_setup')
1409
+ registerTool('vibe_math_save_settings', 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.', objParams({}), 'vibe_math_save_settings')
1410
+ registerTool('vibe_math_template', 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.', objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
1411
+ registerTool('vibe_math_add_problem', 'Add a problem to the current project qs/qs.json.', objParams({ id: { type: 'string' }, description: { type: 'string' }, priority: { type: 'integer' } }, ['id', 'description']), 'vibe_math_add_problem')
1412
+ registerTool('vibe_math_add_proposition', 'Add a proposition to Propos/ (with 概述, 布尔估计, 细类型, 优先级, 价值/关键性).', objParams({ id: { type: 'string' }, 概述: { type: 'string' }, 布尔估计: { type: 'number' }, 优先级: { type: 'integer' }, '价值/关键性': { type: 'number' }, 细类型: { type: 'object' } }, ['id', '概述']), 'vibe_math_add_proposition')
1413
+ registerTool('vibe_math_list_propositions', 'List propositions from Propos/ (summary index: id, 概述, 布尔估计, 优先级, 价值/关键性, category).', objParams({}), 'vibe_math_list_propositions')
1414
+ registerTool('vibe_math_new_project', 'Create a new math project folder and switch to it.', objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_new_project')
1415
+ registerTool('vibe_math_set_project', 'Switch the current math project.', objParams({ name: { type: 'string' } }, ['name']), 'vibe_math_set_project')
1416
+ registerTool('vibe_math_list_projects', 'List math projects.', objParams({}), 'vibe_math_list_projects')
1417
+ registerTool('vibe_math_list_decisions', 'List pending manual decisions.', objParams({}), 'vibe_math_list_decisions')
1418
+ registerTool('vibe_math_decide', 'Resolve a pending manual decision (verdict override uses verdict: 1|0).', objParams({ id: { type: 'string' }, action: { type: 'string', enum: ['approve', 'reject', 'override'] }, verdict: { type: 'number' } }, ['id', 'action']), 'vibe_math_decide')
1419
+ registerTool('vibe_math_list_agents', 'List tracked sub-agents (child sessions).', objParams({}), 'vibe_math_list_agents')
1420
+ registerTool('vibe_math_message_agent', 'Send a message to a tracked child agent (next turn).', objParams({ childId: { type: 'string' }, message: { type: 'string' } }, ['childId', 'message']), 'vibe_math_message_agent')
1421
+ registerTool('vibe_math_interrupt_agent', 'Interrupt a tracked child agent.', objParams({ childId: { type: 'string' } }, ['childId']), 'vibe_math_interrupt_agent')
1422
+
1423
+ // /vibe slash command (registered once; routed per session)
1320
1424
  ctx.effect(() => commands.register({
1321
1425
  name: 'vibe',
1322
1426
  description: 'control the Vibe Math V2 solver (start/pause/projects/setup/save/decisions/agents/propositions)',
1323
1427
  input: { hint: '[start|resume|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|project [list|new <name>|<name>]|decisions|agents]' },
1324
1428
  handler: async function (invocation) {
1429
+ const s = getSession(invocation && invocation.agent)
1430
+ if (!s) return { kind: 'success', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
1325
1431
  const line = String(invocation && invocation.rawInput ? invocation.rawInput : '').trim()
1326
1432
  const parts = line.length > 0 ? line.split(/\s+/) : []
1327
1433
  const cmd = parts[0] || ''
1328
1434
  const rest = parts.slice(1)
1329
- const result = await dispatchVibeCommand(cmd, rest, invocation.agent)
1435
+ const result = await s.dispatchVibeCommand(cmd, rest)
1330
1436
  return { kind: 'success', text: JSON.stringify(result, null, 2) }
1331
1437
  },
1332
1438
  }))
1439
+
1440
+ // subagent/end (registered once; routed to the owning session via childOwner)
1441
+ ctx.on('subagent/end', function (info) {
1442
+ const sid = childOwner.get(info.id)
1443
+ const s = sid !== undefined ? sessions.get(sid) : undefined
1444
+ if (s) s.onChildEnd(info).catch(function (e) { console.error('vibe-math-v2 onChildEnd reject: ' + String((e && e.stack) || e)) })
1445
+ })
1446
+
1447
+ // tick timer (registered once; ticks every running session at its own pace)
1448
+ ctx.effect(() => { const t = setInterval(function () { for (const s of sessions.values()) { if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick() } }, 1000); return () => clearInterval(t) })
1333
1449
  }
@@ -5,6 +5,7 @@
5
5
  1. **断点续跑**:保存各代理的对话记录及任务栈,支持状态恢复。
6
6
  2. **人工干预**:允许在任意时刻手动介入工作流,例如调整代理参数、控制会话、决策关键节点,并可随时在“人工 / 自动(预设)”模式间切换。
7
7
  3. **进度汇报**:每隔一段时间(如发生变动、出现新进展、发生新调用或代理会话结束时),委托主代理汇总当前进展及各代理状态。
8
+ 4. **多会话并行隔离**:DSH 的 agent preset 是 standing mount——同一 preset 的所有会话共享同一个插件实例,因此插件**必须自行按根会话 id 隔离内部状态**(`rootAgent`、当前项目、调度器、代理注册表、决策队列、参数、任务栈等全部 per-session)。两个会话可同时各跑一个项目,各自的子代理挂在各自会话名下、互不干扰;当前项目按会话持久化(`VibeMath/current.<会话id>.json`,兼容读取旧 `current.json`)。
8
9
 
9
10
  ---
10
11