mio-agent-runtime 0.5.16 → 0.5.22

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.
@@ -86,6 +86,9 @@ function loadState(home) {
86
86
  hmSeeded: parsed.hmSeeded || false,
87
87
  hmLastMsgId: parsed.hmLastMsgId || 0,
88
88
  hmTurns: parsed.hmTurns || {},
89
+ clFiles: parsed.clFiles || {},
90
+ clBuffers: parsed.clBuffers || {},
91
+ clTurns: parsed.clTurns || {},
89
92
  }
90
93
  }
91
94
  } catch (_) {}
@@ -106,6 +109,9 @@ function loadState(home) {
106
109
  hmSeeded: false,
107
110
  hmLastMsgId: 0,
108
111
  hmTurns: {},
112
+ clFiles: {},
113
+ clBuffers: {},
114
+ clTurns: {},
109
115
  }
110
116
  }
111
117
 
@@ -282,9 +288,9 @@ const CONTEXT_MAX = 6
282
288
  // Maintain a compact 'recent Mio context' block inside <workspace>/AGENTS.md,
283
289
  // which WorkBuddy's MemoryLoader auto-injects into every session. This makes
284
290
  // retrieval guaranteed (not dependent on the model deciding to read memory).
285
- function updateProjectContext(state, cwd, line) {
291
+ function updateProjectContext(state, cwd, line, targetFile) {
286
292
  if (!cwd || !line) return
287
- const target = path.join(cwd, 'AGENTS.md')
293
+ const target = path.join(cwd, targetFile || 'AGENTS.md')
288
294
  const list = state.contexts[cwd] || []
289
295
  list.unshift(line)
290
296
  if (list.length > CONTEXT_MAX) list.pop()
@@ -1133,6 +1139,275 @@ function runHermesCycle(home, state, sinks, opts) {
1133
1139
  }
1134
1140
 
1135
1141
 
1142
+ // ---------------------------------------------------------------------------
1143
+ // Claude Code session observation (passive, transcript JSONL tailing)
1144
+ // ---------------------------------------------------------------------------
1145
+
1146
+ // Claude Code writes append-only JSONL transcripts under
1147
+ // <configDir>/projects/<munged-cwd>/<sessionId>.jsonl. Each line carries
1148
+ // {type, message:{role, content}, cwd, sessionId, timestamp, uuid, ...}.
1149
+ // Assistant content items are text/thinking/tool_use; tool results come back
1150
+ // as user lines whose content array holds tool_result items with is_error.
1151
+ // A user prompt line (string content or a text item) closes the previous
1152
+ // turn; sidechain lines (subagent transcripts) are skipped as noise.
1153
+
1154
+ function claudeConfigDir() {
1155
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
1156
+ }
1157
+
1158
+ function claudeProjectsDir() {
1159
+ return path.join(claudeConfigDir(), 'projects')
1160
+ }
1161
+
1162
+ function listClaudeTranscripts() {
1163
+ const root = claudeProjectsDir()
1164
+ const out = []
1165
+ if (!fs.existsSync(root)) return out
1166
+ const walk = (dir) => {
1167
+ let entries
1168
+ try {
1169
+ entries = fs.readdirSync(dir, { withFileTypes: true })
1170
+ } catch (_) {
1171
+ return
1172
+ }
1173
+ for (const entry of entries) {
1174
+ const full = path.join(dir, entry.name)
1175
+ if (entry.isDirectory()) walk(full)
1176
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full)
1177
+ }
1178
+ }
1179
+ walk(root)
1180
+ return out
1181
+ }
1182
+
1183
+ function createClaudeTurn(meta) {
1184
+ meta = meta || {}
1185
+ return {
1186
+ id: null,
1187
+ ts: null,
1188
+ sessionId: meta.sessionId || null,
1189
+ cwd: meta.cwd || null,
1190
+ project: meta.cwd ? path.basename(meta.cwd) : 'unknown',
1191
+ lastText: null,
1192
+ toolCount: 0,
1193
+ errors: [],
1194
+ apiError: false,
1195
+ }
1196
+ }
1197
+
1198
+ function claudeContentItems(obj) {
1199
+ const content = obj && obj.message && obj.message.content
1200
+ if (typeof content === 'string') return [{ type: 'text', text: content }]
1201
+ return Array.isArray(content) ? content.filter(Boolean) : []
1202
+ }
1203
+
1204
+ function finishClaudeTurn(turn, outcome) {
1205
+ return {
1206
+ id: turn.id,
1207
+ ts: turn.ts,
1208
+ sessionId: turn.sessionId,
1209
+ cwd: turn.cwd,
1210
+ project: turn.project,
1211
+ lastText: turn.lastText,
1212
+ toolCount: turn.toolCount,
1213
+ errors: turn.errors.slice(),
1214
+ apiError: turn.apiError,
1215
+ outcome: outcome,
1216
+ }
1217
+ }
1218
+
1219
+ function feedClaudeLine(turn, obj) {
1220
+ if (!obj || typeof obj !== 'object') return { next: turn }
1221
+ // Subagent (sidechain) transcripts duplicate main-thread work; skip them.
1222
+ if (obj.isSidechain === true) return { next: turn }
1223
+ if (obj.sessionId) turn.sessionId = obj.sessionId
1224
+ if (obj.cwd) {
1225
+ turn.cwd = obj.cwd
1226
+ turn.project = path.basename(obj.cwd)
1227
+ }
1228
+
1229
+ if (obj.type === 'user') {
1230
+ const items = claudeContentItems(obj)
1231
+ let hasToolError = false
1232
+ for (const item of items) {
1233
+ if (item.type === 'tool_result' && item.is_error) {
1234
+ turn.errors.push({ toolUseId: item.tool_use_id || 'unknown' })
1235
+ hasToolError = true
1236
+ }
1237
+ }
1238
+ if (turn.errors.length > 20) turn.errors.splice(0, turn.errors.length - 20)
1239
+
1240
+ // tool_result-only user lines continue the current turn; a string content
1241
+ // or text item means a fresh prompt, which closes the previous turn.
1242
+ const isPrompt =
1243
+ typeof (obj.message && obj.message.content) === 'string' ||
1244
+ items.some((item) => item.type === 'text')
1245
+ if (isPrompt) {
1246
+ let finalized = null
1247
+ if (turn.id) {
1248
+ finalized = finishClaudeTurn(turn, turn.apiError ? 'failure' : turn.lastText ? 'success' : 'aborted')
1249
+ }
1250
+ const next = createClaudeTurn({ sessionId: turn.sessionId, cwd: turn.cwd })
1251
+ next.id = obj.uuid || null
1252
+ next.ts = obj.timestamp ? Date.parse(obj.timestamp) || Date.now() : Date.now()
1253
+ return { finalized, next }
1254
+ }
1255
+ return { next: turn }
1256
+ }
1257
+
1258
+ if (obj.type === 'assistant') {
1259
+ for (const item of claudeContentItems(obj)) {
1260
+ if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
1261
+ turn.lastText = item.text
1262
+ } else if (item.type === 'tool_use') {
1263
+ turn.toolCount += 1
1264
+ }
1265
+ }
1266
+ if (obj.isApiErrorMessage === true || obj.error) turn.apiError = true
1267
+ const ts = obj.timestamp ? Date.parse(obj.timestamp) : NaN
1268
+ if (Number.isFinite(ts)) turn.ts = ts
1269
+ return { next: turn }
1270
+ }
1271
+
1272
+ return { next: turn }
1273
+ }
1274
+
1275
+ function emitClaudeOutcome(turn, sinks, state) {
1276
+ if (!turn.id) return false
1277
+ const summary = cleanText(turn.lastText)
1278
+ if (!summary && !turn.apiError) return false
1279
+ const outcome = turn.apiError && turn.outcome !== 'failure' ? 'failure' : turn.outcome
1280
+ const turnToken = String(turn.id).slice(0, 8)
1281
+ const tsToken = turn.ts ? Math.floor(turn.ts / 1000) : Math.floor(Date.now() / 1000)
1282
+
1283
+ sinks.trace({
1284
+ trace_id: 'claude:' + (turn.sessionId || 'unknown') + ':' + turnToken + ':' + tsToken,
1285
+ event_type: 'task_outcome',
1286
+ outcome: outcome,
1287
+ payload: {
1288
+ host: 'claude-code',
1289
+ project: turn.project,
1290
+ sessionId: turn.sessionId,
1291
+ turnId: turn.id,
1292
+ toolCalls: turn.toolCount,
1293
+ errors: turn.errors.length,
1294
+ summary: summary.slice(0, 300),
1295
+ source: 'claude-observer',
1296
+ },
1297
+ agent: 'claude-code',
1298
+ host: 'claude-code',
1299
+ project: turn.project,
1300
+ })
1301
+
1302
+ if (outcome === 'success' && summary.length >= 10) {
1303
+ sinks.memory({
1304
+ kind: 'note',
1305
+ content: '[claude-code] 任务完成: ' + summary.slice(0, 160),
1306
+ tags: ['task-outcome', 'claude-code', turn.project],
1307
+ project: turn.project,
1308
+ source: 'claude-observer',
1309
+ })
1310
+ // Claude Code auto-loads <workspace>/CLAUDE.md, so write the context
1311
+ // block there instead of AGENTS.md.
1312
+ if (turn.cwd) {
1313
+ var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
1314
+ updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80), 'CLAUDE.md')
1315
+ }
1316
+ }
1317
+
1318
+ for (const err of turn.errors) {
1319
+ sinks.trace({
1320
+ trace_id: 'claude:' + (turn.sessionId || 'unknown') + ':' + turnToken + ':' + String(err.toolUseId || 'err').slice(0, 12),
1321
+ event_type: 'error',
1322
+ outcome: 'error',
1323
+ payload: {
1324
+ host: 'claude-code',
1325
+ project: turn.project,
1326
+ sessionId: turn.sessionId,
1327
+ turnId: turn.id,
1328
+ toolUseId: err.toolUseId,
1329
+ },
1330
+ agent: 'claude-code',
1331
+ host: 'claude-code',
1332
+ project: turn.project,
1333
+ })
1334
+ }
1335
+ return true
1336
+ }
1337
+
1338
+ function runClaudeCycle(home, state, sinks, opts) {
1339
+ const out = { files: 0, lines: 0, outcomes: 0 }
1340
+ const files = listClaudeTranscripts()
1341
+ const now = Date.now()
1342
+ const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
1343
+
1344
+ for (const file of files) {
1345
+ out.files += 1
1346
+ let st
1347
+ try {
1348
+ st = fs.statSync(file)
1349
+ } catch (_) {
1350
+ continue
1351
+ }
1352
+ const info = state.clFiles[file] || { size: 0 }
1353
+ if (!state.clFiles[file] && now - st.mtimeMs > maxAgeMs) continue
1354
+ const size = st.size
1355
+ if (size < info.size) {
1356
+ info.size = 0
1357
+ state.clBuffers[file] = ''
1358
+ }
1359
+
1360
+ let turn = state.clTurns[file] || createClaudeTurn()
1361
+
1362
+ if (size > info.size) {
1363
+ let chunk = ''
1364
+ try {
1365
+ const fd = fs.openSync(file, 'r')
1366
+ const buf = Buffer.alloc(size - info.size)
1367
+ fs.readSync(fd, buf, 0, buf.length, info.size)
1368
+ fs.closeSync(fd)
1369
+ chunk = buf.toString('utf8')
1370
+ } catch (_) {
1371
+ continue
1372
+ }
1373
+ const pending = (state.clBuffers[file] || '') + chunk
1374
+ const lines = pending.split(/\r?\n/)
1375
+ state.clBuffers[file] = lines.pop()
1376
+ for (const line of lines) {
1377
+ if (!line.trim()) continue
1378
+ out.lines += 1
1379
+ let obj
1380
+ try {
1381
+ obj = JSON.parse(line)
1382
+ } catch (_) {
1383
+ continue
1384
+ }
1385
+ const result = feedClaudeLine(turn, obj)
1386
+ turn = result.next
1387
+ if (result.finalized) {
1388
+ if (emitClaudeOutcome(result.finalized, sinks, state)) out.outcomes += 1
1389
+ }
1390
+ }
1391
+ state.clFiles[file] = { size: size, mtimeMs: st.mtimeMs }
1392
+ }
1393
+
1394
+ // Idle finalize: the session has answered but no new prompt arrived yet.
1395
+ if (turn.id && turn.lastText && now - st.mtimeMs > IDLE_MS) {
1396
+ if (emitClaudeOutcome(finishClaudeTurn(turn, turn.apiError ? 'failure' : 'success'), sinks, state)) {
1397
+ out.outcomes += 1
1398
+ }
1399
+ state.clTurns[file] = null
1400
+ } else if (turn.id) {
1401
+ state.clTurns[file] = turn
1402
+ } else {
1403
+ state.clTurns[file] = null
1404
+ }
1405
+ }
1406
+
1407
+ return out
1408
+ }
1409
+
1410
+
1136
1411
  // ---------------------------------------------------------------------------
