mio-agent-runtime 0.5.22 → 0.5.23

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
@@ -10,7 +10,7 @@ Mio Agent Runtime 的可发布 CLI 包。
10
10
  npm install -g mio-agent-runtime
11
11
  ```
12
12
 
13
- > 新机器 / 升级:请安装 `>=0.3.6`(Codex + OpenCode + Hermes 被动观察)。
13
+ > 新机器 / 升级:请安装 `>=0.5.22`(5 host 被动观察 + trace 查询 + digest 价值管线)。
14
14
  > 旧版本只有主动 MCP 规则注入,没有「无感存/取」。安装后执行一次
15
15
  > `mio init && mio install <host>`。
16
16
 
@@ -43,12 +43,12 @@ mio --json evolution status Machine-readable evolution module health
43
43
 
44
44
  ## MCP Tools
45
45
 
46
- MCP server exposes 47 tools across 5 domains:
46
+ MCP server exposes 46 tools across 5 domains:
47
47
 
48
- ### Memory (10)
48
+ ### Memory (9)
49
49
  `mio.memory.query` · `mio.memory.record` · `mio.memory.archive` · `mio.memory.migrate` · `mio.memory.analyze` · `mio.experience.list` · `mio.experience.confirm` · `mio.experience.reuse` · `mio.policy.check`
50
50
 
51
- ### Observer Pipeline (13)
51
+ ### Observer Pipeline (14)
52
52
  `mio.observer.world_model` · `mio.observer.trends` · `mio.observer.research` · `mio.observer.insights` · `mio.observer.status` · `mio.observer.collect` · `mio.observer.ferment` · `mio.observer.essays` · `mio.observer.dag` · `mio.observer.ingest` · `mio.observer.subscribe` · `mio.observer.digest`
53
53
  `mio.trace.query` — query the trace log (task outcomes, tool errors) by type/outcome/agent/project/time window; also available as `mio traces` on the CLI
54
54
  `mio.digest.generate` — aggregate recent data into an actionable digest (agent success rates, project activity, error hotspots, reuse evidence, suggestions); also available as `mio digest` on the CLI
@@ -59,8 +59,8 @@ MCP server exposes 47 tools across 5 domains:
59
59
  ### Creativity Engine (4)
60
60
  `mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status`
61
61
 
62
- ### Idea & Evolution (7)
63
- `mio.idea.generate` · `mio.idea.list` · `mio.evolution.suggest` · `mio.evolution.report` · `mio.evolution.strategies` · `mio.phase0.report`
62
+ ### Task, Agent & Evolution (15)
63
+ `mio.task.route` · `mio.task.record_outcome` · `mio.agent.register` · `mio.agent.list` · `mio.agent.report` · `mio.host.capabilities` · `mio.evolution.status` · `mio.evolution.report` · `mio.evolution.shadow.record` · `mio.evolution.dual_write.record` · `mio.evolution.cutover.readiness` · `mio.evolution.cutover.apply` · `mio.evolution.migration.plan` · `mio.evolution.authority.plan` · `mio.phase0.report`
64
64
 
65
65
  ## Packages
66
66
 
@@ -77,6 +77,11 @@ Published to npm under `@akemi-mio` scope:
77
77
  | `@akemi-mio/evolution-strategy` | Evolution strategy engine |
78
78
  | `@akemi-mio/evolution-safety` | Safety guardrails |
79
79
  | `@akemi-mio/evolution-scheduler` | Evolution scheduler |
80
+ | `@akemi-mio/analysis` | ModuleScanner — static analysis of package source trees (zero deps) |
81
+ | `@akemi-mio/messaging` | ExternalMessageGateway — inbound message routing + notification dispatch via injected renderer/outbox (zero deps; telegram adapters stay host-side) |
82
+ | `@akemi-mio/reasoning` | Reasoning planner — pure-function scoring for reasoning directives (injectable logger) |
83
+ | `@akemi-mio/resource-control` | Resource budgets, background task runner, budget rebalancing (injectable runtime) |
84
+ | `@akemi-mio/agent-persona` | Persona drift control, content classification, user behavior analysis (injectable logger) |
80
85
  | `@akemi-mio/creativity` | CreativityService — LLM-powered concept generation, conflict detection, merging, fermentation (TypeScript, depends on `@akemi-mio/core`) |
81
86
  | `@akemi-mio/observer` | ObserverService — multi-source data collection, trend analysis, deep research, world model, fermentation, DAG state machine, self-evolution engine (zero npm deps) |
82
87
  | `@akemi-mio/insight` | InsightService — LLM-based insight generation, conflict/drift/repetition/stalled-goal/friction detectors, presence service, insight scoring (zero npm deps) |
@@ -132,6 +137,21 @@ schema,`source=workbuddy-observer`)。
132
137
  - 已处理的文件偏移记录在 `MIO_HOME/observe-state.json`,不会重复写入。
133
138
  - 只回填最近 24h 内更新的会话,避免把历史记录灌入记忆库。
134
139
 
140
+ ### Context 块的写入范围(`MIO_CONTEXT`)
141
+
142
+ `<workspace>/AGENTS.md`(或 `CLAUDE.md`)里的 `MIO_CONTEXT` 块最多保留
143
+ 6 条(`CONTEXT_MAX`),按时间倒序。两条规则保证真实历史不被挤掉:
144
+
145
+ - **同一条内容只占一个位置**。重复写入只把它移到最前。旧版本会把同一行
146
+ 反复插入,占满全部 6 个位置,把真实任务摘要整体挤出(存量
147
+ `observe-state.json` 里的重复条目会在下次写入时自动清理)。
148
+ - **`mio digest --write-back` 只写回有数据的项目**。写回的是一行项目维度
149
+ 摘要(`digest(7d): <项目> N 任务 M% 成功`),不是全局统计;`observe-state`
150
+ 里没有对应 digest 数据的 workspace 会被直接跳过,不会被写入。旧版本把
151
+ 同一条全局 headline 广播给所有历史项目,导致无关仓库(如 ComfyUI)被污染。
152
+
153
+ 需要强制指定目标时用 `--cwd <path>` 或 `--project <name>`;`--cwd` 优先。
154
+
135
155
  OpenCode 会话不依赖模型自觉:`mio observe` 通过 `opencode db` 增量读取
136
156
  `~/.local/share/opencode/opencode.db`(Windows 下该文件被 OpenCode 独占锁定,
137
157
  必须经 opencode 自带 CLI 读取),按 `part` 表的 rowid 做增量游标,只传输
@@ -169,6 +189,8 @@ packages/mio-cli
169
189
  │ ├── runtime-modules.js
170
190
  │ ├── creativity-engine.js
171
191
  │ ├── memory-store.js
192
+ │ ├── retention.js
193
+ │ ├── digest.js
172
194
  │ └── mio-intelligence-mcp/index.js
173
195
  └── package.json
174
196
  ```
