mio-agent-runtime 0.3.3 → 0.3.4

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.
Files changed (2) hide show
  1. package/observe/observer.js +215 -1
  2. package/package.json +1 -1
@@ -75,10 +75,22 @@ function loadState(home) {
75
75
  buffers: parsed.buffers || {},
76
76
  openTurns: parsed.openTurns || {},
77
77
  contexts: parsed.contexts || {},
78
+ codexFiles: parsed.codexFiles || {},
79
+ codexBuffers: parsed.codexBuffers || {},
80
+ codexTurns: parsed.codexTurns || {},
78
81
  }
79
82
  }
80
83
  } catch (_) {}
81
- return { version: 1, files: {}, buffers: {}, openTurns: {}, contexts: {} }
84
+ return {
85
+ version: 1,
86
+ files: {},
87
+ buffers: {},
88
+ openTurns: {},
89
+ contexts: {},
90
+ codexFiles: {},
91
+ codexBuffers: {},
92
+ codexTurns: {},
93
+ }
82
94
  }
83
95
 
84
96
  function saveState(home, state) {
@@ -109,6 +121,31 @@ function listTranscripts() {
109
121
  return out
110
122
  }
111
123
 
124
+ function codexConfigDir() {
125
+ return process.env.CODEX_CONFIG_DIR || path.join(os.homedir(), '.codex')
126
+ }
127
+
128
+ function listCodexTranscripts() {
129
+ const root = path.join(codexConfigDir(), 'sessions')
130
+ const out = []
131
+ if (!fs.existsSync(root)) return out
132
+ const walk = (dir) => {
133
+ let entries
134
+ try {
135
+ entries = fs.readdirSync(dir, { withFileTypes: true })
136
+ } catch (_) {
137
+ return
138
+ }
139
+ for (const entry of entries) {
140
+ const full = path.join(dir, entry.name)
141
+ if (entry.isDirectory()) walk(full)
142
+ else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) out.push(full)
143
+ }
144
+ }
145
+ walk(root)
146
+ return out
147
+ }
148
+
112
149
  function fileMeta(file) {
113
150
  const dirName = path.basename(path.dirname(file))
114
151
  return {
@@ -343,6 +380,178 @@ function emitWorkbuddyOutcome(turn, sinks, state) {
343
380
  return true
344
381
  }
345
382
 
383
+ // ---------------------------------------------------------------------------
384
+ // Codex session turn machine (passive observation)
385
+ // ---------------------------------------------------------------------------
386
+
387
+ // Codex rollout transcripts are append-only JSONL under
388
+ // ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl. The first session_meta line
389
+ // carries the workspace cwd; task_started / task_complete / turn_aborted mark
390
+ // turn boundaries, agent_message carries the running answer text.
391
+ function createCodexTurn() {
392
+ return {
393
+ id: null,
394
+ ts: null,
395
+ sessionId: null,
396
+ cwd: null,
397
+ lastAgent: null,
398
+ aborted: false,
399
+ }
400
+ }
401
+
402
+ function feedCodexLine(turn, obj) {
403
+ if (!obj || typeof obj !== 'object') return {}
404
+ if (obj.type === 'session_meta') {
405
+ const p = obj.payload || {}
406
+ if (!turn.cwd && p.cwd) turn.cwd = p.cwd
407
+ if (!turn.sessionId && (p.session_id || p.id)) turn.sessionId = p.session_id || p.id
408
+ return {}
409
+ }
410
+ if (obj.type !== 'event_msg') return {}
411
+ const p = obj.payload || {}
412
+ if (p.type === 'task_started') {
413
+ // A turn that started earlier but never completed is discarded.
414
+ turn.id = p.turn_id || null
415
+ turn.ts = p.started_at ? Number(p.started_at) * 1000 : null
416
+ turn.lastAgent = null
417
+ turn.aborted = false
418
+ return {}
419
+ }
420
+ if (p.type === 'agent_message' && p.message) {
421
+ turn.lastAgent = p.message
422
+ return {}
423
+ }
424
+ if (p.type === 'task_complete') {
425
+ const finalized = {
426
+ id: turn.id,
427
+ ts: p.completed_at ? Number(p.completed_at) * 1000 : turn.ts,
428
+ sessionId: turn.sessionId,
429
+ cwd: turn.cwd,
430
+ project: path.basename(turn.cwd || '') || 'unknown',
431
+ lastAgent: p.last_agent_message || turn.lastAgent,
432
+ aborted: false,
433
+ }
434
+ turn.id = null
435
+ turn.lastAgent = null
436
+ return { finalized: finalized }
437
+ }
438
+ if (p.type === 'turn_aborted') {
439
+ const finalized = {
440
+ id: turn.id,
441
+ ts: p.completed_at ? Number(p.completed_at) * 1000 : turn.ts,
442
+ sessionId: turn.sessionId,
443
+ cwd: turn.cwd,
444
+ project: path.basename(turn.cwd || '') || 'unknown',
445
+ lastAgent: turn.lastAgent,
446
+ aborted: true,
447
+ }
448
+ turn.id = null
449
+ turn.lastAgent = null
450
+ return { finalized: finalized }
451
+ }
452
+ return {}
453
+ }
454
+
455
+ function emitCodexOutcome(turn, sinks, state) {
456
+ if (!turn.lastAgent) return false
457
+ const summary = cleanText(turn.lastAgent)
458
+ if (!summary) return false
459
+ const outcome = turn.aborted ? 'aborted' : 'success'
460
+ const turnToken = String(turn.id || 'unknown').slice(0, 8)
461
+
462
+ sinks.trace({
463
+ trace_id: 'codex:' + (turn.sessionId || 'unknown') + ':' + turnToken,
464
+ event_type: 'task_outcome',
465
+ outcome: outcome,
466
+ payload: {
467
+ host: 'codex',
468
+ project: turn.project,
469
+ sessionId: turn.sessionId,
470
+ turnId: turn.id,
471
+ toolCalls: 0,
472
+ errors: 0,
473
+ summary: summary.slice(0, 300),
474
+ source: 'codex-observer',
475
+ },
476
+ agent: 'codex',
477
+ host: 'codex',
478
+ project: turn.project,
479
+ })
480
+
481
+ if (outcome === 'success' && summary.length >= 10) {
482
+ sinks.memory({
483
+ kind: 'note',
484
+ content: '[codex] 任务完成: ' + summary.slice(0, 160),
485
+ tags: ['task-outcome', 'codex', turn.project],
486
+ project: turn.project,
487
+ source: 'codex-observer',
488
+ })
489
+ var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
490
+ updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80))
491
+ }
492
+ return true
493
+ }
494
+
495
+ function runCodexCycle(home, state, sinks, opts) {
496
+ const out = { files: 0, lines: 0, outcomes: 0 }
497
+ const files = listCodexTranscripts()
498
+ const now = Date.now()
499
+ const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
500
+
501
+ for (const file of files) {
502
+ out.files += 1
503
+ let st
504
+ try {
505
+ st = fs.statSync(file)
506
+ } catch (_) {
507
+ continue
508
+ }
509
+ const info = state.codexFiles[file] || { size: 0 }
510
+ if (!state.codexFiles[file] && now - st.mtimeMs > maxAgeMs) continue
511
+ const size = st.size
512
+ if (size < info.size) {
513
+ info.size = 0
514
+ state.codexBuffers[file] = ''
515
+ }
516
+
517
+ if (!state.codexTurns[file]) state.codexTurns[file] = createCodexTurn()
518
+ const turn = state.codexTurns[file]
519
+
520
+ if (size > info.size) {
521
+ let chunk = ''
522
+ try {
523
+ const fd = fs.openSync(file, 'r')
524
+ const buf = Buffer.alloc(size - info.size)
525
+ fs.readSync(fd, buf, 0, buf.length, info.size)
526
+ fs.closeSync(fd)
527
+ chunk = buf.toString('utf8')
528
+ } catch (_) {
529
+ continue
530
+ }
531
+ const pending = (state.codexBuffers[file] || '') + chunk
532
+ const lines = pending.split(/\r?\n/)
533
+ state.codexBuffers[file] = lines.pop()
534
+ for (const line of lines) {
535
+ if (!line.trim()) continue
536
+ out.lines += 1
537
+ let obj
538
+ try {
539
+ obj = JSON.parse(line)
540
+ } catch (_) {
541
+ continue
542
+ }
543
+ const result = feedCodexLine(turn, obj)
544
+ if (result.finalized) {
545
+ if (emitCodexOutcome(result.finalized, sinks, state)) out.outcomes += 1
546
+ }
547
+ }
548
+ state.codexFiles[file] = { size: size, mtimeMs: st.mtimeMs }
549
+ }
550
+ }
551
+
552
+ return out
553
+ }
554
+
346
555
  // ---------------------------------------------------------------------------
347
556
  // cycle
348
557
  // ---------------------------------------------------------------------------
@@ -420,6 +629,11 @@ function runCycle(home, state, sinks, opts) {
420
629
  }
421
630
  }
422
631
 
632
+ const codexRes = runCodexCycle(home, state, sinks, opts)
633
+ out.files += codexRes.files
634
+ out.lines += codexRes.lines
635
+ out.outcomes += codexRes.outcomes
636
+
423
637
  saveState(home, state)
424
638
  return out
425
639
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",