1137
1412
  // cycle
1138
1413
  // ---------------------------------------------------------------------------
@@ -1225,6 +1500,11 @@ function runCycle(home, state, sinks, opts) {
1225
1500
  out.lines += hmRes.lines
1226
1501
  out.outcomes += hmRes.outcomes
1227
1502
 
1503
+ const clRes = runClaudeCycle(home, state, sinks, opts)
1504
+ out.files += clRes.files
1505
+ out.lines += clRes.lines
1506
+ out.outcomes += clRes.outcomes
1507
+
1228
1508
  saveState(home, state)
1229
1509
  return out
1230
1510
  }
@@ -1317,6 +1597,10 @@ module.exports = {
1317
1597
  runOpencodeCycle,
1318
1598
  runHermesCycle,
1319
1599
  hermesDbPath,
1600
+ claudeProjectsDir,
1601
+ runClaudeCycle,
1602
+ createClaudeTurn,
1603
+ feedClaudeLine,
1320
1604
  createSinks,
1321
1605
  startForeground,
1322
1606
  startBackground,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.5.16",
3
+ "version": "0.5.22",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -9,28 +9,30 @@
9
9
  },
10
10
  "files": [
11
11
  "bin/",
12
- "server/",
13
- "adapters/",
14
- "observe/",
15
- "scripts/",
16
- "README.md",
12
+ "server/",
13
+ "adapters/",
14
+ "observe/",
15
+ "scripts/",
16
+ "README.md",
17
17
  "!server/mio-intelligence-mcp/__tests__"
18
18
  ],
