mio-agent-runtime 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,79 +1,93 @@
1
- # mio-agent-runtime
2
-
3
- Mio Agent Runtime 的可发布 CLI 包。
4
-
5
- 安装一次,所有 Agent 自动接入 Mio。
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install -g mio-agent-runtime
11
- ```
12
-
13
- ## Commands
14
-
15
- ```text
16
- mio init Initialize MIO_HOME
17
- mio mcp Start Mio MCP server (stdio)
18
- mio install <host> Install Mio into a host (codex|opencode|workbuddy)
19
- mio status Show runtime and adapter status
20
- mio agents List installed agents
21
- mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes (foreground)
22
- mio observe --start|--stop|--status|--once Manage the background observer daemon
23
- mio --json status Machine-readable status
24
- mio --json agents Machine-readable agents
25
- ```
26
-
27
- ## Host notes
28
-
29
- - OpenCode uses `~/.config/opencode/opencode.json` for MCP registration and
30
- `~/.config/opencode/AGENTS.md` for global instructions. `mio install opencode`
31
- writes both files and injects Mio usage rules into `AGENTS.md` so OpenCode
32
- calls Mio memory/observer/policy automatically. If OpenCode is already
33
- running, restart it once so it reloads both files.
34
- - WorkBuddy uses `~/.workbuddy/mcp.json` and requires each custom MCP server to
35
- be approved in `~/.workbuddy/mcp-approvals.json`. `mio install workbuddy`
36
- writes both files and injects Mio usage rules into
37
- `~/.workbuddy/CODEBUDDY.md` (user-level memory rules, auto-loaded by
38
- WorkBuddy into every session; the CLI hardcodes the CodeBuddy product name, so the file is CODEBUDDY.md not WORKBUDDY.md) plus `<workspace>/AGENTS.md` (project-level).
39
- If WorkBuddy is already running, restart it once so it reloads the approval
40
- and memory rule files.
41
-
42
- ## Passive observer (`mio observe`)
43
-
44
- WorkBuddy 的默认模型不一定会主动调用 Mio MCP 工具,即使规则已注入
45
- (`CODEBUDDY.md` / `AGENTS.md`)。`mio observe` 是一个不依赖模型自觉的
46
- 兜底:它持续监听 `~/.workbuddy/projects/**/*.jsonl` 会话记录,把每个任务的
47
- 结果(`task_outcome`)、工具调用错误(`error`)以及完成任务的摘要自动写入
48
- `MIO_HOME/traces.jsonl` 和 `MIO_HOME/memory.jsonl`(与 MCP 服务同一套
49
- schema,`source=workbuddy-observer`)。
50
-
51
- - `mio install <host>` 会自动执行一次回填并启动后台观察器。
52
-
53
- - 除了写入 `MIO_HOME`,观察器还会把任务摘要**写回 WorkBuddy 自己的记忆文件**
54
- (`<workspace>/.workbuddy/memory/YYYY-MM-DD.md`,标为 `## Mio 自动摘要`)。
55
- WorkBuddy 的系统提示要求模型每次会话维护并阅读这些文件,所以下一次会话
56
- WorkBuddy 会**被动取到** Mio 记录的内容——单 Agent 的「存 + 取」闭环不依赖
57
- 模型调用 Mio 工具。
58
- - 已处理的文件偏移记录在 `MIO_HOME/observe-state.json`,不会重复写入。
59
- - 只回填最近 24h 内更新的会话,避免把历史记录灌入记忆库。
60
-
61
- ## MIO_HOME
62
-
63
- 默认 `~/.mio-intelligence`,可通过环境变量 `MIO_HOME` 覆盖。
64
-
65
- ## Package layout
66
-
67
- ```text
68
- packages/mio-cli
69
- ├── bin/mio.js
70
- ├── adapters/
71
- │ ├── codex.js
72
- │ ├── opencode.js
73
- │ └── workbuddy.js
74
- ├── observe/
75
- │ └── observer.js
76
- ├── server/
77
- │ └── mio-intelligence-mcp/index.js
78
- └── package.json
79
- ```
1
+ # mio-agent-runtime
2
+
3
+ Mio Agent Runtime 的可发布 CLI 包。
4
+
5
+ 安装一次,所有 Agent 自动接入 Mio。
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g mio-agent-runtime
11
+ ```
12
+
13
+ > 新机器 / 升级:请安装 `>=0.3.4`(Codex 被动观察)或 `>=0.3.5`
14
+ > (Codex + OpenCode 被动观察)。旧版本只有主动 MCP 规则注入,
15
+ > 没有「无感存/取」。安装后执行一次 `mio init && mio install <host>`。
16
+
17
+ ## Commands
18
+
19
+ ```text
20
+ mio init Initialize MIO_HOME
21
+ mio mcp Start Mio MCP server (stdio)
22
+ mio install <host> Install Mio into a host (codex|opencode|workbuddy)
23
+ mio status Show runtime and adapter status
24
+ mio agents List installed agents
25
+ mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes (foreground)
26
+ mio observe --start|--stop|--status|--once Manage the background observer daemon
27
+ mio --json status Machine-readable status
28
+ mio --json agents Machine-readable agents
29
+ ```
30
+
31
+ ## Host notes
32
+
33
+ - OpenCode uses `~/.config/opencode/opencode.json` for MCP registration and
34
+ `~/.config/opencode/AGENTS.md` for global instructions. `mio install opencode`
35
+ writes both files and injects Mio usage rules into `AGENTS.md` so OpenCode
36
+ calls Mio memory/observer/policy automatically. If OpenCode is already
37
+ running, restart it once so it reloads both files. `mio observe` also
38
+ passively tails the OpenCode session database (`opencode db`) without
39
+ requiring the model to call Mio tools.
40
+ - WorkBuddy uses `~/.workbuddy/mcp.json` and requires each custom MCP server to
41
+ be approved in `~/.workbuddy/mcp-approvals.json`. `mio install workbuddy`
42
+ writes both files and injects Mio usage rules into
43
+ `~/.workbuddy/CODEBUDDY.md` (user-level memory rules, auto-loaded by
44
+ WorkBuddy into every session; the CLI hardcodes the CodeBuddy product name, so the file is CODEBUDDY.md not WORKBUDDY.md) plus `<workspace>/AGENTS.md` (project-level).
45
+ If WorkBuddy is already running, restart it once so it reloads the approval
46
+ and memory rule files.
47
+
48
+ ## Passive observer (`mio observe`)
49
+
50
+ WorkBuddy 的默认模型不一定会主动调用 Mio MCP 工具,即使规则已注入
51
+ (`CODEBUDDY.md` / `AGENTS.md`)。`mio observe` 是一个不依赖模型自觉的
52
+ 兜底:它持续监听 `~/.workbuddy/projects/**/*.jsonl` 会话记录,把每个任务的
53
+ 结果(`task_outcome`)、工具调用错误(`error`)以及完成任务的摘要自动写入
54
+ `MIO_HOME/traces.jsonl` 和 `MIO_HOME/memory.jsonl`(与 MCP 服务同一套
55
+ schema,`source=workbuddy-observer`)。
56
+
57
+ - `mio install <host>` 会自动执行一次回填并启动后台观察器。
58
+
59
+ - 除了写入 `MIO_HOME`,观察器还会把任务摘要**写回 WorkBuddy 自己的记忆文件**
60
+ (`<workspace>/.workbuddy/memory/YYYY-MM-DD.md`,标为 `## Mio 自动摘要`)。
61
+ WorkBuddy 的系统提示要求模型每次会话维护并阅读这些文件,所以下一次会话
62
+ WorkBuddy 会**被动取到** Mio 记录的内容——单 Agent 的「存 + 取」闭环不依赖
63
+ 模型调用 Mio 工具。
64
+ - 已处理的文件偏移记录在 `MIO_HOME/observe-state.json`,不会重复写入。
65
+ - 只回填最近 24h 内更新的会话,避免把历史记录灌入记忆库。
66
+
67
+ OpenCode 会话不依赖模型自觉:`mio observe` 通过 `opencode db` 增量读取
68
+ `~/.local/share/opencode/opencode.db`(Windows 下该文件被 OpenCode 独占锁定,
69
+ 必须经 opencode 自带 CLI 读取),按 `part` 表的 rowid 做增量游标,只传输
70
+ `text`/`tool` 两种 part,把每个用户回合的 `task_outcome` 与工具错误写入
71
+ `MIO_HOME/traces.jsonl` / `memory.jsonl`(`source=opencode-observer`),并同样
72
+ 把摘要写回 `<workspace>/AGENTS.md` 的 `MIO_CONTEXT` 块,供 OpenCode 下一会话
73
+ 被动取用。首次启动只回填最近 24h 的 part,避免历史会话灌入记忆库。
74
+
75
+ ## MIO_HOME
76
+
77
+ 默认 `~/.mio-intelligence`,可通过环境变量 `MIO_HOME` 覆盖。
78
+
79
+ ## Package layout
80
+
81
+ ```text
82
+ packages/mio-cli
83
+ ├── bin/mio.js
84
+ ├── adapters/
85
+ │ ├── codex.js
86
+ │ ├── opencode.js
87
+ │ └── workbuddy.js
88
+ ├── observe/
89
+ │ └── observer.js
90
+ ├── server/
91
+ │ └── mio-intelligence-mcp/index.js
92
+ └── package.json
93
+ ```
package/bin/mio.js CHANGED
@@ -68,6 +68,7 @@ function statusCommand(useJson) {
68
68
  codexInstalled: installed.codex,
69
69
  opencodeInstalled: installed.opencode,
70
70
  workbuddyInstalled: installed.workbuddy,
71
+ opencodeObserverReady: fs.existsSync(observer.opencodeDbPath()),
71
72
  observerRunning: observer.isRunning(MIO_HOME),
72
73
  observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
73
74
  }
