mio-agent-runtime 0.5.23 → 0.7.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.
@@ -10,75 +10,32 @@ const { getEvolutionStatus } = require('../runtime-modules.js')
10
10
  const { listHostCapabilities } = require('../host-capabilities.js')
11
11
  const { createEvolutionCutoverTools } = require('../evolution-cutover.js')
12
12
  const { createMemoryStore } = require('../memory-store.js')
13
+ const { createExperienceStore, REUSE_STATUS_FILTERS } = require('../experience-store.js')
14
+ const { createPolicyStore } = require('../policy-store.js')
15
+ const { createAgentStore } = require('../agent-store.js')
16
+ const { createTaskStore } = require('../task-store.js')
17
+ const { createQueryLog } = require('../query-log.js')
18
+ const { createSubscriptionStore } = require('../subscription-store.js')
13
19
  const { createDigest } = require('../digest.js')
20
+ const { createEvolutionReport } = require('../evolution-report.js')
14
21
 
15
22
  const { CreativityEngine } = require('../creativity-engine.js')
16
-
17
- // Observer research pipeline (optional — installed via @akemi-mio/observer)
18
- let ObserverStore = null
19
- let ObserverService = null
20
- try {
21
- const obs = require('@akemi-mio/observer')
22
- ObserverStore = obs.ObserverStore
23
- ObserverService = obs.ObserverService
24
- } catch {}
25
-
26
- // Insight self-observation engine (optional — installed via @akemi-mio/insight)
27
- let InsightStore = null
28
- let InsightGenerator = null
29
- let PresenceService = null
30
- try {
31
- const insight = require('@akemi-mio/insight')
32
- InsightStore = insight.InsightStore
33
- InsightGenerator = insight.InsightGenerator
34
- PresenceService = insight.PresenceService
35
- } catch {}
23
+ // The observer and insight engines are optional dependencies. Their stores own
24
+ // the require and expose isObserverAvailable() / isInsightAvailable() so tool
25
+ // registration can be gated on them, and both the MCP server and the CLI share
26
+ // one implementation (see ../observer-store.js and ../insight-store.js).
27
+ const { createObserverStore, isObserverAvailable } = require('../observer-store.js')
28
+ const { createInsightStore, isInsightAvailable } = require('../insight-store.js')
36
29
 
37
30
  const SERVER_INFO = { name: 'mio-intelligence-mcp', version: '0.1.0' }
38
31
  const PROTOCOL_VERSION = '2024-11-05'
39
32
 
40
33
  // ═══════════════════════════════════════════════
41
- // LLM chatJson — used by creativity engine
34
+ // LLM chatJson — shared with the CLI (see ../llm-client.js)
42
35
  // ═══════════════════════════════════════════════
43
36
 
44
- async function chatJson(userText, opts = {}) {
45
- const apiUrl = process.env.LLM_API_URL || 'https://opencode.ai/zen/go/v1/chat/completions'
46
- const apiKey = process.env.LLM_KEY || ''
47
- const model = process.env.LLM_CHAT_MODEL || process.env.LLM_MODEL || 'deepseek-v4-flash'
48
- const system = opts.system || 'You are a creative AI assistant. Output JSON.'
49
- const temperature = opts.temperature ?? 0.3
37
+ const { chatJson } = require('../llm-client.js')
50
38
 
51
- try {
52
- const resp = await fetch(apiUrl, {
53
- method: 'POST',
54
- headers: {
55
- 'Content-Type': 'application/json',
56
- ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
57
- },
58
- body: JSON.stringify({
59
- model,
60
- temperature,
61
- messages: [
62
- { role: 'system', content: system },
63
- { role: 'user', content: userText },
64
- ],
65
- response_format: { type: 'json_object' },
66
- }),
67
- signal: AbortSignal.timeout(opts.timeoutMs || 30000),
68
- })
69
-
70
- if (!resp.ok) return { error: `HTTP ${resp.status}` }
71
- const json = await resp.json()
72
- const text = json.choices?.[0]?.message?.content || ''
73
- try {
74
- return { data: JSON.parse(text) }
75
- } catch {
76
- return { data: text }
77
- }
78
- } catch (err) {
79
- return { error: String(err) }
80
- }
81
- }
82
39
 