19
- "scripts": {
20
- "check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check server/runtime-modules.js && node --check server/host-capabilities.js && node --check server/evolution-cutover.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js && node --check scripts/verify-packed-runtime.js",
21
- "prepack": "npm run check",
22
- "verify:pack-install": "node scripts/verify-packed-runtime.js"
23
- },
24
- "engines": {
25
- "node": ">=18"
26
- },
27
- "dependencies": {
28
- "@akemi-mio/runtime-contracts": "^0.1.0",
29
- "@akemi-mio/runtime-foundation": "^0.1.0",
30
- "@akemi-mio/experience-memory": "^0.1.0",
31
- "@akemi-mio/evolution-learning": "^0.1.0",
32
- "@akemi-mio/evolution-strategy": "^0.1.0",
33
- "@akemi-mio/evolution-safety": "^0.1.0",
34
- "@akemi-mio/evolution-scheduler": "^0.1.0"
35
- }
36
- }
19
+ "scripts": {
20
+ "check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check adapters/claude-code.js && node --check server/runtime-modules.js && node --check server/memory-store.js && node --check server/retention.js && node --check server/digest.js && node --check server/host-capabilities.js && node --check server/evolution-cutover.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js && node --check scripts/verify-packed-runtime.js",
21
+ "prepack": "npm run check",
22
+ "verify:pack-install": "node scripts/verify-packed-runtime.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "dependencies": {
28
+ "@akemi-mio/runtime-contracts": "^0.1.0",
29
+ "@akemi-mio/runtime-foundation": "^0.1.0",
30
+ "@akemi-mio/experience-memory": "^0.1.0",
31
+ "@akemi-mio/evolution-learning": "^0.1.0",
32
+ "@akemi-mio/evolution-strategy": "^0.1.0",
33
+ "@akemi-mio/evolution-safety": "^0.1.0",
34
+ "@akemi-mio/evolution-scheduler": "^0.1.0",
35
+ "@akemi-mio/observer": "^0.1.0",
36
+ "@akemi-mio/insight": "^0.1.0"
37
+ }
38
+ }