mio-agent-runtime 0.3.15 → 0.5.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.
@@ -1,1587 +1,2023 @@
1
- #!/usr/bin/env node
2
- 'use strict'
3
-
4
- const fs = require('fs')
5
- const path = require('path')
6
- const os = require('os')
7
- const crypto = require('crypto')
8
- const readline = require('readline')
9
- const { execFileSync } = require('child_process')
10
-
11
- const SERVER_INFO = { name: 'mio-intelligence-mcp', version: '0.1.0' }
12
- const PROTOCOL_VERSION = '2024-11-05'
13
-
14
- const dataDir = path.resolve(
15
- process.env.MIO_DATA_DIR ||
16
- process.env.MIO_HOME ||
17
- path.join(os.homedir(), '.mio-intelligence')
18
- )
19
- const memoryPath = path.join(dataDir, 'memory.jsonl')
20
- const tracePath = path.join(dataDir, 'traces.jsonl')
21
- const experienceReusePath = path.join(dataDir, 'experience_reuse.jsonl')
22
- const queryPath = path.join(dataDir, 'queries.jsonl')
23
- const subscriptionPath = path.join(dataDir, 'subscriptions.jsonl')
24
- const digestStatePath = path.join(dataDir, 'digest_state.json')
25
- fs.mkdirSync(dataDir, { recursive: true })
26
-
27
- const { loadPhase0, summarizePhase0, renderPhase0Markdown } = require('./phase0')
28
-
29
- let runtimeContext = {}
30
- try {
31
- const rawContext = process.env.MIO_CONTEXT
32
- if (rawContext) {
33
- const parsed = JSON.parse(rawContext)
34
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
35
- runtimeContext = parsed
36
- }
37
- }
38
- } catch (_) {
39
- runtimeContext = {}
40
- }
41
-
42
- function runtimeAgentId() {
43
- return runtimeContext.agentId || null
44
- }
45
-
46
- // Phase 0 auto-claim: match memory.query results with a later task_outcome
47
- // from the same agent+project, so reuse evidence does not depend on the
48
- // agent self-reporting mio.experience.reuse.
49
- const REUSE_MATCH_WINDOW_MS = (() => {
50
- const minutes = Number(process.env.MIO_REUSE_MATCH_WINDOW_MIN)
51
- return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : 60 * 60 * 1000
52
- })()
53
- const MAX_RECENT_QUERIES = 200
54
- let recentQueries = []
55
-
56
- function loadRecentQueries() {
57
- const now = Date.now()
58
- const entries = readJsonl(queryPath)
59
- .filter(
60
- (entry) =>
61
- entry &&
62
- typeof entry.agent === 'string' &&
63
- entry.agent &&
64
- typeof entry.expiresAt === 'number' &&
65
- entry.expiresAt > now
66
- )
67
- .slice(-MAX_RECENT_QUERIES)
68
- recentQueries = entries
69
- return entries
70
- }
71
-
72
- function persistRecentQueries(entries) {
73
- const limited = entries.slice(-MAX_RECENT_QUERIES)
74
- fs.mkdirSync(dataDir, { recursive: true })
75
- fs.writeFileSync(
76
- queryPath,
77
- limited.length > 0 ? limited.map((entry) => JSON.stringify(entry)).join('\n') + '\n' : '',
78
- 'utf8'
79
- )
80
- recentQueries = limited
81
- return limited
82
- }
83
-
84
- function createId(prefix) {
85
- return `${prefix}_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`
86
- }
87
-
88
- function appendJsonl(file, value) {
89
- fs.appendFileSync(file, `${JSON.stringify(value)}\n`, 'utf8')
90
- }
91
-
92
- function writeJsonl(file, values) {
93
- fs.mkdirSync(path.dirname(file), { recursive: true })
94
- fs.writeFileSync(
95
- file,
96
- values.length > 0 ? values.map((value) => JSON.stringify(value)).join('\n') + '\n' : '',
97
- 'utf8'
98
- )
99
- }
100
-
101
- function readJsonl(file) {
102
- if (!fs.existsSync(file)) return []
103
- return fs
104
- .readFileSync(file, 'utf8')
105
- .split(/\r?\n/)
106
- .filter(Boolean)
107
- .map((line) => {
108
- try {
109
- return JSON.parse(line)
110
- } catch (_) {
111
- return null
112
- }
113
- })
114
- .filter(Boolean)
115
- }
116
-
117
- let lastProjectCwd = null
118
- let lastProjectName = null
119
-
120
- function projectName() {
121
- const currentCwd = process.cwd()
122
- if (lastProjectCwd === currentCwd && lastProjectName) {
123
- return lastProjectName
124
- }
125
- lastProjectCwd = currentCwd
126
- if (runtimeContext.project) {
127
- lastProjectName = runtimeContext.project
128
- return lastProjectName
129
- }
130
- try {
131
- const commonDir = execFileSync(
132
- 'git',
133
- ['rev-parse', '--path-format=absolute', '--git-common-dir'],
134
- {
135
- cwd: currentCwd,
136
- encoding: 'utf8',
137
- stdio: ['ignore', 'pipe', 'ignore'],
138
- }
139
- ).trim()
140
- if (commonDir) {
141
- const repoRoot = path.dirname(commonDir)
142
- const name = path.basename(repoRoot)
143
- if (name) {
144
- lastProjectName = name
145
- return lastProjectName
146
- }
147
- }
148
- } catch (_) {
149
- // fall through to cwd basename
150
- }
151
- lastProjectName = path.basename(currentCwd)
152
- return lastProjectName
153
- }
154
-
155
- function normalizeTags(tags) {
156
- if (Array.isArray(tags)) return tags.map((tag) => String(tag).trim()).filter(Boolean)
157
- if (typeof tags === 'string') {
158
- return tags
159
- .split(',')
160
- .map((tag) => tag.trim())
161
- .filter(Boolean)
162
- }
163
- return []
164
- }
165
-
166
- function tokenize(text) {
167
- const normalized = String(text || '').toLowerCase()
168
- const latin = normalized.match(/[a-z0-9]+/g) || []
169
- const cjk = (normalized.match(/[\u4e00-\u9fff]/g) || []).map((char) => `cjk:${char}`)
170
- return [...latin, ...cjk]
171
- }
172
-
173
- function normalizeScope(value) {
174
- const s = String(value || 'project').trim().toLowerCase()
175
- if (s === 'global' || s === 'all') return s
176
- return 'project'
177
- }
178
-
179
- function isGlobalRecord(record) {
180
- return String(record.scope || '').trim().toLowerCase() === 'global'
181
- }
182
-
183
- function matchesProjectScope(record, project, scope) {
184
- const s = normalizeScope(scope)
185
- if (s === 'global') return isGlobalRecord(record)
186
- if (s === 'all') {
187
- if (isGlobalRecord(record)) return true
188
- return !project || record.project === project
189
- }
190
- // project (default): project-scoped records matching the project only
191
- if (isGlobalRecord(record)) return false
192
- return !project || record.project === project
193
- }
194
-
195
- function latinTokens(text) {
196
- return String(text || '').toLowerCase().match(/[a-z0-9]+/g) || []
197
- }
198
-
199
- function cjkBigrams(text) {
200
- const chars = String(text || '').toLowerCase().match(/[\u4e00-\u9fff]/g) || []
201
- const bigrams = new Set()
202
- for (let i = 0; i + 1 < chars.length; i += 1) {
203
- bigrams.add(`${chars[i]}${chars[i + 1]}`)
204
- }
205
- return bigrams
206
- }
207
-
208
- function scoreRecord(record, queryText, project, evidence) {
209
- if (project && record.project && record.project !== project) return 0
210
- const content = `${record.content || ''} ${(record.tags || []).join(' ')} ${record.kind || ''}`
211
- const haystackTokens = tokenize(content)
212
- const haystackLatin = latinTokens(content)
213
- const queryTokens = tokenize(queryText)
214
- let score = 0
215
- for (const token of queryTokens) {
216
- if (haystackTokens.includes(token)) score += 2
217
- }
218
- const queryLatin = queryTokens.filter((token) => !token.startsWith('cjk:'))
219
- for (const token of queryLatin) {
220
- if (
221
- token.length >= 3 &&
222
- haystackLatin.some((item) => item.startsWith(token) || token.startsWith(item))
223
- ) {
224
- score += 1
225
- }
226
- }
227
- const queryBigrams = cjkBigrams(queryText)
228
- const haystackBigrams = cjkBigrams(content)
229
- if (queryBigrams.size > 0 && haystackBigrams.size > 0) {
230
- let matched = 0
231
- for (const bigram of queryBigrams) {
232
- if (haystackBigrams.has(bigram)) matched += 1
233
- }
234
- score += matched * 1.5
235
- }
236
- // Recency only boosts ranking among actual matches; it must not enable recall.
237
- if (score > 0 && record.timestamp) {
238
- const ageDays = Math.max(0, (Date.now() - Date.parse(record.timestamp)) / 86400000)
239
- score += Math.max(0, 0.5 - ageDays * 0.05)
240
- }
241
- // Evidence weighting (P2): memories successfully reused rank higher; never enables recall.
242
- if (score > 0 && evidence && evidence.has(record.id)) {
243
- const ev = evidence.get(record.id)
244
- score += Math.min(2, ev.reuseCount * 0.6 + (ev.confirmedCount > 0 ? 1 : 0))
245
- }
246
- return score
247
- }
248
-
249
- function matchesMemoryFilters(record, kind, tags) {
250
- if (kind) {
251
- const recordKind = String(record.kind || '').trim().toLowerCase()
252
- if (recordKind !== kind) return false
253
- }
254
- if (tags.length > 0) {
255
- const recordTags = new Set(
256
- (record.tags || []).map((tag) => String(tag).trim().toLowerCase()),
257
- )
258
- for (const tag of tags) {
259
- if (!recordTags.has(tag)) return false
260
- }
261
- }
262
- return true
263
- }
264
-
265
- function queryMemory(args = {}) {
266
- const query = String(args.query || '')
267
- const project = args.project || projectName()
268
- const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
269
- const kind = args.kind ? String(args.kind).trim().toLowerCase() : null
270
- const tags = Array.isArray(args.tags)
271
- ? args.tags.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean)
272
- : []
273
- const scope = normalizeScope(args.scope)
274
- const evidence = loadEvidenceWeights()
275
- const scoreFor = (record) =>
276
- scoreRecord(record, query, project, evidence) +
277
- (scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
278
- const results = readJsonl(memoryPath)
279
- .filter((record) => record.archived !== true)
280
- .filter((record) => matchesMemoryFilters(record, kind, tags))
281
- .filter((record) => matchesProjectScope(record, project, scope))
282
- .filter((record) => scoreFor(record) > 0)
283
- .sort((a, b) => {
284
- return scoreFor(b) - scoreFor(a)
285
- })
286
- .slice(0, limit)
287
- .map((record) => {
288
- const ev = evidence.get(record.id)
289
- if (!ev) return record
290
- return {
291
- ...record,
292
- evidence: {
293
- reuseCount: ev.reuseCount,
294
- confirmedCount: ev.confirmedCount,
295
- lastReusedAt: ev.lastReusedAt,
296
- },
297
- }
298
- })
299
- const layerCounts = { project: 0, global: 0 }
300
- for (const record of results) {
301
- if (record.scope === 'global' || !record.project) layerCounts.global += 1
302
- else layerCounts.project += 1
303
- }
304
- if (results.length > 0) {
305
- const timestamp = Date.now()
306
- const entries = loadRecentQueries()
307
- entries.push({
308
- agent: runtimeAgentId() || 'mcp',
309
- project,
310
- query,
311
- resultIds: results.map((record) => record.id).filter(Boolean),
312
- resultSources: results.map((record) => record.source || null),
313
- timestamp,
314
- expiresAt: timestamp + REUSE_MATCH_WINDOW_MS,
315
- })
316
- persistRecentQueries(entries)
317
- }
318
- return {
319
- query,
320
- project,
321
- scope,
322
- layers: layerCounts,
323
- kind: kind || null,
324
- tags: tags,
325
- count: results.length,
326
- results,
327
- }
328
- }
329
-
330
- function recordMemory(args = {}) {
331
- const content = String(args.content || '').trim()
332
- if (!content) throw new Error('memory.record requires a non-empty content')
333
- const scope = normalizeScope(args.scope)
334
- const record = {
335
- id: createId('mem'),
336
- timestamp: new Date().toISOString(),
337
- kind: args.kind || 'note',
338
- content,
339
- tags: normalizeTags(args.tags),
340
- project: scope === 'global' ? null : args.project || projectName(),
341
- scope,
342
- source: args.source || runtimeAgentId() || 'mcp',
343
- }
344
- appendJsonl(memoryPath, record)
345
- return record
346
- }
347
-
348
- function ingestObservation(args = {}) {
349
- const traceId = String(args.trace_id || '').trim()
350
- const eventType = String(args.event_type || '').trim()
351
- if (!traceId) throw new Error('observer.ingest requires trace_id')
352
- if (!eventType) throw new Error('observer.ingest requires event_type')
353
- const event = {
354
- id: createId('trace'),
355
- timestamp: new Date().toISOString(),
356
- trace_id: traceId,
357
- event_type: eventType,
358
- outcome: args.outcome || null,
359
- payload: args.payload || {},
360
- agent: args.agent || runtimeAgentId(),
361
- host: args.host || 'mcp',
362
- project: args.project || projectName(),
363
- }
364
- appendJsonl(tracePath, event)
365
- const autoClaims = autoClaimExperienceReuse(event)
366
- if (autoClaims.length > 0) {
367
- return { recorded: true, event, autoClaims }
368
- }
369
- return { recorded: true, event }
370
- }
371
-
372
- function autoClaimExperienceReuse(event) {
373
- if (String(event.event_type || '').toLowerCase() !== 'task_outcome') return []
374
- const targetAgent = String(event.agent || '').trim()
375
- if (!targetAgent) return []
376
- const now = Date.now()
377
- const claims = []
378
- const remaining = []
379
- const entries = loadRecentQueries()
380
- for (const entry of entries) {
381
- const expired = now - entry.timestamp > REUSE_MATCH_WINDOW_MS
382
- const matches =
383
- entry.agent === targetAgent && (!event.project || entry.project === event.project)
384
- if (expired || matches) {
385
- if (matches && !expired) {
386
- claims.push(buildAutoClaim(entry, event, targetAgent))
387
- }
388
- continue
389
- }
390
- remaining.push(entry)
391
- }
392
- persistRecentQueries(remaining)
393
- for (const evidence of claims) {
394
- appendJsonl(experienceReusePath, evidence)
395
- }
396
- return claims
397
- }
398
-
399
- function buildAutoClaim(entry, event, targetAgent) {
400
- const crossSourceIndex = entry.resultSources.findIndex(
401
- (source) => source && String(source).toLowerCase() !== targetAgent.toLowerCase()
402
- )
403
- const sourceAgent =
404
- crossSourceIndex >= 0
405
- ? String(entry.resultSources[crossSourceIndex])
406
- : targetAgent
407
- const experienceId =
408
- entry.resultIds[crossSourceIndex >= 0 ? crossSourceIndex : 0] ||
409
- entry.resultIds[0] ||
410
- 'unknown'
411
- const outcomeImproved = String(event.outcome || '').toLowerCase() === 'success'
412
- return {
413
- id: createId('xfer'),
414
- timestamp: new Date().toISOString(),
415
- sourceAgent,
416
- targetAgent,
417
- experienceId,
418
- reuse: true,
419
- behaviorChanged: false,
420
- outcomeImproved,
421
- project: entry.project,
422
- source: 'auto_claim',
423
- traceId: event.trace_id || null,
424
- notes: `Auto-claimed: memory.query "${entry.query}" matched task_outcome ${event.outcome || 'unknown'} in trace ${event.trace_id || 'unknown'}.`,
425
- }
426
- }
427
-
428
- const SUBSCRIPTION_DEFAULT_TTL_DAYS = 30
429
- const MAX_SUBSCRIPTIONS_PER_AGENT = 20
430
- const MAX_DIGEST_EVENTS = 100
431
-
432
- function readDigestState() {
433
- if (!fs.existsSync(digestStatePath)) return {}
434
- try {
435
- const parsed = JSON.parse(fs.readFileSync(digestStatePath, 'utf8'))
436
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
437
- } catch (_) {
438
- return {}
439
- }
440
- }
441
-
442
- function writeDigestState(state) {
443
- fs.mkdirSync(dataDir, { recursive: true })
444
- fs.writeFileSync(digestStatePath, JSON.stringify(state), 'utf8')
445
- }
446
-
447
- function subscribeKey(agent, project, eventTypes, topic) {
448
- return `${agent}|${project}|${eventTypes.join(',')}|${topic || ''}`
449
- }
450
-
451
- function loadActiveSubscriptions(agent) {
452
- const now = Date.now()
453
- return readJsonl(subscriptionPath)
454
- .filter((subscription) => subscription && subscription.agent === agent)
455
- .filter((subscription) => {
456
- if (subscription.expiresAt) {
457
- const expires = new Date(subscription.expiresAt).getTime()
458
- if (Number.isFinite(expires) && expires <= now) return false
459
- }
460
- return true
461
- })
462
- }
463
-
464
- function subscribeObserver(args = {}) {
465
- const agent = runtimeAgentId() || 'mcp'
466
- const project = args.project || projectName()
467
- const eventTypes = Array.isArray(args.eventTypes)
468
- ? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
469
- : []
470
- const topic = args.topic ? String(args.topic).trim().slice(0, 120) : ''
471
- const ttlDays = Number(args.ttlDays) || SUBSCRIPTION_DEFAULT_TTL_DAYS
472
- const ttlMs = Math.max(1, ttlDays) * 24 * 60 * 60 * 1000
473
-
474
- const subscriptions = readJsonl(subscriptionPath)
475
- const key = subscribeKey(agent, project, eventTypes, topic)
476
- let existing = subscriptions.find((subscription) => {
477
- return (
478
- subscription &&
479
- subscribeKey(
480
- subscription.agent,
481
- subscription.project,
482
- subscription.eventTypes || [],
483
- subscription.topic || '',
484
- ) === key
485
- )
486
- })
487
- const now = new Date()
488
- const expiresAt = new Date(now.getTime() + ttlMs).toISOString()
489
- if (existing) {
490
- existing.updatedAt = now.toISOString()
491
- existing.expiresAt = expiresAt
492
- } else {
493
- existing = {
494
- id: createId('sub'),
495
- agent,
496
- project,
497
- eventTypes,
498
- topic: topic || null,
499
- createdAt: now.toISOString(),
500
- updatedAt: now.toISOString(),
501
- expiresAt,
502
- }
503
- subscriptions.push(existing)
504
- }
505
- const agentSubscriptions = subscriptions.filter((subscription) => subscription.agent === agent)
506
- if (agentSubscriptions.length > MAX_SUBSCRIPTIONS_PER_AGENT) {
507
- throw new Error(`Too many subscriptions for agent ${agent} (max ${MAX_SUBSCRIPTIONS_PER_AGENT})`)
508
- }
509
- writeJsonl(subscriptionPath, subscriptions)
510
- return { subscribed: true, subscription: existing, project }
511
- }
512
-
513
- function observerDigest(args = {}) {
514
- const agent = runtimeAgentId() || 'mcp'
515
- const project = args.project || projectName()
516
- const limit = Math.min(Math.max(Number(args.limit) || 20, 1), MAX_DIGEST_EVENTS)
517
- const filterEventTypes = Array.isArray(args.eventTypes)
518
- ? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
519
- : null
520
-
521
- let subscriptions = loadActiveSubscriptions(agent)
522
- if (args.project) {
523
- subscriptions = subscriptions.filter((subscription) => subscription.project === project)
524
- }
525
- if (filterEventTypes) {
526
- subscriptions = subscriptions.filter((subscription) => {
527
- if ((subscription.eventTypes || []).length === 0) return true
528
- return filterEventTypes.some((eventType) =>
529
- (subscription.eventTypes || []).includes(eventType),
530
- )
531
- })
532
- }
533
-
534
- const state = readDigestState()
535
- const traces = readJsonl(tracePath)
536
- const digestTime = new Date().toISOString()
537
- const events = []
538
- const seenIds = new Set()
539
- const matchedSubscriptionIds = new Set()
540
-
541
- for (const subscription of subscriptions) {
542
- const cursor = state[subscription.id] || null
543
- const cursorTime = cursor ? Date.parse(cursor) : null
544
- const subscriptionEventTypes = subscription.eventTypes || []
545
- let lastEventTime = null
546
- for (const event of traces) {
547
- if (subscription.project && event.project && event.project !== subscription.project) {
548
- continue
549
- }
550
- if (
551
- subscriptionEventTypes.length > 0 &&
552
- !subscriptionEventTypes.includes(String(event.event_type || '').toLowerCase())
553
- ) {
554
- continue
555
- }
556
- if (subscription.topic) {
557
- const haystack = `${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
558
- .toLowerCase()
559
- if (!haystack.includes(subscription.topic.toLowerCase())) continue
560
- }
561
- const eventTime = event.timestamp ? Date.parse(event.timestamp) : null
562
- if (cursorTime !== null && eventTime !== null && eventTime <= cursorTime) continue
563
- if (eventTime !== null && (lastEventTime === null || eventTime > lastEventTime)) {
564
- lastEventTime = eventTime
565
- }
566
- if (seenIds.has(event.id)) continue
567
- seenIds.add(event.id)
568
- events.push({
569
- id: event.id,
570
- trace_id: event.trace_id || null,
571
- event_type: event.event_type,
572
- outcome: event.outcome || null,
573
- timestamp: event.timestamp,
574
- agent: event.agent || null,
575
- payload: event.payload || null,
576
- })
577
- matchedSubscriptionIds.add(subscription.id)
578
- if (events.length >= limit) break
579
- }
580
- if (lastEventTime !== null) {
581
- state[subscription.id] = new Date(lastEventTime).toISOString()
582
- } else if (cursor === null) {
583
- state[subscription.id] = digestTime
584
- }
585
- if (events.length >= limit) break
586
- }
587
-
588
- writeDigestState(state)
589
- return {
590
- project,
591
- agent,
592
- subscriptionCount: subscriptions.length,
593
- matchedSubscriptions: Array.from(matchedSubscriptionIds),
594
- count: events.length,
595
- events,
596
- }
597
- }
598
-
599
- function contentTokens(value) {
600
- const text = String(value || '').toLowerCase()
601
- const latin = text.match(/[a-z0-9]+/g) || []
602
- const chars = text.match(/[\u4e00-\u9fff]/g) || []
603
- const bigrams = []
604
- for (let i = 0; i < chars.length - 1; i++) {
605
- bigrams.push(`${chars[i]}${chars[i + 1]}`)
606
- }
607
- return [...latin, ...bigrams]
608
- }
609
-
610
- function jaccardSimilarity(a, b) {
611
- const setA = new Set(contentTokens(a))
612
- const setB = new Set(contentTokens(b))
613
- if (setA.size === 0 || setB.size === 0) return 0
614
- let intersection = 0
615
- for (const token of setA) {
616
- if (setB.has(token)) intersection += 1
617
- }
618
- const union = setA.size + setB.size - intersection
619
- return union === 0 ? 0 : intersection / union
620
- }
621
-
622
- function findDuplicateGroups(records, limit) {
623
- const parent = new Map()
624
- for (const record of records) parent.set(record.id, record.id)
625
- const find = (id) => {
626
- while (parent.get(id) !== id) {
627
- parent.set(id, parent.get(parent.get(id)))
628
- id = parent.get(id)
629
- }
630
- return id
631
- }
632
- const union = (a, b) => {
633
- const ra = find(a)
634
- const rb = find(b)
635
- if (ra !== rb) parent.set(ra, rb)
636
- }
637
- for (let i = 0; i < records.length; i++) {
638
- for (let j = i + 1; j < records.length; j++) {
639
- if (
640
- jaccardSimilarity(records[i].content, records[j].content) >= MEMORY_DUPLICATE_SIMILARITY
641
- ) {
642
- union(records[i].id, records[j].id)
643
- }
644
- }
645
- }
646
- const byRoot = new Map()
647
- for (const record of records) {
648
- const root = find(record.id)
649
- if (!byRoot.has(root)) byRoot.set(root, [])
650
- byRoot.get(root).push(record)
651
- }
652
- return Array.from(byRoot.values())
653
- .filter((group) => group.length >= 2)
654
- .sort((a, b) => b.length - a.length)
655
- .slice(0, limit)
656
- }
657
-
658
- function analyzeMemory(args = {}) {
659
- const project = args.project || projectName()
660
- const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
661
- const records = readJsonl(memoryPath).filter(
662
- (record) => !project || record.project === project || !record.project,
663
- )
664
- const archived = records.filter((record) => record.archived === true).length
665
- const activeRecords = records.filter((record) => record.archived !== true)
666
- const total = activeRecords.length
667
- const layerCounts = { project: 0, global: 0 }
668
- for (const record of activeRecords) {
669
- if (record.scope === 'global' || !record.project) layerCounts.global += 1
670
- else layerCounts.project += 1
671
- }
672
-
673
- const byKind = {}
674
- const lowQuality = []
675
- for (const record of activeRecords) {
676
- const kind = String(record.kind || '').trim() || 'unknown'
677
- byKind[kind] = (byKind[kind] || 0) + 1
678
- const content = String(record.content || '').trim()
679
- const issues = []
680
- if (!content) issues.push('missing content')
681
- else if (content.length < MEMORY_SHORT_CONTENT_MIN) issues.push('content too short')
682
- if (!record.kind || !String(record.kind).trim()) issues.push('missing kind')
683
- if (issues.length > 0) {
684
- lowQuality.push({
685
- id: record.id,
686
- timestamp: record.timestamp,
687
- kind: record.kind || null,
688
- content: content.slice(0, 200) || null,
689
- issues,
690
- })
691
- }
692
- }
693
-
694
- let duplicateGroups = []
695
- let duplicatesSkipped = false
696
- if (activeRecords.length > MAX_PAIRWISE_MEMORY) {
697
- duplicatesSkipped = true
698
- } else {
699
- duplicateGroups = findDuplicateGroups(activeRecords, limit).map((group) => ({
700
- size: group.length,
701
- records: group.map((record) => ({
702
- id: record.id,
703
- timestamp: record.timestamp,
704
- kind: record.kind || null,
705
- content: String(record.content || '').slice(0, 200),
706
- })),
707
- }))
708
- }
709
-
710
- const issues = {
711
- lowQuality: lowQuality.length,
712
- duplicateGroups: duplicateGroups.length,
713
- duplicatesSkipped,
714
- }
715
-
716
- const suggestions = []
717
- if (duplicateGroups.length > 0) {
718
- suggestions.push(
719
- `Found ${duplicateGroups.length} duplicate group(s); consider archiving or merging duplicates to keep recall precise.`,
720
- )
721
- }
722
- if (lowQuality.length > 0) {
723
- suggestions.push(
724
- `${lowQuality.length} low-quality record(s) (missing kind/content or too short); consider fixing or removing them.`,
725
- )
726
- }
727
- if (total === 0) {
728
- suggestions.push('No memory records for this project yet.')
729
- } else if (duplicateGroups.length === 0 && lowQuality.length === 0) {
730
- suggestions.push('Memory looks healthy; no dedup or quality fixes needed.')
731
- }
732
-
733
- return {
734
- project: project || null,
735
- total,
736
- archived,
737
- layers: layerCounts,
738
- byKind,
739
- issues,
740
- duplicates: duplicateGroups,
741
- lowQuality: lowQuality.slice(0, limit),
742
- suggestions,
743
- }
744
- }
745
-
746
- function migrateMemory(args = {}) {
747
- const idsInput = Array.isArray(args.ids) ? args.ids : []
748
- const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
749
- if (ids.length === 0) throw new Error('memory.migrate requires ids (array of memory record ids)')
750
- const targetScope = normalizeScope(args.scope)
751
- if (targetScope === 'all') throw new Error('memory.migrate scope must be project or global')
752
- const project = args.project || projectName()
753
- const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
754
-
755
- const records = readJsonl(memoryPath)
756
- const idSet = new Set(ids)
757
- const updated = []
758
- const unchanged = []
759
- const notFound = [...ids]
760
- for (const record of records) {
761
- if (!idSet.has(record.id)) continue
762
- const fromScope = record.scope || 'project'
763
- const fromProject = record.project || null
764
- if (fromScope === targetScope) {
765
- unchanged.push(record.id)
766
- } else {
767
- record.scope = targetScope
768
- record.project = targetScope === 'global' ? null : project
769
- record.migratedAt = new Date().toISOString()
770
- record.migratedFrom = fromScope === 'global' ? 'global' : fromProject || 'project'
771
- if (reason) record.migrateReason = reason
772
- updated.push(record.id)
773
- }
774
- const index = notFound.indexOf(record.id)
775
- if (index >= 0) notFound.splice(index, 1)
776
- }
777
- writeJsonl(memoryPath, records)
778
-
779
- return {
780
- project: project || null,
781
- scope: targetScope,
782
- updated,
783
- updatedCount: updated.length,
784
- unchanged,
785
- notFound,
786
- }
787
- }
788
- function archiveMemory(args = {}) {
789
- const idsInput = Array.isArray(args.ids) ? args.ids : []
790
- const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
791
- if (ids.length === 0) throw new Error('memory.archive requires ids (array of memory record ids)')
792
- const project = args.project || projectName()
793
- const restore = args.restore === true || args.restore === 'true'
794
- const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
795
-
796
- const records = readJsonl(memoryPath)
797
- const idSet = new Set(ids)
798
- const archived = []
799
- const notFound = [...ids]
800
- for (const record of records) {
801
- if (!idSet.has(record.id)) continue
802
- if (project && record.project && record.project !== project) continue
803
- if (restore) {
804
- if (record.archived === true) {
805
- delete record.archived
806
- delete record.archivedAt
807
- delete record.archiveReason
808
- archived.push(record.id)
809
- }
810
- } else if (record.archived !== true) {
811
- record.archived = true
812
- record.archivedAt = new Date().toISOString()
813
- if (reason) record.archiveReason = reason
814
- archived.push(record.id)
815
- }
816
- const index = notFound.indexOf(record.id)
817
- if (index >= 0) notFound.splice(index, 1)
818
- }
819
- writeJsonl(memoryPath, records)
820
-
821
- return {
822
- project: project || null,
823
- restored: restore,
824
- archived,
825
- archivedCount: archived.length,
826
- notFound,
827
- }
828
- }
829
-
830
- const FAILURE_OUTCOMES = new Set(['failure', 'error', 'aborted', 'retry'])
831
- const MEMORY_DUPLICATE_SIMILARITY = 0.75
832
- const MEMORY_SHORT_CONTENT_MIN = 20
833
- const MAX_PAIRWISE_MEMORY = 1500
834
-
835
- function isFailureOutcome(value) {
836
- return FAILURE_OUTCOMES.has(String(value || '').toLowerCase())
837
- }
838
-
839
- function summarizeTrace(event) {
840
- const payload = event.payload
841
- if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
842
- if (typeof payload.summary === 'string' && payload.summary.trim()) {
843
- return payload.summary.trim().slice(0, 160)
844
- }
845
- if (typeof payload.tool === 'string' && payload.tool.trim()) {
846
- return `tool: ${payload.tool.trim().slice(0, 120)}`
847
- }
848
- if (typeof payload.error === 'string' && payload.error.trim()) {
849
- return payload.error.trim().slice(0, 160)
850
- }
851
- }
852
- return String(event.event_type || 'trace').slice(0, 120)
853
- }
854
-
855
- function buildPolicyGuidance({ total, failures, risk, failureExamples, relatedMemories }) {
856
- const guidance = {
857
- level: 'none',
858
- rationale: null,
859
- saferAlternatives: [],
860
- verificationSteps: [],
861
- avoid: [],
862
- hardGate: false,
863
- }
864
- if (total === 0) {
865
- guidance.rationale =
866
- 'No historical evidence for this action yet. Treat as normal risk and record the outcome.'
867
- return guidance
868
- }
869
- const verifiedMemories = relatedMemories.filter((record) => record.evidence)
870
- const hasVerified = verifiedMemories.length > 0
871
- if (risk >= 0.4 && failures >= 2) {
872
- guidance.level = hasVerified ? 'actionable' : 'advisory'
873
- guidance.rationale =
874
- 'Repeated historical failures. Prefer an alternative approach and add rollback/verification.'
875
- guidance.avoid = failureExamples
876
- .map((example) => example.summary)
877
- .filter(Boolean)
878
- .slice(0, 3)
879
- } else if (risk >= 0.2 || hasVerified) {
880
- guidance.level = hasVerified ? 'actionable' : 'advisory'
881
- guidance.rationale = hasVerified
882
- ? 'Verified experience exists for this action; apply it before proceeding.'
883
- : 'Moderate historical risk. Add verification before proceeding.'
884
- } else {
885
- guidance.level = 'advisory'
886
- guidance.rationale = 'Low historical risk. Proceed with normal checks and record the outcome.'
887
- }
888
- if (hasVerified) {
889
- guidance.saferAlternatives = verifiedMemories.slice(0, 3).map((record) => ({
890
- memoryId: record.id,
891
- reuseCount: record.evidence.reuseCount,
892
- content: String(record.content || '').slice(0, 300),
893
- }))
894
- }
895
- guidance.verificationSteps = [
896
- 'Add an explicit verification step after execution (dry-run, rollback plan, or assertion).',
897
- 'Record the outcome with mio.observer.ingest so the evidence base improves.',
898
- ]
899
- return guidance
900
- }
901
-
902
- function policyCheck(args = {}) {
903
- const action = String(args.action || '').trim()
904
- if (!action) throw new Error('policy.check requires action')
905
- const project = args.project || projectName()
906
- const actionTokens = tokenize(action)
907
- const traces = readJsonl(tracePath).filter((event) => {
908
- if (project && event.project && event.project !== project) return false
909
- const haystack = tokenize(
910
- `${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
911
- )
912
- return actionTokens.some((token) => haystack.includes(token))
913
- })
914
- const total = traces.length
915
- const failures = traces.filter((event) => isFailureOutcome(event.outcome)).length
916
- const risk = total > 0 ? failures / total : null
917
- const riskLevel = total === 0 ? 'unknown' : risk >= 0.4 ? 'high' : risk >= 0.2 ? 'moderate' : 'low'
918
- const outcomeCounts = traces.reduce((counts, event) => {
919
- const outcome = String(event.outcome || 'unknown').toLowerCase() || 'unknown'
920
- counts[outcome] = (counts[outcome] || 0) + 1
921
- return counts
922
- }, {})
923
- const failureExamples = traces
924
- .filter((event) => isFailureOutcome(event.outcome))
925
- .sort((a, b) => new Date(b.timestamp || 0).getTime() - new Date(a.timestamp || 0).getTime())
926
- .slice(0, 3)
927
- .map((event) => ({
928
- trace_id: event.trace_id || event.id || null,
929
- event_type: event.event_type || null,
930
- outcome: event.outcome || null,
931
- timestamp: event.timestamp || null,
932
- agent: event.agent || null,
933
- summary: summarizeTrace(event),
934
- }))
935
- let suggestion
936
- if (total === 0) {
937
- suggestion = 'No history for this action. Treat as normal risk and record the outcome.'
938
- } else if (risk >= 0.4) {
939
- suggestion = 'Historically risky. Prefer an alternative approach or add rollback/verification.'
940
- } else if (risk >= 0.2) {
941
- suggestion = 'Moderate risk. Add verification before proceeding.'
942
- } else {
943
- suggestion = 'Low historical risk. Proceed with normal checks.'
944
- }
945
- const evidence = loadEvidenceWeights()
946
- const relatedMemories = readJsonl(memoryPath)
947
- .filter((record) => scoreRecord(record, action, project, evidence) > 0)
948
- .sort((a, b) => scoreRecord(b, action, project, evidence) - scoreRecord(a, action, project, evidence))
949
- .slice(0, 3)
950
- .map((record) => {
951
- const ev = evidence.get(record.id)
952
- return {
953
- id: record.id,
954
- timestamp: record.timestamp,
955
- kind: record.kind,
956
- content: record.content,
957
- tags: record.tags,
958
- evidence: ev
959
- ? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
960
- : null,
961
- }
962
- })
963
- return {
964
- action,
965
- project,
966
- total,
967
- failures,
968
- risk: risk === null ? null : Number(risk.toFixed(3)),
969
- riskLevel,
970
- outcomeCounts,
971
- failureExamples,
972
- suggestion,
973
- guidance: buildPolicyGuidance({
974
- total,
975
- failures,
976
- risk,
977
- failureExamples,
978
- relatedMemories,
979
- }),
980
- related_memories: relatedMemories,
981
- }
982
- }
983
-
984
- function recordExperienceReuse(args = {}) {
985
- const sourceAgent = String(args.sourceAgent || '').trim()
986
- const targetAgent = String(args.targetAgent || '').trim()
987
- const experienceId = String(args.experienceId || '').trim()
988
- if (!sourceAgent) throw new Error('experience.reuse requires sourceAgent')
989
- if (!targetAgent) throw new Error('experience.reuse requires targetAgent')
990
- if (!experienceId) throw new Error('experience.reuse requires experienceId')
991
-
992
- const project = args.project || projectName()
993
- const evidence = {
994
- id: createId('xfer'),
995
- timestamp: new Date().toISOString(),
996
- sourceAgent,
997
- targetAgent,
998
- experienceId,
999
- reuse: args.reuse === true || args.reuse === 'true',
1000
- behaviorChanged: args.behaviorChanged === true || args.behaviorChanged === 'true',
1001
- outcomeImproved: args.outcomeImproved === true || args.outcomeImproved === 'true',
1002
- project,
1003
- source: args.source || 'agent_report',
1004
- notes: args.notes || null,
1005
- }
1006
- appendJsonl(experienceReusePath, evidence)
1007
-
1008
- const report = loadPhase0(dataDir, project)
1009
- return {
1010
- recorded: true,
1011
- evidence,
1012
- phase0: summarizePhase0(report),
1013
- }
1014
- }
1015
-
1016
- function confirmExperienceReuse(args = {}) {
1017
- const id = String(args.id || '').trim()
1018
- if (!id) throw new Error('experience.confirm requires id of an auto-claimed reuse record')
1019
- const records = readJsonl(experienceReusePath)
1020
- const index = records.findIndex((record) => record && record.id === id)
1021
- if (index < 0) {
1022
- throw new Error(`No experience reuse record found with id ${id}`)
1023
- }
1024
- const record = records[index]
1025
- if (record.source !== 'auto_claim') {
1026
- throw new Error('experience.confirm only applies to source=auto_claim reuse records')
1027
- }
1028
- const outcomeImproved =
1029
- args.outcomeImproved === undefined || args.outcomeImproved === null
1030
- ? record.outcomeImproved === true
1031
- : args.outcomeImproved === true || args.outcomeImproved === 'true'
1032
- const updated = {
1033
- ...record,
1034
- behaviorChanged: true,
1035
- outcomeImproved,
1036
- confirmed: true,
1037
- confirmedAt: new Date().toISOString(),
1038
- confirmedBy: String(args.confirmedBy || 'agent').trim() || 'agent',
1039
- notes: args.notes ? String(args.notes).trim() : record.notes || null,
1040
- }
1041
- records[index] = updated
1042
- writeJsonl(experienceReusePath, records)
1043
-
1044
- const report = loadPhase0(dataDir, updated.project)
1045
- return {
1046
- confirmed: true,
1047
- evidence: updated,
1048
- phase0: summarizePhase0(report),
1049
- }
1050
- }
1051
-
1052
- const REUSE_STATUS_FILTERS = Object.freeze({
1053
- pending: (record) => record.source === 'auto_claim' && record.confirmed !== true,
1054
- confirmed: (record) => record.confirmed === true,
1055
- verified: (record) =>
1056
- (record.reuse === true || record.reuse === 'true') &&
1057
- (record.behaviorChanged === true || record.behaviorChanged === 'true') &&
1058
- (record.outcomeImproved === true || record.outcomeImproved === 'true'),
1059
- auto_claim: (record) => record.source === 'auto_claim',
1060
- agent_report: (record) => record.source !== 'auto_claim',
1061
- })
1062
-
1063
- function loadEvidenceWeights() {
1064
- const weights = new Map()
1065
- for (const record of readJsonl(experienceReusePath)) {
1066
- if (!REUSE_STATUS_FILTERS.verified(record)) continue
1067
- const id = record.experienceId
1068
- if (!id) continue
1069
- let entry = weights.get(id)
1070
- if (!entry) {
1071
- entry = { reuseCount: 0, confirmedCount: 0, lastReusedAt: null }
1072
- weights.set(id, entry)
1073
- }
1074
- entry.reuseCount += 1
1075
- if (record.confirmed === true) entry.confirmedCount += 1
1076
- if (!entry.lastReusedAt || new Date(record.timestamp || 0) > new Date(entry.lastReusedAt)) {
1077
- entry.lastReusedAt = record.timestamp || null
1078
- }
1079
- }
1080
- return weights
1081
- }
1082
-
1083
- function listExperienceReuse(args = {}) {
1084
- const project = args.project || projectName()
1085
- const status = String(args.status || 'all').trim().toLowerCase()
1086
- const targetAgent = String(args.targetAgent || '').trim().toLowerCase()
1087
- const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100)
1088
-
1089
- let records = readJsonl(experienceReusePath)
1090
- if (project) records = records.filter((record) => record.project === project)
1091
- if (targetAgent) {
1092
- records = records.filter(
1093
- (record) => String(record.targetAgent || '').toLowerCase() === targetAgent,
1094
- )
1095
- }
1096
- const matcher = REUSE_STATUS_FILTERS[status]
1097
- if (matcher) records = records.filter(matcher)
1098
- records.sort((a, b) => {
1099
- const at = new Date(a.timestamp || 0).getTime()
1100
- const bt = new Date(b.timestamp || 0).getTime()
1101
- return bt - at
1102
- })
1103
-
1104
- const visible = records.slice(0, limit).map((record) => ({
1105
- id: record.id,
1106
- source: record.source,
1107
- sourceAgent: record.sourceAgent,
1108
- targetAgent: record.targetAgent,
1109
- experienceId: record.experienceId,
1110
- reuse: record.reuse === true || record.reuse === 'true',
1111
- behaviorChanged: record.behaviorChanged === true || record.behaviorChanged === 'true',
1112
- outcomeImproved: record.outcomeImproved === true || record.outcomeImproved === 'true',
1113
- confirmed: record.confirmed === true,
1114
- confirmedBy: record.confirmedBy || null,
1115
- traceId: record.traceId || null,
1116
- timestamp: record.timestamp,
1117
- project: record.project,
1118
- notes:
1119
- typeof record.notes === 'string' && record.notes ? record.notes.slice(0, 300) : null,
1120
- }))
1121
-
1122
- return {
1123
- project: project || null,
1124
- status,
1125
- count: visible.length,
1126
- total: records.length,
1127
- records: visible,
1128
- }
1129
- }
1130
-
1131
- function routeTask(args = {}) {
1132
- const task = String(args.task || '').trim()
1133
- if (!task) throw new Error('task.route requires a non-empty task')
1134
- const project = args.project || projectName()
1135
- const scope = normalizeScope(args.scope)
1136
- const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 10)
1137
-
1138
- const memories = readJsonl(memoryPath).filter((record) => record.archived !== true)
1139
- const memoryById = new Map(memories.map((record) => [record.id, record]))
1140
- const evidence = loadEvidenceWeights()
1141
-
1142
- // Match verified reuse experiences whose source memory is relevant to the task.
1143
- const routeByMemory = new Map()
1144
- for (const record of readJsonl(experienceReusePath)) {
1145
- if (!REUSE_STATUS_FILTERS.verified(record)) continue
1146
- const memory = memoryById.get(record.experienceId)
1147
- if (!memory || !matchesProjectScope(memory, project, scope)) continue
1148
- const score = scoreRecord(memory, task, project)
1149
- if (score <= 0) continue
1150
- let entry = routeByMemory.get(record.experienceId)
1151
- if (!entry) {
1152
- entry = {
1153
- experienceId: record.experienceId,
1154
- memory,
1155
- weight: 0,
1156
- confirmed: false,
1157
- sourceAgents: new Set(),
1158
- targetAgents: new Set(),
1159
- reuseCount: 0,
1160
- }
1161
- routeByMemory.set(record.experienceId, entry)
1162
- }
1163
- entry.reuseCount += 1
1164
- if (record.confirmed === true) entry.confirmed = true
1165
- if (record.sourceAgent) entry.sourceAgents.add(record.sourceAgent)
1166
- if (record.targetAgent) entry.targetAgents.add(record.targetAgent)
1167
- entry.weight = Math.max(entry.weight, score + (entry.confirmed ? 2 : 1.5))
1168
- }
1169
-
1170
- const routes = [...routeByMemory.values()]
1171
- .sort((a, b) => b.weight - a.weight)
1172
- .slice(0, limit)
1173
- .map((entry) => ({
1174
- experienceId: entry.experienceId,
1175
- reuseCount: entry.reuseCount,
1176
- confirmed: entry.confirmed,
1177
- sourceAgents: [...entry.sourceAgents].filter(Boolean),
1178
- targetAgents: [...entry.targetAgents].filter(Boolean),
1179
- score: Number(entry.weight.toFixed(2)),
1180
- memory: {
1181
- id: entry.memory.id,
1182
- timestamp: entry.memory.timestamp,
1183
- kind: entry.memory.kind,
1184
- content: entry.memory.content,
1185
- tags: entry.memory.tags || [],
1186
- project: entry.memory.project || null,
1187
- scope: entry.memory.scope || 'project',
1188
- },
1189
- }))
1190
-
1191
- // Broader context: top related memories not already routed.
1192
- const routedIds = new Set(routes.map((route) => route.experienceId))
1193
- const scoreRelated = (record) =>
1194
- scoreRecord(record, task, project, evidence) +
1195
- (scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
1196
- const relatedMemories = memories
1197
- .filter((record) => !routedIds.has(record.id))
1198
- .filter((record) => matchesProjectScope(record, project, scope))
1199
- .filter((record) => scoreRelated(record) > 0)
1200
- .sort((a, b) => scoreRelated(b) - scoreRelated(a))
1201
- .slice(0, Math.max(limit, 5))
1202
- .map((record) => {
1203
- const ev = evidence.get(record.id)
1204
- return {
1205
- id: record.id,
1206
- timestamp: record.timestamp,
1207
- kind: record.kind,
1208
- content: record.content,
1209
- tags: record.tags || [],
1210
- project: record.project || null,
1211
- scope: record.scope || 'project',
1212
- evidence: ev
1213
- ? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
1214
- : null,
1215
- }
1216
- })
1217
-
1218
- // Feed the query log so a later task_outcome can auto-claim route-driven reuse.
1219
- if (routes.length > 0 || relatedMemories.length > 0) {
1220
- const now = Date.now()
1221
- const entries = loadRecentQueries()
1222
- entries.push({
1223
- agent: runtimeAgentId() || 'mcp',
1224
- project,
1225
- query: task,
1226
- resultIds: [...routedIds, ...relatedMemories.map((record) => record.id)].filter(Boolean),
1227
- resultSources: [...routes.map(() => 'experience'), ...relatedMemories.map((record) => record.source || null)],
1228
- timestamp: now,
1229
- expiresAt: now + REUSE_MATCH_WINDOW_MS,
1230
- })
1231
- persistRecentQueries(entries)
1232
- }
1233
-
1234
- let suggestion
1235
- if (routes.length > 0) {
1236
- suggestion =
1237
- `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.`
1238
- } else if (relatedMemories.length > 0) {
1239
- suggestion =
1240
- 'No verified experience matches yet. Consult the related memories, then record the outcome so this task can seed future routes.'
1241
- } else {
1242
- suggestion =
1243
- 'No matching experience or memory found. Proceed with care and record the outcome to grow the experience base.'
1244
- }
1245
-
1246
- return {
1247
- task,
1248
- project,
1249
- scope,
1250
- count: routes.length,
1251
- routes,
1252
- relatedMemories,
1253
- summary: {
1254
- verifiedRoutes: routes.length,
1255
- relatedMemories: relatedMemories.length,
1256
- suggestion,
1257
- },
1258
- }
1259
- }
1260
- function phase0Report(args = {}) {
1261
- const project = args.project || projectName()
1262
- const report = loadPhase0(dataDir, project)
1263
- const format = String(args.format || 'json').trim().toLowerCase()
1264
- if (format === 'markdown') {
1265
- return {
1266
- project,
1267
- format: 'markdown',
1268
- markdown: renderPhase0Markdown(report),
1269
- }
1270
- }
1271
- return {
1272
- project,
1273
- ...report,
1274
- }
1275
- }
1276
-
1277
- const TOOLS = [
1278
- {
1279
- name: 'mio.memory.query',
1280
- description: 'Search local Mio project memory for context, decisions, and historical problems.',
1281
- inputSchema: {
1282
- type: 'object',
1283
- properties: {
1284
- query: { type: 'string', description: 'Question or topic to recall.' },
1285
- project: { type: 'string', description: 'Project name filter. Defaults to current directory name.' },
1286
- limit: { type: 'number', description: 'Maximum results, between 1 and 20. Defaults to 5.' },
1287
- kind: { type: 'string', description: 'Only return records with this kind (decision, context, problem, note).' },
1288
- tags: { type: 'array', items: { type: 'string' }, description: 'Only return records containing all of these tags (AND).' },
1289
- scope: { type: 'string', description: 'Search scope: project (default), global, or all.' },
1290
- },
1291
- required: ['query'],
1292
- },
1293
- },
1294
- {
1295
- name: 'mio.memory.archive',
1296
- description: 'Archive (or restore) memory records by id. Archived records are excluded from memory.query and memory.analyze.',
1297
- inputSchema: {
1298
- type: 'object',
1299
- properties: {
1300
- ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to archive or restore.' },
1301
- project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1302
- restore: { type: 'boolean', description: 'When true, un-archive the given ids instead.' },
1303
- reason: { type: 'string', description: 'Optional archive reason.' },
1304
- },
1305
- required: ['ids'],
1306
- },
1307
- },
1308
- {
1309
- name: 'mio.memory.migrate',
1310
- 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.',
1311
- inputSchema: {
1312
- type: 'object',
1313
- properties: {
1314
- ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to migrate.' },
1315
- scope: { type: 'string', description: 'Target layer: global or project.' },
1316
- project: { type: 'string', description: 'Target project for project scope. Defaults to current repository.' },
1317
- reason: { type: 'string', description: 'Optional migration reason.' },
1318
- },
1319
- required: ['ids', 'scope'],
1320
- },
1321
- },
1322
- {
1323
- name: 'mio.memory.record',
1324
- description: 'Record a decision, project context, or historical problem into local Mio memory.',
1325
- inputSchema: {
1326
- type: 'object',
1327
- properties: {
1328
- content: { type: 'string', description: 'Memory content.' },
1329
- kind: { type: 'string', description: 'Memory kind, such as decision, context, or problem.' },
1330
- tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
1331
- project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
1332
- scope: { type: 'string', description: 'Record scope: project (default) or global.' },
1333
- source: { type: 'string', description: 'Recording source. Defaults to mcp.' },
1334
- },
1335
- required: ['content'],
1336
- },
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ const fs = require('fs')
5
+ const path = require('path')
6
+ const crypto = require('crypto')
7
+ const readline = require('readline')
8
+ const { execFileSync } = require('child_process')
9
+
10
+ const SERVER_INFO = { name: 'mio-intelligence-mcp', version: '0.1.0' }
11
+ const PROTOCOL_VERSION = '2024-11-05'
12
+
13
+ const dataDir = path.resolve(
14
+ process.env.MIO_DATA_DIR || path.join(process.cwd(), '.mio-intelligence')
15
+ )
16
+ const memoryPath = path.join(dataDir, 'memory.jsonl')
17
+ const tracePath = path.join(dataDir, 'traces.jsonl')
18
+ const experienceReusePath = path.join(dataDir, 'experience_reuse.jsonl')
19
+ const queryPath = path.join(dataDir, 'queries.jsonl')
20
+ const agentsPath = path.join(dataDir, 'agents.jsonl')
21
+ const subscriptionPath = path.join(dataDir, 'subscriptions.jsonl')
22
+ const digestStatePath = path.join(dataDir, 'digest_state.json')
23
+ fs.mkdirSync(dataDir, { recursive: true })
24
+
25
+ const { loadPhase0, summarizePhase0, renderPhase0Markdown } = require('./phase0')
26
+
27
+ let runtimeContext = {}
28
+ try {
29
+ const rawContext = process.env.MIO_CONTEXT
30
+ if (rawContext) {
31
+ const parsed = JSON.parse(rawContext)
32
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
33
+ runtimeContext = parsed
34
+ }
35
+ }
36
+ } catch (_) {
37
+ runtimeContext = {}
38
+ }
39
+
40
+ function runtimeAgentId() {
41
+ return runtimeContext.agentId || null
42
+ }
43
+
44
+ // Phase 0 auto-claim: match memory.query results with a later task_outcome
45
+ // from the same agent+project, so reuse evidence does not depend on the
46
+ // agent self-reporting mio.experience.reuse.
47
+ const REUSE_MATCH_WINDOW_MS = (() => {
48
+ const minutes = Number(process.env.MIO_REUSE_MATCH_WINDOW_MIN)
49
+ return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : 60 * 60 * 1000
50
+ })()
51
+ const MAX_RECENT_QUERIES = 200
52
+ let recentQueries = []
53
+
54
+ function loadRecentQueries() {
55
+ const now = Date.now()
56
+ const entries = readJsonl(queryPath)
57
+ .filter(
58
+ (entry) =>
59
+ entry &&
60
+ typeof entry.agent === 'string' &&
61
+ entry.agent &&
62
+ typeof entry.expiresAt === 'number' &&
63
+ entry.expiresAt > now
64
+ )
65
+ .slice(-MAX_RECENT_QUERIES)
66
+ recentQueries = entries
67
+ return entries
68
+ }
69
+
70
+ function persistRecentQueries(entries) {
71
+ const limited = entries.slice(-MAX_RECENT_QUERIES)
72
+ fs.mkdirSync(dataDir, { recursive: true })
73
+ fs.writeFileSync(
74
+ queryPath,
75
+ limited.length > 0 ? limited.map((entry) => JSON.stringify(entry)).join('\n') + '\n' : '',
76
+ 'utf8'
77
+ )
78
+ recentQueries = limited
79
+ return limited
80
+ }
81
+
82
+ function createId(prefix) {
83
+ return `${prefix}_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`
84
+ }
85
+
86
+ function appendJsonl(file, value) {
87
+ fs.appendFileSync(file, `${JSON.stringify(value)}\n`, 'utf8')
88
+ }
89
+
90
+ function writeJsonl(file, values) {
91
+ fs.mkdirSync(path.dirname(file), { recursive: true })
92
+ fs.writeFileSync(
93
+ file,
94
+ values.length > 0 ? values.map((value) => JSON.stringify(value)).join('\n') + '\n' : '',
95
+ 'utf8'
96
+ )
97
+ }
98
+
99
+ function readJsonl(file) {
100
+ if (!fs.existsSync(file)) return []
101
+ return fs
102
+ .readFileSync(file, 'utf8')
103
+ .split(/\r?\n/)
104
+ .filter(Boolean)
105
+ .map((line) => {
106
+ try {
107
+ return JSON.parse(line)
108
+ } catch (_) {
109
+ return null
110
+ }
111
+ })
112
+ .filter(Boolean)
113
+ }
114
+
115
+ let lastProjectCwd = null
116
+ let lastProjectName = null
117
+
118
+ function projectName() {
119
+ const currentCwd = process.cwd()
120
+ if (lastProjectCwd === currentCwd && lastProjectName) {
121
+ return lastProjectName
122
+ }
123
+ lastProjectCwd = currentCwd
124
+ if (runtimeContext.project) {
125
+ lastProjectName = runtimeContext.project
126
+ return lastProjectName
127
+ }
128
+ try {
129
+ const commonDir = execFileSync(
130
+ 'git',
131
+ ['rev-parse', '--path-format=absolute', '--git-common-dir'],
132
+ {
133
+ cwd: currentCwd,
134
+ encoding: 'utf8',
135
+ stdio: ['ignore', 'pipe', 'ignore'],
136
+ }
137
+ ).trim()
138
+ if (commonDir) {
139
+ const repoRoot = path.dirname(commonDir)
140
+ const name = path.basename(repoRoot)
141
+ if (name) {
142
+ lastProjectName = name
143
+ return lastProjectName
144
+ }
145
+ }
146
+ } catch (_) {
147
+ // fall through to cwd basename
148
+ }
149
+ lastProjectName = path.basename(currentCwd)
150
+ return lastProjectName
151
+ }
152
+
153
+ function normalizeTags(tags) {
154
+ if (Array.isArray(tags)) return tags.map((tag) => String(tag).trim()).filter(Boolean)
155
+ if (typeof tags === 'string') {
156
+ return tags
157
+ .split(',')
158
+ .map((tag) => tag.trim())
159
+ .filter(Boolean)
160
+ }
161
+ return []
162
+ }
163
+
164
+ function tokenize(text) {
165
+ const normalized = String(text || '').toLowerCase()
166
+ const latin = normalized.match(/[a-z0-9]+/g) || []
167
+ const cjk = (normalized.match(/[\u4e00-\u9fff]/g) || []).map((char) => `cjk:${char}`)
168
+ return [...latin, ...cjk]
169
+ }
170
+
171
+ function normalizeScope(value) {
172
+ const s = String(value || 'project').trim().toLowerCase()
173
+ if (s === 'global' || s === 'all') return s
174
+ return 'project'
175
+ }
176
+
177
+ function isGlobalRecord(record) {
178
+ return String(record.scope || '').trim().toLowerCase() === 'global'
179
+ }
180
+
181
+ function matchesProjectScope(record, project, scope) {
182
+ const s = normalizeScope(scope)
183
+ if (s === 'global') return isGlobalRecord(record)
184
+ if (s === 'all') {
185
+ if (isGlobalRecord(record)) return true
186
+ return !project || record.project === project
187
+ }
188
+ // project (default): project-scoped records matching the project only
189
+ if (isGlobalRecord(record)) return false
190
+ return !project || record.project === project
191
+ }
192
+
193
+ function latinTokens(text) {
194
+ return String(text || '').toLowerCase().match(/[a-z0-9]+/g) || []
195
+ }
196
+
197
+ function cjkBigrams(text) {
198
+ const chars = String(text || '').toLowerCase().match(/[\u4e00-\u9fff]/g) || []
199
+ const bigrams = new Set()
200
+ for (let i = 0; i + 1 < chars.length; i += 1) {
201
+ bigrams.add(`${chars[i]}${chars[i + 1]}`)
202
+ }
203
+ return bigrams
204
+ }
205
+
206
+ function scoreRecord(record, queryText, project, evidence) {
207
+ if (project && record.project && record.project !== project) return 0
208
+ const content = `${record.content || ''} ${(record.tags || []).join(' ')} ${record.kind || ''}`
209
+ const haystackTokens = tokenize(content)
210
+ const haystackLatin = latinTokens(content)
211
+ const queryTokens = tokenize(queryText)
212
+ let score = 0
213
+ for (const token of queryTokens) {
214
+ if (haystackTokens.includes(token)) score += 2
215
+ }
216
+ const queryLatin = queryTokens.filter((token) => !token.startsWith('cjk:'))
217
+ for (const token of queryLatin) {
218
+ if (
219
+ token.length >= 3 &&
220
+ haystackLatin.some((item) => item.startsWith(token) || token.startsWith(item))
221
+ ) {
222
+ score += 1
223
+ }
224
+ }
225
+ const queryBigrams = cjkBigrams(queryText)
226
+ const haystackBigrams = cjkBigrams(content)
227
+ if (queryBigrams.size > 0 && haystackBigrams.size > 0) {
228
+ let matched = 0
229
+ for (const bigram of queryBigrams) {
230
+ if (haystackBigrams.has(bigram)) matched += 1
231
+ }
232
+ score += matched * 1.5
233
+ }
234
+ // Recency only boosts ranking among actual matches; it must not enable recall.
235
+ if (score > 0 && record.timestamp) {
236
+ const ageDays = Math.max(0, (Date.now() - Date.parse(record.timestamp)) / 86400000)
237
+ score += Math.max(0, 0.5 - ageDays * 0.05)
238
+ }
239
+ // Evidence weighting (P2): memories successfully reused rank higher; never enables recall.
240
+ if (score > 0 && evidence && evidence.has(record.id)) {
241
+ const ev = evidence.get(record.id)
242
+ score += Math.min(2, ev.reuseCount * 0.6 + (ev.confirmedCount > 0 ? 1 : 0))
243
+ }
244
+ return score
245
+ }
246
+
247
+ function matchesMemoryFilters(record, kind, tags) {
248
+ if (kind) {
249
+ const recordKind = String(record.kind || '').trim().toLowerCase()
250
+ if (recordKind !== kind) return false
251
+ }
252
+ if (tags.length > 0) {
253
+ const recordTags = new Set(
254
+ (record.tags || []).map((tag) => String(tag).trim().toLowerCase()),
255
+ )
256
+ for (const tag of tags) {
257
+ if (!recordTags.has(tag)) return false
258
+ }
259
+ }
260
+ return true
261
+ }
262
+
263
+ function queryMemory(args = {}) {
264
+ const query = String(args.query || '')
265
+ const project = args.project || projectName()
266
+ const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
267
+ const kind = args.kind ? String(args.kind).trim().toLowerCase() : null
268
+ const tags = Array.isArray(args.tags)
269
+ ? args.tags.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean)
270
+ : []
271
+ const scope = normalizeScope(args.scope)
272
+ const evidence = loadEvidenceWeights()
273
+ const scoreFor = (record) =>
274
+ scoreRecord(record, query, project, evidence) +
275
+ (scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
276
+ const results = readJsonl(memoryPath)
277
+ .filter((record) => record.archived !== true)
278
+ .filter((record) => matchesMemoryFilters(record, kind, tags))
279
+ .filter((record) => matchesProjectScope(record, project, scope))
280
+ .filter((record) => scoreFor(record) > 0)
281
+ .sort((a, b) => {
282
+ return scoreFor(b) - scoreFor(a)
283
+ })
284
+ .slice(0, limit)
285
+ .map((record) => {
286
+ const ev = evidence.get(record.id)
287
+ if (!ev) return record
288
+ return {
289
+ ...record,
290
+ evidence: {
291
+ reuseCount: ev.reuseCount,
292
+ confirmedCount: ev.confirmedCount,
293
+ lastReusedAt: ev.lastReusedAt,
294
+ },
295
+ }
296
+ })
297
+ const layerCounts = { project: 0, global: 0 }
298
+ for (const record of results) {
299
+ if (record.scope === 'global' || !record.project) layerCounts.global += 1
300
+ else layerCounts.project += 1
301
+ }
302
+ if (results.length > 0) {
303
+ const timestamp = Date.now()
304
+ const entries = loadRecentQueries()
305
+ entries.push({
306
+ agent: runtimeAgentId() || 'mcp',
307
+ project,
308
+ query,
309
+ resultIds: results.map((record) => record.id).filter(Boolean),
310
+ resultSources: results.map((record) => record.source || null),
311
+ timestamp,
312
+ expiresAt: timestamp + REUSE_MATCH_WINDOW_MS,
313
+ })
314
+ persistRecentQueries(entries)
315
+ }
316
+ return {
317
+ query,
318
+ project,
319
+ scope,
320
+ layers: layerCounts,
321
+ kind: kind || null,
322
+ tags: tags,
323
+ count: results.length,
324
+ results,
325
+ }
326
+ }
327
+
328
+ function recordMemory(args = {}) {
329
+ const content = String(args.content || '').trim()
330
+ if (!content) throw new Error('memory.record requires a non-empty content')
331
+ const scope = normalizeScope(args.scope)
332
+ const record = {
333
+ id: createId('mem'),
334
+ timestamp: new Date().toISOString(),
335
+ kind: args.kind || 'note',
336
+ content,
337
+ tags: normalizeTags(args.tags),
338
+ project: scope === 'global' ? null : args.project || projectName(),
339
+ scope,
340
+ source: args.source || runtimeAgentId() || 'mcp',
341
+ }
342
+ appendJsonl(memoryPath, record)
343
+ return record
344
+ }
345
+
346
+ function ingestObservation(args = {}) {
347
+ const traceId = String(args.trace_id || '').trim()
348
+ const eventType = String(args.event_type || '').trim()
349
+ if (!traceId) throw new Error('observer.ingest requires trace_id')
350
+ if (!eventType) throw new Error('observer.ingest requires event_type')
351
+ const event = {
352
+ id: createId('trace'),
353
+ timestamp: new Date().toISOString(),
354
+ trace_id: traceId,
355
+ event_type: eventType,
356
+ outcome: args.outcome || null,
357
+ payload: args.payload || {},
358
+ agent: args.agent || runtimeAgentId(),
359
+ host: args.host || 'mcp',
360
+ project: args.project || projectName(),
361
+ }
362
+ appendJsonl(tracePath, event)
363
+ const autoClaims = autoClaimExperienceReuse(event)
364
+ if (autoClaims.length > 0) {
365
+ return { recorded: true, event, autoClaims }
366
+ }
367
+ return { recorded: true, event }
368
+ }
369
+
370
+ function autoClaimExperienceReuse(event) {
371
+ if (String(event.event_type || '').toLowerCase() !== 'task_outcome') return []
372
+ const targetAgent = String(event.agent || '').trim()
373
+ if (!targetAgent) return []
374
+ const now = Date.now()
375
+ const claims = []
376
+ const remaining = []
377
+ const entries = loadRecentQueries()
378
+ for (const entry of entries) {
379
+ const expired = now - entry.timestamp > REUSE_MATCH_WINDOW_MS
380
+ const matches =
381
+ entry.agent === targetAgent && (!event.project || entry.project === event.project)
382
+ if (expired || matches) {
383
+ if (matches && !expired) {
384
+ claims.push(buildAutoClaim(entry, event, targetAgent))
385
+ }
386
+ continue
387
+ }
388
+ remaining.push(entry)
389
+ }
390
+ persistRecentQueries(remaining)
391
+ for (const evidence of claims) {
392
+ appendJsonl(experienceReusePath, evidence)
393
+ }
394
+ return claims
395
+ }
396
+
397
+ function buildAutoClaim(entry, event, targetAgent) {
398
+ const crossSourceIndex = entry.resultSources.findIndex(
399
+ (source) => source && String(source).toLowerCase() !== targetAgent.toLowerCase()
400
+ )
401
+ const sourceAgent =
402
+ crossSourceIndex >= 0
403
+ ? String(entry.resultSources[crossSourceIndex])
404
+ : targetAgent
405
+ const experienceId =
406
+ entry.resultIds[crossSourceIndex >= 0 ? crossSourceIndex : 0] ||
407
+ entry.resultIds[0] ||
408
+ 'unknown'
409
+ const outcomeImproved = String(event.outcome || '').toLowerCase() === 'success'
410
+ return {
411
+ id: createId('xfer'),
412
+ timestamp: new Date().toISOString(),
413
+ sourceAgent,
414
+ targetAgent,
415
+ experienceId,
416
+ reuse: true,
417
+ behaviorChanged: false,
418
+ outcomeImproved,
419
+ project: entry.project,
420
+ source: 'auto_claim',
421
+ traceId: event.trace_id || null,
422
+ notes: `Auto-claimed: memory.query "${entry.query}" matched task_outcome ${event.outcome || 'unknown'} in trace ${event.trace_id || 'unknown'}.`,
423
+ }
424
+ }
425
+
426
+ const SUBSCRIPTION_DEFAULT_TTL_DAYS = 30
427
+ const MAX_SUBSCRIPTIONS_PER_AGENT = 20
428
+ const MAX_DIGEST_EVENTS = 100
429
+
430
+ function readDigestState() {
431
+ if (!fs.existsSync(digestStatePath)) return {}
432
+ try {
433
+ const parsed = JSON.parse(fs.readFileSync(digestStatePath, 'utf8'))
434
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
435
+ } catch (_) {
436
+ return {}
437
+ }
438
+ }
439
+
440
+ function writeDigestState(state) {
441
+ fs.mkdirSync(dataDir, { recursive: true })
442
+ fs.writeFileSync(digestStatePath, JSON.stringify(state), 'utf8')
443
+ }
444
+
445
+ function subscribeKey(agent, project, eventTypes, topic) {
446
+ return `${agent}|${project}|${eventTypes.join(',')}|${topic || ''}`
447
+ }
448
+
449
+ function loadActiveSubscriptions(agent) {
450
+ const now = Date.now()
451
+ return readJsonl(subscriptionPath)
452
+ .filter((subscription) => subscription && subscription.agent === agent)
453
+ .filter((subscription) => {
454
+ if (subscription.expiresAt) {
455
+ const expires = new Date(subscription.expiresAt).getTime()
456
+ if (Number.isFinite(expires) && expires <= now) return false
457
+ }
458
+ return true
459
+ })
460
+ }
461
+
462
+ function subscribeObserver(args = {}) {
463
+ const agent = runtimeAgentId() || 'mcp'
464
+ const project = args.project || projectName()
465
+ const eventTypes = Array.isArray(args.eventTypes)
466
+ ? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
467
+ : []
468
+ const topic = args.topic ? String(args.topic).trim().slice(0, 120) : ''
469
+ const ttlDays = Number(args.ttlDays) || SUBSCRIPTION_DEFAULT_TTL_DAYS
470
+ const ttlMs = Math.max(1, ttlDays) * 24 * 60 * 60 * 1000
471
+
472
+ const subscriptions = readJsonl(subscriptionPath)
473
+ const key = subscribeKey(agent, project, eventTypes, topic)
474
+ let existing = subscriptions.find((subscription) => {
475
+ return (
476
+ subscription &&
477
+ subscribeKey(
478
+ subscription.agent,
479
+ subscription.project,
480
+ subscription.eventTypes || [],
481
+ subscription.topic || '',
482
+ ) === key
483
+ )
484
+ })
485
+ const now = new Date()
486
+ const expiresAt = new Date(now.getTime() + ttlMs).toISOString()
487
+ if (existing) {
488
+ existing.updatedAt = now.toISOString()
489
+ existing.expiresAt = expiresAt
490
+ } else {
491
+ existing = {
492
+ id: createId('sub'),
493
+ agent,
494
+ project,
495
+ eventTypes,
496
+ topic: topic || null,
497
+ createdAt: now.toISOString(),
498
+ updatedAt: now.toISOString(),
499
+ expiresAt,
500
+ }
501
+ subscriptions.push(existing)
502
+ }
503
+ const agentSubscriptions = subscriptions.filter((subscription) => subscription.agent === agent)
504
+ if (agentSubscriptions.length > MAX_SUBSCRIPTIONS_PER_AGENT) {
505
+ throw new Error(`Too many subscriptions for agent ${agent} (max ${MAX_SUBSCRIPTIONS_PER_AGENT})`)
506
+ }
507
+ writeJsonl(subscriptionPath, subscriptions)
508
+ return { subscribed: true, subscription: existing, project }
509
+ }
510
+
511
+ function observerDigest(args = {}) {
512
+ const agent = runtimeAgentId() || 'mcp'
513
+ const project = args.project || projectName()
514
+ const limit = Math.min(Math.max(Number(args.limit) || 20, 1), MAX_DIGEST_EVENTS)
515
+ const filterEventTypes = Array.isArray(args.eventTypes)
516
+ ? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
517
+ : null
518
+
519
+ let subscriptions = loadActiveSubscriptions(agent)
520
+ if (args.project) {
521
+ subscriptions = subscriptions.filter((subscription) => subscription.project === project)
522
+ }
523
+ if (filterEventTypes) {
524
+ subscriptions = subscriptions.filter((subscription) => {
525
+ if ((subscription.eventTypes || []).length === 0) return true
526
+ return filterEventTypes.some((eventType) =>
527
+ (subscription.eventTypes || []).includes(eventType),
528
+ )
529
+ })
530
+ }
531
+
532
+ const state = readDigestState()
533
+ const traces = readJsonl(tracePath)
534
+ const digestTime = new Date().toISOString()
535
+ const events = []
536
+ const seenIds = new Set()
537
+ const matchedSubscriptionIds = new Set()
538
+
539
+ for (const subscription of subscriptions) {
540
+ const cursor = state[subscription.id] || null
541
+ const cursorTime = cursor ? Date.parse(cursor) : null
542
+ const subscriptionEventTypes = subscription.eventTypes || []
543
+ let lastEventTime = null
544
+ for (const event of traces) {
545
+ if (subscription.project && event.project && event.project !== subscription.project) {
546
+ continue
547
+ }
548
+ if (
549
+ subscriptionEventTypes.length > 0 &&
550
+ !subscriptionEventTypes.includes(String(event.event_type || '').toLowerCase())
551
+ ) {
552
+ continue
553
+ }
554
+ if (subscription.topic) {
555
+ const haystack = `${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
556
+ .toLowerCase()
557
+ if (!haystack.includes(subscription.topic.toLowerCase())) continue
558
+ }
559
+ const eventTime = event.timestamp ? Date.parse(event.timestamp) : null
560
+ if (cursorTime !== null && eventTime !== null && eventTime <= cursorTime) continue
561
+ if (eventTime !== null && (lastEventTime === null || eventTime > lastEventTime)) {
562
+ lastEventTime = eventTime
563
+ }
564
+ if (seenIds.has(event.id)) continue
565
+ seenIds.add(event.id)
566
+ events.push({
567
+ id: event.id,
568
+ trace_id: event.trace_id || null,
569
+ event_type: event.event_type,
570
+ outcome: event.outcome || null,
571
+ timestamp: event.timestamp,
572
+ agent: event.agent || null,
573
+ payload: event.payload || null,
574
+ })
575
+ matchedSubscriptionIds.add(subscription.id)
576
+ if (events.length >= limit) break
577
+ }
578
+ if (lastEventTime !== null) {
579
+ state[subscription.id] = new Date(lastEventTime).toISOString()
580
+ } else if (cursor === null) {
581
+ state[subscription.id] = digestTime
582
+ }
583
+ if (events.length >= limit) break
584
+ }
585
+
586
+ writeDigestState(state)
587
+ return {
588
+ project,
589
+ agent,
590
+ subscriptionCount: subscriptions.length,
591
+ matchedSubscriptions: Array.from(matchedSubscriptionIds),
592
+ count: events.length,
593
+ events,
594
+ }
595
+ }
596
+
597
+ function contentTokens(value) {
598
+ const text = String(value || '').toLowerCase()
599
+ const latin = text.match(/[a-z0-9]+/g) || []
600
+ const chars = text.match(/[\u4e00-\u9fff]/g) || []
601
+ const bigrams = []
602
+ for (let i = 0; i < chars.length - 1; i++) {
603
+ bigrams.push(`${chars[i]}${chars[i + 1]}`)
604
+ }
605
+ return [...latin, ...bigrams]
606
+ }
607
+
608
+ function jaccardSimilarity(a, b) {
609
+ const setA = new Set(contentTokens(a))
610
+ const setB = new Set(contentTokens(b))
611
+ if (setA.size === 0 || setB.size === 0) return 0
612
+ let intersection = 0
613
+ for (const token of setA) {
614
+ if (setB.has(token)) intersection += 1
615
+ }
616
+ const union = setA.size + setB.size - intersection
617
+ return union === 0 ? 0 : intersection / union
618
+ }
619
+
620
+ function findDuplicateGroups(records, limit) {
621
+ const parent = new Map()
622
+ for (const record of records) parent.set(record.id, record.id)
623
+ const find = (id) => {
624
+ while (parent.get(id) !== id) {
625
+ parent.set(id, parent.get(parent.get(id)))
626
+ id = parent.get(id)
627
+ }
628
+ return id
629
+ }
630
+ const union = (a, b) => {
631
+ const ra = find(a)
632
+ const rb = find(b)
633
+ if (ra !== rb) parent.set(ra, rb)
634
+ }
635
+ for (let i = 0; i < records.length; i++) {
636
+ for (let j = i + 1; j < records.length; j++) {
637
+ if (
638
+ jaccardSimilarity(records[i].content, records[j].content) >= MEMORY_DUPLICATE_SIMILARITY
639
+ ) {
640
+ union(records[i].id, records[j].id)
641
+ }
642
+ }
643
+ }
644
+ const byRoot = new Map()
645
+ for (const record of records) {
646
+ const root = find(record.id)
647
+ if (!byRoot.has(root)) byRoot.set(root, [])
648
+ byRoot.get(root).push(record)
649
+ }
650
+ return Array.from(byRoot.values())
651
+ .filter((group) => group.length >= 2)
652
+ .sort((a, b) => b.length - a.length)
653
+ .slice(0, limit)
654
+ }
655
+
656
+ function analyzeMemory(args = {}) {
657
+ const project = args.project || projectName()
658
+ const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
659
+ const records = readJsonl(memoryPath).filter(
660
+ (record) => !project || record.project === project || !record.project,
661
+ )
662
+ const archived = records.filter((record) => record.archived === true).length
663
+ const activeRecords = records.filter((record) => record.archived !== true)
664
+ const total = activeRecords.length
665
+ const layerCounts = { project: 0, global: 0 }
666
+ for (const record of activeRecords) {
667
+ if (record.scope === 'global' || !record.project) layerCounts.global += 1
668
+ else layerCounts.project += 1
669
+ }
670
+
671
+ const byKind = {}
672
+ const lowQuality = []
673
+ for (const record of activeRecords) {
674
+ const kind = String(record.kind || '').trim() || 'unknown'
675
+ byKind[kind] = (byKind[kind] || 0) + 1
676
+ const content = String(record.content || '').trim()
677
+ const issues = []
678
+ if (!content) issues.push('missing content')
679
+ else if (content.length < MEMORY_SHORT_CONTENT_MIN) issues.push('content too short')
680
+ if (!record.kind || !String(record.kind).trim()) issues.push('missing kind')
681
+ if (issues.length > 0) {
682
+ lowQuality.push({
683
+ id: record.id,
684
+ timestamp: record.timestamp,
685
+ kind: record.kind || null,
686
+ content: content.slice(0, 200) || null,
687
+ issues,
688
+ })
689
+ }
690
+ }
691
+
692
+ let duplicateGroups = []
693
+ let duplicatesSkipped = false
694
+ if (activeRecords.length > MAX_PAIRWISE_MEMORY) {
695
+ duplicatesSkipped = true
696
+ } else {
697
+ duplicateGroups = findDuplicateGroups(activeRecords, limit).map((group) => ({
698
+ size: group.length,
699
+ records: group.map((record) => ({
700
+ id: record.id,
701
+ timestamp: record.timestamp,
702
+ kind: record.kind || null,
703
+ content: String(record.content || '').slice(0, 200),
704
+ })),
705
+ }))
706
+ }
707
+
708
+ const issues = {
709
+ lowQuality: lowQuality.length,
710
+ duplicateGroups: duplicateGroups.length,
711
+ duplicatesSkipped,
712
+ }
713
+
714
+ const suggestions = []
715
+ if (duplicateGroups.length > 0) {
716
+ suggestions.push(
717
+ `Found ${duplicateGroups.length} duplicate group(s); consider archiving or merging duplicates to keep recall precise.`,
718
+ )
719
+ }
720
+ if (lowQuality.length > 0) {
721
+ suggestions.push(
722
+ `${lowQuality.length} low-quality record(s) (missing kind/content or too short); consider fixing or removing them.`,
723
+ )
724
+ }
725
+ if (total === 0) {
726
+ suggestions.push('No memory records for this project yet.')
727
+ } else if (duplicateGroups.length === 0 && lowQuality.length === 0) {
728
+ suggestions.push('Memory looks healthy; no dedup or quality fixes needed.')
729
+ }
730
+
731
+ return {
732
+ project: project || null,
733
+ total,
734
+ archived,
735
+ layers: layerCounts,
736
+ byKind,
737
+ issues,
738
+ duplicates: duplicateGroups,
739
+ lowQuality: lowQuality.slice(0, limit),
740
+ suggestions,
741
+ }
742
+ }
743
+
744
+ function migrateMemory(args = {}) {
745
+ const idsInput = Array.isArray(args.ids) ? args.ids : []
746
+ const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
747
+ if (ids.length === 0) throw new Error('memory.migrate requires ids (array of memory record ids)')
748
+ const targetScope = normalizeScope(args.scope)
749
+ if (targetScope === 'all') throw new Error('memory.migrate scope must be project or global')
750
+ const project = args.project || projectName()
751
+ const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
752
+
753
+ const records = readJsonl(memoryPath)
754
+ const idSet = new Set(ids)
755
+ const updated = []
756
+ const unchanged = []
757
+ const notFound = [...ids]
758
+ for (const record of records) {
759
+ if (!idSet.has(record.id)) continue
760
+ const fromScope = record.scope || 'project'
761
+ const fromProject = record.project || null
762
+ if (fromScope === targetScope) {
763
+ unchanged.push(record.id)
764
+ } else {
765
+ record.scope = targetScope
766
+ record.project = targetScope === 'global' ? null : project
767
+ record.migratedAt = new Date().toISOString()
768
+ record.migratedFrom = fromScope === 'global' ? 'global' : fromProject || 'project'
769
+ if (reason) record.migrateReason = reason
770
+ updated.push(record.id)
771
+ }
772
+ const index = notFound.indexOf(record.id)
773
+ if (index >= 0) notFound.splice(index, 1)
774
+ }
775
+ writeJsonl(memoryPath, records)
776
+
777
+ return {
778
+ project: project || null,
779
+ scope: targetScope,
780
+ updated,
781
+ updatedCount: updated.length,
782
+ unchanged,
783
+ notFound,
784
+ }
785
+ }
786
+ function archiveMemory(args = {}) {
787
+ const idsInput = Array.isArray(args.ids) ? args.ids : []
788
+ const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
789
+ if (ids.length === 0) throw new Error('memory.archive requires ids (array of memory record ids)')
790
+ const project = args.project || projectName()
791
+ const restore = args.restore === true || args.restore === 'true'
792
+ const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
793
+
794
+ const records = readJsonl(memoryPath)
795
+ const idSet = new Set(ids)
796
+ const archived = []
797
+ const notFound = [...ids]
798
+ for (const record of records) {
799
+ if (!idSet.has(record.id)) continue
800
+ if (project && record.project && record.project !== project) continue
801
+ if (restore) {
802
+ if (record.archived === true) {
803
+ delete record.archived
804
+ delete record.archivedAt
805
+ delete record.archiveReason
806
+ archived.push(record.id)
807
+ }
808
+ } else if (record.archived !== true) {
809
+ record.archived = true
810
+ record.archivedAt = new Date().toISOString()
811
+ if (reason) record.archiveReason = reason
812
+ archived.push(record.id)
813
+ }
814
+ const index = notFound.indexOf(record.id)
815
+ if (index >= 0) notFound.splice(index, 1)
816
+ }
817
+ writeJsonl(memoryPath, records)
818
+
819
+ return {
820
+ project: project || null,
821
+ restored: restore,
822
+ archived,
823
+ archivedCount: archived.length,
824
+ notFound,
825
+ }
826
+ }
827
+
828
+ const FAILURE_OUTCOMES = new Set(['failure', 'error', 'aborted', 'retry'])
829
+ const MEMORY_DUPLICATE_SIMILARITY = 0.75
830
+ const MEMORY_SHORT_CONTENT_MIN = 20
831
+ const MAX_PAIRWISE_MEMORY = 1500
832
+
833
+ function isFailureOutcome(value) {
834
+ return FAILURE_OUTCOMES.has(String(value || '').toLowerCase())
835
+ }
836
+
837
+ function summarizeTrace(event) {
838
+ const payload = event.payload
839
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
840
+ if (typeof payload.summary === 'string' && payload.summary.trim()) {
841
+ return payload.summary.trim().slice(0, 160)
842
+ }
843
+ if (typeof payload.tool === 'string' && payload.tool.trim()) {
844
+ return `tool: ${payload.tool.trim().slice(0, 120)}`
845
+ }
846
+ if (typeof payload.error === 'string' && payload.error.trim()) {
847
+ return payload.error.trim().slice(0, 160)
848
+ }
849
+ }
850
+ return String(event.event_type || 'trace').slice(0, 120)
851
+ }
852
+
853
+ function buildPolicyGuidance({ total, failures, risk, failureExamples, relatedMemories }) {
854
+ const guidance = {
855
+ level: 'none',
856
+ rationale: null,
857
+ saferAlternatives: [],
858
+ verificationSteps: [],
859
+ avoid: [],
860
+ hardGate: false,
861
+ }
862
+ if (total === 0) {
863
+ guidance.rationale =
864
+ 'No historical evidence for this action yet. Treat as normal risk and record the outcome.'
865
+ return guidance
866
+ }
867
+ const verifiedMemories = relatedMemories.filter((record) => record.evidence)
868
+ const hasVerified = verifiedMemories.length > 0
869
+ if (risk >= 0.4 && failures >= 2) {
870
+ guidance.level = hasVerified ? 'actionable' : 'advisory'
871
+ guidance.rationale =
872
+ 'Repeated historical failures. Prefer an alternative approach and add rollback/verification.'
873
+ guidance.avoid = failureExamples
874
+ .map((example) => example.summary)
875
+ .filter(Boolean)
876
+ .slice(0, 3)
877
+ } else if (risk >= 0.2 || hasVerified) {
878
+ guidance.level = hasVerified ? 'actionable' : 'advisory'
879
+ guidance.rationale = hasVerified
880
+ ? 'Verified experience exists for this action; apply it before proceeding.'
881
+ : 'Moderate historical risk. Add verification before proceeding.'
882
+ } else {
883
+ guidance.level = 'advisory'
884
+ guidance.rationale = 'Low historical risk. Proceed with normal checks and record the outcome.'
885
+ }
886
+ if (hasVerified) {
887
+ guidance.saferAlternatives = verifiedMemories.slice(0, 3).map((record) => ({
888
+ memoryId: record.id,
889
+ reuseCount: record.evidence.reuseCount,
890
+ content: String(record.content || '').slice(0, 300),
891
+ }))
892
+ }
893
+ guidance.verificationSteps = [
894
+ 'Add an explicit verification step after execution (dry-run, rollback plan, or assertion).',
895
+ 'Record the outcome with mio.observer.ingest so the evidence base improves.',
896
+ ]
897
+ return guidance
898
+ }
899
+
900
+ function policyCheck(args = {}) {
901
+ const action = String(args.action || '').trim()
902
+ if (!action) throw new Error('policy.check requires action')
903
+ const project = args.project || projectName()
904
+ const actionTokens = tokenize(action)
905
+ const traces = readJsonl(tracePath).filter((event) => {
906
+ if (project && event.project && event.project !== project) return false
907
+ const haystack = tokenize(
908
+ `${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
909
+ )
910
+ return actionTokens.some((token) => haystack.includes(token))
911
+ })
912
+ const total = traces.length
913
+ const failures = traces.filter((event) => isFailureOutcome(event.outcome)).length
914
+ const risk = total > 0 ? failures / total : null
915
+ const riskLevel = total === 0 ? 'unknown' : risk >= 0.4 ? 'high' : risk >= 0.2 ? 'moderate' : 'low'
916
+ const outcomeCounts = traces.reduce((counts, event) => {
917
+ const outcome = String(event.outcome || 'unknown').toLowerCase() || 'unknown'
918
+ counts[outcome] = (counts[outcome] || 0) + 1
919
+ return counts
920
+ }, {})
921
+ const failureExamples = traces
922
+ .filter((event) => isFailureOutcome(event.outcome))
923
+ .sort((a, b) => new Date(b.timestamp || 0).getTime() - new Date(a.timestamp || 0).getTime())
924
+ .slice(0, 3)
925
+ .map((event) => ({
926
+ trace_id: event.trace_id || event.id || null,
927
+ event_type: event.event_type || null,
928
+ outcome: event.outcome || null,
929
+ timestamp: event.timestamp || null,
930
+ agent: event.agent || null,
931
+ summary: summarizeTrace(event),
932
+ }))
933
+ let suggestion
934
+ if (total === 0) {
935
+ suggestion = 'No history for this action. Treat as normal risk and record the outcome.'
936
+ } else if (risk >= 0.4) {
937
+ suggestion = 'Historically risky. Prefer an alternative approach or add rollback/verification.'
938
+ } else if (risk >= 0.2) {
939
+ suggestion = 'Moderate risk. Add verification before proceeding.'
940
+ } else {
941
+ suggestion = 'Low historical risk. Proceed with normal checks.'
942
+ }
943
+ const evidence = loadEvidenceWeights()
944
+ const relatedMemories = readJsonl(memoryPath)
945
+ .filter((record) => scoreRecord(record, action, project, evidence) > 0)
946
+ .sort((a, b) => scoreRecord(b, action, project, evidence) - scoreRecord(a, action, project, evidence))
947
+ .slice(0, 3)
948
+ .map((record) => {
949
+ const ev = evidence.get(record.id)
950
+ return {
951
+ id: record.id,
952
+ timestamp: record.timestamp,
953
+ kind: record.kind,
954
+ content: record.content,
955
+ tags: record.tags,
956
+ evidence: ev
957
+ ? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
958
+ : null,
959
+ }
960
+ })
961
+ return {
962
+ action,
963
+ project,
964
+ total,
965
+ failures,
966
+ risk: risk === null ? null : Number(risk.toFixed(3)),
967
+ riskLevel,
968
+ outcomeCounts,
969
+ failureExamples,
970
+ suggestion,
971
+ guidance: buildPolicyGuidance({
972
+ total,
973
+ failures,
974
+ risk,
975
+ failureExamples,
976
+ relatedMemories,
977
+ }),
978
+ related_memories: relatedMemories,
979
+ }
980
+ }
981
+
982
+ function recordExperienceReuse(args = {}) {
983
+ const sourceAgent = String(args.sourceAgent || '').trim()
984
+ const targetAgent = String(args.targetAgent || '').trim()
985
+ const experienceId = String(args.experienceId || '').trim()
986
+ if (!sourceAgent) throw new Error('experience.reuse requires sourceAgent')
987
+ if (!targetAgent) throw new Error('experience.reuse requires targetAgent')
988
+ if (!experienceId) throw new Error('experience.reuse requires experienceId')
989
+
990
+ const project = args.project || projectName()
991
+ const evidence = {
992
+ id: createId('xfer'),
993
+ timestamp: new Date().toISOString(),
994
+ sourceAgent,
995
+ targetAgent,
996
+ experienceId,
997
+ reuse: args.reuse === true || args.reuse === 'true',
998
+ behaviorChanged: args.behaviorChanged === true || args.behaviorChanged === 'true',
999
+ outcomeImproved: args.outcomeImproved === true || args.outcomeImproved === 'true',
1000
+ project,
1001
+ source: args.source || 'agent_report',
1002
+ notes: args.notes || null,
1003
+ }
1004
+ appendJsonl(experienceReusePath, evidence)
1005
+
1006
+ const report = loadPhase0(dataDir, project)
1007
+ return {
1008
+ recorded: true,
1009
+ evidence,
1010
+ phase0: summarizePhase0(report),
1011
+ }
1012
+ }
1013
+
1014
+ function confirmExperienceReuse(args = {}) {
1015
+ const id = String(args.id || '').trim()
1016
+ if (!id) throw new Error('experience.confirm requires id of an auto-claimed reuse record')
1017
+ const records = readJsonl(experienceReusePath)
1018
+ const index = records.findIndex((record) => record && record.id === id)
1019
+ if (index < 0) {
1020
+ throw new Error(`No experience reuse record found with id ${id}`)
1021
+ }
1022
+ const record = records[index]
1023
+ if (record.source !== 'auto_claim') {
1024
+ throw new Error('experience.confirm only applies to source=auto_claim reuse records')
1025
+ }
1026
+ const outcomeImproved =
1027
+ args.outcomeImproved === undefined || args.outcomeImproved === null
1028
+ ? record.outcomeImproved === true
1029
+ : args.outcomeImproved === true || args.outcomeImproved === 'true'
1030
+ const updated = {
1031
+ ...record,
1032
+ behaviorChanged: true,
1033
+ outcomeImproved,
1034
+ confirmed: true,
1035
+ confirmedAt: new Date().toISOString(),
1036
+ confirmedBy: String(args.confirmedBy || 'agent').trim() || 'agent',
1037
+ notes: args.notes ? String(args.notes).trim() : record.notes || null,
1038
+ }
1039
+ records[index] = updated
1040
+ writeJsonl(experienceReusePath, records)
1041
+
1042
+ const report = loadPhase0(dataDir, updated.project)
1043
+ return {
1044
+ confirmed: true,
1045
+ evidence: updated,
1046
+ phase0: summarizePhase0(report),
1047
+ }
1048
+ }
1049
+
1050
+ const REUSE_STATUS_FILTERS = Object.freeze({
1051
+ pending: (record) => record.source === 'auto_claim' && record.confirmed !== true,
1052
+ confirmed: (record) => record.confirmed === true,
1053
+ verified: (record) =>
1054
+ (record.reuse === true || record.reuse === 'true') &&
1055
+ (record.behaviorChanged === true || record.behaviorChanged === 'true') &&
1056
+ (record.outcomeImproved === true || record.outcomeImproved === 'true'),
1057
+ auto_claim: (record) => record.source === 'auto_claim',
1058
+ agent_report: (record) => record.source !== 'auto_claim',
1059
+ })
1060
+
1061
+ function loadEvidenceWeights() {
1062
+ const weights = new Map()
1063
+ for (const record of readJsonl(experienceReusePath)) {
1064
+ if (!REUSE_STATUS_FILTERS.verified(record)) continue
1065
+ const id = record.experienceId
1066
+ if (!id) continue
1067
+ let entry = weights.get(id)
1068
+ if (!entry) {
1069
+ entry = { reuseCount: 0, confirmedCount: 0, lastReusedAt: null }
1070
+ weights.set(id, entry)
1071
+ }
1072
+ entry.reuseCount += 1
1073
+ if (record.confirmed === true) entry.confirmedCount += 1
1074
+ if (!entry.lastReusedAt || new Date(record.timestamp || 0) > new Date(entry.lastReusedAt)) {
1075
+ entry.lastReusedAt = record.timestamp || null
1076
+ }
1077
+ }
1078
+ return weights
1079
+ }
1080
+
1081
+ function listExperienceReuse(args = {}) {
1082
+ const project = args.project || projectName()
1083
+ const status = String(args.status || 'all').trim().toLowerCase()
1084
+ const targetAgent = String(args.targetAgent || '').trim().toLowerCase()
1085
+ const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100)
1086
+
1087
+ let records = readJsonl(experienceReusePath)
1088
+ if (project) records = records.filter((record) => record.project === project)
1089
+ if (targetAgent) {
1090
+ records = records.filter(
1091
+ (record) => String(record.targetAgent || '').toLowerCase() === targetAgent,
1092
+ )
1093
+ }
1094
+ const matcher = REUSE_STATUS_FILTERS[status]
1095
+ if (matcher) records = records.filter(matcher)
1096
+ records.sort((a, b) => {
1097
+ const at = new Date(a.timestamp || 0).getTime()
1098
+ const bt = new Date(b.timestamp || 0).getTime()
1099
+ return bt - at
1100
+ })
1101
+
1102
+ const visible = records.slice(0, limit).map((record) => ({
1103
+ id: record.id,
1104
+ source: record.source,
1105
+ sourceAgent: record.sourceAgent,
1106
+ targetAgent: record.targetAgent,
1107
+ experienceId: record.experienceId,
1108
+ reuse: record.reuse === true || record.reuse === 'true',
1109
+ behaviorChanged: record.behaviorChanged === true || record.behaviorChanged === 'true',
1110
+ outcomeImproved: record.outcomeImproved === true || record.outcomeImproved === 'true',
1111
+ confirmed: record.confirmed === true,
1112
+ confirmedBy: record.confirmedBy || null,
1113
+ traceId: record.traceId || null,
1114
+ timestamp: record.timestamp,
1115
+ project: record.project,
1116
+ notes:
1117
+ typeof record.notes === 'string' && record.notes ? record.notes.slice(0, 300) : null,
1118
+ }))
1119
+
1120
+ return {
1121
+ project: project || null,
1122
+ status,
1123
+ count: visible.length,
1124
+ total: records.length,
1125
+ records: visible,
1126
+ }
1127
+ }
1128
+
1129
+ function registerAgent(args = {}) {
1130
+ const agentId = String(args.agentId || '').trim()
1131
+ if (!agentId) throw new Error('agent.register requires a non-empty agentId')
1132
+ const project = args.project || projectName()
1133
+ const hostType = args.hostType || 'mcp'
1134
+ const capabilities = Array.isArray(args.capabilities) ? args.capabilities : []
1135
+ const now = new Date().toISOString()
1136
+ const agents = readJsonl(agentsPath)
1137
+ const existing = agents.find((a) => a.agentId === agentId && a.project === project)
1138
+ let result
1139
+ if (existing) {
1140
+ existing.lastSeenAt = now
1141
+ existing.sessionCount = (existing.sessionCount || 1) + 1
1142
+ existing.hostType = hostType
1143
+ if (capabilities.length > 0) existing.capabilities = capabilities
1144
+ result = existing
1145
+ } else {
1146
+ result = {
1147
+ id: createId('agent'),
1148
+ agentId,
1149
+ hostType,
1150
+ capabilities,
1151
+ registeredAt: now,
1152
+ lastSeenAt: now,
1153
+ sessionCount: 1,
1154
+ project,
1155
+ }
1156
+ agents.push(result)
1157
+ }
1158
+ writeJsonl(agentsPath, agents)
1159
+ return { id: result.id, agentId: result.agentId, registered: !existing, sessionCount: result.sessionCount, hostType: result.hostType, capabilities: result.capabilities }
1160
+ }
1161
+
1162
+ function listAgents(args = {}) {
1163
+ const project = args.project || null
1164
+ let agents = readJsonl(agentsPath)
1165
+ if (project) agents = agents.filter((a) => a.project === project)
1166
+ return {
1167
+ count: agents.length,
1168
+ agents: agents.map((a) => ({
1169
+ id: a.id, agentId: a.agentId, hostType: a.hostType,
1170
+ capabilities: a.capabilities, registeredAt: a.registeredAt,
1171
+ lastSeenAt: a.lastSeenAt, sessionCount: a.sessionCount,
1172
+ taskCount: a.taskCount || 0, successCount: a.successCount || 0, failureCount: a.failureCount || 0,
1173
+ project: a.project,
1174
+ })),
1175
+ }
1176
+ }
1177
+
1178
+ function reportAgent(args = {}) {
1179
+ const targetAgent = args.agentId || null
1180
+ const project = args.project || projectName()
1181
+ const agents = readJsonl(agentsPath)
1182
+ const filtered = agents.filter((a) =>
1183
+ (!targetAgent || a.agentId === targetAgent) && (!project || a.project === project)
1184
+ )
1185
+ const traces = readJsonl(tracePath)
1186
+ const memories = readJsonl(memoryPath)
1187
+ const reuses = readJsonl(experienceReusePath)
1188
+ const reports = filtered.map((agent) => {
1189
+ const agentTraces = traces.filter((t) => t.agent === agent.agentId)
1190
+ const outcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
1191
+ const successes = outcomes.filter((t) => t.outcome === 'success').length
1192
+ const failures = outcomes.filter((t) => t.outcome === 'failure').length
1193
+ const agentMemories = memories.filter((m) => m.source === agent.agentId)
1194
+ const agentReuses = reuses.filter((r) => r.sourceAgent === agent.agentId || r.targetAgent === agent.agentId)
1195
+ const verified = agentReuses.filter((r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved)
1196
+ const regTasks = agent.taskCount || 0
1197
+ const regSuccess = agent.successCount || 0
1198
+ const regFail = agent.failureCount || 0
1199
+ const taskTotal = regTasks > 0 ? regTasks : outcomes.length
1200
+ const taskSuccess = regTasks > 0 ? regSuccess : successes
1201
+ const taskFail = regTasks > 0 ? regFail : failures
1202
+ return {
1203
+ agentId: agent.agentId,
1204
+ hostType: agent.hostType,
1205
+ sessionCount: agent.sessionCount,
1206
+ registeredAt: agent.registeredAt,
1207
+ lastSeenAt: agent.lastSeenAt,
1208
+ taskOutcomes: { total: taskTotal, success: taskSuccess, failure: taskFail, successRate: taskTotal > 0 ? Math.round((taskSuccess / taskTotal) * 100) : 0 },
1209
+ memories: agentMemories.length,
1210
+ experienceReuses: { total: agentReuses.length, verified: verified.length },
1211
+ active: (Date.now() - new Date(agent.lastSeenAt).getTime()) < 24 * 60 * 60 * 1000,
1212
+ }
1213
+ })
1214
+ return { project, count: reports.length, reports }
1215
+ }
1216
+
1217
+ function routeTask(args = {}) {
1218
+ const task = String(args.task || '').trim()
1219
+ if (!task) throw new Error('task.route requires a non-empty task')
1220
+ const project = args.project || projectName()
1221
+ const scope = normalizeScope(args.scope)
1222
+ const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 10)
1223
+
1224
+ const memories = readJsonl(memoryPath).filter((record) => record.archived !== true)
1225
+ const memoryById = new Map(memories.map((record) => [record.id, record]))
1226
+ const evidence = loadEvidenceWeights()
1227
+
1228
+ // Match verified reuse experiences whose source memory is relevant to the task.
1229
+ const routeByMemory = new Map()
1230
+ for (const record of readJsonl(experienceReusePath)) {
1231
+ if (!REUSE_STATUS_FILTERS.verified(record)) continue
1232
+ const memory = memoryById.get(record.experienceId)
1233
+ if (!memory || !matchesProjectScope(memory, project, scope)) continue
1234
+ const score = scoreRecord(memory, task, project)
1235
+ if (score <= 0) continue
1236
+ let entry = routeByMemory.get(record.experienceId)
1237
+ if (!entry) {
1238
+ entry = {
1239
+ experienceId: record.experienceId,
1240
+ memory,
1241
+ weight: 0,
1242
+ confirmed: false,
1243
+ sourceAgents: new Set(),
1244
+ targetAgents: new Set(),
1245
+ reuseCount: 0,
1246
+ }
1247
+ routeByMemory.set(record.experienceId, entry)
1248
+ }
1249
+ entry.reuseCount += 1
1250
+ if (record.confirmed === true) entry.confirmed = true
1251
+ if (record.sourceAgent) entry.sourceAgents.add(record.sourceAgent)
1252
+ if (record.targetAgent) entry.targetAgents.add(record.targetAgent)
1253
+ entry.weight = Math.max(entry.weight, score + (entry.confirmed ? 2 : 1.5))
1254
+ }
1255
+
1256
+ const routes = [...routeByMemory.values()]
1257
+ .sort((a, b) => b.weight - a.weight)
1258
+ .slice(0, limit)
1259
+ .map((entry) => ({
1260
+ experienceId: entry.experienceId,
1261
+ reuseCount: entry.reuseCount,
1262
+ confirmed: entry.confirmed,
1263
+ sourceAgents: [...entry.sourceAgents].filter(Boolean),
1264
+ targetAgents: [...entry.targetAgents].filter(Boolean),
1265
+ score: Number(entry.weight.toFixed(2)),
1266
+ memory: {
1267
+ id: entry.memory.id,
1268
+ timestamp: entry.memory.timestamp,
1269
+ kind: entry.memory.kind,
1270
+ content: entry.memory.content,
1271
+ tags: entry.memory.tags || [],
1272
+ project: entry.memory.project || null,
1273
+ scope: entry.memory.scope || 'project',
1274
+ },
1275
+ }))
1276
+
1277
+ // Broader context: top related memories not already routed.
1278
+ const routedIds = new Set(routes.map((route) => route.experienceId))
1279
+ const scoreRelated = (record) =>
1280
+ scoreRecord(record, task, project, evidence) +
1281
+ (scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
1282
+ const relatedMemories = memories
1283
+ .filter((record) => !routedIds.has(record.id))
1284
+ .filter((record) => matchesProjectScope(record, project, scope))
1285
+ .filter((record) => scoreRelated(record) > 0)
1286
+ .sort((a, b) => scoreRelated(b) - scoreRelated(a))
1287
+ .slice(0, Math.max(limit, 5))
1288
+ .map((record) => {
1289
+ const ev = evidence.get(record.id)
1290
+ return {
1291
+ id: record.id,
1292
+ timestamp: record.timestamp,
1293
+ kind: record.kind,
1294
+ content: record.content,
1295
+ tags: record.tags || [],
1296
+ project: record.project || null,
1297
+ scope: record.scope || 'project',
1298
+ evidence: ev
1299
+ ? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
1300
+ : null,
1301
+ }
1302
+ })
1303
+
1304
+ // Feed the query log so a later task_outcome can auto-claim route-driven reuse.
1305
+ if (routes.length > 0 || relatedMemories.length > 0) {
1306
+ const now = Date.now()
1307
+ const entries = loadRecentQueries()
1308
+ entries.push({
1309
+ agent: runtimeAgentId() || 'mcp',
1310
+ project,
1311
+ query: task,
1312
+ resultIds: [...routedIds, ...relatedMemories.map((record) => record.id)].filter(Boolean),
1313
+ resultSources: [...routes.map(() => 'experience'), ...relatedMemories.map((record) => record.source || null)],
1314
+ timestamp: now,
1315
+ expiresAt: now + REUSE_MATCH_WINDOW_MS,
1316
+ })
1317
+ persistRecentQueries(entries)
1318
+ }
1319
+
1320
+ let suggestion
1321
+ if (routes.length > 0) {
1322
+ suggestion =
1323
+ `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.`
1324
+ } else if (relatedMemories.length > 0) {
1325
+ suggestion =
1326
+ 'No verified experience matches yet. Consult the related memories, then record the outcome so this task can seed future routes.'
1327
+ } else {
1328
+ suggestion =
1329
+ 'No matching experience or memory found. Proceed with care and record the outcome to grow the experience base.'
1330
+ }
1331
+
1332
+ return {
1333
+ task,
1334
+ project,
1335
+ scope,
1336
+ count: routes.length,
1337
+ routes,
1338
+ relatedMemories,
1339
+ summary: {
1340
+ verifiedRoutes: routes.length,
1341
+ relatedMemories: relatedMemories.length,
1342
+ suggestion,
1343
+ },
1344
+ }
1345
+ }
1346
+ function phase0Report(args = {}) {
1347
+ const project = args.project || projectName()
1348
+ const report = loadPhase0(dataDir, project)
1349
+ const format = String(args.format || 'json').trim().toLowerCase()
1350
+ if (format === 'markdown') {
1351
+ return {
1352
+ project,
1353
+ format: 'markdown',
1354
+ markdown: renderPhase0Markdown(report),
1355
+ }
1356
+ }
1357
+ return {
1358
+ project,
1359
+ ...report,
1360
+ }
1361
+ }
1362
+
1363
+ const TOOLS = [
1364
+ {
1365
+ name: 'mio.memory.query',
1366
+ description: 'Search local Mio project memory for context, decisions, and historical problems.',
1367
+ inputSchema: {
1368
+ type: 'object',
1369
+ properties: {
1370
+ query: { type: 'string', description: 'Question or topic to recall.' },
1371
+ project: { type: 'string', description: 'Project name filter. Defaults to current directory name.' },
1372
+ limit: { type: 'number', description: 'Maximum results, between 1 and 20. Defaults to 5.' },
1373
+ kind: { type: 'string', description: 'Only return records with this kind (decision, context, problem, note).' },
1374
+ tags: { type: 'array', items: { type: 'string' }, description: 'Only return records containing all of these tags (AND).' },
1375
+ scope: { type: 'string', description: 'Search scope: project (default), global, or all.' },
1376
+ },
1377
+ required: ['query'],
1378
+ },
1379
+ },
1380
+ {
1381
+ name: 'mio.memory.archive',
1382
+ description: 'Archive (or restore) memory records by id. Archived records are excluded from memory.query and memory.analyze.',
1383
+ inputSchema: {
1384
+ type: 'object',
1385
+ properties: {
1386
+ ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to archive or restore.' },
1387
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1388
+ restore: { type: 'boolean', description: 'When true, un-archive the given ids instead.' },
1389
+ reason: { type: 'string', description: 'Optional archive reason.' },
1390
+ },
1391
+ required: ['ids'],
1392
+ },
1393
+ },
1394
+ {
1395
+ name: 'mio.memory.migrate',
1396
+ 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.',
1397
+ inputSchema: {
1398
+ type: 'object',
1399
+ properties: {
1400
+ ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to migrate.' },
1401
+ scope: { type: 'string', description: 'Target layer: global or project.' },
1402
+ project: { type: 'string', description: 'Target project for project scope. Defaults to current repository.' },
1403
+ reason: { type: 'string', description: 'Optional migration reason.' },
1404
+ },
1405
+ required: ['ids', 'scope'],
1406
+ },
1407
+ },
1408
+ {
1409
+ name: 'mio.memory.record',
1410
+ description: 'Record a decision, project context, or historical problem into local Mio memory.',
1411
+ inputSchema: {
1412
+ type: 'object',
1413
+ properties: {
1414
+ content: { type: 'string', description: 'Memory content.' },
1415
+ kind: { type: 'string', description: 'Memory kind, such as decision, context, or problem.' },
1416
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
1417
+ project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
1418
+ scope: { type: 'string', description: 'Record scope: project (default) or global.' },
1419
+ source: { type: 'string', description: 'Recording source. Defaults to mcp.' },
1420
+ },
1421
+ required: ['content'],
1422
+ },
1423
+ },
1424
+ {
1425
+ name: 'mio.observer.ingest',
1426
+ description: 'Ingest an Agent Trace event such as tool_call, error, retry, or task_outcome.',
1427
+ inputSchema: {
1428
+ type: 'object',
1429
+ properties: {
1430
+ trace_id: { type: 'string', description: 'Stable trace identifier.' },
1431
+ event_type: { type: 'string', description: 'Event type, such as tool_call, error, retry, task_outcome.' },
1432
+ payload: { type: 'object', description: 'Event payload.' },
1433
+ outcome: { type: 'string', description: 'Optional outcome: success, failure, error, aborted, retry.' },
1434
+ agent: { type: 'string', description: 'Agent identifier.' },
1435
+ host: { type: 'string', description: 'Host name. Defaults to mcp.' },
1436
+ project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
1437
+ },
1438
+ required: ['trace_id', 'event_type'],
1439
+ },
1440
+ },
1441
+ {
1442
+ name: 'mio.observer.subscribe',
1443
+ description: 'Subscribe to observer events (project, event types, optional topic). New matching events are delivered via mio.observer.digest.',
1444
+ inputSchema: {
1445
+ type: 'object',
1446
+ properties: {
1447
+ eventTypes: { type: 'array', items: { type: 'string' }, description: 'Event types to watch, e.g. error, retry, task_outcome. Empty means all.' },
1448
+ project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1449
+ topic: { type: 'string', description: 'Optional keyword; only events whose payload mentions it match.' },
1450
+ ttlDays: { type: 'number', description: 'Subscription lifetime in days. Defaults to 30.' },
1451
+ },
1452
+ },
1453
+ },
1454
+ {
1455
+ name: 'mio.observer.digest',
1456
+ description: 'Return new observer events matching active subscriptions since the last digest, and advance the cursor.',
1457
+ inputSchema: {
1458
+ type: 'object',
1459
+ properties: {
1460
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1461
+ eventTypes: { type: 'array', items: { type: 'string' }, description: 'Optional extra event type filter.' },
1462
+ limit: { type: 'number', description: 'Maximum events to return, 1-100. Defaults to 20.' },
1463
+ },
1464
+ },
1465
+ },
1466
+ {
1467
+ name: 'mio.policy.check',
1468
+ description: 'Check historical risk for an action before executing it, returning risk level, outcome counts, and recent failure examples.',
1469
+ inputSchema: {
1470
+ type: 'object',
1471
+ properties: {
1472
+ action: { type: 'string', description: 'The action or tool operation to check.' },
1473
+ project: { type: 'string', description: 'Project name filter. Defaults to current directory name.' },
1474
+ context: { type: 'string', description: 'Optional additional context.' },
1475
+ },
1476
+ required: ['action'],
1477
+ },
1478
+ },
1479
+ {
1480
+ name: 'mio.memory.analyze',
1481
+ description: 'Analyze local Mio memory quality: kind distribution, duplicate groups, and low-quality records.',
1482
+ inputSchema: {
1483
+ type: 'object',
1484
+ properties: {
1485
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1486
+ limit: { type: 'number', description: 'Maximum duplicates/low-quality examples to return, 1-20. Defaults to 5.' },
1487
+ },
1488
+ },
1489
+ },
1490
+ {
1491
+ name: 'mio.experience.reuse',
1492
+ description: 'Record Phase 0 evidence of cross-agent experience reuse.',
1493
+ inputSchema: {
1494
+ type: 'object',
1495
+ properties: {
1496
+ sourceAgent: { type: 'string', description: 'Agent that produced the experience.' },
1497
+ targetAgent: { type: 'string', description: 'Agent that reused the experience.' },
1498
+ experienceId: { type: 'string', description: 'Stable experience or memory id.' },
1499
+ reuse: { type: 'boolean', description: 'True when memory.query returned historical experience and it was reused.' },
1500
+ behaviorChanged: { type: 'boolean', description: 'True when target Agent behavior changed because of the reused experience.' },
1501
+ outcomeImproved: { type: 'boolean', description: 'True when the task outcome improved.' },
1502
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1503
+ notes: { type: 'string', description: 'Optional evidence notes.' },
1504
+ },
1505
+ required: ['sourceAgent', 'targetAgent', 'experienceId', 'reuse', 'behaviorChanged', 'outcomeImproved'],
1506
+ },
1507
+ },
1508
+ {
1509
+ name: 'mio.experience.confirm',
1510
+ description: 'Confirm an auto-claimed reuse record as verified reuse evidence.',
1511
+ inputSchema: {
1512
+ type: 'object',
1513
+ properties: {
1514
+ id: { type: 'string', description: 'Id of the auto-claimed reuse record returned by mio.observer.ingest.' },
1515
+ confirmedBy: { type: 'string', description: 'Who confirmed the reuse: agent or human. Defaults to agent.' },
1516
+ outcomeImproved: { type: 'boolean', description: 'Override: true when the reused experience improved the task outcome. Defaults to the auto-claim value.' },
1517
+ notes: { type: 'string', description: 'Optional confirmation notes.' },
1518
+ },
1519
+ required: ['id'],
1520
+ },
1521
+ },
1522
+ {
1523
+ name: 'mio.experience.list',
1524
+ description: 'List Phase 0 experience reuse evidence, optionally filtered by status or target agent.',
1525
+ inputSchema: {
1526
+ type: 'object',
1527
+ properties: {
1528
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1529
+ status: { type: 'string', description: 'Filter: pending (auto-claim awaiting confirmation), confirmed, verified, auto_claim, agent_report, all. Defaults to all.' },
1530
+ targetAgent: { type: 'string', description: 'Filter records by target agent.' },
1531
+ limit: { type: 'number', description: 'Maximum records to return, 1-100. Defaults to 20.' },
1532
+ },
1533
+ },
1534
+ },
1535
+ {
1536
+ name: 'mio.phase0.report',
1537
+ description: 'Generate the current Phase 0 cross-agent validation report, as JSON or rendered Markdown.',
1538
+ inputSchema: {
1539
+ type: 'object',
1540
+ properties: {
1541
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1542
+ format: { type: 'string', description: 'Output format: json or markdown. Defaults to json.' },
1543
+ },
1544
+ },
1545
+ },
1546
+ {
1547
+ name: 'mio.task.route',
1548
+ description: 'Route an incoming task to the most relevant verified experiences and related memories, turning recall into decision influence (P1).',
1549
+ inputSchema: {
1550
+ type: 'object',
1551
+ properties: {
1552
+ task: { type: 'string', description: 'Task description to route, e.g. \'fix IPC communication bug\'.' },
1553
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1554
+ scope: { type: 'string', description: 'Search scope: project (default), global, or all.' },
1555
+ limit: { type: 'number', description: 'Maximum routes, between 1 and 10. Defaults to 5.' },
1556
+ },
1557
+ required: ['task'],
1558
+ },
1559
+ },
1560
+ {
1561
+ name: 'mio.agent.register',
1562
+ description: 'Register or update an agent in the Mio Agent Registry. Upserts by agentId+project; updates lastSeenAt and sessionCount.',
1563
+ inputSchema: {
1564
+ type: 'object',
1565
+ properties: {
1566
+ agentId: { type: 'string', description: 'Unique agent identifier (e.g. codex, opencode).' },
1567
+ hostType: { type: 'string', description: 'Host type: mcp, observer, or plugin. Defaults to mcp.' },
1568
+ capabilities: { type: 'array', items: { type: 'string' }, description: 'Agent capabilities.' },
1569
+ project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
1570
+ },
1571
+ required: ['agentId'],
1572
+ },
1573
+ },
1574
+ {
1575
+ name: 'mio.agent.list',
1576
+ description: 'List all registered agents, optionally filtered by project.',
1577
+ inputSchema: {
1578
+ type: 'object',
1579
+ properties: {
1580
+ project: { type: 'string', description: 'Project name filter. Omit for all agents.' },
1581
+ },
1582
+ },
1583
+ },
1584
+ {
1585
+ name: 'mio.agent.report',
1586
+ description: 'Performance report for one or all agents, aggregating task outcomes, memory, and experience reuse.',
1587
+ inputSchema: {
1588
+ type: 'object',
1589
+ properties: {
1590
+ agentId: { type: 'string', description: 'Agent to report on. Omit for all agents.' },
1591
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1592
+ },
1593
+ },
1594
+ },
1595
+ {
1596
+ name: 'mio.evolution.report',
1597
+ description: 'Generate cross-agent evolution report: ecosystem summary, agent performance comparison, cross-agent patterns, memory health, and optimization suggestions (ADR-017 Evolution plane).',
1598
+ inputSchema: {
1599
+ type: 'object',
1600
+ properties: {
1601
+ project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1602
+ period: { type: 'string', description: 'Time window: 24h, 7d, 30d, or all (default).' },
1603
+ },
1604
+ },
1605
+ },
1606
+ {
1607
+ name: 'mio.task.record_outcome',
1608
+ description: 'Record task outcome with agent registry update and auto-claim experience reuse in one atomic call. Combines observer.ingest + agent stats + auto-claim for a complete task lifecycle closure.',
1609
+ inputSchema: {
1610
+ type: 'object',
1611
+ properties: {
1612
+ outcome: { type: 'string', description: 'Task outcome: success, failure, or aborted.' },
1613
+ task: { type: 'string', description: 'Brief task description.' },
1614
+ summary: { type: 'string', description: 'Optional task summary.' },
1615
+ verification: { type: 'string', description: 'Optional verification details.' },
1616
+ agentId: { type: 'string', description: 'Agent identifier. Defaults to MIO_CONTEXT agentId.' },
1617
+ project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1618
+ traceId: { type: 'string', description: 'Optional custom trace ID. Auto-generated if omitted.' },
1619
+ },
1620
+ required: ['outcome'],
1621
+ },
1337
1622
  },
1338
1623
  {
1339
- name: 'mio.observer.ingest',
1340
- description: 'Ingest an Agent Trace event such as tool_call, error, retry, or task_outcome.',
1624
+ name: 'mio.idea.generate',
1625
+ description: 'Generate creative ideas or multiple candidate solutions based on a given goal, context, and constraints.',
1341
1626
  inputSchema: {
1342
1627
  type: 'object',
1628
+ required: ['goal'],
1343
1629
  properties: {
1344
- trace_id: { type: 'string', description: 'Stable trace identifier.' },
1345
- event_type: { type: 'string', description: 'Event type, such as tool_call, error, retry, task_outcome.' },
1346
- payload: { type: 'object', description: 'Event payload.' },
1347
- outcome: { type: 'string', description: 'Optional outcome: success, failure, error, aborted, retry.' },
1348
- agent: { type: 'string', description: 'Agent identifier.' },
1349
- host: { type: 'string', description: 'Host name. Defaults to mcp.' },
1350
- project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
1630
+ goal: { type: 'string', description: 'The goal or problem description for which to generate ideas.' },
1631
+ context: { type: 'string', description: '(Optional) Additional background information, such as prior decisions or known problems.' },
1632
+ constraints: { type: 'array', items: { type: 'string' }, description: '(Optional) List of constraints, e.g. "no new dependencies", "must be implementable as a plugin".' },
1633
+ numIdeas: { type: 'integer', description: '(Optional) Number of ideas to return, default 3.', default: 3, minimum: 1 }
1351
1634
  },
1352
- required: ['trace_id', 'event_type'],
1635
+ additionalProperties: false
1353
1636
  },
1354
1637
  },
1355
- {
1356
- name: 'mio.observer.subscribe',
1357
- description: 'Subscribe to observer events (project, event types, optional topic). New matching events are delivered via mio.observer.digest.',
1358
- inputSchema: {
1359
- type: 'object',
1360
- properties: {
1361
- eventTypes: { type: 'array', items: { type: 'string' }, description: 'Event types to watch, e.g. error, retry, task_outcome. Empty means all.' },
1362
- project: { type: 'string', description: 'Project name. Defaults to current repository.' },
1363
- topic: { type: 'string', description: 'Optional keyword; only events whose payload mentions it match.' },
1364
- ttlDays: { type: 'number', description: 'Subscription lifetime in days. Defaults to 30.' },
1365
- },
1366
- },
1367
- },
1368
- {
1369
- name: 'mio.observer.digest',
1370
- description: 'Return new observer events matching active subscriptions since the last digest, and advance the cursor.',
1371
- inputSchema: {
1372
- type: 'object',
1373
- properties: {
1374
- project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1375
- eventTypes: { type: 'array', items: { type: 'string' }, description: 'Optional extra event type filter.' },
1376
- limit: { type: 'number', description: 'Maximum events to return, 1-100. Defaults to 20.' },
1377
- },
1378
- },
1379
- },
1380
- {
1381
- name: 'mio.policy.check',
1382
- description: 'Check historical risk for an action before executing it, returning risk level, outcome counts, and recent failure examples.',
1383
- inputSchema: {
1384
- type: 'object',
1385
- properties: {
1386
- action: { type: 'string', description: 'The action or tool operation to check.' },
1387
- project: { type: 'string', description: 'Project name filter. Defaults to current directory name.' },
1388
- context: { type: 'string', description: 'Optional additional context.' },
1389
- },
1390
- required: ['action'],
1391
- },
1392
- },
1393
- {
1394
- name: 'mio.memory.analyze',
1395
- description: 'Analyze local Mio memory quality: kind distribution, duplicate groups, and low-quality records.',
1396
- inputSchema: {
1397
- type: 'object',
1398
- properties: {
1399
- project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1400
- limit: { type: 'number', description: 'Maximum duplicates/low-quality examples to return, 1-20. Defaults to 5.' },
1401
- },
1402
- },
1403
- },
1404
- {
1405
- name: 'mio.experience.reuse',
1406
- description: 'Record Phase 0 evidence of cross-agent experience reuse.',
1407
- inputSchema: {
1408
- type: 'object',
1409
- properties: {
1410
- sourceAgent: { type: 'string', description: 'Agent that produced the experience.' },
1411
- targetAgent: { type: 'string', description: 'Agent that reused the experience.' },
1412
- experienceId: { type: 'string', description: 'Stable experience or memory id.' },
1413
- reuse: { type: 'boolean', description: 'True when memory.query returned historical experience and it was reused.' },
1414
- behaviorChanged: { type: 'boolean', description: 'True when target Agent behavior changed because of the reused experience.' },
1415
- outcomeImproved: { type: 'boolean', description: 'True when the task outcome improved.' },
1416
- project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1417
- notes: { type: 'string', description: 'Optional evidence notes.' },
1418
- },
1419
- required: ['sourceAgent', 'targetAgent', 'experienceId', 'reuse', 'behaviorChanged', 'outcomeImproved'],
1420
- },
1421
- },
1422
- {
1423
- name: 'mio.experience.confirm',
1424
- description: 'Confirm an auto-claimed reuse record as verified reuse evidence.',
1425
- inputSchema: {
1426
- type: 'object',
1427
- properties: {
1428
- id: { type: 'string', description: 'Id of the auto-claimed reuse record returned by mio.observer.ingest.' },
1429
- confirmedBy: { type: 'string', description: 'Who confirmed the reuse: agent or human. Defaults to agent.' },
1430
- outcomeImproved: { type: 'boolean', description: 'Override: true when the reused experience improved the task outcome. Defaults to the auto-claim value.' },
1431
- notes: { type: 'string', description: 'Optional confirmation notes.' },
1432
- },
1433
- required: ['id'],
1434
- },
1435
- },
1436
- {
1437
- name: 'mio.experience.list',
1438
- description: 'List Phase 0 experience reuse evidence, optionally filtered by status or target agent.',
1439
- inputSchema: {
1440
- type: 'object',
1441
- properties: {
1442
- project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1443
- status: { type: 'string', description: 'Filter: pending (auto-claim awaiting confirmation), confirmed, verified, auto_claim, agent_report, all. Defaults to all.' },
1444
- targetAgent: { type: 'string', description: 'Filter records by target agent.' },
1445
- limit: { type: 'number', description: 'Maximum records to return, 1-100. Defaults to 20.' },
1446
- },
1447
- },
1448
- },
1449
- {
1450
- name: 'mio.phase0.report',
1451
- description: 'Generate the current Phase 0 cross-agent validation report, as JSON or rendered Markdown.',
1452
- inputSchema: {
1453
- type: 'object',
1454
- properties: {
1455
- project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1456
- format: { type: 'string', description: 'Output format: json or markdown. Defaults to json.' },
1457
- },
1458
- },
1459
- },
1460
- {
1461
- name: 'mio.task.route',
1462
- description: 'Route an incoming task to the most relevant verified experiences and related memories, turning recall into decision influence (P1).',
1463
- inputSchema: {
1464
- type: 'object',
1465
- properties: {
1466
- task: { type: 'string', description: 'Task description to route, e.g. \'fix IPC communication bug\'.' },
1467
- project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
1468
- scope: { type: 'string', description: 'Search scope: project (default), global, or all.' },
1469
- limit: { type: 'number', description: 'Maximum routes, between 1 and 10. Defaults to 5.' },
1470
- },
1471
- required: ['task'],
1472
- },
1473
- },
1474
- ]
1475
-
1476
- async function callTool(name, args = {}) {
1477
- switch (name) {
1478
- case 'mio.memory.query':
1479
- return queryMemory(args)
1480
- case 'mio.memory.record':
1481
- return recordMemory(args)
1482
- case 'mio.memory.archive':
1483
- return archiveMemory(args)
1484
- case 'mio.memory.migrate':
1485
- return migrateMemory(args)
1486
- case 'mio.observer.ingest':
1487
- return ingestObservation(args)
1488
- case 'mio.observer.subscribe':
1489
- return subscribeObserver(args)
1490
- case 'mio.observer.digest':
1491
- return observerDigest(args)
1492
- case 'mio.policy.check':
1493
- return policyCheck(args)
1494
- case 'mio.memory.analyze':
1495
- return analyzeMemory(args)
1496
- case 'mio.experience.reuse':
1497
- return recordExperienceReuse(args)
1498
- case 'mio.experience.confirm':
1499
- return confirmExperienceReuse(args)
1500
- case 'mio.experience.list':
1501
- return listExperienceReuse(args)
1502
- case 'mio.phase0.report':
1503
- return phase0Report(args)
1504
- case 'mio.task.route':
1505
- return routeTask(args)
1506
- default:
1507
- throw new Error(`Unknown tool: ${name}`)
1638
+ ]
1639
+
1640
+ async function callTool(name, args = {}) {
1641
+ switch (name) {
1642
+ case 'mio.memory.query':
1643
+ return queryMemory(args)
1644
+ case 'mio.memory.record':
1645
+ return recordMemory(args)
1646
+ case 'mio.memory.archive':
1647
+ return archiveMemory(args)
1648
+ case 'mio.memory.migrate':
1649
+ return migrateMemory(args)
1650
+ case 'mio.observer.ingest':
1651
+ return ingestObservation(args)
1652
+ case 'mio.observer.subscribe':
1653
+ return subscribeObserver(args)
1654
+ case 'mio.observer.digest':
1655
+ return observerDigest(args)
1656
+ case 'mio.policy.check':
1657
+ return policyCheck(args)
1658
+ case 'mio.memory.analyze':
1659
+ return analyzeMemory(args)
1660
+ case 'mio.experience.reuse':
1661
+ return recordExperienceReuse(args)
1662
+ case 'mio.experience.confirm':
1663
+ return confirmExperienceReuse(args)
1664
+ case 'mio.experience.list':
1665
+ return listExperienceReuse(args)
1666
+ case 'mio.phase0.report':
1667
+ return phase0Report(args)
1668
+ case 'mio.task.route':
1669
+ return routeTask(args)
1670
+ case 'mio.agent.register':
1671
+ return registerAgent(args)
1672
+ case 'mio.agent.list':
1673
+ return listAgents(args)
1674
+ case 'mio.agent.report':
1675
+ return reportAgent(args)
1676
+ case 'mio.evolution.report':
1677
+ return evolutionReport(args)
1678
+ case 'mio.task.record_outcome':
1679
+ return recordTaskOutcome(args)
1680
+ case 'mio.idea.generate':
1681
+ return handleIdeaGenerate(args)
1682
+ default:
1683
+ throw new Error(`Unknown tool: ${name}`)
1684
+ }
1685
+ }
1686
+
1687
+ function jsonRpcResult(id, result) {
1688
+ return { jsonrpc: '2.0', id, result }
1689
+ }
1690
+
1691
+ function jsonRpcError(id, code, message, data) {
1692
+ const error = { code, message }
1693
+ if (data !== undefined) error.data = data
1694
+ return { jsonrpc: '2.0', id, error }
1695
+ }
1696
+
1697
+ function send(message) {
1698
+ if (message) process.stdout.write(`${JSON.stringify(message)}\n`)
1699
+ }
1700
+
1701
+ async function handleMessage(message) {
1702
+ if (!message || typeof message !== 'object' || message.jsonrpc !== '2.0') return
1703
+ if (message.method && message.method.startsWith('notifications/')) return
1704
+ if (message.id === undefined) return
1705
+
1706
+ try {
1707
+ if (message.method === 'initialize') {
1708
+ send(
1709
+ jsonRpcResult(message.id, {
1710
+ protocolVersion: PROTOCOL_VERSION,
1711
+ capabilities: { tools: {} },
1712
+ serverInfo: SERVER_INFO,
1713
+ })
1714
+ )
1715
+ return
1716
+ }
1717
+ if (message.method === 'ping') {
1718
+ send(jsonRpcResult(message.id, {}))
1719
+ return
1720
+ }
1721
+ if (message.method === 'tools/list') {
1722
+ send(jsonRpcResult(message.id, { tools: TOOLS }))
1723
+ return
1724
+ }
1725
+ if (message.method === 'tools/call') {
1726
+ const name = message.params && message.params.name
1727
+ const args = (message.params && message.params.arguments) || {}
1728
+ const result = await callTool(name, args)
1729
+ send(
1730
+ jsonRpcResult(message.id, {
1731
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1732
+ isError: false,
1733
+ })
1734
+ )
1735
+ return
1736
+ }
1737
+ send(jsonRpcError(message.id, -32601, `Method not found: ${message.method}`))
1738
+ } catch (error) {
1739
+ send(jsonRpcError(message.id, -32603, error.message || String(error)))
1740
+ }
1741
+ }
1742
+
1743
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity })
1744
+
1745
+ rl.on('line', (line) => {
1746
+ let message
1747
+ try {
1748
+ message = JSON.parse(line)
1749
+ } catch (_) {
1750
+ return
1751
+ }
1752
+ handleMessage(message).catch((error) => {
1753
+ send(jsonRpcError(message && message.id, -32603, error.message || String(error)))
1754
+ })
1755
+ })
1756
+
1757
+ rl.on('close', () => {
1758
+ if (require.main === module) {
1759
+ process.exit(0)
1760
+ }
1761
+ })
1762
+
1763
+
1764
+ function evolutionReport(args = {}) {
1765
+ const project = args.project || projectName()
1766
+ const period = String(args.period || 'all').trim()
1767
+ const now = Date.now()
1768
+ const periodMs = period === '24h' ? 86400000 : period === '7d' ? 604800000 : period === '30d' ? 2592000000 : Infinity
1769
+ const cutoff = periodMs === Infinity ? 0 : now - periodMs
1770
+
1771
+ const memories = readJsonl(memoryPath).filter((m) => !project || m.project === project || !m.project)
1772
+ const traces = readJsonl(tracePath).filter((t) => !project || t.project === project)
1773
+ const reuses = readJsonl(experienceReusePath).filter((r) => !project || r.project === project)
1774
+ const agents = readJsonl(agentsPath).filter((a) => !project || a.project === project)
1775
+ const queries = readJsonl(queryPath).filter((q) => !project || q.project === project)
1776
+
1777
+ const filteredTraces = cutoff ? traces.filter((t) => new Date(t.timestamp).getTime() >= cutoff) : traces
1778
+ const filteredMemories = cutoff ? memories.filter((m) => new Date(m.timestamp).getTime() >= cutoff) : memories
1779
+
1780
+ // Ecosystem summary
1781
+ const agentIds = new Set()
1782
+ filteredTraces.forEach((t) => { if (t.agent) agentIds.add(t.agent) })
1783
+ agents.forEach((a) => agentIds.add(a.agentId))
1784
+ const outcomes = filteredTraces.filter((t) => t.event_type === 'task_outcome')
1785
+ const successes = outcomes.filter((t) => t.outcome === 'success').length
1786
+ const failures = outcomes.filter((t) => t.outcome === 'failure').length
1787
+ const verifiedReuses = reuses.filter((r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved)
1788
+ const pendingReuses = reuses.filter((r) => r.source === 'auto_claim' && !r.confirmed)
1789
+
1790
+ const ecosystem = {
1791
+ agents: agentIds.size,
1792
+ totalTasks: outcomes.length,
1793
+ successRate: outcomes.length > 0 ? Math.round((successes / outcomes.length) * 100) : 0,
1794
+ totalMemories: filteredMemories.length,
1795
+ totalReuses: reuses.length,
1796
+ verifiedReuses: verifiedReuses.length,
1797
+ pendingReuses: pendingReuses.length,
1798
+ }
1799
+
1800
+ // Agent performance comparison
1801
+ const agentPerf = []
1802
+ for (const agentId of agentIds) {
1803
+ const agentTraces = filteredTraces.filter((t) => t.agent === agentId)
1804
+ const agentOutcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
1805
+ const agentSuccesses = agentOutcomes.filter((t) => t.outcome === 'success').length
1806
+ const agentFailures = agentOutcomes.filter((t) => t.outcome === 'failure').length
1807
+ const agentMemories = filteredMemories.filter((m) => m.source === agentId)
1808
+ const agentVerified = reuses.filter((r) =>
1809
+ (r.sourceAgent === agentId || r.targetAgent === agentId) && r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved
1810
+ )
1811
+ const reg = agents.find((a) => a.agentId === agentId)
1812
+ agentPerf.push({
1813
+ agentId,
1814
+ tasks: {
1815
+ total: agentOutcomes.length,
1816
+ success: agentSuccesses,
1817
+ failure: agentFailures,
1818
+ successRate: agentOutcomes.length > 0 ? Math.round((agentSuccesses / agentOutcomes.length) * 100) : 0,
1819
+ },
1820
+ memories: agentMemories.length,
1821
+ verifiedReuses: agentVerified.length,
1822
+ registered: !!reg,
1823
+ lastActive: reg ? reg.lastSeenAt : (agentTraces.length > 0 ? agentTraces[agentTraces.length - 1].timestamp : null),
1824
+ })
1825
+ }
1826
+ agentPerf.sort((a, b) => b.tasks.successRate - a.tasks.successRate || b.tasks.total - a.tasks.total)
1827
+
1828
+ // Cross-agent patterns
1829
+ const crossPatterns = []
1830
+ const reuseByPair = {}
1831
+ for (const r of reuses) {
1832
+ const key = r.sourceAgent + '->' + r.targetAgent
1833
+ if (!reuseByPair[key]) reuseByPair[key] = { source: r.sourceAgent, target: r.targetAgent, total: 0, verified: 0, improved: 0 }
1834
+ reuseByPair[key].total++
1835
+ if (r.confirmed) reuseByPair[key].verified++
1836
+ if (r.outcomeImproved) reuseByPair[key].improved++
1837
+ }
1838
+ for (const [key, pair] of Object.entries(reuseByPair)) {
1839
+ if (pair.source !== pair.target) {
1840
+ crossPatterns.push({
1841
+ type: 'reuse_path',
1842
+ from: pair.source,
1843
+ to: pair.target,
1844
+ total: pair.total,
1845
+ verified: pair.verified,
1846
+ improved: pair.improved,
1847
+ })
1848
+ }
1849
+ }
1850
+ crossPatterns.sort((a, b) => b.verified - a.verified || b.total - a.total)
1851
+
1852
+ // Memory health
1853
+ const byKind = {}
1854
+ let shortContent = 0
1855
+ let missingKind = 0
1856
+ for (const m of filteredMemories) {
1857
+ const kind = m.kind || 'unknown'
1858
+ byKind[kind] = (byKind[kind] || 0) + 1
1859
+ const content = String(m.content || '').trim()
1860
+ if (content.length < 20) shortContent++
1861
+ if (!m.kind) missingKind++
1862
+ }
1863
+ const memoryHealth = {
1864
+ total: filteredMemories.length,
1865
+ byKind,
1866
+ shortContent,
1867
+ missingKind,
1868
+ qualityScore: filteredMemories.length > 0
1869
+ ? Math.round(((filteredMemories.length - shortContent - missingKind) / filteredMemories.length) * 100)
1870
+ : 100,
1871
+ }
1872
+
1873
+ // Suggestions engine
1874
+ const suggestions = []
1875
+
1876
+ // Routing suggestion: compare agent success rates
1877
+ if (agentPerf.length >= 2) {
1878
+ const best = agentPerf[0]
1879
+ const worst = agentPerf[agentPerf.length - 1]
1880
+ if (best.tasks.total >= 5 && worst.tasks.total >= 5 && best.tasks.successRate - worst.tasks.successRate >= 15) {
1881
+ suggestions.push({
1882
+ category: 'routing',
1883
+ priority: 'high',
1884
+ description: best.agentId + ' has ' + best.tasks.successRate + '% success rate vs ' + worst.agentId + ' at ' + worst.tasks.successRate + '%; prefer routing to ' + best.agentId,
1885
+ basedOn: 'task_outcome comparison',
1886
+ })
1887
+ }
1888
+ }
1889
+
1890
+ // Pending auto-claim suggestion
1891
+ if (pendingReuses.length >= 10) {
1892
+ suggestions.push({
1893
+ category: 'evidence',
1894
+ priority: 'medium',
1895
+ description: pendingReuses.length + ' pending auto-claims awaiting confirmation; review cross-agent claims to increase verified reuse count',
1896
+ basedOn: 'experience_reuse.jsonl',
1897
+ })
1898
+ }
1899
+
1900
+ // Memory quality suggestion
1901
+ if (memoryHealth.qualityScore < 80 && filteredMemories.length > 10) {
1902
+ suggestions.push({
1903
+ category: 'memory',
1904
+ priority: 'medium',
1905
+ description: 'Memory quality score is ' + memoryHealth.qualityScore + '%; ' + shortContent + ' short records and ' + missingKind + ' missing kind fields found',
1906
+ basedOn: 'memory.jsonl quality analysis',
1907
+ })
1908
+ }
1909
+
1910
+ // Cross-agent value suggestion
1911
+ if (crossPatterns.length === 0 && agentIds.size >= 2) {
1912
+ suggestions.push({
1913
+ category: 'interop',
1914
+ priority: 'high',
1915
+ description: 'Multiple agents registered but no cross-agent reuse patterns detected; ensure agents query shared memory',
1916
+ basedOn: 'experience_reuse.jsonl',
1917
+ })
1918
+ }
1919
+
1920
+ // Active agent suggestion
1921
+ const inactiveAgents = agentPerf.filter((a) => {
1922
+ if (!a.lastActive) return false
1923
+ return (now - new Date(a.lastActive).getTime()) > 7 * 86400000
1924
+ })
1925
+ if (inactiveAgents.length > 0) {
1926
+ suggestions.push({
1927
+ category: 'coverage',
1928
+ priority: 'low',
1929
+ description: inactiveAgents.map((a) => a.agentId).join(', ') + ' inactive for >7 days; consider removal or reactivation',
1930
+ basedOn: 'agent registry lastSeenAt',
1931
+ })
1932
+ }
1933
+
1934
+ suggestions.sort((a, b) => {
1935
+ const p = { high: 0, medium: 1, low: 2 }
1936
+ return (p[a.priority] || 2) - (p[b.priority] || 2)
1937
+ })
1938
+
1939
+ return {
1940
+ project,
1941
+ generatedAt: new Date().toISOString(),
1942
+ period,
1943
+ ecosystem,
1944
+ agentPerformance: agentPerf,
1945
+ crossAgentPatterns: crossPatterns,
1946
+ memoryHealth,
1947
+ suggestions,
1948
+ }
1949
+ }
1950
+
1951
+
1952
+ function recordTaskOutcome(args = {}) {
1953
+ const outcome = String(args.outcome || '').trim().toLowerCase()
1954
+ if (!['success', 'failure', 'aborted'].includes(outcome)) {
1955
+ throw new Error('task.record_outcome requires outcome to be success, failure, or aborted')
1956
+ }
1957
+ const agentId = String(args.agentId || runtimeAgentId() || 'unknown').trim()
1958
+ const project = args.project || projectName()
1959
+ const task = String(args.task || '').trim()
1960
+ const summary = String(args.summary || '').trim()
1961
+ const verification = String(args.verification || '').trim()
1962
+ const traceId = String(args.traceId || '').trim() || createId('trace:' + agentId + ':' + project)
1963
+
1964
+ // 1. Record trace event
1965
+ const event = {
1966
+ id: createId('trace'),
1967
+ timestamp: new Date().toISOString(),
1968
+ trace_id: traceId,
1969
+ event_type: 'task_outcome',
1970
+ outcome,
1971
+ payload: { task, summary, verification },
1972
+ agent: agentId,
1973
+ host: 'mcp',
1974
+ project,
1975
+ }
1976
+ appendJsonl(tracePath, event)
1977
+
1978
+ // 2. Auto-claim experience reuse
1979
+ const autoClaims = autoClaimExperienceReuse(event)
1980
+
1981
+ // 3. Update agent registry
1982
+ const now = new Date().toISOString()
1983
+ const agents = readJsonl(agentsPath)
1984
+ const agent = agents.find((a) => a.agentId === agentId && a.project === project)
1985
+ if (agent) {
1986
+ agent.lastSeenAt = now
1987
+ agent.taskCount = (agent.taskCount || 0) + 1
1988
+ agent.successCount = (agent.successCount || 0) + (outcome === 'success' ? 1 : 0)
1989
+ agent.failureCount = (agent.failureCount || 0) + (outcome === 'failure' ? 1 : 0)
1990
+ writeJsonl(agentsPath, agents)
1991
+ }
1992
+
1993
+ return {
1994
+ recorded: true,
1995
+ event: { id: event.id, trace_id: traceId, outcome, agent: agentId, project },
1996
+ agentUpdated: !!agent,
1997
+ autoClaims: autoClaims.length > 0 ? autoClaims : undefined,
1998
+ }
1999
+ }
2000
+
2001
+
2002
+ async function handleIdeaGenerate(args = {}) {
2003
+ const { goal, context = '', constraints = [], numIdeas = 3 } = args
2004
+ const ideas = []
2005
+ for (let i = 0; i < numIdeas; i++) {
2006
+ ideas.push({
2007
+ id: 'idea_' + Date.now() + '_' + i,
2008
+ title: 'Idea #' + (i + 1) + ': ' + goal,
2009
+ description: [
2010
+ 'Goal: ' + goal,
2011
+ context ? 'Context: ' + context : '',
2012
+ constraints.length ? 'Constraints: ' + constraints.join(', ') : '',
2013
+ 'Suggestion: Consider prototyping the idea and measuring its impact.'
2014
+ ].filter(Boolean).join('\n\n')
2015
+ })
1508
2016
  }
1509
- }
1510
-
1511
- function jsonRpcResult(id, result) {
1512
- return { jsonrpc: '2.0', id, result }
1513
- }
1514
-
1515
- function jsonRpcError(id, code, message, data) {
1516
- const error = { code, message }
1517
- if (data !== undefined) error.data = data
1518
- return { jsonrpc: '2.0', id, error }
1519
- }
1520
-
1521
- function send(message) {
1522
- if (message) process.stdout.write(`${JSON.stringify(message)}\n`)
1523
- }
1524
-
1525
- async function handleMessage(message) {
1526
- if (!message || typeof message !== 'object' || message.jsonrpc !== '2.0') return
1527
- if (message.method && message.method.startsWith('notifications/')) return
1528
- if (message.id === undefined) return
1529
-
1530
- try {
1531
- if (message.method === 'initialize') {
1532
- send(
1533
- jsonRpcResult(message.id, {
1534
- protocolVersion: PROTOCOL_VERSION,
1535
- capabilities: { tools: {} },
1536
- serverInfo: SERVER_INFO,
1537
- })
1538
- )
1539
- return
1540
- }
1541
- if (message.method === 'ping') {
1542
- send(jsonRpcResult(message.id, {}))
1543
- return
1544
- }
1545
- if (message.method === 'tools/list') {
1546
- send(jsonRpcResult(message.id, { tools: TOOLS }))
1547
- return
1548
- }
1549
- if (message.method === 'tools/call') {
1550
- const name = message.params && message.params.name
1551
- const args = (message.params && message.params.arguments) || {}
1552
- const result = await callTool(name, args)
1553
- send(
1554
- jsonRpcResult(message.id, {
1555
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1556
- isError: false,
1557
- })
1558
- )
1559
- return
1560
- }
1561
- send(jsonRpcError(message.id, -32601, `Method not found: ${message.method}`))
1562
- } catch (error) {
1563
- send(jsonRpcError(message.id, -32603, error.message || String(error)))
2017
+ return {
2018
+ ideas: ideas,
2019
+ generatedAt: new Date().toISOString()
1564
2020
  }
1565
2021
  }
1566
2022
 
1567
- const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity })
1568
-
1569
- rl.on('line', (line) => {
1570
- let message
1571
- try {
1572
- message = JSON.parse(line)
1573
- } catch (_) {
1574
- return
1575
- }
1576
- handleMessage(message).catch((error) => {
1577
- send(jsonRpcError(message && message.id, -32603, error.message || String(error)))
1578
- })
1579
- })
1580
-
1581
- rl.on('close', () => {
1582
- if (require.main === module) {
1583
- process.exit(0)
1584
- }
1585
- })
1586
-
1587
- module.exports = { TOOLS, callTool, handleMessage, readJsonl, appendJsonl, rl }
2023
+ module.exports = { TOOLS, callTool, handleMessage, readJsonl, appendJsonl, rl }