mio-agent-runtime 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -11
- package/adapters/hermes.js +59 -5
- package/adapters/host-utils.js +66 -61
- package/bin/mio.js +3216 -2943
- package/observe/observer.js +82 -12
- package/package.json +1 -1
- package/scripts/check-syntax.cjs +16 -2
- package/server/agent-store.js +7 -7
- package/server/creativity-engine.js +5 -0
- package/server/digest.js +3 -18
- package/server/evaluation-store.js +244 -0
- package/server/evolution-report.js +5 -21
- package/server/experience-store.js +3 -3
- package/server/memory-store.js +119 -23
- package/server/mio-intelligence-mcp/index.js +19 -0
- package/server/mio-intelligence-mcp/phase0.js +4 -20
- package/server/observer-store.js +6 -2
- package/server/policy-store.js +3 -3
- package/server/query-log.js +3 -9
- package/server/retention.js +1 -9
- package/server/subscription-store.js +62 -8
- package/server/task-store.js +100 -10
package/server/task-store.js
CHANGED
|
@@ -18,12 +18,32 @@
|
|
|
18
18
|
|
|
19
19
|
const fs = require('fs')
|
|
20
20
|
const path = require('path')
|
|
21
|
-
const { createId,
|
|
21
|
+
const { createId, readJsonlCached, appendJsonl, writeJsonl } = require('./memory-store.js')
|
|
22
22
|
const { REUSE_STATUS_FILTERS } = require('./experience-store.js')
|
|
23
23
|
const { createQueryLog } = require('./query-log.js')
|
|
24
24
|
|
|
25
25
|
const AGENT_HEALTH_FRESH_MS = 48 * 3600000
|
|
26
26
|
|
|
27
|
+
// Token overlap helpers for the routing-gate diagnostic below. Kept local and
|
|
28
|
+
// deliberately stricter than memory-store's scoreRecord: that scorer also
|
|
29
|
+
// counts prefix matches, which are right for fuzzy recall but would make this
|
|
30
|
+
// diagnostic fire on unrelated records. These require exact token equality.
|
|
31
|
+
function tokenizeForOverlap(text) {
|
|
32
|
+
const normalized = String(text || '').toLowerCase()
|
|
33
|
+
const latin = normalized.match(/[a-z0-9]+/g) || []
|
|
34
|
+
const cjk = (normalized.match(/[\u4e00-\u9fff]/g) || []).map((char) => `cjk:${char}`)
|
|
35
|
+
return [...latin, ...cjk]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cjkBigramsForOverlap(text) {
|
|
39
|
+
const chars = String(text || '').toLowerCase().match(/[\u4e00-\u9fff]/g) || []
|
|
40
|
+
const bigrams = new Set()
|
|
41
|
+
for (let i = 0; i + 1 < chars.length; i += 1) {
|
|
42
|
+
bigrams.add(`${chars[i]}${chars[i + 1]}`)
|
|
43
|
+
}
|
|
44
|
+
return bigrams
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
function createTaskStore(options) {
|
|
28
48
|
const dataDir = options.dataDir
|
|
29
49
|
const projectName = options.projectName || (() => null)
|
|
@@ -92,13 +112,28 @@ function createTaskStore(options) {
|
|
|
92
112
|
const scope = normalizeScope(args.scope)
|
|
93
113
|
const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 10)
|
|
94
114
|
|
|
95
|
-
const memories =
|
|
115
|
+
const memories = readJsonlCached(memoryPath).filter((record) => record.archived !== true)
|
|
96
116
|
const memoryById = new Map(memories.map((record) => [record.id, record]))
|
|
97
117
|
const evidence = loadEvidenceWeights()
|
|
98
118
|
|
|
99
119
|
// Match verified reuse experiences whose source memory is relevant to the task.
|
|
120
|
+
//
|
|
121
|
+
// ⚠️ The `verified` predicate (experience-store.js) is
|
|
122
|
+
// `reuse && behaviorChanged && outcomeImproved` -- note it does NOT include
|
|
123
|
+
// `confirmed`. It has a failure mode that looks exactly like "no match": an
|
|
124
|
+
// auto-claim is written with behaviorChanged=false (task-store.js
|
|
125
|
+
// buildAutoClaim), and nothing flips it to true except
|
|
126
|
+
// `mio.experience.confirm` (experience-store.js:146). So if nothing was ever
|
|
127
|
+
// confirmed, verified is empty and routing returns nothing for every task,
|
|
128
|
+
// forever. Measured on the real dataset 2026-09-20: 21 reuse records,
|
|
129
|
+
// behaviorChanged=false in all of them, verified == 0, and `task.route`
|
|
130
|
+
// could not return a single hit on a task that plainly had matching
|
|
131
|
+
// experience. Confirming three records made the same route return 1. So
|
|
132
|
+
// `count` is not a routing-quality signal until confirmations exist --
|
|
133
|
+
// `gatedBy` below reports that distinction.
|
|
134
|
+
const reuseRecords = readJsonlCached(experienceReusePath)
|
|
100
135
|
const routeByMemory = new Map()
|
|
101
|
-
for (const record of
|
|
136
|
+
for (const record of reuseRecords) {
|
|
102
137
|
if (!REUSE_STATUS_FILTERS.verified(record)) continue
|
|
103
138
|
const memory = memoryById.get(record.experienceId)
|
|
104
139
|
if (!memory || !matchesProjectScope(memory, project, scope)) continue
|
|
@@ -145,18 +180,65 @@ function createTaskStore(options) {
|
|
|
145
180
|
},
|
|
146
181
|
}))
|
|
147
182
|
|
|
183
|
+
// Why routing came back empty, when it did. `no-match` means the strict
|
|
184
|
+
// filter was satisfied by nothing relevant; `unconfirmed` means relevant
|
|
185
|
+
// experience exists but was never confirmed, so the filter excluded it. The
|
|
186
|
+
// two look identical in `count` but need opposite responses -- the first is
|
|
187
|
+
// a recall problem, the second is a one-command fix.
|
|
188
|
+
//
|
|
189
|
+
// "Relevant" here deliberately does NOT reuse scoreRecord: that scorer's
|
|
190
|
+
// prefix rule is symmetric (`token.startsWith(item) || item.startsWith(token)`),
|
|
191
|
+
// so a query token merely starting with any short haystack token counts --
|
|
192
|
+
// "nonsense" matches a record containing "no read path". Fine for fuzzy
|
|
193
|
+
// recall, useless as a diagnostic: measured on the real dataset, a nonsense
|
|
194
|
+
// task scored 10 unrelated records as "relevant". Require a real shared
|
|
195
|
+
// token (or CJK bigram) instead, so the count only moves for genuine overlap.
|
|
196
|
+
let gatedBy = null
|
|
197
|
+
if (routes.length === 0) {
|
|
198
|
+
const queryTokens = new Set(tokenizeForOverlap(task))
|
|
199
|
+
const bigrams = cjkBigramsForOverlap(task)
|
|
200
|
+
const overlaps = (memory) => {
|
|
201
|
+
const content = `${memory.content || ''} ${(memory.tags || []).join(' ')} ${memory.kind || ''}`
|
|
202
|
+
if (tokenizeForOverlap(content).some((token) => queryTokens.has(token))) return true
|
|
203
|
+
if (bigrams.size === 0) return false
|
|
204
|
+
const haystack = cjkBigramsForOverlap(content)
|
|
205
|
+
for (const bigram of bigrams) if (haystack.has(bigram)) return true
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
const relevantUnconfirmed = reuseRecords.filter((record) => {
|
|
209
|
+
if (!REUSE_STATUS_FILTERS.pending(record)) return false
|
|
210
|
+
const memory = memoryById.get(record.experienceId)
|
|
211
|
+
if (!memory || !matchesProjectScope(memory, project, scope)) return false
|
|
212
|
+
return overlaps(memory)
|
|
213
|
+
}).length
|
|
214
|
+
const totalVerified = reuseRecords.filter(REUSE_STATUS_FILTERS.verified).length
|
|
215
|
+
gatedBy = {
|
|
216
|
+
reason: relevantUnconfirmed > 0 ? 'unconfirmed' : 'no-match',
|
|
217
|
+
verifiedTotal: totalVerified,
|
|
218
|
+
relevantUnconfirmed,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
148
222
|
// Broader context: top related memories not already routed.
|
|
149
223
|
const routedIds = new Set(routes.map((route) => route.experienceId))
|
|
150
224
|
const scoreRelated = (record) =>
|
|
151
225
|
scoreRecord(record, task, project, evidence) +
|
|
152
226
|
(scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
227
|
+
// Same shape as memory-store.queryMemory: score once, then sort the scored
|
|
228
|
+
// pairs. Scoring inside the comparator re-tokenized every candidate on every
|
|
229
|
+
// comparison, which is what made mio.task.route the slowest tool call
|
|
230
|
+
// (~100 ms median on a 1094-record store).
|
|
231
|
+
const scoredRelated = []
|
|
232
|
+
for (const record of memories) {
|
|
233
|
+
if (routedIds.has(record.id)) continue
|
|
234
|
+
if (!matchesProjectScope(record, project, scope)) continue
|
|
235
|
+
const score = scoreRelated(record)
|
|
236
|
+
if (score > 0) scoredRelated.push({ record, score })
|
|
237
|
+
}
|
|
238
|
+
const relatedMemories = scoredRelated
|
|
239
|
+
.sort((a, b) => b.score - a.score)
|
|
158
240
|
.slice(0, Math.max(limit, 5))
|
|
159
|
-
.map((record) => {
|
|
241
|
+
.map(({ record }) => {
|
|
160
242
|
const ev = evidence.get(record.id)
|
|
161
243
|
return {
|
|
162
244
|
id: record.id,
|
|
@@ -196,6 +278,13 @@ function createTaskStore(options) {
|
|
|
196
278
|
if (routes.length > 0) {
|
|
197
279
|
suggestion =
|
|
198
280
|
`Route ${routes.length} verified experience(s) to this task; apply the top match first and record the outcome with mio.observer.ingest / mio.experience.reuse.`
|
|
281
|
+
} else if (gatedBy && gatedBy.reason === 'unconfirmed') {
|
|
282
|
+
// Distinct from "no match": the experience is there and relevant, it just
|
|
283
|
+
// was never confirmed. `verified` requires behaviorChanged, which only
|
|
284
|
+
// `experience.confirm` sets on an auto-claim, so an unconfirmed backlog
|
|
285
|
+
// silently empties the routing set. Point at the fix, not at recall.
|
|
286
|
+
suggestion =
|
|
287
|
+
`${gatedBy.relevantUnconfirmed} relevant experience(s) match this task but are unconfirmed, so routing skipped them (only verified experience routes, and confirming is what marks behaviorChanged). Run "mio status" to see the backlog, then "mio experience confirm --ids <id,...>".`
|
|
199
288
|
} else if (relatedMemories.length > 0) {
|
|
200
289
|
suggestion =
|
|
201
290
|
'No verified experience matches yet. Consult the related memories, then record the outcome so this task can seed future routes.'
|
|
@@ -232,6 +321,7 @@ function createTaskStore(options) {
|
|
|
232
321
|
routes,
|
|
233
322
|
relatedMemories,
|
|
234
323
|
agentHealth,
|
|
324
|
+
gatedBy,
|
|
235
325
|
summary: {
|
|
236
326
|
verifiedRoutes: routes.length,
|
|
237
327
|
relatedMemories: relatedMemories.length,
|
|
@@ -331,7 +421,7 @@ function createTaskStore(options) {
|
|
|
331
421
|
|
|
332
422
|
// 3. Update agent registry
|
|
333
423
|
const now = new Date().toISOString()
|
|
334
|
-
const agents =
|
|
424
|
+
const agents = readJsonlCached(agentsPath)
|
|
335
425
|
const agent = agents.find((a) => a.agentId === agentId && a.project === project)
|
|
336
426
|
if (agent) {
|
|
337
427
|
agent.lastSeenAt = now
|