@@ -78,7 +79,8 @@ function statusCommand(useJson) {
78
79
  console.log(`MIO_HOME: ${MIO_HOME}`)
79
80
  console.log(`MCP Server: ${SERVER_SCRIPT}`)
80
81
  console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
81
- console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
82
+ console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
83
+ console.log(`OpenCode observer: ${fs.existsSync(observer.opencodeDbPath()) ? 'ready' : 'no database'}`)
82
84
  console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
83
85
  console.log(`Observer: ${observer.isRunning(MIO_HOME) ? 'running (pid ' + observer.readPid(MIO_HOME) + ')' : 'not running'}`)
84
86
  }
@@ -5,7 +5,7 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const os = require('os')
7
7
  const crypto = require('crypto')
8
- const { spawn } = require('child_process')
8
+ const { spawn, spawnSync } = require('child_process')
9
9
 
10
10
  const INTERVAL_MS = 3000
11
11
  // A WorkBuddy turn is considered finished when its transcript file has not
@@ -78,6 +78,11 @@ function loadState(home) {
78
78
  codexFiles: parsed.codexFiles || {},
79
79
  codexBuffers: parsed.codexBuffers || {},
80
80
  codexTurns: parsed.codexTurns || {},
81
+ ocSeeded: parsed.ocSeeded || false,
82
+ ocLastRowid: parsed.ocLastRowid || 0,
83
+ ocDbMtime: parsed.ocDbMtime || null,
84
+ ocPending: parsed.ocPending || false,
85
+ ocTurns: parsed.ocTurns || {},
81
86
  }