@@ -178,7 +200,10 @@ query/record ranking (Latin token + CJK bigram scoring, project/global
178
200
  scope layers, reuse-evidence weighting). Both the MCP server
179
201
  (`mio.memory.query` / `mio.memory.record`) and the CLI
180
202
  (`mio recall` / `mio remember`) go through it, so rankings are identical
181
- across entry points.
203
+ across entry points. `server/digest.js` aggregates traces/memory/reuse
204
+ into actionable reports (also exposed as `mio.digest.generate`), and
205
+ `server/retention.js` powers `mio prune` (age/expiry trimming with
206
+ backups; `memory.jsonl` is never touched without explicit `--memory --yes`).
182
207
 
183
208
  ## Release verification
184
209
 
package/bin/mio.js CHANGED
@@ -348,6 +348,13 @@ function digestCommand(args, useJson) {
348
348
  // Feed the actionable lines back into each active workspace context file
349
349
  // (AGENTS.md / CLAUDE.md MIO_CONTEXT block), so every agent passively
350
350
  // receives the digest's value on its next session.
351
+ //
352
+ // Targets are resolved per project, never broadcast. An earlier version
353
+ // wrote one global headline into Object.keys(state.contexts) -- every
354
+ // workspace ever observed -- so unrelated repos (ComfyUI, douyin_store)
355
+ // received the same line and their real history was pushed out of the
356
+ // six-slot block. Write only where the digest actually has data for that
357
+ // project, or where the caller explicitly asked with --cwd/--project.
351
358
  const stateFile = path.join(MIO_HOME, 'observe-state.json')
352
359
  let state = {}
353
360
  try {
@@ -355,27 +362,50 @@ function digestCommand(args, useJson) {
355
362
  } catch (_) {}
356
363
  state.contexts = state.contexts || {}
357
364
  const stamp = new Date().toLocaleString('zh-CN', { hour12: false })
358
- const rate =
359
- report.overview.tasks > 0
360
- ? Math.round(((report.overview.byOutcome.success || 0) / report.overview.tasks) * 100)
361
- : 0
362
- const headline =
363
- `digest(${days}d): 任务 ${report.overview.tasks} 条 成功率 ${rate}%, error ${report.overview.errorTraces}` +
364
- (report.suggestions.length > 0 ? ' | ' + report.suggestions[0] : '')
365
- const targetCwds = optionValue(flags, '--cwd')
366
- ? [optionValue(flags, '--cwd')]
367
- : Object.keys(state.contexts)
365
+
366
+ // Project name -> per-project digest line.
367
+ const projectLine = new Map()
368
+ for (const entry of report.projects || []) {
369
+ if (!entry || !entry.project) continue
370
+ const rate = entry.tasks > 0 ? Math.round(((entry.success || 0) / entry.tasks) * 100) : 0
371
+ projectLine.set(String(entry.project).toLowerCase(), `digest(${days}d): ${entry.project} ${entry.tasks} 任务 ${rate}% 成功`)
372
+ }
373
+
374
+ const explicitCwd = optionValue(flags, '--cwd')
375
+ const explicitProject = optionValue(flags, '--project')
376
+ let targetCwds = []
377
+ if (explicitCwd) {
378
+ targetCwds = [explicitCwd]
379
+ } else if (explicitProject) {
380
+ const wanted = String(explicitProject).toLowerCase()
381
+ targetCwds = Object.keys(state.contexts).filter(
382
+ (cwd) => path.basename(cwd).toLowerCase() === wanted,
383
+ )
384
+ } else {
385
+ // No explicit target: only write to workspaces this digest has data for.
386
+ // A workspace with no matching project gets nothing.
387
+ targetCwds = Object.keys(state.contexts).filter((cwd) =>
388
+ projectLine.has(path.basename(cwd).toLowerCase()),
389
+ )
390
+ }
391
+
368
392
  let written = 0
369
393
  for (const cwd of targetCwds) {
394
+ const line = projectLine.get(path.basename(cwd).toLowerCase())
395
+ if (!line) continue
370
396
  try {
371
- observer.updateProjectContext(state, cwd, stamp + ' | ' + headline, 'CLAUDE.md')
372
- observer.updateProjectContext(state, cwd, stamp + ' | ' + headline, 'AGENTS.md')
397
+ observer.updateProjectContext(state, cwd, stamp + ' | ' + line, 'CLAUDE.md')
398
+ observer.updateProjectContext(state, cwd, stamp + ' | ' + line, 'AGENTS.md')
373
399
  written += 1
374
400
  } catch (_) {}
375
401
  }
376
402
  fs.mkdirSync(MIO_HOME, { recursive: true })
377
403
  fs.writeFileSync(stateFile, JSON.stringify(state) + '\n', 'utf8')
378
- report.writeBack = { workspaces: written, headline }
404
+ report.writeBack = {
405
+ workspaces: written,
406
+ skipped: Object.keys(state.contexts).length - written,
407
+ projects: [...projectLine.values()],
408
+ }
379
409
  }
380
410
 
381
411
  // Persist the report so it becomes part of the data asset itself.
@@ -388,7 +418,10 @@ function digestCommand(args, useJson) {
388
418
  console.log(report.markdown)
389
419
  console.log(`Report saved: ${reportFile}`)
390
420
  if (report.writeBack) {
391
- console.log(`Write-back: ${report.writeBack.workspaces} workspace(s) <- "${report.writeBack.headline}"`)
421
+ console.log(
422
+ `Write-back: ${report.writeBack.workspaces} workspace(s), ${report.writeBack.skipped} skipped (no digest data)`,
423
+ )
424
+ for (const line of report.writeBack.projects) console.log(` - ${line}`)
392
425
  }
393
426
  }
394
427
 
@@ -65,6 +65,30 @@ function stateFile(home) {
65
65
  return path.join(home, 'observe-state.json')
66
66
  }
67
67
 
68
+ // Drop repeated lines from stored context lists, preserving the most recent
69
+ // occurrence order. Older builds could write the same line into every slot of
70
+ // a workspace block (a generated digest headline broadcast to all projects),
71
+ // so existing observe-state files still carry duplicates until they are
72
+ // rewritten. Normalising on load heals that data without a migration step.
73
+ function dedupeContexts(contexts) {
74
+ const out = {}
75
+ for (const [cwd, list] of Object.entries(contexts)) {
76
+ if (!Array.isArray(list)) {
77
+ out[cwd] = []
78
+ continue
79
+ }
80
+ const seen = new Set()
81
+ const deduped = []
82
+ for (const line of list) {
83
+ if (seen.has(line)) continue
84
+ seen.add(line)
85
+ deduped.push(line)
86
+ }
87
+ out[cwd] = deduped
88
+ }
89
+ return out
90
+ }
91
+
68
92
  function loadState(home) {
69
93
  try {
70
94
  const parsed = JSON.parse(fs.readFileSync(stateFile(home), 'utf8'))
@@ -74,7 +98,7 @@ function loadState(home) {
74
98
  files: parsed.files || {},
75
99
  buffers: parsed.buffers || {},
76
100
  openTurns: parsed.openTurns || {},
77
- contexts: parsed.contexts || {},
101
+ contexts: dedupeContexts(parsed.contexts || {}),
78
102
  codexFiles: parsed.codexFiles || {},
79
103
  codexBuffers: parsed.codexBuffers || {},
80
104
  codexTurns: parsed.codexTurns || {},
@@ -291,7 +315,20 @@ const CONTEXT_MAX = 6
291
315
  function updateProjectContext(state, cwd, line, targetFile) {
292
316
  if (!cwd || !line) return
293
317
  const target = path.join(cwd, targetFile || 'AGENTS.md')
294
- const list = state.contexts[cwd] || []
318
+ // Normalise the whole list before inserting, not just the incoming line.
319
+ // Callers may load state without going through loadState (the digest
320
+ // write-back reads the file directly), so stale duplicates have to be
321
+ // cleared here or they keep consuming slots and push real history out.
322
+ const list = []
323
+ const seen = new Set()
324
+ for (const item of state.contexts[cwd] || []) {
325
+ if (seen.has(item)) continue
326
+ seen.add(item)
327
+ list.push(item)
328
+ }
329
+ // Re-inserting an existing line moves it to the front instead of duplicating.
330
+ const existing = list.indexOf(line)
331
+ if (existing !== -1) list.splice(existing, 1)
295
332
  list.unshift(line)
296
333
  if (list.length > CONTEXT_MAX) list.pop()
297
334
  state.contexts[cwd] = list
@@ -1591,6 +1628,7 @@ module.exports = {
1591
1628
  workbuddyConfigDir,
1592
1629
  loadState,
1593
1630
  saveState,
1631
+ dedupeContexts,
1594
1632
  updateProjectContext,
1595
1633
  runCycle,
1596
1634
  opencodeDbPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.5.22",
3
+ "version": "0.5.23",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -18,6 +18,7 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check adapters/claude-code.js && node --check server/runtime-modules.js && node --check server/memory-store.js && node --check server/retention.js && node --check server/digest.js && node --check server/host-capabilities.js && node --check server/evolution-cutover.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js && node --check scripts/verify-packed-runtime.js",
21
+ "test": "node --test \"__tests__/*.test.js\" \"server/mio-intelligence-mcp/__tests__/*.test.js\"",
21
22
  "prepack": "npm run check",
22
23
  "verify:pack-install": "node scripts/verify-packed-runtime.js"
23
24
  },
@@ -34,5 +35,11 @@
34
35
  "@akemi-mio/evolution-scheduler": "^0.1.0",
35
36
  "@akemi-mio/observer": "^0.1.0",
36
37
  "@akemi-mio/insight": "^0.1.0"
37
- }
38
+ },
39
+ "main": "index.js",
40
+ "directories": {
41
+ "doc": "docs"
42
+ },
43
+ "keywords": [],
44
+ "author": ""
38
45
  }
@@ -76,12 +76,13 @@ function npmCommand(platform = process.platform) {
76
76
 
77
77
  function npmInvocation(args, env = process.env, platform = process.platform, node = process.execPath) {
78
78
  if (env.npm_execpath) {
79
- return { command: node, args: [env.npm_execpath, ...args] }
79
+ return { command: node, args: [env.npm_execpath, ...args], shell: false }
80
80
  }
81
- if (platform === 'win32') {
82
- return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'npm', ...args] }
83
- }
84
- return { command: npmCommand(platform), args }
81
+ // Fall back to the npm shim on PATH instead of routing through cmd.exe.
82
+ // cmd.exe is unavailable in some sandboxes, and a bare .cmd/.bat shim cannot
83
+ // be spawned directly (Node requires shell:true for those on Windows), so
84
+ // signal the shell requirement to the runner instead of guessing there.
85
+ return { command: npmCommand(platform), args: [...args], shell: platform === 'win32' }
85
86
  }
86
87
 
87
88
  function packageDirName(name) {
@@ -95,9 +96,12 @@ function tarballFileName(name, version) {
95
96
 
96
97
  function readPackageJson(workspaceRoot, name) {
97
98
  const packageDir = path.join(workspaceRoot, 'packages', packageDirName(name))
99
+ // Windows editors may write package.json with a UTF-8 BOM; JSON.parse
100
+ // rejects the leading U+FEFF, so strip it before parsing.
101
+ const raw = fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8').replace(/^\uFEFF/, '')
98
102
  return {
99
103
  packageDir,
100
- manifest: JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')),
104
+ manifest: JSON.parse(raw),
101
105
  }
102
106
  }
103
107
 
@@ -211,6 +215,11 @@ function run(command, args, options = {}) {
211
215
  env: options.env || process.env,
212
216
  encoding: 'utf8',
213
217
  stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
218
+ // Windows .cmd/.bat shims (npm.cmd) cannot be spawned directly by
219
+ // execFileSync since Node's CVE-2024-27980 hardening; they need a shell.
220
+ // This only kicks in for the npm shim path — node-based invocations pass
221
+ // an absolute node executable and still run shell-free.
222
+ shell: options.shell === true,
214
223
  })
215
224
  return output ? output.trim() : ''
216
225
  }
@@ -296,14 +305,19 @@ function verifyPackedRuntimeInstall(input = {}) {
296
305
  run(pack.command, pack.args, {
297
306
  cwd: pkg.packageDir,
298
307
  stdio: ['ignore', 'pipe', 'pipe'],
308
+ shell: pack.shell,
299
309
  })
300
310
  packageArtifactEvidence(pkg)
301
311
  }
302
312
 
303
313
  const init = npmInvocation(['init', '-y'])
304
- run(init.command, init.args, { cwd: plan.installDir, stdio: ['ignore', 'pipe', 'pipe'] })
314
+ run(init.command, init.args, { cwd: plan.installDir, stdio: ['ignore', 'pipe', 'pipe'], shell: init.shell })
305
315
  const install = npmInvocation(plan.install.args)
306
- run(install.command, install.args, { cwd: plan.install.cwd, stdio: ['ignore', 'pipe', 'pipe'] })
316
+ run(install.command, install.args, {
317
+ cwd: plan.install.cwd,
318
+ stdio: ['ignore', 'pipe', 'pipe'],
319
+ shell: install.shell,
320
+ })
307
321
  const smokeEnv = {
308
322
  ...process.env,
309
323
  MIO_HOME: path.join(plan.installDir, '.mio-home'),
@@ -360,6 +374,7 @@ module.exports = {
360
374
  createVerifyPackedRuntimeOptions,
361
375
  createPackedRuntimeInstallPlan,
362
376
  packageArtifactEvidence,
377
+ readPackageJson,
363
378
  npmInvocation,
364
379
  npmCommand,
365
380
  parseVerifyPackedRuntimeArgs,
package/server/digest.js CHANGED
@@ -186,6 +186,18 @@ function createDigest(options = {}) {
186
186
  suggestions,
187
187
  }
188
188
  report.markdown = renderMarkdown(report)
189
+
190
+ // Persist a machine-readable snapshot for consumers that should not
191
+ // recompute the aggregation — notably mio.task.route's agent-health signal.
192
+ try {
193
+ const digestDir = path.join(home, 'digest')
194
+ fs.mkdirSync(digestDir, { recursive: true })
195
+ const snapshot = { ...report }
196
+ delete snapshot.markdown
197
+ fs.writeFileSync(path.join(digestDir, 'latest.json'), JSON.stringify(snapshot) + '\n', 'utf8')
198
+ } catch (_) {
199
+ // best-effort persistence; the in-memory report is still returned
200
+ }
189
201
  return report
190
202
  }
191
203
 
@@ -1108,6 +1108,40 @@ function reportAgent(args = {}) {
1108
1108
  return { project, count: reports.length, reports }
1109
1109
  }
1110
1110
 
1111
+ const AGENT_HEALTH_FRESH_MS = 48 * 3600000
1112
+
1113
+ function readAgentHealth() {
1114
+ const file = path.join(dataDir, 'digest', 'latest.json')
1115
+ if (!fs.existsSync(file)) return null
1116
+ try {
1117
+ const snapshot = JSON.parse(fs.readFileSync(file, 'utf8'))
1118
+ const ts = Date.parse(snapshot.generatedAt || '')
1119
+ const generatedAt = Number.isFinite(ts) ? snapshot.generatedAt : null
1120
+ if (!Number.isFinite(ts) || Date.now() - ts > AGENT_HEALTH_FRESH_MS) {
1121
+ return { stale: true, generatedAt, best: null, degraded: [], agents: [] }
1122
+ }
1123
+ const agents = Array.isArray(snapshot.agents) ? snapshot.agents : []
1124
+ const reliable = agents.filter((a) => Number(a.total) >= 5)
1125
+ const best =
1126
+ reliable.length > 0
1127
+ ? reliable.reduce((a, b) => (b.successRate > a.successRate ? b : a))
1128
+ : null
1129
+ const degraded = reliable
1130
+ .filter((a) => a.successRate < 50)
1131
+ .map((a) => ({ agent: a.agent, successRate: a.successRate, total: a.total }))
1132
+ return {
1133
+ stale: false,
1134
+ generatedAt,
1135
+ periodDays: snapshot.periodDays || null,
1136
+ best: best ? { agent: best.agent, successRate: best.successRate, total: best.total } : null,
1137
+ degraded,
1138
+ agents: agents.map((a) => ({ agent: a.agent, total: a.total, successRate: a.successRate })),
1139
+ }
1140
+ } catch (_) {
1141
+ return null
1142
+ }
1143
+ }
1144
+
1111
1145
  function routeTask(args = {}) {
1112
1146
  const task = String(args.task || '').trim()
1113
1147
  if (!task) throw new Error('task.route requires a non-empty task')
@@ -1223,6 +1257,26 @@ function routeTask(args = {}) {
1223
1257
  'No matching experience or memory found. Proceed with care and record the outcome to grow the experience base.'
1224
1258
  }
1225
1259
 
1260
+ // Agent-health routing signal from the latest digest snapshot (value loop:
1261
+ // collected task outcomes influence who should execute the next task).
1262
+ const agentHealth = readAgentHealth()
1263
+ let routingSignal = null
1264
+ if (agentHealth && !agentHealth.stale && agentHealth.best) {
1265
+ const parts = []
1266
+ if (agentHealth.best.successRate >= 80) {
1267
+ parts.push(
1268
+ `prefer ${agentHealth.best.agent} (${agentHealth.best.successRate}% success over ${agentHealth.best.total} tasks)`,
1269
+ )
1270
+ }
1271
+ for (const bad of agentHealth.degraded) {
1272
+ parts.push(`avoid ${bad.agent} (${bad.successRate}% success over ${bad.total} tasks)`)
1273
+ }
1274
+ if (parts.length > 0) {
1275
+ routingSignal = `Agent health (last ${agentHealth.periodDays || '?'}d): ${parts.join('; ')}.`
1276
+ suggestion = `${suggestion} ${routingSignal}`
1277
+ }
1278
+ }
1279
+
1226
1280
  return {
1227
1281
  task,
1228
1282
  project,
@@ -1230,9 +1284,11 @@ function routeTask(args = {}) {
1230
1284
  count: routes.length,
1231
1285
  routes,
1232
1286
  relatedMemories,
1287
+ agentHealth,
1233
1288
  summary: {
1234
1289
  verifiedRoutes: routes.length,
1235
1290
  relatedMemories: relatedMemories.length,
1291
+ routingSignal,
1236
1292
  suggestion,
1237
1293
  },
1238
1294
  }
@@ -1468,7 +1524,7 @@ const TOOLS = [
1468
1524
  },
1469
1525
  {
1470
1526
  name: 'mio.task.route',
1471
- description: 'Route an incoming task to the most relevant verified experiences and related memories, turning recall into decision influence (P1).',
1527
+ description: 'Route an incoming task to the most relevant verified experiences and related memories, and include an agent-health signal (from the latest digest) saying which agents to prefer or avoid. Turns recall plus observed performance into decision influence (P1).',
1472
1528
  inputSchema: {
1473
1529
  type: 'object',
1474
1530
  properties: {