83
40
  const dataDir = path.resolve(
84
41
  process.env.MIO_DATA_DIR || path.join(process.cwd(), '.mio-intelligence')
@@ -95,7 +52,10 @@ fs.mkdirSync(dataDir, { recursive: true })
95
52
  const { loadPhase0, summarizePhase0, renderPhase0Markdown } = require('./phase0')
96
53
  const evolutionCutover = createEvolutionCutoverTools({ dataDir, appendJsonl, readJsonl, projectName })
97
54
  const creativityEngine = new CreativityEngine(path.join(dataDir, 'creativity'), chatJson)
98
- const insightStore = InsightStore ? new InsightStore(path.join(dataDir, 'insights', 'insights.json')) : null
55
+ const insightStore = createInsightStore({ dataDir, chatJson })
56
+ // baseDir defaults to <cwd>/.local/observer, matching the previous inline
57
+ // behaviour; individual calls may still override it via args.baseDir.
58
+ const observerStore = createObserverStore({})
99
59
 
100
60
  let runtimeContext = {}
101
61
  try {
@@ -117,40 +77,6 @@ function runtimeAgentId() {
117
77
  // Phase 0 auto-claim: match memory.query results with a later task_outcome
118
78
  // from the same agent+project, so reuse evidence does not depend on the
119
79
  // agent self-reporting mio.experience.reuse.
120
- const REUSE_MATCH_WINDOW_MS = (() => {
121
- const minutes = Number(process.env.MIO_REUSE_MATCH_WINDOW_MIN)
122
- return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : 60 * 60 * 1000
123
- })()
124
- const MAX_RECENT_QUERIES = 200
125
- let recentQueries = []
126
-
127
- function loadRecentQueries() {
128
- const now = Date.now()
129
- const entries = readJsonl(queryPath)
130
- .filter(
131
- (entry) =>
132
- entry &&
133
- typeof entry.agent === 'string' &&
134
- entry.agent &&
135
- typeof entry.expiresAt === 'number' &&
136
- entry.expiresAt > now
137
- )
138
- .slice(-MAX_RECENT_QUERIES)
139
- recentQueries = entries
140
- return entries
141
- }
142
-
143
- function persistRecentQueries(entries) {
144
- const limited = entries.slice(-MAX_RECENT_QUERIES)
145
- fs.mkdirSync(dataDir, { recursive: true })
146
- fs.writeFileSync(
147
- queryPath,
148
- limited.length > 0 ? limited.map((entry) => JSON.stringify(entry)).join('\n') + '\n' : '',
149
- 'utf8'
150
- )
151
- recentQueries = limited
152
- return limited
153
- }
154
80
 
155
81
  function createId(prefix) {
156
82
  return `${prefix}_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`
@@ -226,28 +152,29 @@ function projectName() {
226
152
  // Memory store: shared implementation with the CLI (`mio recall`/`mio remember`).
227
153
  // The pure scoring/filter helpers and queryMemory/recordMemory live in
228
154
  // server/memory-store.js so both entry points rank and persist identically.
229
- const REUSE_STATUS_FILTERS = Object.freeze({
230
- pending: (record) => record.source === 'auto_claim' && record.confirmed !== true,
231
- confirmed: (record) => record.confirmed === true,
232
- verified: (record) =>
233
- (record.reuse === true || record.reuse === 'true') &&
234
- (record.behaviorChanged === true || record.behaviorChanged === 'true') &&
235
- (record.outcomeImproved === true || record.outcomeImproved === 'true'),
236
- auto_claim: (record) => record.source === 'auto_claim',
237
- agent_report: (record) => record.source !== 'auto_claim',
155
+ const experienceStore = createExperienceStore({
156
+ dataDir,
157
+ projectName,
158
+ // Preserves the phase0 summary these tools have always returned.
159
+ phase0Summary: (project) => summarizePhase0(loadPhase0(dataDir, project)),
238
160
  })
161
+ const {
162
+ listReuse: listExperienceReuse,
163
+ confirmReuse: confirmExperienceReuse,
164
+ recordReuse: recordExperienceReuse,
165
+ } = experienceStore
166
+
167
+ // One query-log instance shared by the memory store (which records queries) and
168
+ // the task store (which consumes them for auto-claim). This is what removes the
169
+ // previous duplicate load/persist implementations of queries.jsonl.
170
+ const queryLog = createQueryLog({ dataDir })
239
171
 
240
172
  const memoryStore = createMemoryStore({
241
173
  dataDir,
242
174
  projectName,
243
175
  agentId: runtimeAgentId,
244
- reuseMatchWindowMs: REUSE_MATCH_WINDOW_MS,
176
+ queryLog,
245
177
  verifiedFilter: REUSE_STATUS_FILTERS.verified,
246
- recordQuery(entry) {
247
- const entries = loadRecentQueries()
248
- entries.push(entry)
249
- persistRecentQueries(entries)
250
- },
251
178
  })
252
179
 
253
180
  const digestEngine = createDigest({ home: dataDir })
@@ -265,1034 +192,49 @@ const {
265
192
  queryMemory,
266
193
  recordMemory,
267
194
  queryTraces,
195
+ analyzeMemory,
196
+ archiveMemory,
197
+ forgetMemory,
198
+ mergeMemory,
199
+ migrateMemory,
268
200
  loadEvidenceWeights,
269
201
  } = memoryStore
270
202
 
271
- function ingestObservation(args = {}) {
272
- const traceId = String(args.trace_id || '').trim()
273
- const eventType = String(args.event_type || '').trim()
274
- if (!traceId) throw new Error('observer.ingest requires trace_id')
275
- if (!eventType) throw new Error('observer.ingest requires event_type')
276
- const event = {
277
- id: createId('trace'),
278
- timestamp: new Date().toISOString(),
279
- trace_id: traceId,
280
- event_type: eventType,
281
- outcome: args.outcome || null,
282
- payload: args.payload || {},
283
- agent: args.agent || runtimeAgentId(),
284
- host: args.host || 'mcp',
285
- project: args.project || projectName(),
286
- }
287
- appendJsonl(tracePath, event)
288
- const autoClaims = autoClaimExperienceReuse(event)
289
- if (autoClaims.length > 0) {
290
- return { recorded: true, event, autoClaims }
291
- }
292
- return { recorded: true, event }
293
- }
294
-
295
- function autoClaimExperienceReuse(event) {
296
- if (String(event.event_type || '').toLowerCase() !== 'task_outcome') return []
297
- const targetAgent = String(event.agent || '').trim()
298
- if (!targetAgent) return []
299
- const now = Date.now()
300
- const claims = []
301
- const remaining = []
302
- const entries = loadRecentQueries()
303
- for (const entry of entries) {
304
- const expired = now - entry.timestamp > REUSE_MATCH_WINDOW_MS
305
- const matches =
306
- entry.agent === targetAgent && (!event.project || entry.project === event.project)
307
- if (expired || matches) {
308
- if (matches && !expired) {
309
- claims.push(buildAutoClaim(entry, event, targetAgent))
310
- }
311
- continue
312
- }
313
- remaining.push(entry)
314
- }
315
- persistRecentQueries(remaining)
316
- for (const evidence of claims) {
317
- appendJsonl(experienceReusePath, evidence)
318
- }
319
- return claims
320
- }
321
-
322
- function buildAutoClaim(entry, event, targetAgent) {
323
- const crossSourceIndex = entry.resultSources.findIndex(
324
- (source) => source && String(source).toLowerCase() !== targetAgent.toLowerCase()
325
- )
326
- const sourceAgent =
327
- crossSourceIndex >= 0
328
- ? String(entry.resultSources[crossSourceIndex])
329
- : targetAgent
330
- const experienceId =
331
- entry.resultIds[crossSourceIndex >= 0 ? crossSourceIndex : 0] ||
332
- entry.resultIds[0] ||
333
- 'unknown'
334
- const outcomeImproved = String(event.outcome || '').toLowerCase() === 'success'
335
- return {
336
- id: createId('xfer'),
337
- timestamp: new Date().toISOString(),
338
- sourceAgent,
339
- targetAgent,
340
- experienceId,
341
- reuse: true,
342
- behaviorChanged: false,
343
- outcomeImproved,
344
- project: entry.project,
345
- source: 'auto_claim',
346
- traceId: event.trace_id || null,
347
- notes: `Auto-claimed: memory.query "${entry.query}" matched task_outcome ${event.outcome || 'unknown'} in trace ${event.trace_id || 'unknown'}.`,
348
- }
349
- }
350
-
351
- const SUBSCRIPTION_DEFAULT_TTL_DAYS = 30
352
- const MAX_SUBSCRIPTIONS_PER_AGENT = 20
353
- const MAX_DIGEST_EVENTS = 100
354
-
355
- function readDigestState() {
356
- if (!fs.existsSync(digestStatePath)) return {}
357
- try {
358
- const parsed = JSON.parse(fs.readFileSync(digestStatePath, 'utf8'))
359
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
360
- } catch (_) {
361
- return {}
362
- }
363
- }
364
-
365
- function writeDigestState(state) {
366
- fs.mkdirSync(dataDir, { recursive: true })
367
- fs.writeFileSync(digestStatePath, JSON.stringify(state), 'utf8')
368
- }
369
-
370
- function subscribeKey(agent, project, eventTypes, topic) {
371
- return `${agent}|${project}|${eventTypes.join(',')}|${topic || ''}`
372
- }
373
-
374
- function loadActiveSubscriptions(agent) {
375
- const now = Date.now()
376
- return readJsonl(subscriptionPath)
377
- .filter((subscription) => subscription && subscription.agent === agent)
378
- .filter((subscription) => {
379
- if (subscription.expiresAt) {
380
- const expires = new Date(subscription.expiresAt).getTime()
381
- if (Number.isFinite(expires) && expires <= now) return false
382
- }
383
- return true
384
- })
385
- }
386
-
387
- function subscribeObserver(args = {}) {
388
- const agent = runtimeAgentId() || 'mcp'
389
- const project = args.project || projectName()
390
- const eventTypes = Array.isArray(args.eventTypes)
391
- ? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
392
- : []
393
- const topic = args.topic ? String(args.topic).trim().slice(0, 120) : ''
394
- const ttlDays = Number(args.ttlDays) || SUBSCRIPTION_DEFAULT_TTL_DAYS
395
- const ttlMs = Math.max(1, ttlDays) * 24 * 60 * 60 * 1000
396
-
397
- const subscriptions = readJsonl(subscriptionPath)
398
- const key = subscribeKey(agent, project, eventTypes, topic)
399
- let existing = subscriptions.find((subscription) => {
400
- return (
401
- subscription &&
402
- subscribeKey(
403
- subscription.agent,
404
- subscription.project,
405
- subscription.eventTypes || [],
406
- subscription.topic || '',
407
- ) === key
408
- )
409
- })
410
- const now = new Date()
411
- const expiresAt = new Date(now.getTime() + ttlMs).toISOString()
412
- if (existing) {
413
- existing.updatedAt = now.toISOString()
414
- existing.expiresAt = expiresAt
415
- } else {
416
- existing = {
417
- id: createId('sub'),
418
- agent,
419
- project,
420
- eventTypes,
421
- topic: topic || null,
422
- createdAt: now.toISOString(),
423
- updatedAt: now.toISOString(),
424
- expiresAt,
425
- }
426
- subscriptions.push(existing)
427
- }
428
- const agentSubscriptions = subscriptions.filter((subscription) => subscription.agent === agent)
429
- if (agentSubscriptions.length > MAX_SUBSCRIPTIONS_PER_AGENT) {
430
- throw new Error(`Too many subscriptions for agent ${agent} (max ${MAX_SUBSCRIPTIONS_PER_AGENT})`)
431
- }
432
- writeJsonl(subscriptionPath, subscriptions)
433
- return { subscribed: true, subscription: existing, project }
434
- }
435
-
436
- function observerDigest(args = {}) {
437
- const agent = runtimeAgentId() || 'mcp'
438
- const project = args.project || projectName()
439
- const limit = Math.min(Math.max(Number(args.limit) || 20, 1), MAX_DIGEST_EVENTS)
440
- const filterEventTypes = Array.isArray(args.eventTypes)
441
- ? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
442
- : null
443
-
444
- let subscriptions = loadActiveSubscriptions(agent)
445
- if (args.project) {
446
- subscriptions = subscriptions.filter((subscription) => subscription.project === project)
447
- }
448
- if (filterEventTypes) {
449
- subscriptions = subscriptions.filter((subscription) => {
450
- if ((subscription.eventTypes || []).length === 0) return true
451
- return filterEventTypes.some((eventType) =>
452
- (subscription.eventTypes || []).includes(eventType),
453
- )
454
- })
455
- }
456
-
457
- const state = readDigestState()
458
- const traces = readJsonl(tracePath)
459
- const digestTime = new Date().toISOString()
460
- const events = []
461
- const seenIds = new Set()
462
- const matchedSubscriptionIds = new Set()
463
-
464
- for (const subscription of subscriptions) {
465
- const cursor = state[subscription.id] || null
466
- const cursorTime = cursor ? Date.parse(cursor) : null
467
- const subscriptionEventTypes = subscription.eventTypes || []
468
- let lastEventTime = null
469
- for (const event of traces) {
470
- if (subscription.project && event.project && event.project !== subscription.project) {
471
- continue
472
- }
473
- if (
474
- subscriptionEventTypes.length > 0 &&
475
- !subscriptionEventTypes.includes(String(event.event_type || '').toLowerCase())
476
- ) {
477
- continue
478
- }
479
- if (subscription.topic) {
480
- const haystack = `${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
481
- .toLowerCase()
482
- if (!haystack.includes(subscription.topic.toLowerCase())) continue
483
- }
484
- const eventTime = event.timestamp ? Date.parse(event.timestamp) : null
485
- if (cursorTime !== null && eventTime !== null && eventTime <= cursorTime) continue
486
- if (eventTime !== null && (lastEventTime === null || eventTime > lastEventTime)) {
487
- lastEventTime = eventTime
488
- }
489
- if (seenIds.has(event.id)) continue
490
- seenIds.add(event.id)
491
- events.push({
492
- id: event.id,
493
- trace_id: event.trace_id || null,
494
- event_type: event.event_type,
495
- outcome: event.outcome || null,
496
- timestamp: event.timestamp,
497
- agent: event.agent || null,
498
- payload: event.payload || null,
499
- })
500
- matchedSubscriptionIds.add(subscription.id)
501
- if (events.length >= limit) break
502
- }
503
- if (lastEventTime !== null) {
504
- state[subscription.id] = new Date(lastEventTime).toISOString()
505
- } else if (cursor === null) {
506
- state[subscription.id] = digestTime
507
- }
508
- if (events.length >= limit) break
509
- }
510
-
511
- writeDigestState(state)
512
- return {
513
- project,
514
- agent,
515
- subscriptionCount: subscriptions.length,
516
- matchedSubscriptions: Array.from(matchedSubscriptionIds),
517
- count: events.length,
518
- events,
519
- }
520
- }
521
-
522
- function contentTokens(value) {
523
- const text = String(value || '').toLowerCase()
524
- const latin = text.match(/[a-z0-9]+/g) || []
525
- const chars = text.match(/[\u4e00-\u9fff]/g) || []
526
- const bigrams = []
527
- for (let i = 0; i < chars.length - 1; i++) {
528
- bigrams.push(`${chars[i]}${chars[i + 1]}`)
529
- }
530
- return [...latin, ...bigrams]
531
- }
532
-
533
- function jaccardSimilarity(a, b) {
534
- const setA = new Set(contentTokens(a))
535
- const setB = new Set(contentTokens(b))
536
- if (setA.size === 0 || setB.size === 0) return 0
537
- let intersection = 0
538
- for (const token of setA) {
539
- if (setB.has(token)) intersection += 1
540
- }
541
- const union = setA.size + setB.size - intersection
542
- return union === 0 ? 0 : intersection / union
543
- }
544
-
545
- function findDuplicateGroups(records, limit) {
546
- const parent = new Map()
547
- for (const record of records) parent.set(record.id, record.id)
548
- const find = (id) => {
549
- while (parent.get(id) !== id) {
550
- parent.set(id, parent.get(parent.get(id)))
551
- id = parent.get(id)
552
- }
553
- return id
554
- }
555
- const union = (a, b) => {
556
- const ra = find(a)
557
- const rb = find(b)
558
- if (ra !== rb) parent.set(ra, rb)
559
- }
560
- for (let i = 0; i < records.length; i++) {
561
- for (let j = i + 1; j < records.length; j++) {
562
- if (
563
- jaccardSimilarity(records[i].content, records[j].content) >= MEMORY_DUPLICATE_SIMILARITY
564
- ) {
565
- union(records[i].id, records[j].id)
566
- }
567
- }
568
- }
569
- const byRoot = new Map()
570
- for (const record of records) {
571
- const root = find(record.id)
572
- if (!byRoot.has(root)) byRoot.set(root, [])
573
- byRoot.get(root).push(record)
574
- }
575
- return Array.from(byRoot.values())
576
- .filter((group) => group.length >= 2)
577
- .sort((a, b) => b.length - a.length)
578
- .slice(0, limit)
579
- }
580
-
581
- function analyzeMemory(args = {}) {
582
- const project = args.project || projectName()
583
- const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
584
- const records = readJsonl(memoryPath).filter(
585
- (record) => !project || record.project === project || !record.project,
586
- )
587
- const archived = records.filter((record) => record.archived === true).length
588
- const activeRecords = records.filter((record) => record.archived !== true)
589
- const total = activeRecords.length
590
- const layerCounts = { project: 0, global: 0 }
591
- for (const record of activeRecords) {
592
- if (record.scope === 'global' || !record.project) layerCounts.global += 1
593
- else layerCounts.project += 1
594
- }
595
-
596
- const byKind = {}
597
- const lowQuality = []
598
- for (const record of activeRecords) {
599
- const kind = String(record.kind || '').trim() || 'unknown'
600
- byKind[kind] = (byKind[kind] || 0) + 1
601
- const content = String(record.content || '').trim()
602
- const issues = []
603
- if (!content) issues.push('missing content')
604
- else if (content.length < MEMORY_SHORT_CONTENT_MIN) issues.push('content too short')
605
- if (!record.kind || !String(record.kind).trim()) issues.push('missing kind')
606
- if (issues.length > 0) {
607
- lowQuality.push({
608
- id: record.id,
609
- timestamp: record.timestamp,
610
- kind: record.kind || null,
611
- content: content.slice(0, 200) || null,
612
- issues,
613
- })
614
- }
615
- }
616
-
617
- let duplicateGroups = []
618
- let duplicatesSkipped = false
619
- if (activeRecords.length > MAX_PAIRWISE_MEMORY) {
620
- duplicatesSkipped = true
621
- } else {
622
- duplicateGroups = findDuplicateGroups(activeRecords, limit).map((group) => ({
623
- size: group.length,
624
- records: group.map((record) => ({
625
- id: record.id,
626
- timestamp: record.timestamp,
627
- kind: record.kind || null,
628
- content: String(record.content || '').slice(0, 200),
629
- })),
630
- }))
631
- }
632
-
633
- const issues = {
634
- lowQuality: lowQuality.length,
635
- duplicateGroups: duplicateGroups.length,
636
- duplicatesSkipped,
637
- }
638
-
639
- const suggestions = []
640
- if (duplicateGroups.length > 0) {
641
- suggestions.push(
642
- `Found ${duplicateGroups.length} duplicate group(s); consider archiving or merging duplicates to keep recall precise.`,
643
- )
644
- }
645
- if (lowQuality.length > 0) {
646
- suggestions.push(
647
- `${lowQuality.length} low-quality record(s) (missing kind/content or too short); consider fixing or removing them.`,
648
- )
649
- }
650
- if (total === 0) {
651
- suggestions.push('No memory records for this project yet.')
652
- } else if (duplicateGroups.length === 0 && lowQuality.length === 0) {
653
- suggestions.push('Memory looks healthy; no dedup or quality fixes needed.')
654
- }
655
-
656
- return {
657
- project: project || null,
658
- total,
659
- archived,
660
- layers: layerCounts,
661
- byKind,
662
- issues,
663
- duplicates: duplicateGroups,
664
- lowQuality: lowQuality.slice(0, limit),
665
- suggestions,
666
- }
667
- }
668
-
669
- function migrateMemory(args = {}) {
670
- const idsInput = Array.isArray(args.ids) ? args.ids : []
671
- const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
672
- if (ids.length === 0) throw new Error('memory.migrate requires ids (array of memory record ids)')
673
- const targetScope = normalizeScope(args.scope)
674
- if (targetScope === 'all') throw new Error('memory.migrate scope must be project or global')
675
- const project = args.project || projectName()
676
- const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
677
-
678
- const records = readJsonl(memoryPath)
679
- const idSet = new Set(ids)
680
- const updated = []
681
- const unchanged = []
682
- const notFound = [...ids]
683
- for (const record of records) {
684
- if (!idSet.has(record.id)) continue
685
- const fromScope = record.scope || 'project'
686
- const fromProject = record.project || null
687
- if (fromScope === targetScope) {
688
- unchanged.push(record.id)
689
- } else {
690
- record.scope = targetScope
691
- record.project = targetScope === 'global' ? null : project
692
- record.migratedAt = new Date().toISOString()
693
- record.migratedFrom = fromScope === 'global' ? 'global' : fromProject || 'project'
694
- if (reason) record.migrateReason = reason
695
- updated.push(record.id)
696
- }
697
- const index = notFound.indexOf(record.id)
698
- if (index >= 0) notFound.splice(index, 1)
699
- }
700
- writeJsonl(memoryPath, records)
701
-
702
- return {
703
- project: project || null,
704
- scope: targetScope,
705
- updated,
706
- updatedCount: updated.length,
707
- unchanged,
708
- notFound,
709
- }
710
- }
711
- function archiveMemory(args = {}) {
712
- const idsInput = Array.isArray(args.ids) ? args.ids : []
713
- const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
714
- if (ids.length === 0) throw new Error('memory.archive requires ids (array of memory record ids)')
715
- const project = args.project || projectName()
716
- const restore = args.restore === true || args.restore === 'true'
717
- const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
718
-
719
- const records = readJsonl(memoryPath)
720
- const idSet = new Set(ids)
721
- const archived = []
722
- const notFound = [...ids]
723
- for (const record of records) {
724
- if (!idSet.has(record.id)) continue
725
- if (project && record.project && record.project !== project) continue
726
- if (restore) {
727
- if (record.archived === true) {
728
- delete record.archived
729
- delete record.archivedAt
730
- delete record.archiveReason
731
- archived.push(record.id)
732
- }
733
- } else if (record.archived !== true) {
734
- record.archived = true
735
- record.archivedAt = new Date().toISOString()
736
- if (reason) record.archiveReason = reason
737
- archived.push(record.id)
738
- }
739
- const index = notFound.indexOf(record.id)
740
- if (index >= 0) notFound.splice(index, 1)
741
- }
742
- writeJsonl(memoryPath, records)
743
-
744
- return {
745
- project: project || null,
746
- restored: restore,
747
- archived,
748
- archivedCount: archived.length,
749
- notFound,
750
- }
751
- }
752
-
753
- const FAILURE_OUTCOMES = new Set(['failure', 'error', 'aborted', 'retry'])
754
- const MEMORY_DUPLICATE_SIMILARITY = 0.75
755
- const MEMORY_SHORT_CONTENT_MIN = 20
756
- const MAX_PAIRWISE_MEMORY = 1500
757
-
758
- function isFailureOutcome(value) {
759
- return FAILURE_OUTCOMES.has(String(value || '').toLowerCase())
760
- }
761
-
762
- function summarizeTrace(event) {
763
- const payload = event.payload
764
- if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
765
- if (typeof payload.summary === 'string' && payload.summary.trim()) {
766
- return payload.summary.trim().slice(0, 160)
767
- }
768
- if (typeof payload.tool === 'string' && payload.tool.trim()) {
769
- return `tool: ${payload.tool.trim().slice(0, 120)}`
770
- }
771
- if (typeof payload.error === 'string' && payload.error.trim()) {
772
- return payload.error.trim().slice(0, 160)
773
- }
774
- }
775
- return String(event.event_type || 'trace').slice(0, 120)
776
- }
777
-
778
- function buildPolicyGuidance({ total, failures, risk, failureExamples, relatedMemories }) {
779
- const guidance = {
780
- level: 'none',
781
- rationale: null,
782
- saferAlternatives: [],
783
- verificationSteps: [],
784
- avoid: [],
785
- hardGate: false,
786
- }
787
- if (total === 0) {
788
- guidance.rationale =
789
- 'No historical evidence for this action yet. Treat as normal risk and record the outcome.'
790
- return guidance
791
- }
792
- const verifiedMemories = relatedMemories.filter((record) => record.evidence)
793
- const hasVerified = verifiedMemories.length > 0
794
- if (risk >= 0.4 && failures >= 2) {
795
- guidance.level = hasVerified ? 'actionable' : 'advisory'
796
- guidance.rationale =
797
- 'Repeated historical failures. Prefer an alternative approach and add rollback/verification.'
798
- guidance.avoid = failureExamples
799
- .map((example) => example.summary)
800
- .filter(Boolean)
801
- .slice(0, 3)
802
- } else if (risk >= 0.2 || hasVerified) {
803
- guidance.level = hasVerified ? 'actionable' : 'advisory'
804
- guidance.rationale = hasVerified
805
- ? 'Verified experience exists for this action; apply it before proceeding.'
806
- : 'Moderate historical risk. Add verification before proceeding.'
807
- } else {
808
- guidance.level = 'advisory'
809
- guidance.rationale = 'Low historical risk. Proceed with normal checks and record the outcome.'
810
- }
811
- if (hasVerified) {
812
- guidance.saferAlternatives = verifiedMemories.slice(0, 3).map((record) => ({
813
- memoryId: record.id,
814
- reuseCount: record.evidence.reuseCount,
815
- content: String(record.content || '').slice(0, 300),
816
- }))
817
- }
818
- guidance.verificationSteps = [
819
- 'Add an explicit verification step after execution (dry-run, rollback plan, or assertion).',
820
- 'Record the outcome with mio.observer.ingest so the evidence base improves.',
821
- ]
822
- return guidance
823
- }
824
-
825
- function policyCheck(args = {}) {
826
- const action = String(args.action || '').trim()
827
- if (!action) throw new Error('policy.check requires action')
828
- const project = args.project || projectName()
829
- const actionTokens = tokenize(action)
830
- const traces = readJsonl(tracePath).filter((event) => {
831
- if (project && event.project && event.project !== project) return false
832
- const haystack = tokenize(
833
- `${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
834
- )
835
- return actionTokens.some((token) => haystack.includes(token))
836
- })
837
- const total = traces.length
838
- const failures = traces.filter((event) => isFailureOutcome(event.outcome)).length
839
- const risk = total > 0 ? failures / total : null
840
- const riskLevel = total === 0 ? 'unknown' : risk >= 0.4 ? 'high' : risk >= 0.2 ? 'moderate' : 'low'
841
- const outcomeCounts = traces.reduce((counts, event) => {
842
- const outcome = String(event.outcome || 'unknown').toLowerCase() || 'unknown'
843
- counts[outcome] = (counts[outcome] || 0) + 1
844
- return counts
845
- }, {})
846
- const failureExamples = traces
847
- .filter((event) => isFailureOutcome(event.outcome))
848
- .sort((a, b) => new Date(b.timestamp || 0).getTime() - new Date(a.timestamp || 0).getTime())
849
- .slice(0, 3)
850
- .map((event) => ({
851
- trace_id: event.trace_id || event.id || null,
852
- event_type: event.event_type || null,
853
- outcome: event.outcome || null,
854
- timestamp: event.timestamp || null,
855
- agent: event.agent || null,
856
- summary: summarizeTrace(event),
857
- }))
858
- let suggestion
859
- if (total === 0) {
860
- suggestion = 'No history for this action. Treat as normal risk and record the outcome.'
861
- } else if (risk >= 0.4) {
862
- suggestion = 'Historically risky. Prefer an alternative approach or add rollback/verification.'
863
- } else if (risk >= 0.2) {
864
- suggestion = 'Moderate risk. Add verification before proceeding.'
865
- } else {
866
- suggestion = 'Low historical risk. Proceed with normal checks.'
867
- }
868
- const evidence = loadEvidenceWeights()
869
- const relatedMemories = readJsonl(memoryPath)
870
- .filter((record) => scoreRecord(record, action, project, evidence) > 0)
871
- .sort((a, b) => scoreRecord(b, action, project, evidence) - scoreRecord(a, action, project, evidence))
872
- .slice(0, 3)
873
- .map((record) => {
874
- const ev = evidence.get(record.id)
875
- return {
876
- id: record.id,
877
- timestamp: record.timestamp,
878
- kind: record.kind,
879
- content: record.content,
880
- tags: record.tags,
881
- evidence: ev
882
- ? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
883
- : null,
884
- }
885
- })
886
- return {
887
- action,
888
- project,
889
- total,
890
- failures,
891
- risk: risk === null ? null : Number(risk.toFixed(3)),
892
- riskLevel,
893
- outcomeCounts,
894
- failureExamples,
895
- suggestion,
896
- guidance: buildPolicyGuidance({
897
- total,
898
- failures,
899
- risk,
900
- failureExamples,
901
- relatedMemories,
902
- }),
903
- related_memories: relatedMemories,
904
- }
905
- }
906
-
907
- function recordExperienceReuse(args = {}) {
908
- const sourceAgent = String(args.sourceAgent || '').trim()
909
- const targetAgent = String(args.targetAgent || '').trim()
910
- const experienceId = String(args.experienceId || '').trim()
911
- if (!sourceAgent) throw new Error('experience.reuse requires sourceAgent')
912
- if (!targetAgent) throw new Error('experience.reuse requires targetAgent')
913
- if (!experienceId) throw new Error('experience.reuse requires experienceId')
914
-
915
- const project = args.project || projectName()
916
- const evidence = {
917
- id: createId('xfer'),
918
- timestamp: new Date().toISOString(),
919
- sourceAgent,
920
- targetAgent,
921
- experienceId,
922
- reuse: args.reuse === true || args.reuse === 'true',
923
- behaviorChanged: args.behaviorChanged === true || args.behaviorChanged === 'true',
924
- outcomeImproved: args.outcomeImproved === true || args.outcomeImproved === 'true',
925
- project,
926
- source: args.source || 'agent_report',
927
- notes: args.notes || null,
928
- }
929
- appendJsonl(experienceReusePath, evidence)
930
-
931
- const report = loadPhase0(dataDir, project)
932
- return {
933
- recorded: true,
934
- evidence,
935
- phase0: summarizePhase0(report),
936
- }
937
- }
938
-
939
- function confirmExperienceReuse(args = {}) {
940
- const id = String(args.id || '').trim()
941
- if (!id) throw new Error('experience.confirm requires id of an auto-claimed reuse record')
942
- const records = readJsonl(experienceReusePath)
943
- const index = records.findIndex((record) => record && record.id === id)
944
- if (index < 0) {
945
- throw new Error(`No experience reuse record found with id ${id}`)
946
- }
947
- const record = records[index]
948
- if (record.source !== 'auto_claim') {
949
- throw new Error('experience.confirm only applies to source=auto_claim reuse records')
950
- }
951
- const outcomeImproved =
952
- args.outcomeImproved === undefined || args.outcomeImproved === null
953
- ? record.outcomeImproved === true
954
- : args.outcomeImproved === true || args.outcomeImproved === 'true'
955
- const updated = {
956
- ...record,
957
- behaviorChanged: true,
958
- outcomeImproved,
959
- confirmed: true,
960
- confirmedAt: new Date().toISOString(),
961
- confirmedBy: String(args.confirmedBy || 'agent').trim() || 'agent',
962
- notes: args.notes ? String(args.notes).trim() : record.notes || null,
963
- }
964
- records[index] = updated
965
- writeJsonl(experienceReusePath, records)
966
-
967
- const report = loadPhase0(dataDir, updated.project)
968
- return {
969
- confirmed: true,
970
- evidence: updated,
971
- phase0: summarizePhase0(report),
972
- }
973
- }
974
-
975
- function listExperienceReuse(args = {}) {
976
- const project = args.project || projectName()
977
- const status = String(args.status || 'all').trim().toLowerCase()
978
- const targetAgent = String(args.targetAgent || '').trim().toLowerCase()
979
- const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100)
980
-
981
- let records = readJsonl(experienceReusePath)
982
- if (project) records = records.filter((record) => record.project === project)
983
- if (targetAgent) {
984
- records = records.filter(
985
- (record) => String(record.targetAgent || '').toLowerCase() === targetAgent,
986
- )
987
- }
988
- const matcher = REUSE_STATUS_FILTERS[status]
989
- if (matcher) records = records.filter(matcher)
990
- records.sort((a, b) => {
991
- const at = new Date(a.timestamp || 0).getTime()
992
- const bt = new Date(b.timestamp || 0).getTime()
993
- return bt - at
994
- })
995
-
996
- const visible = records.slice(0, limit).map((record) => ({
997
- id: record.id,
998
- source: record.source,
999
- sourceAgent: record.sourceAgent,
1000
- targetAgent: record.targetAgent,
1001
- experienceId: record.experienceId,
1002
- reuse: record.reuse === true || record.reuse === 'true',
1003
- behaviorChanged: record.behaviorChanged === true || record.behaviorChanged === 'true',
1004
- outcomeImproved: record.outcomeImproved === true || record.outcomeImproved === 'true',
1005
- confirmed: record.confirmed === true,
1006
- confirmedBy: record.confirmedBy || null,
1007
- traceId: record.traceId || null,
1008
- timestamp: record.timestamp,
1009
- project: record.project,
1010
- notes:
1011
- typeof record.notes === 'string' && record.notes ? record.notes.slice(0, 300) : null,
1012
- }))
1013
-
1014
- return {
1015
- project: project || null,
1016
- status,
1017
- count: visible.length,
1018
- total: records.length,
1019
- records: visible,
1020
- }
1021
- }
1022
-
1023
- function registerAgent(args = {}) {
1024
- const agentId = String(args.agentId || '').trim()
1025
- if (!agentId) throw new Error('agent.register requires a non-empty agentId')
1026
- const project = args.project || projectName()
1027
- const hostType = args.hostType || 'mcp'
1028
- const capabilities = Array.isArray(args.capabilities) ? args.capabilities : []
1029
- const now = new Date().toISOString()
1030
- const agents = readJsonl(agentsPath)
1031
- const existing = agents.find((a) => a.agentId === agentId && a.project === project)
1032
- let result
1033
- if (existing) {
1034
- existing.lastSeenAt = now
1035
- existing.sessionCount = (existing.sessionCount || 1) + 1
1036
- existing.hostType = hostType
1037
- if (capabilities.length > 0) existing.capabilities = capabilities
1038
- result = existing
1039
- } else {
1040
- result = {
1041
- id: createId('agent'),
1042
- agentId,
1043
- hostType,
1044
- capabilities,
1045
- registeredAt: now,
1046
- lastSeenAt: now,
1047
- sessionCount: 1,
1048
- project,
1049
- }
1050
- agents.push(result)
1051
- }
1052
- writeJsonl(agentsPath, agents)
1053
- return { id: result.id, agentId: result.agentId, registered: !existing, sessionCount: result.sessionCount, hostType: result.hostType, capabilities: result.capabilities }
1054
- }
1055
-
1056
- function listAgents(args = {}) {
1057
- const project = args.project || null
1058
- let agents = readJsonl(agentsPath)
1059
- if (project) agents = agents.filter((a) => a.project === project)
1060
- return {
1061
- count: agents.length,
1062
- agents: agents.map((a) => ({
1063
- id: a.id, agentId: a.agentId, hostType: a.hostType,
1064
- capabilities: a.capabilities, registeredAt: a.registeredAt,
1065
- lastSeenAt: a.lastSeenAt, sessionCount: a.sessionCount,
1066
- taskCount: a.taskCount || 0, successCount: a.successCount || 0, failureCount: a.failureCount || 0,
1067
- project: a.project,
1068
- })),
1069
- }
1070
- }
1071
-
1072
- function reportAgent(args = {}) {
1073
- const targetAgent = args.agentId || null
1074
- const project = args.project || projectName()
1075
- const agents = readJsonl(agentsPath)
1076
- const filtered = agents.filter((a) =>
1077
- (!targetAgent || a.agentId === targetAgent) && (!project || a.project === project)
1078
- )
1079
- const traces = readJsonl(tracePath)
1080
- const memories = readJsonl(memoryPath)
1081
- const reuses = readJsonl(experienceReusePath)
1082
- const reports = filtered.map((agent) => {
1083
- const agentTraces = traces.filter((t) => t.agent === agent.agentId)
1084
- const outcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
1085
- const successes = outcomes.filter((t) => t.outcome === 'success').length
1086
- const failures = outcomes.filter((t) => t.outcome === 'failure').length
1087
- const agentMemories = memories.filter((m) => m.source === agent.agentId)
1088
- const agentReuses = reuses.filter((r) => r.sourceAgent === agent.agentId || r.targetAgent === agent.agentId)
1089
- const verified = agentReuses.filter((r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved)
1090
- const regTasks = agent.taskCount || 0
1091
- const regSuccess = agent.successCount || 0
1092
- const regFail = agent.failureCount || 0
1093
- const taskTotal = regTasks > 0 ? regTasks : outcomes.length
1094
- const taskSuccess = regTasks > 0 ? regSuccess : successes
1095
- const taskFail = regTasks > 0 ? regFail : failures
1096
- return {
1097
- agentId: agent.agentId,
1098
- hostType: agent.hostType,
1099
- sessionCount: agent.sessionCount,
1100
- registeredAt: agent.registeredAt,
1101
- lastSeenAt: agent.lastSeenAt,
1102
- taskOutcomes: { total: taskTotal, success: taskSuccess, failure: taskFail, successRate: taskTotal > 0 ? Math.round((taskSuccess / taskTotal) * 100) : 0 },
1103
- memories: agentMemories.length,
1104
- experienceReuses: { total: agentReuses.length, verified: verified.length },
1105
- active: (Date.now() - new Date(agent.lastSeenAt).getTime()) < 24 * 60 * 60 * 1000,
1106
- }
1107
- })
1108
- return { project, count: reports.length, reports }
1109
- }
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
-
1145
- function routeTask(args = {}) {
1146
- const task = String(args.task || '').trim()
1147
- if (!task) throw new Error('task.route requires a non-empty task')
1148
- const project = args.project || projectName()
1149
- const scope = normalizeScope(args.scope)
1150
- const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 10)
1151
-
1152
- const memories = readJsonl(memoryPath).filter((record) => record.archived !== true)
1153
- const memoryById = new Map(memories.map((record) => [record.id, record]))
1154
- const evidence = loadEvidenceWeights()
1155
-
1156
- // Match verified reuse experiences whose source memory is relevant to the task.
1157
- const routeByMemory = new Map()
1158
- for (const record of readJsonl(experienceReusePath)) {
1159
- if (!REUSE_STATUS_FILTERS.verified(record)) continue
1160
- const memory = memoryById.get(record.experienceId)
1161
- if (!memory || !matchesProjectScope(memory, project, scope)) continue
1162
- const score = scoreRecord(memory, task, project)
1163
- if (score <= 0) continue
1164
- let entry = routeByMemory.get(record.experienceId)
1165
- if (!entry) {
1166
- entry = {
1167
- experienceId: record.experienceId,
1168
- memory,
1169
- weight: 0,
1170
- confirmed: false,
1171
- sourceAgents: new Set(),
1172
- targetAgents: new Set(),
1173
- reuseCount: 0,
1174
- }
1175
- routeByMemory.set(record.experienceId, entry)
1176
- }
1177
- entry.reuseCount += 1
1178
- if (record.confirmed === true) entry.confirmed = true
1179
- if (record.sourceAgent) entry.sourceAgents.add(record.sourceAgent)
1180
- if (record.targetAgent) entry.targetAgents.add(record.targetAgent)
1181
- entry.weight = Math.max(entry.weight, score + (entry.confirmed ? 2 : 1.5))
1182
- }
1183
-
1184
- const routes = [...routeByMemory.values()]
1185
- .sort((a, b) => b.weight - a.weight)
1186
- .slice(0, limit)
1187
- .map((entry) => ({
1188
- experienceId: entry.experienceId,
1189
- reuseCount: entry.reuseCount,
1190
- confirmed: entry.confirmed,
1191
- sourceAgents: [...entry.sourceAgents].filter(Boolean),
1192
- targetAgents: [...entry.targetAgents].filter(Boolean),
1193
- score: Number(entry.weight.toFixed(2)),
1194
- memory: {
1195
- id: entry.memory.id,
1196
- timestamp: entry.memory.timestamp,
1197
- kind: entry.memory.kind,
1198
- content: entry.memory.content,
1199
- tags: entry.memory.tags || [],
1200
- project: entry.memory.project || null,
1201
- scope: entry.memory.scope || 'project',
1202
- },
1203
- }))
1204
-
1205
- // Broader context: top related memories not already routed.
1206
- const routedIds = new Set(routes.map((route) => route.experienceId))
1207
- const scoreRelated = (record) =>
1208
- scoreRecord(record, task, project, evidence) +
1209
- (scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
1210
- const relatedMemories = memories
1211
- .filter((record) => !routedIds.has(record.id))
1212
- .filter((record) => matchesProjectScope(record, project, scope))
1213
- .filter((record) => scoreRelated(record) > 0)
1214
- .sort((a, b) => scoreRelated(b) - scoreRelated(a))
1215
- .slice(0, Math.max(limit, 5))
1216
- .map((record) => {
1217
- const ev = evidence.get(record.id)
1218
- return {
1219
- id: record.id,
1220
- timestamp: record.timestamp,
1221
- kind: record.kind,
1222
- content: record.content,
1223
- tags: record.tags || [],
1224
- project: record.project || null,
1225
- scope: record.scope || 'project',
1226
- evidence: ev
1227
- ? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
1228
- : null,
1229
- }
1230
- })
203
+ // policy.check reuses the memory store's tokenizer and scoring so its risk
204
+ // evidence and related-memory ranking match mio.memory.query exactly.
205
+ const policyStore = createPolicyStore({
206
+ dataDir,
207
+ projectName,
208
+ memoryStore,
209
+ tracePath,
210
+ memoryPath,
211
+ })
212
+ const { policyCheck } = policyStore
1231
213
 
1232
- // Feed the query log so a later task_outcome can auto-claim route-driven reuse.
1233
- if (routes.length > 0 || relatedMemories.length > 0) {
1234
- const now = Date.now()
1235
- const entries = loadRecentQueries()
1236
- entries.push({
1237
- agent: runtimeAgentId() || 'mcp',
1238
- project,
1239
- query: task,
1240
- resultIds: [...routedIds, ...relatedMemories.map((record) => record.id)].filter(Boolean),
1241
- resultSources: [...routes.map(() => 'experience'), ...relatedMemories.map((record) => record.source || null)],
1242
- timestamp: now,
1243
- expiresAt: now + REUSE_MATCH_WINDOW_MS,
1244
- })
1245
- persistRecentQueries(entries)
1246
- }
214
+ // agent.list / register / report delegate to the shared store so the CLI
215
+ // (mio agents list / report) and this MCP tool report identical telemetry.
216
+ const agentStore = createAgentStore({ dataDir, projectName })
217
+ const { listAgents, reportAgent, registerAgent } = agentStore
1247
218
 
1248
- let suggestion
1249
- if (routes.length > 0) {
1250
- suggestion =
1251
- `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.`
1252
- } else if (relatedMemories.length > 0) {
1253
- suggestion =
1254
- 'No verified experience matches yet. Consult the related memories, then record the outcome so this task can seed future routes.'
1255
- } else {
1256
- suggestion =
1257
- 'No matching experience or memory found. Proceed with care and record the outcome to grow the experience base.'
1258
- }
219
+ // task.route delegates to the shared store so the CLI (`mio task route`) and
220
+ // this MCP tool route identically. The MCP keeps feeding the query log
221
+ // (auto-claim depends on it); the CLI passes persistQuery: false.
222
+ const taskStore = createTaskStore({
223
+ dataDir,
224
+ projectName,
225
+ memoryStore,
226
+ queryLog,
227
+ agentId: runtimeAgentId,
228
+ })
229
+ const { routeTask, autoClaimExperienceReuse, recordTaskOutcome, ingestObservation } = taskStore
1259
230
 
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
- }
231
+ const subscriptionStore = createSubscriptionStore({
232
+ dataDir,
233
+ projectName,
234
+ agentId: runtimeAgentId,
235
+ })
236
+ const { subscribe: subscribeObserver, digest: observerDigest } = subscriptionStore
1279
237
 
1280
- return {
1281
- task,
1282
- project,
1283
- scope,
1284
- count: routes.length,
1285
- routes,
1286
- relatedMemories,
1287
- agentHealth,
1288
- summary: {
1289
- verifiedRoutes: routes.length,
1290
- relatedMemories: relatedMemories.length,
1291
- routingSignal,
1292
- suggestion,
1293
- },
1294
- }
1295
- }
1296
238
  function phase0Report(args = {}) {
1297
239
  const project = args.project || projectName()
1298
240
  const report = loadPhase0(dataDir, project)
@@ -1341,6 +283,34 @@ const TOOLS = [
1341
283
  required: ['ids'],
1342
284
  },
1343
285
  },
286
+ {
287
+ name: 'mio.memory.forget',
288
+ description: 'Permanently delete memory records by id. This is the only irreversible memory operation: archive is reversible, and forget removes the record outright. An audit entry (id, project, timestamp, reason, kind, length, excerpt) is written to memory-forget-audit.jsonl before the delete, so a removal is always reconstructable. Prefer archive unless the record must actually be gone.',
289
+ inputSchema: {
290
+ type: 'object',
291
+ properties: {
292
+ ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to permanently delete.' },
293
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
294
+ reason: { type: 'string', description: 'Why the record is being forgotten. Recorded in the audit entry.' },
295
+ },
296
+ required: ['ids'],
297
+ },
298
+ },
299
+ {
300
+ name: 'mio.memory.merge',
301
+ description: 'Merge near-duplicate memory records into one survivor: the survivor gains a supersedes list and the others are archived. Refuses to merge records whose content differs unless keep and allowDivergent are both given, because concatenating divergent bodies corrupts them. Reversible via mio.memory.archive with restore.',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to merge (at least 2).' },
306
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
307
+ keep: { type: 'string', description: 'Id of the record whose content survives. Defaults to the newest record in the set.' },
308
+ allowDivergent: { type: 'boolean', description: 'When true, allow merging records whose content differs. Requires keep so the surviving body is explicit.' },
309
+ reason: { type: 'string', description: 'Optional merge reason.' },
310
+ },
311
+ required: ['ids'],
312
+ },
313
+ },
1344
314
  {
1345
315
  name: 'mio.memory.migrate',
1346
316
  description: 'Migrate memory records between project and global layers (P3). Moving to global makes the record reusable across projects (project set to null); moving to project restores project scoping.',
@@ -1751,7 +721,7 @@ const TOOLS = [
1751
721
 
1752
722
  // ═══ Insight Self-Observation ═══
1753
723
 
1754
- ...(insightStore ? [{
724
+ ...(isInsightAvailable() ? [{
1755
725
  name: 'mio.insight.status',
1756
726
  description: 'Get insight engine status: total insights, unreported count, cooldown.',
1757
727
  inputSchema: { type: 'object', properties: {} },
@@ -1795,7 +765,7 @@ const TOOLS = [
1795
765
 
1796
766
  // ═══ Observer Research Pipeline ═══
1797
767
 
1798
- ...(ObserverStore ? [{
768
+ ...(isObserverAvailable() ? [{
1799
769
  name: 'mio.observer.world_model',
1800
770
  description: 'Query the Observer world model: entities, events, trends, narratives, uncertainties, and relations.',
1801
771
  inputSchema: {
@@ -1899,204 +869,67 @@ const TOOLS = [
1899
869
  ]
1900
870
 
1901
871
  // ═══ Insight Self-Observation Handlers ═══
872
+ // Thin delegates to ../insight-store.js: the store owns the implementation so
873
+ // `mio insight` (CLI) and mio.insight.* (MCP) cannot drift.
1902
874
 
1903
875
  function insightStatus() {
1904
- if (!insightStore) throw new Error('@akemi-mio/insight not installed')
1905
- const all = insightStore.getAll()
1906
- const unreported = insightStore.getUnreported()
1907
- return {
1908
- total: all.length,
1909
- unreported: unreported.length,
1910
- reported: all.length - unreported.length,
1911
- highValue: insightStore.getHighValueUnreported(50, 0.7).length,
1912
- }
876
+ return insightStore.status()
1913
877
  }
1914
878
 
1915
879
  function insightList(args = {}) {
1916
- if (!insightStore) throw new Error('@akemi-mio/insight not installed')
1917
- let insights = insightStore.getAll()
1918
- if (args.unreported) insights = insightStore.getUnreported()
1919
- if (args.minScore) insights = insights.filter(i => i.score >= args.minScore)
1920
- if (args.detector) insights = insights.filter(i => i.detector === args.detector)
1921
- if (args.limit) insights = insights.slice(-args.limit)
1922
- return insights
880
+ return insightStore.list(args)
1923
881
  }
1924
882
 
1925
883
  function insightMarkReported(args = {}) {
1926
- if (!insightStore) throw new Error('@akemi-mio/insight not installed')
1927
- if (!args.ids || !Array.isArray(args.ids)) throw new Error('ids array required')
1928
- for (const id of args.ids) insightStore.markReported(id)
1929
- return { marked: args.ids.length }
884
+ return insightStore.markReported(args)
1930
885
  }
1931
886
 
1932
887
  async function insightGenerate(args = {}) {
1933
- if (!InsightGenerator) throw new Error('@akemi-mio/insight not installed')
1934
- const generator = new InsightGenerator({ chatJson })
1935
- const ctx = {
1936
- memoryEntries: (args.memories || []).map(m => ({
1937
- type: m.kind || m.type || 'note',
1938
- content: m.content || '',
1939
- createdAt: m.createdAt || m.timestamp || Date.now(),
1940
- })),
1941
- summaries: args.summaries || [],
1942
- interactionCount: 0,
1943
- plans: (args.plans || []).map(p => ({
1944
- title: p.title || p.name || '',
1945
- status: p.status || 'unknown',
1946
- updatedAt: p.updatedAt || Date.now(),
1947
- steps: p.steps || [],
1948
- createdAt: p.createdAt || Date.now(),
1949
- })),
1950
- eventCount: 0,
1951
- }
1952
- const insights = await generator.generate(ctx)
1953
- if (insightStore) {
1954
- for (const i of insights) insightStore.addMany([i])
1955
- }
1956
- return { generated: insights.length, insights }
888
+ return insightStore.generate(args)
1957
889
  }
1958
890
 
1959
891
  // ═══ Observer Research Pipeline Handlers ═══
1960
-
1961
- function getObserverStore(baseDir) {
1962
- if (!ObserverStore) throw new Error('@akemi-mio/observer not installed')
1963
- return new ObserverStore(baseDir || path.join(process.cwd(), '.local', 'observer'))
1964
- }
1965
-
1966
- function getObserverBaseDir(args) {
1967
- return args.baseDir || path.join(process.cwd(), '.local', 'observer')
1968
- }
1969
-
1970
- function readJsonSafe(filePath) {
1971
- if (!fs.existsSync(filePath)) return null
1972
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8')) } catch { return null }
1973
- }
1974
-
1975
- function readJsonlSafe(filePath) {
1976
- if (!fs.existsSync(filePath)) return []
1977
- try {
1978
- return fs.readFileSync(filePath, 'utf8').trim().split('\n').filter(Boolean)
1979
- .map(l => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean)
1980
- } catch { return [] }
1981
- }
892
+ // Thin delegates to ../observer-store.js (same rationale as above).
893
+ // readJsonSafe / readJsonlSafe / getObserverBaseDir used to live here; they now
894
+ // live with the store, which is their only consumer.
1982
895
 
1983
896
  function observerWorldModel(args = {}) {
1984
- const baseDir = getObserverBaseDir(args)
1985
- const wmDir = path.join(baseDir, 'world_model')
1986
- return {
1987
- entities: readJsonSafe(path.join(wmDir, 'entities.json')) || [],
1988
- events: readJsonSafe(path.join(wmDir, 'events.json')) || [],
1989
- trends: readJsonSafe(path.join(wmDir, 'trends.json')) || [],
1990
- narratives: readJsonSafe(path.join(wmDir, 'narratives.json')) || [],
1991
- }
897
+ return observerStore.worldModel(args)
1992
898
  }
1993
899
 
1994
900
  function observerTrends(args = {}) {
1995
- const baseDir = getObserverBaseDir(args)
1996
- const trendsDir = path.join(baseDir, 'trends')
1997
- if (!fs.existsSync(trendsDir)) return []
1998
- if (args.date) {
1999
- const report = readJsonSafe(path.join(trendsDir, `${args.date}.json`))
2000
- return report ? [report] : []
2001
- }
2002
- const files = fs.readdirSync(trendsDir).filter(f => f.endsWith('.json')).sort().reverse()
2003
- return files.slice(0, args.limit || 7).map(f => readJsonSafe(path.join(trendsDir, f))).filter(Boolean)
901
+ return observerStore.trends(args)
2004
902
  }
2005
903
 
2006
904
  function observerResearch(args = {}) {
2007
- const baseDir = getObserverBaseDir(args)
2008
- const researchDir = path.join(baseDir, 'research')
2009
- if (!fs.existsSync(researchDir)) return []
2010
- const files = fs.readdirSync(researchDir).filter(f => f.endsWith('.json')).sort().reverse()
2011
- return files.slice(0, args.limit || 10).map(f => readJsonSafe(path.join(researchDir, f))).filter(Boolean)
905
+ return observerStore.research(args)
2012
906
  }
2013
907
 
2014
908
  function observerInsights(args = {}) {
2015
- const baseDir = getObserverBaseDir(args)
2016
- const insightsDir = path.join(baseDir, 'insights')
2017
- if (!fs.existsSync(insightsDir)) return []
2018
- const files = fs.readdirSync(insightsDir).filter(f => f.endsWith('.json')).sort().reverse()
2019
- return files.slice(0, args.limit || 20).map(f => readJsonSafe(path.join(insightsDir, f))).filter(Boolean)
909
+ return observerStore.insights(args)
2020
910
  }
2021
911
 
2022
912
  function observerStatus(args = {}) {
2023
- const baseDir = getObserverBaseDir(args)
2024
- const dirs = ['observations', 'trends', 'topics', 'research', 'insights', 'world_model', 'essays']
2025
- const status = {}
2026
- for (const d of dirs) {
2027
- const dir = path.join(baseDir, d)
2028
- if (fs.existsSync(dir)) {
2029
- status[d] = fs.readdirSync(dir).filter(f => !f.startsWith('.')).length
2030
- } else {
2031
- status[d] = 0
2032
- }
2033
- }
2034
- return status
913
+ return observerStore.status(args)
2035
914
  }
2036
915
 
916
+ // Both return promises (the upstream collector and fermentation engine are
917
+ // async). callTool is async, so the promise is awaited before the result is
918
+ // serialized -- but nothing here may treat them as synchronous values.
2037
919
  function observerCollect(args = {}) {
2038
- if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
2039
- const baseDir = getObserverBaseDir(args)
2040
- const service = new ObserverService({ baseDir, collectorConfig: {} })
2041
- const sources = args.sources || ['bilibili', 'hackernews', 'github', 'douyin', 'rss']
2042
- const allObs = []
2043
- for (const src of sources) {
2044
- try {
2045
- const collectFn = service.collectBySource ? service.collectBySource : service.collect
2046
- if (collectFn) {
2047
- const result = collectFn.call(service, [src], args.keywords || [], args.limit || 20)
2048
- if (Array.isArray(result)) allObs.push(...result)
2049
- else if (result && typeof result === 'object') {
2050
- for (const [key, items] of Object.entries(result)) allObs.push(...items)
2051
- }
2052
- }
2053
- } catch (e) {
2054
- allObs.push({ source: src, error: e.message })
2055
- }
2056
- }
2057
- return { collected: allObs.length, observations: allObs }
920
+ return observerStore.collect(args)
2058
921
  }
2059
922
 
2060
923
  function observerFerment(args = {}) {
2061
- if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
2062
- const baseDir = getObserverBaseDir(args)
2063
- const service = new ObserverService({ baseDir, collectorConfig: {} })
2064
- const session = args.session || 'afternoon'
2065
- try {
2066
- const fermentation = service.getFermentation()
2067
- if (fermentation && fermentation.ferment) return fermentation.ferment(session)
2068
- return { status: 'fermentation engine available but no ferment method' }
2069
- } catch (e) {
2070
- return { error: e.message }
2071
- }
924
+ return observerStore.ferment(args)
2072
925
  }
2073
926
 
2074
927
  function observerEssays(args = {}) {
2075
- const baseDir = getObserverBaseDir(args)
2076
- const type = args.type || 'published'
2077
- const essaysDir = path.join(baseDir, 'essays', type)
2078
- if (!fs.existsSync(essaysDir)) return []
2079
- const files = fs.readdirSync(essaysDir).filter(f => f.endsWith('.md')).sort().reverse()
2080
- return files.slice(0, args.limit || 10).map(f => {
2081
- const content = fs.readFileSync(path.join(essaysDir, f), 'utf8')
2082
- return { file: f, type, content, created: fs.statSync(path.join(essaysDir, f)).birthtime.toISOString() }
2083
- })
928
+ return observerStore.essays(args)
2084
929
  }
2085
930
 
2086
931
  function observerDag(args = {}) {
2087
- const baseDir = getObserverBaseDir(args)
2088
- const days = args.days || 7
2089
- const summaries = []
2090
- const todayDate = new Date()
2091
- for (let i = 0; i < days; i++) {
2092
- const d = new Date(todayDate)
2093
- d.setDate(d.getDate() - i)
2094
- const dateStr = d.toISOString().slice(0, 10)
2095
- const filePath = path.join(baseDir, 'summaries', `${dateStr}.jsonl`)
2096
- const items = readJsonlSafe(filePath)
2097
- if (items.length) summaries.push({ date: dateStr, summaries: items })
2098
- }
2099
- return { summaries, summaryCount: summaries.reduce((a, s) => a + s.summaries.length, 0) }
932
+ return observerStore.dag(args)
2100
933
  }
2101
934
 
2102
935
  async function callTool(name, args = {}) {
@@ -2107,6 +940,10 @@ async function callTool(name, args = {}) {
2107
940
  return recordMemory(args)
2108
941
  case 'mio.memory.archive':
2109
942
  return archiveMemory(args)
943
+ case 'mio.memory.forget':
944
+ return forgetMemory(args)
945
+ case 'mio.memory.merge':
946
+ return mergeMemory(args)
2110
947
  case 'mio.memory.migrate':
2111
948
  return migrateMemory(args)
2112
949
  case 'mio.observer.ingest':
@@ -2275,241 +1112,12 @@ rl.on('close', () => {
2275
1112
  })
2276
1113
 
2277
1114
 
2278
- function evolutionReport(args = {}) {
2279
- const project = args.project || projectName()
2280
- const period = String(args.period || 'all').trim()
2281
- const now = Date.now()
2282
- const periodMs = period === '24h' ? 86400000 : period === '7d' ? 604800000 : period === '30d' ? 2592000000 : Infinity
2283
- const cutoff = periodMs === Infinity ? 0 : now - periodMs
2284
-
2285
- const memories = readJsonl(memoryPath).filter((m) => !project || m.project === project || !m.project)
2286
- const traces = readJsonl(tracePath).filter((t) => !project || t.project === project)
2287
- const reuses = readJsonl(experienceReusePath).filter((r) => !project || r.project === project)
2288
- const agents = readJsonl(agentsPath).filter((a) => !project || a.project === project)
2289
- const queries = readJsonl(queryPath).filter((q) => !project || q.project === project)
2290
-
2291
- const filteredTraces = cutoff ? traces.filter((t) => new Date(t.timestamp).getTime() >= cutoff) : traces
2292
- const filteredMemories = cutoff ? memories.filter((m) => new Date(m.timestamp).getTime() >= cutoff) : memories
2293
-
2294
- // Ecosystem summary
2295
- const agentIds = new Set()
2296
- filteredTraces.forEach((t) => { if (t.agent) agentIds.add(t.agent) })
2297
- agents.forEach((a) => agentIds.add(a.agentId))
2298
- const outcomes = filteredTraces.filter((t) => t.event_type === 'task_outcome')
2299
- const successes = outcomes.filter((t) => t.outcome === 'success').length
2300
- const failures = outcomes.filter((t) => t.outcome === 'failure').length
2301
- const verifiedReuses = reuses.filter((r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved)
2302
- const pendingReuses = reuses.filter((r) => r.source === 'auto_claim' && !r.confirmed)
2303
-
2304
- const ecosystem = {
2305
- agents: agentIds.size,
2306
- totalTasks: outcomes.length,
2307
- successRate: outcomes.length > 0 ? Math.round((successes / outcomes.length) * 100) : 0,
2308
- totalMemories: filteredMemories.length,
2309
- totalReuses: reuses.length,
2310
- verifiedReuses: verifiedReuses.length,
2311
- pendingReuses: pendingReuses.length,
2312
- }
2313
-
2314
- // Agent performance comparison
2315
- const agentPerf = []
2316
- for (const agentId of agentIds) {
2317
- const agentTraces = filteredTraces.filter((t) => t.agent === agentId)
2318
- const agentOutcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
2319
- const agentSuccesses = agentOutcomes.filter((t) => t.outcome === 'success').length
2320
- const agentFailures = agentOutcomes.filter((t) => t.outcome === 'failure').length
2321
- const agentMemories = filteredMemories.filter((m) => m.source === agentId)
2322
- const agentVerified = reuses.filter((r) =>
2323
- (r.sourceAgent === agentId || r.targetAgent === agentId) && r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved
2324
- )
2325
- const reg = agents.find((a) => a.agentId === agentId)
2326
- agentPerf.push({
2327
- agentId,
2328
- tasks: {
2329
- total: agentOutcomes.length,
2330
- success: agentSuccesses,
2331
- failure: agentFailures,
2332
- successRate: agentOutcomes.length > 0 ? Math.round((agentSuccesses / agentOutcomes.length) * 100) : 0,
2333
- },
2334
- memories: agentMemories.length,
2335
- verifiedReuses: agentVerified.length,
2336
- registered: !!reg,
2337
- lastActive: reg ? reg.lastSeenAt : (agentTraces.length > 0 ? agentTraces[agentTraces.length - 1].timestamp : null),
2338
- })
2339
- }
2340
- agentPerf.sort((a, b) => b.tasks.successRate - a.tasks.successRate || b.tasks.total - a.tasks.total)
2341
-
2342
- // Cross-agent patterns
2343
- const crossPatterns = []
2344
- const reuseByPair = {}
2345
- for (const r of reuses) {
2346
- const key = r.sourceAgent + '->' + r.targetAgent
2347
- if (!reuseByPair[key]) reuseByPair[key] = { source: r.sourceAgent, target: r.targetAgent, total: 0, verified: 0, improved: 0 }
2348
- reuseByPair[key].total++
2349
- if (r.confirmed) reuseByPair[key].verified++
2350
- if (r.outcomeImproved) reuseByPair[key].improved++
2351
- }
2352
- for (const [key, pair] of Object.entries(reuseByPair)) {
2353
- if (pair.source !== pair.target) {
2354
- crossPatterns.push({
2355
- type: 'reuse_path',
2356
- from: pair.source,
2357
- to: pair.target,
2358
- total: pair.total,
2359
- verified: pair.verified,
2360
- improved: pair.improved,
2361
- })
2362
- }
2363
- }
2364
- crossPatterns.sort((a, b) => b.verified - a.verified || b.total - a.total)
2365
-
2366
- // Memory health
2367
- const byKind = {}
2368
- let shortContent = 0
2369
- let missingKind = 0
2370
- for (const m of filteredMemories) {
2371
- const kind = m.kind || 'unknown'
2372
- byKind[kind] = (byKind[kind] || 0) + 1
2373
- const content = String(m.content || '').trim()
2374
- if (content.length < 20) shortContent++
2375
- if (!m.kind) missingKind++
2376
- }
2377
- const memoryHealth = {
2378
- total: filteredMemories.length,
2379
- byKind,
2380
- shortContent,
2381
- missingKind,
2382
- qualityScore: filteredMemories.length > 0
2383
- ? Math.round(((filteredMemories.length - shortContent - missingKind) / filteredMemories.length) * 100)
2384
- : 100,
2385
- }
2386
-
2387
- // Suggestions engine
2388
- const suggestions = []
2389
-
2390
- // Routing suggestion: compare agent success rates
2391
- if (agentPerf.length >= 2) {
2392
- const best = agentPerf[0]
2393
- const worst = agentPerf[agentPerf.length - 1]
2394
- if (best.tasks.total >= 5 && worst.tasks.total >= 5 && best.tasks.successRate - worst.tasks.successRate >= 15) {
2395
- suggestions.push({
2396
- category: 'routing',
2397
- priority: 'high',
2398
- description: best.agentId + ' has ' + best.tasks.successRate + '% success rate vs ' + worst.agentId + ' at ' + worst.tasks.successRate + '%; prefer routing to ' + best.agentId,
2399
- basedOn: 'task_outcome comparison',
2400
- })
2401
- }
2402
- }
2403
-
2404
- // Pending auto-claim suggestion
2405
- if (pendingReuses.length >= 10) {
2406
- suggestions.push({
2407
- category: 'evidence',
2408
- priority: 'medium',
2409
- description: pendingReuses.length + ' pending auto-claims awaiting confirmation; review cross-agent claims to increase verified reuse count',
2410
- basedOn: 'experience_reuse.jsonl',
2411
- })
2412
- }
1115
+ const evolutionReportStore = createEvolutionReport({ dataDir, projectName })
2413
1116
 
2414
- // Memory quality suggestion
2415
- if (memoryHealth.qualityScore < 80 && filteredMemories.length > 10) {
2416
- suggestions.push({
2417
- category: 'memory',
2418
- priority: 'medium',
2419
- description: 'Memory quality score is ' + memoryHealth.qualityScore + '%; ' + shortContent + ' short records and ' + missingKind + ' missing kind fields found',
2420
- basedOn: 'memory.jsonl quality analysis',
2421
- })
2422
- }
2423
-
2424
- // Cross-agent value suggestion
2425
- if (crossPatterns.length === 0 && agentIds.size >= 2) {
2426
- suggestions.push({
2427
- category: 'interop',
2428
- priority: 'high',
2429
- description: 'Multiple agents registered but no cross-agent reuse patterns detected; ensure agents query shared memory',
2430
- basedOn: 'experience_reuse.jsonl',
2431
- })
2432
- }
2433
-
2434
- // Active agent suggestion
2435
- const inactiveAgents = agentPerf.filter((a) => {
2436
- if (!a.lastActive) return false
2437
- return (now - new Date(a.lastActive).getTime()) > 7 * 86400000
2438
- })
2439
- if (inactiveAgents.length > 0) {
2440
- suggestions.push({
2441
- category: 'coverage',
2442
- priority: 'low',
2443
- description: inactiveAgents.map((a) => a.agentId).join(', ') + ' inactive for >7 days; consider removal or reactivation',
2444
- basedOn: 'agent registry lastSeenAt',
2445
- })
2446
- }
2447
-
2448
- suggestions.sort((a, b) => {
2449
- const p = { high: 0, medium: 1, low: 2 }
2450
- return (p[a.priority] || 2) - (p[b.priority] || 2)
2451
- })
2452
-
2453
- return {
2454
- project,
2455
- generatedAt: new Date().toISOString(),
2456
- period,
2457
- ecosystem,
2458
- agentPerformance: agentPerf,
2459
- crossAgentPatterns: crossPatterns,
2460
- memoryHealth,
2461
- suggestions,
2462
- }
1117
+ function evolutionReport(args = {}) {
1118
+ return evolutionReportStore.report(args)
2463
1119
  }
2464
1120
 
2465
1121
 
2466
- function recordTaskOutcome(args = {}) {
2467
- const outcome = String(args.outcome || '').trim().toLowerCase()
2468
- if (!['success', 'failure', 'aborted'].includes(outcome)) {
2469
- throw new Error('task.record_outcome requires outcome to be success, failure, or aborted')
2470
- }
2471
- const agentId = String(args.agentId || runtimeAgentId() || 'unknown').trim()
2472
- const project = args.project || projectName()
2473
- const task = String(args.task || '').trim()
2474
- const summary = String(args.summary || '').trim()
2475
- const verification = String(args.verification || '').trim()
2476
- const traceId = String(args.traceId || '').trim() || createId('trace:' + agentId + ':' + project)
2477
-
2478
- // 1. Record trace event
2479
- const event = {
2480
- id: createId('trace'),
2481
- timestamp: new Date().toISOString(),
2482
- trace_id: traceId,
2483
- event_type: 'task_outcome',
2484
- outcome,
2485
- payload: { task, summary, verification },
2486
- agent: agentId,
2487
- host: 'mcp',
2488
- project,
2489
- }
2490
- appendJsonl(tracePath, event)
2491
-
2492
- // 2. Auto-claim experience reuse
2493
- const autoClaims = autoClaimExperienceReuse(event)
2494
-
2495
- // 3. Update agent registry
2496
- const now = new Date().toISOString()
2497
- const agents = readJsonl(agentsPath)
2498
- const agent = agents.find((a) => a.agentId === agentId && a.project === project)
2499
- if (agent) {
2500
- agent.lastSeenAt = now
2501
- agent.taskCount = (agent.taskCount || 0) + 1
2502
- agent.successCount = (agent.successCount || 0) + (outcome === 'success' ? 1 : 0)
2503
- agent.failureCount = (agent.failureCount || 0) + (outcome === 'failure' ? 1 : 0)
2504
- writeJsonl(agentsPath, agents)
2505
- }
2506
-
2507
- return {
2508
- recorded: true,
2509
- event: { id: event.id, trace_id: traceId, outcome, agent: agentId, project },
2510
- agentUpdated: !!agent,
2511
- autoClaims: autoClaims.length > 0 ? autoClaims : undefined,
2512
- }
2513
- }
2514
1122
 
2515
1123
  module.exports = { TOOLS, callTool, handleMessage, readJsonl, appendJsonl, rl }