82
87
  }
83
88
  } catch (_) {}
@@ -90,6 +95,11 @@ function loadState(home) {
90
95
  codexFiles: {},
91
96
  codexBuffers: {},
92
97
  codexTurns: {},
98
+ ocSeeded: false,
99
+ ocLastRowid: 0,
100
+ ocDbMtime: null,
101
+ ocPending: false,
102
+ ocTurns: {},
93
103
  }
94
104
  }
95
105
 
@@ -552,6 +562,327 @@ function runCodexCycle(home, state, sinks, opts) {
552
562
  return out
553
563
  }
554
564
 
565
+ // ---------------------------------------------------------------------------
566
+ // OpenCode session observation (passive, via `opencode db`)
567
+ // ---------------------------------------------------------------------------
568
+
569
+ // OpenCode stores everything in ~/.local/share/opencode/opencode.db (SQLite).
570
+ // On Windows the db file is held with an exclusive lock while OpenCode runs,
571
+ // so we cannot open it with node:sqlite directly. Instead we shell out to the
572
+ // `opencode db` CLI, which opens the same database through the app itself and
573
+ // works even while OpenCode is running. We tail `part` rows by rowid so each
574
+ // session is scanned incrementally and only text/tool parts are transferred.
575
+
576
+ function opencodeDataDir() {
577
+ return (
578
+ process.env.OPENCODE_DATA_DIR ||
579
+ path.join(os.homedir(), '.local', 'share', 'opencode')
580
+ )
581
+ }
582
+
583
+ function opencodeDbPath() {
584
+ return path.join(opencodeDataDir(), 'opencode.db')
585
+ }
586
+
587
+ function findOpencodeBin() {
588
+ if (process.env.OPENCODE_BIN && fs.existsSync(process.env.OPENCODE_BIN)) {
589
+ return process.env.OPENCODE_BIN
590
+ }
591
+ const exeDir = path.dirname(process.execPath)
592
+ const candidates = [
593
+ path.join(exeDir, 'node_modules', 'opencode-ai', 'bin', 'opencode.exe'),
594
+ path.join(exeDir, '..', 'node_modules', 'opencode-ai', 'bin', 'opencode.exe'),
595
+ path.join(exeDir, '..', '..', 'node_modules', 'opencode-ai', 'bin', 'opencode.exe'),
596
+ ]
597
+ for (const c of candidates) {
598
+ const resolved = path.resolve(c)
599
+ if (fs.existsSync(resolved)) return resolved
600
+ }
601
+ return null
602
+ }
603
+
604
+ function runOpencodeQuery(sql) {
605
+ const bin = findOpencodeBin()
606
+ const args = ['db', sql, '--format', 'json']
607
+ let res
608
+ try {
609
+ res = bin
610
+ ? spawnSync(bin, args, { encoding: 'utf8', timeout: 90000, windowsHide: true, maxBuffer: 256 * 1024 * 1024 })
611
+ : spawnSync('opencode', args, { encoding: 'utf8', timeout: 90000, shell: true, maxBuffer: 256 * 1024 * 1024 })
612
+ } catch (_) {
613
+ return null
614
+ }
615
+ if (res.error || res.status !== 0 || !res.stdout) return null
616
+ try {
617
+ return JSON.parse(res.stdout)
618
+ } catch (_) {
619
+ return null
620
+ }
621
+ }
622
+
623
+ // Only transfer text and tool parts; skip reasoning/step bookkeeping rows to
624
+ // keep the transferred payload small.
625
+ const OPENCODE_PART_FILTER = " AND (p.data LIKE '%\"type\":\"text\"%' OR p.data LIKE '%\"type\":\"tool\"%')"
626
+
627
+ function createOpencodeTurn(meta) {
628
+ meta = meta || {}
629
+ return {
630
+ id: null,
631
+ ts: null,
632
+ sessionId: meta.sessionId || null,
633
+ cwd: meta.directory || null,
634
+ project: meta.directory ? path.basename(meta.directory) : 'unknown',
635
+ agent: meta.agent || null,
636
+ title: meta.title || null,
637
+ lastMid: null,
638
+ lastText: null,
639
+ toolCount: 0,
640
+ errors: [],
641
+ pendingTerminal: null,
642
+ done: false,
643
+ }
644
+ }
645
+
646
+ function opencodeOutcomeFor(finish) {
647
+ if (finish === 'error') return 'failure'
648
+ if (finish === 'cancel' || finish === 'aborted' || finish === 'interrupted') return 'aborted'
649
+ return 'success'
650
+ }
651
+
652
+ function finalizeOpencodeTurn(turn, outcome) {
653
+ if (!turn.id) return null
654
+ const finalized = {
655
+ id: turn.id,
656
+ ts: turn.ts,
657
+ sessionId: turn.sessionId,
658
+ cwd: turn.cwd,
659
+ project: turn.project,
660
+ agent: turn.agent,
661
+ title: turn.title,
662
+ lastText: turn.lastText,
663
+ toolCount: turn.toolCount,
664
+ errors: turn.errors.slice(),
665
+ outcome: outcome,
666
+ }
667
+ turn.done = true
668
+ return finalized
669
+ }
670
+
671
+ function applyOpencodeAssistantPart(turn, pd, md, row) {
672
+ if (pd.type === 'text' && typeof pd.text === 'string' && pd.text.trim()) {
673
+ turn.lastText = pd.text
674
+ } else if (pd.type === 'tool') {
675
+ turn.toolCount += 1
676
+ const status = pd.state && pd.state.status
677
+ if (status && status !== 'completed' && status !== 'ok' && status !== 'success' && status !== 'running') {
678
+ turn.errors.push({ tool: pd.tool || 'unknown', status: status })
679
+ if (turn.errors.length > 20) turn.errors.shift()
680
+ }
681
+ }
682
+ if (md.finish && md.finish !== 'tool-calls') {
683
+ turn.pendingTerminal = md.finish
684
+ }
685
+ turn.lastMid = row.mid
686
+ }
687
+
688
+ function feedOpencodePart(turn, row) {
689
+ let pd
690
+ let md
691
+ try {
692
+ pd = JSON.parse(row.pd)
693
+ } catch (_) {
694
+ pd = null
695
+ }
696
+ try {
697
+ md = JSON.parse(row.md)
698
+ } catch (_) {
699
+ md = null
700
+ }
701
+ if (!pd || !md) return { next: turn }
702
+
703
+ // A user message starts a new turn; any unfinished previous turn is aborted.
704
+ if (md.role === 'user') {
705
+ let finalized = null
706
+ if (turn.id && !turn.done) {
707
+ finalized = finalizeOpencodeTurn(turn, turn.pendingTerminal ? opencodeOutcomeFor(turn.pendingTerminal) : 'aborted')
708
+ }
709
+ const next = createOpencodeTurn({
710
+ sessionId: row.sid,
711
+ directory: row.sdir,
712
+ title: row.stitle,
713
+ agent: row.sagent,
714
+ })
715
+ next.id = row.mid
716
+ next.ts = Number(row.t) || Date.now()
717
+ next.lastMid = row.mid
718
+ return { finalized, next }
719
+ }
720
+
721
+ if (turn.done) return { next: turn }
722
+
723
+ // Message boundary after a terminal finish closes the previous turn.
724
+ if (turn.lastMid && row.mid !== turn.lastMid && turn.pendingTerminal) {
725
+ const finalized = finalizeOpencodeTurn(turn, opencodeOutcomeFor(turn.pendingTerminal))
726
+ const next = createOpencodeTurn({
727
+ sessionId: row.sid,
728
+ directory: row.sdir,
729
+ title: row.stitle,
730
+ agent: row.sagent,
731
+ })
732
+ applyOpencodeAssistantPart(next, pd, md, row)
733
+ return { finalized, next }
734
+ }
735
+
736
+ applyOpencodeAssistantPart(turn, pd, md, row)
737
+ return { next: turn }
738
+ }
739
+
740
+ function emitOpencodeOutcome(turn, sinks, state) {
741
+ if (!turn.id) return false
742
+ const summary = cleanText(turn.lastText)
743
+ if (!summary) return false
744
+ const outcome = turn.outcome
745
+ const turnToken = String(turn.id).slice(0, 8)
746
+ const tsToken = turn.ts ? Math.floor(turn.ts / 1000) : Math.floor(Date.now() / 1000)
747
+
748
+ sinks.trace({
749
+ trace_id: 'opencode:' + (turn.sessionId || 'unknown') + ':' + turnToken + ':' + tsToken,
750
+ event_type: 'task_outcome',
751
+ outcome: outcome,
752
+ payload: {
753
+ host: 'opencode',
754
+ project: turn.project,
755
+ sessionId: turn.sessionId,
756
+ turnId: turn.id,
757
+ title: turn.title,
758
+ agent: turn.agent,
759
+ toolCalls: turn.toolCount,
760
+ errors: turn.errors.length,
761
+ summary: summary.slice(0, 300),
762
+ source: 'opencode-observer',
763
+ },
764
+ agent: 'opencode',
765
+ host: 'opencode',
766
+ project: turn.project,
767
+ })
768
+
769
+ if (outcome === 'success' && summary.length >= 10) {
770
+ sinks.memory({
771
+ kind: 'note',
772
+ content: '[opencode] 任务完成: ' + summary.slice(0, 160),
773
+ tags: ['task-outcome', 'opencode', turn.project],
774
+ project: turn.project,
775
+ source: 'opencode-observer',
776
+ })
777
+ var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
778
+ updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80))
779
+ }
780
+
781
+ for (const err of turn.errors) {
782
+ sinks.trace({
783
+ trace_id: 'opencode:' + (turn.sessionId || 'unknown') + ':' + turnToken + ':' + String(err.tool || 'err').slice(0, 12),
784
+ event_type: 'error',
785
+ outcome: 'error',
786
+ payload: {
787
+ host: 'opencode',
788
+ project: turn.project,
789
+ sessionId: turn.sessionId,
790
+ turnId: turn.id,
791
+ tool: err.tool,
792
+ status: err.status,
793
+ },
794
+ agent: 'opencode',
795
+ host: 'opencode',
796
+ project: turn.project,
797
+ })
798
+ }
799
+ return true
800
+ }
801
+
802
+ function runOpencodeCycle(home, state, sinks, opts) {
803
+ const out = { files: 0, lines: 0, outcomes: 0 }
804
+ const db = opencodeDbPath()
805
+ if (!fs.existsSync(db)) return out
806
+ let st
807
+ try {
808
+ st = fs.statSync(db)
809
+ } catch (_) {
810
+ return out
811
+ }
812
+ out.files = 1
813
+ const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
814
+
815
+ // First run: seed the rowid cursor to the first part written within the
816
+ // backfill window so ancient history never floods MIO_HOME.
817
+ if (!state.ocSeeded) {
818
+ const ts = Date.now() - maxAgeMs
819
+ const first = runOpencodeQuery('SELECT rowid FROM part WHERE time_created >= ' + ts + ' ORDER BY rowid LIMIT 1')
820
+ let rid = 0
821
+ if (Array.isArray(first) && first.length > 0) {
822
+ rid = Number(first[0].rowid) || 0
823
+ } else {
824
+ const maxRow = runOpencodeQuery('SELECT COALESCE(MAX(rowid), 0) AS rid FROM part')
825
+ if (Array.isArray(maxRow) && maxRow.length > 0) rid = Number(maxRow[0].rid) || 0
826
+ }
827
+ state.ocLastRowid = rid
828
+ state.ocSeeded = true
829
+ state.ocDbMtime = st.mtimeMs
830
+ state.ocPending = true
831
+ return out
832
+ }
833
+
834
+ const changed = state.ocDbMtime === null || state.ocDbMtime !== st.mtimeMs
835
+ if (!changed && !state.ocPending) return out
836
+
837
+ const limit = 1500
838
+ const sql =
839
+ 'SELECT p.rowid AS rid, p.session_id AS sid, p.message_id AS mid, p.time_created AS t, ' +
840
+ 'p.data AS pd, m.data AS md, s.directory AS sdir, s.title AS stitle, s.agent AS sagent ' +
841
+ 'FROM part p JOIN message m ON m.id = p.message_id JOIN session s ON s.id = p.session_id ' +
842
+ 'WHERE p.rowid > ' + state.ocLastRowid + OPENCODE_PART_FILTER +
843
+ ' ORDER BY p.rowid LIMIT ' + limit
844
+ const rows = runOpencodeQuery(sql)
845
+ if (!Array.isArray(rows)) return out
846
+ state.ocDbMtime = st.mtimeMs
847
+ if (rows.length === 0) {
848
+ state.ocPending = false
849
+ return out
850
+ }
851
+
852
+ let lastRid = state.ocLastRowid
853
+ for (const row of rows) {
854
+ const rid = Number(row.rid)
855
+ if (rid > lastRid) lastRid = rid
856
+ out.lines += 1
857
+ const sid = row.sid
858
+ let turn = state.ocTurns[sid]
859
+ if (!turn) {
860
+ turn = createOpencodeTurn({ sessionId: sid, directory: row.sdir, title: row.stitle, agent: row.sagent })
861
+ state.ocTurns[sid] = turn
862
+ }
863
+ const result = feedOpencodePart(turn, row)
864
+ if (result.finalized) {
865
+ if (emitOpencodeOutcome(result.finalized, sinks, state)) out.outcomes += 1
866
+ }
867
+ state.ocTurns[sid] = result.next
868
+ }
869
+ state.ocLastRowid = lastRid
870
+ state.ocPending = rows.length >= limit
871
+
872
+ // Caught up: any turn holding a terminal finish is complete.
873
+ if (!state.ocPending) {
874
+ for (const sid of Object.keys(state.ocTurns)) {
875
+ const turn = state.ocTurns[sid]
876
+ if (turn && turn.id && turn.pendingTerminal && !turn.done) {
877
+ const finalized = finalizeOpencodeTurn(turn, opencodeOutcomeFor(turn.pendingTerminal))
878
+ if (finalized && emitOpencodeOutcome(finalized, sinks, state)) out.outcomes += 1
879
+ state.ocTurns[sid] = createOpencodeTurn({ sessionId: sid, directory: turn.cwd, title: turn.title, agent: turn.agent })
880
+ }
881
+ }
882
+ }
883
+ return out
884
+ }
885
+
555
886
  // ---------------------------------------------------------------------------
556
887
  // cycle
557
888
  // ---------------------------------------------------------------------------
@@ -634,6 +965,11 @@ function runCycle(home, state, sinks, opts) {
634
965
  out.lines += codexRes.lines
635
966
  out.outcomes += codexRes.outcomes
636
967
 
968
+ const ocRes = runOpencodeCycle(home, state, sinks, opts)
969
+ out.files += ocRes.files
970
+ out.lines += ocRes.lines
971
+ out.outcomes += ocRes.outcomes
972
+
637
973
  saveState(home, state)
638
974
  return out
639
975
  }
@@ -721,6 +1057,8 @@ module.exports = {
721
1057
  saveState,
722
1058
  updateProjectContext,
723
1059
  runCycle,
1060
+ opencodeDbPath,
1061
+ runOpencodeCycle,
724
1062
  createSinks,
725
1063
  startForeground,
726
1064
  startBackground,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",