mio-agent-runtime 0.13.2 → 0.13.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.
- package/README.md +23 -2
- package/adapters/claude-code.js +6 -6
- package/adapters/opencode.js +32 -6
- package/adapters/workbuddy.js +10 -9
- package/bin/mio.js +115 -8
- package/observe/observer.js +100 -9
- package/package.json +2 -2
- package/server/observer-store.js +59 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ mio evolution authority plan Preview a gated authority switch plan (--readi
|
|
|
40
40
|
mio evolution migration plan Preview state migration diffs (--legacy-records/--modular-records JSON; required)
|
|
41
41
|
mio evolution cutover apply --dry-run Dry-run a cutover plan (--plan JSON and --dry-run are both required)
|
|
42
42
|
mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes (foreground)
|
|
43
|
-
mio observe --start|--stop|--status|--once Manage the background observer daemon
|
|
43
|
+
mio observe --start [--research]|--stop|--status|--once Manage the background observer daemon (--research also starts the research scheduler)
|
|
44
44
|
mio recall "<query>" Search Mio memory from the terminal (--project/--scope/--kind/--tags/--limit)
|
|
45
45
|
mio traces Show recent observer traces (--type/--outcome/--agent/--since/--limit/--compact)
|
|
46
46
|
mio prune --days 30 Trim old traces/queries/reuse records and observe.log (--dry-run to preview; --memory needs --yes)
|
|
@@ -66,6 +66,7 @@ mio observer <view> Observer research pipeline views (research pipeline
|
|
|
66
66
|
mio observer collect Fetch from the configured sources (--sources a,b/--keywords k1,k2/--limit N)
|
|
67
67
|
mio observer ferment Run the fermentation engine (--session morning|afternoon|night)
|
|
68
68
|
mio observer pipeline Run the research DAG (previews by default; --run executes, --mode neutral|analytical|creative)
|
|
69
|
+
mio observer serve Keep the research scheduler running (previews with --dry-run; Ctrl+C stops it)
|
|
69
70
|
mio observer ingest --trace-id T --event-type E Record a trace event (--payload JSON/--outcome)
|
|
70
71
|
mio observer subscribe --event-types a,b Subscribe to events (--yes to apply; previews by default)
|
|
71
72
|
mio observer digest New events since the last digest (advances the cursor)
|
|
@@ -132,7 +133,7 @@ MCP 服务端在 5 大域共暴露 50 个工具:
|
|
|
132
133
|
| `@akemi-mio/evolution-strategy` | 演化策略引擎 |
|
|
133
134
|
| `@akemi-mio/evolution-safety` | 安全护栏 |
|
|
134
135
|
| `@akemi-mio/evolution-scheduler` | 演化调度器 |
|
|
135
|
-
| `@akemi-mio/observer` | ObserverService —— 多源数据采集、趋势分析、深度研究、世界模型、发酵、DAG
|
|
136
|
+
| `@akemi-mio/observer` | ObserverService —— 多源数据采集、趋势分析、深度研究、世界模型、发酵、DAG 状态机、自演化引擎(唯一运行时依赖 `undici`,只用于走代理采集) |
|
|
136
137
|
| `@akemi-mio/insight` | InsightService —— 基于 LLM 的洞察生成、冲突/漂移/重复/停滞目标/摩擦检测器、在场服务、洞察打分(零 npm 依赖) |
|
|
137
138
|
|
|
138
139
|
以下 `@akemi-mio` 包同样位于本 monorepo 中并独立发布,但**不是**本 CLI 的依赖——其源码从不 import 它们,因此未声明在 `package.json` 里:
|
|
@@ -499,6 +500,26 @@ mio observer trends --base-dir /path/to/.local/observer
|
|
|
499
500
|
mio observer pipeline --mode creative --run
|
|
500
501
|
```
|
|
501
502
|
|
|
503
|
+
- **`mio observer serve` 让"日更引擎"真正自动跑起来**(D6)。`ObserverService.start()` 早就配好了各源的采集间隔、60 秒一次的管道 tick 与 5 秒后的首次 tick,并在"今天的 DAG 已 `COMPLETED`"时跳过——但 runtime 里**没有任何长驻进程调用过它**,这些闸门一次都没生效过,trends / research / insights 只有手动 `mio observer pipeline --run` 才会出现。现在有了宿主:
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
mio observer serve # 前台常驻:采集 + 每日一次完整 DAG(Ctrl+C 停)
|
|
507
|
+
mio observer serve --dry-run # 只打印计划:baseDir / LLM / tick 周期 / 锁文件
|
|
508
|
+
mio observe --start --research # 顺带把调度器作为第二个受 pid 管理的子进程拉起
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- `--dry-run` 不构造服务、不建目录、不联网,因此也是文档门禁实跑的参数(`SAFE_ARGS['observer:serve']`)。
|
|
512
|
+
- `--research` 是**显式开关**:默认 `mio observe --start` 只启动转写观察守护,不会悄悄开始花 LLM 调用。研究子进程日志写到 `<MIO_HOME>/research.log`(用 append fd 而不是管道:没人读的管道写满 64KB 就会把调度器卡死),`mio observe --status --json` 给出它的 pid 与日志路径,`mio observe --stop` 一并停掉两者。
|
|
513
|
+
- 研究数据仍落在**启动它的那个工作目录**的 `<cwd>/.local/observer`(与 `mio observer status` 一致):子进程显式收到 `--base-dir`,因为转写守护 spawn 时 cwd 被钉成 `MIO_HOME`,若在守护进程里直接起调度,会多出一套谁也读不到的观察目录。
|
|
514
|
+
- 刻意**不提供** `mio.observer.serve` 这类 MCP 工具:每个连上的 MCP server 都会各起一个调度器,客户端一重启就多一个。
|
|
515
|
+
- `ObserverService` 内的 `pipelineRunning` 只防同进程并发;serve 子进程、守护与手动 `--run` 是三个进程,所以 `runPipeline` 现在还要抢 `<baseDir>/dag/pipeline.lock`(`O_EXCL` 建文件 + pid / mtime TTL,持有者已死即可接管)。锁被占时直接返回 `null`,不排队。
|
|
516
|
+
|
|
517
|
+
- **采集会走系统与环境里配置的代理**(D7)。6 个采集源共 9 处请求此前直接用全局 `fetch`,而 Node 的 fetch **不读任何代理配置**——所以浏览器能打开的页面,采集全部超时(issue #3:Windows 开着系统代理、直连不通时,weibo / rss / hackernews / douyin / bilibili / github-trending 全军覆没)。现在统一走 `@akemi-mio/observer` 的 `httpFetch()`:
|
|
518
|
+
- 代理按顺序取:`HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY`(大小写皆可)→ Windows 系统代理(`reg query` 读 HKCU Internet Settings 的 `ProxyEnable` / `ProxyServer` / `ProxyOverride`,进程内缓存 30 秒)。`ProxyServer` 的 `h=..;https=..` 与单值两种写法都认,**端口读实时注册表**——issue 里写的 7990 早已不是本机端口,硬编码必然过期。
|
|
519
|
+
- 绕过:`NO_PROXY` 与 `ProxyOverride`,支持 `*`、`<local>`、`*.zhihu.com`、`127.*`、裸域名(含子域)。
|
|
520
|
+
- **解析不到代理就走原生 `fetch`,与从前逐字节一致**;解析到代理时用 `undici` 的 `ProxyAgent`(CONNECT 隧道,https 与 http 目标同样处理)。代理连不上(`ECONNREFUSED` / `ENOTFOUND` / `EHOSTUNREACH`)会**回退直连**并记 `proxy_fallback_direct`,避免"系统代理开着但客户端已关"把原本能通的直连也搞坏。
|
|
521
|
+
- `ObserverLlmService` 不在这条通路上(issue 只讲采集失败)。
|
|
522
|
+
|
|
502
523
|
- **观察者的 LLM 现在与 creativity / insight 共用同一份配置**。此前 `ObserverLlmService` 把 Ollama 地址与模型写死(`localhost:11434` / `qwen2.5:7b`,只认 `OBSERVER_*` 环境变量),完全无视 `mio config llm` —— 于是 `mio config llm` 配好的 deepseek 只对 server 侧生效,观察管道仍然空转。现在 `observer-store.js` 通过共享的 `server/llm-client.js` 解析配置并注入:用户配置过(`config.json` 的 `llm` 或 `LLM_*` 环境变量)就用它,否则保持 `@akemi-mio/observer` 自己的 `LLM_*` / `OBSERVER_*` / 本地 Ollama 回退,不会把既有本地 Ollama 用户静默改道到托管默认值。端点按 URL 形态自动选择传输:含 `/chat/completions` 走 OpenAI 兼容协议,否则按 Ollama 处理。
|
|
503
524
|
- **`mio observer ingest` 记录任意 trace 事件**(`tool_call` / `error` / `retry` / `task_outcome`):
|
|
504
525
|
|
package/adapters/claude-code.js
CHANGED
|
@@ -46,12 +46,12 @@ const MEMORY_BLOCK = `${MEMORY_BEGIN}
|
|
|
46
46
|
|
|
47
47
|
Mio MCP tools are available. Treat these as mandatory runtime rules for every workspace.
|
|
48
48
|
|
|
49
|
-
- Before designing or implementing a solution, or when unsure about prior decisions, call \`
|
|
50
|
-
- After a decision affecting architecture, deployment, dependencies, or long-term behavior, call \`
|
|
51
|
-
- When a tool call fails, retries, or returns an error, call \`
|
|
52
|
-
- Before a high-risk operation such as deletion, migration, credential change, or production change, call \`
|
|
53
|
-
- At the end of every task, before producing the final answer, call \`
|
|
54
|
-
- When you reuse a prior decision or experience returned by \`
|
|
49
|
+
- Before designing or implementing a solution, or when unsure about prior decisions, call \`mcp__mio-intelligence__mio.memory.query\` first. Omit \`project\` unless a specific project is required; Mio derives it from the current Git repository.
|
|
50
|
+
- After a decision affecting architecture, deployment, dependencies, or long-term behavior, call \`mcp__mio-intelligence__mio.memory.record\` with \`kind\`: \`decision\`, \`context\`, \`problem\`, or \`note\`.
|
|
51
|
+
- When a tool call fails, retries, or returns an error, call \`mcp__mio-intelligence__mio.observer.ingest\` with a stable \`trace_id\`, relevant \`event_type\`, \`payload\`, and \`outcome\`.
|
|
52
|
+
- Before a high-risk operation such as deletion, migration, credential change, or production change, call \`mcp__mio-intelligence__mio.policy.check\` and follow its suggestion when available.
|
|
53
|
+
- At the end of every task, before producing the final answer, call \`mcp__mio-intelligence__mio.observer.ingest\` with \`event_type\` = \`task_outcome\`, a stable \`trace_id\`, \`outcome\` = \`success\` / \`failure\` / \`aborted\`, and a brief non-sensitive \`payload\`. If Mio is unavailable, do not block the final answer.
|
|
54
|
+
- When you reuse a prior decision or experience returned by \`mcp__mio-intelligence__mio.memory.query\` and it changes your approach or outcome, call \`mcp__mio-intelligence__mio.experience.reuse\` with \`sourceAgent\`, \`targetAgent\`, \`experienceId\`, \`reuse\`, \`behaviorChanged\`, and \`outcomeImproved\`.
|
|
55
55
|
- If Mio MCP is unavailable, do not block the task. Continue normally and note the missed memory or observation.
|
|
56
56
|
- Do not store secrets, credentials, or raw sensitive content in Mio memory.
|
|
57
57
|
${MEMORY_END}`
|
package/adapters/opencode.js
CHANGED
|
@@ -86,6 +86,25 @@ function configNeedsRepair(serverScript) {
|
|
|
86
86
|
return configured !== needle
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Installed configs written before 0.13.4 baked an install-time project into
|
|
90
|
+
// MIO_CONTEXT, which overrides the server's git-based project derivation and
|
|
91
|
+
// mis-attributes memory writes. Rewriting the entry drops it; `cwd` pins the
|
|
92
|
+
// server process to the workspace so derivation has a stable starting point.
|
|
93
|
+
function contextNeedsRepair() {
|
|
94
|
+
const config = readConfig()
|
|
95
|
+
const mcp = config && config.mcp && config.mcp['mio-intelligence']
|
|
96
|
+
if (!mcp) return false
|
|
97
|
+
const raw = mcp.environment && mcp.environment.MIO_CONTEXT
|
|
98
|
+
if (typeof raw !== 'string') return true
|
|
99
|
+
let context
|
|
100
|
+
try {
|
|
101
|
+
context = JSON.parse(raw)
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
return Boolean(context && typeof context === 'object' && ('project' in context || 'workspace' in context))
|
|
106
|
+
}
|
|
107
|
+
|
|
89
108
|
function restartWarningMessage() {
|
|
90
109
|
return restartWarning({
|
|
91
110
|
hostLabel: 'OpenCode Desktop',
|
|
@@ -102,11 +121,11 @@ function isInstalled() {
|
|
|
102
121
|
return instructions.includes(INSTRUCTIONS_BEGIN) && instructions.includes(INSTRUCTIONS_END)
|
|
103
122
|
}
|
|
104
123
|
|
|
105
|
-
function install({ node, serverScript, home
|
|
124
|
+
function install({ node, serverScript, home }) {
|
|
106
125
|
const target = configPath()
|
|
107
126
|
const config = readConfig() || {}
|
|
108
127
|
const configInstalled = Boolean(config.mcp && config.mcp['mio-intelligence'])
|
|
109
|
-
const needsRepair = configInstalled && configNeedsRepair(serverScript)
|
|
128
|
+
const needsRepair = configInstalled && (configNeedsRepair(serverScript) || contextNeedsRepair())
|
|
110
129
|
|
|
111
130
|
let configChanged = false
|
|
112
131
|
let backup = null
|
|
@@ -120,8 +139,6 @@ function install({ node, serverScript, home, workspace, project }) {
|
|
|
120
139
|
const existingMcp = config.mcp && typeof config.mcp === 'object' ? config.mcp : {}
|
|
121
140
|
const context = JSON.stringify({
|
|
122
141
|
agentId: 'opencode',
|
|
123
|
-
project: project || path.basename(workspace || process.cwd()),
|
|
124
|
-
workspace: workspace || process.cwd(),
|
|
125
142
|
sessionId: 'opencode-session',
|
|
126
143
|
})
|
|
127
144
|
const next = {
|
|
@@ -132,6 +149,7 @@ function install({ node, serverScript, home, workspace, project }) {
|
|
|
132
149
|
enabled: true,
|
|
133
150
|
type: 'local',
|
|
134
151
|
command: [node, serverScript],
|
|
152
|
+
cwd: '.',
|
|
135
153
|
environment: {
|
|
136
154
|
MIO_DATA_DIR: home,
|
|
137
155
|
MIO_CONTEXT: context,
|
|
@@ -152,7 +170,7 @@ function install({ node, serverScript, home, workspace, project }) {
|
|
|
152
170
|
if (configChanged && instructionsResult.changed) {
|
|
153
171
|
message = 'OpenCode Mio MCP installed and global Mio rules injected into AGENTS.md. Restart OpenCode to load them.'
|
|
154
172
|
} else if (needsRepair && !instructionsResult.changed) {
|
|
155
|
-
message = 'OpenCode Mio MCP repaired (server path updated). Restart OpenCode to load it.'
|
|
173
|
+
message = 'OpenCode Mio MCP repaired (server path or context updated). Restart OpenCode to load it.'
|
|
156
174
|
} else if (instructionsResult.changed) {
|
|
157
175
|
message = 'OpenCode Mio MCP already installed. Injected global Mio rules into AGENTS.md. Restart OpenCode to load them.'
|
|
158
176
|
} else {
|
|
@@ -173,4 +191,12 @@ function install({ node, serverScript, home, workspace, project }) {
|
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
193
|
|
|
176
|
-
module.exports = {
|
|
194
|
+
module.exports = {
|
|
195
|
+
configPath,
|
|
196
|
+
globalInstructionsPath,
|
|
197
|
+
isInstalled,
|
|
198
|
+
install,
|
|
199
|
+
configNeedsRepair,
|
|
200
|
+
contextNeedsRepair,
|
|
201
|
+
restartWarningMessage,
|
|
202
|
+
}
|
package/adapters/workbuddy.js
CHANGED
|
@@ -55,12 +55,12 @@ const MEMORY_BLOCK = `${MEMORY_BEGIN}
|
|
|
55
55
|
|
|
56
56
|
Mio MCP tools are available. Treat these as mandatory runtime rules for every workspace.
|
|
57
57
|
|
|
58
|
-
- Before designing or implementing a solution, or when unsure about prior decisions, call \`
|
|
59
|
-
- After a decision affecting architecture, deployment, dependencies, or long-term behavior, call \`
|
|
60
|
-
- When a tool call fails, retries, or returns an error, call \`
|
|
61
|
-
- Before a high-risk operation such as deletion, migration, credential change, or production change, call \`
|
|
62
|
-
- At the end of every task, before producing the final answer, call \`
|
|
63
|
-
- When you reuse a prior decision or experience returned by \`
|
|
58
|
+
- Before designing or implementing a solution, or when unsure about prior decisions, call \`mcp__mio-intelligence__mio.memory.query\` first. Omit \`project\` unless a specific project is required; Mio derives it from the current Git repository.
|
|
59
|
+
- After a decision affecting architecture, deployment, dependencies, or long-term behavior, call \`mcp__mio-intelligence__mio.memory.record\` with \`kind\`: \`decision\`, \`context\`, \`problem\`, or \`note\`.
|
|
60
|
+
- When a tool call fails, retries, or returns an error, call \`mcp__mio-intelligence__mio.observer.ingest\` with a stable \`trace_id\`, relevant \`event_type\`, \`payload\`, and \`outcome\`.
|
|
61
|
+
- Before a high-risk operation such as deletion, migration, credential change, or production change, call \`mcp__mio-intelligence__mio.policy.check\` and follow its suggestion when available.
|
|
62
|
+
- At the end of every task, before producing the final answer, call \`mcp__mio-intelligence__mio.observer.ingest\` with \`event_type\` = \`task_outcome\`, a stable \`trace_id\`, \`outcome\` = \`success\` / \`failure\` / \`aborted\`, and a brief non-sensitive \`payload\`. If Mio is unavailable, do not block the final answer.
|
|
63
|
+
- When you reuse a prior decision or experience returned by \`mcp__mio-intelligence__mio.memory.query\` and it changes your approach or outcome, call \`mcp__mio-intelligence__mio.experience.reuse\` with \`sourceAgent\`, \`targetAgent\`, \`experienceId\`, \`reuse\`, \`behaviorChanged\`, and \`outcomeImproved\`.
|
|
64
64
|
- If Mio MCP is unavailable, do not block the task. Continue normally and note the missed memory or observation.
|
|
65
65
|
- Do not store secrets, credentials, or raw sensitive content in Mio memory.
|
|
66
66
|
${MEMORY_END}`
|
|
@@ -124,7 +124,7 @@ function writeApproval(name, entry) {
|
|
|
124
124
|
fs.writeFileSync(target, `${JSON.stringify(approvals, null, 2)}\n`, 'utf8')
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
function install({ node, serverScript, home, workspace
|
|
127
|
+
function install({ node, serverScript, home, workspace }) {
|
|
128
128
|
const target = configPath()
|
|
129
129
|
const canonical = readJson(target) || {}
|
|
130
130
|
const legacy = readJson(legacyConfigPath()) || {}
|
|
@@ -142,10 +142,11 @@ function install({ node, serverScript, home, workspace, project }) {
|
|
|
142
142
|
},
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// No project/workspace here: the MCP server derives the project from its own
|
|
146
|
+
// git cwd. A baked value from install-time cwd overrides that derivation and
|
|
147
|
+
// mis-attributes every memory write for the host's lifetime.
|
|
145
148
|
const context = JSON.stringify({
|
|
146
149
|
agentId: 'workbuddy',
|
|
147
|
-
project: project || path.basename(workspace || process.cwd()),
|
|
148
|
-
workspace: workspace || process.cwd(),
|
|
149
150
|
sessionId: 'workbuddy-session',
|
|
150
151
|
})
|
|
151
152
|
|
package/bin/mio.js
CHANGED
|
@@ -27,7 +27,7 @@ const { CreativityEngine } = require('../server/creativity-engine.js')
|
|
|
27
27
|
const { createAgentStore } = require('../server/agent-store.js')
|
|
28
28
|
const { createEvaluationStore } = require('../server/evaluation-store.js')
|
|
29
29
|
const { createInsightStore } = require('../server/insight-store.js')
|
|
30
|
-
const { createObserverStore } = require('../server/observer-store.js')
|
|
30
|
+
const { createObserverStore, isObserverAvailable } = require('../server/observer-store.js')
|
|
31
31
|
const { loadPhase0, renderPhase0Markdown } = require('../server/mio-intelligence-mcp/phase0.js')
|
|
32
32
|
const { listHostCapabilities } = require('../server/host-capabilities.js')
|
|
33
33
|
const { createTaskStore } = require('../server/task-store.js')
|
|
@@ -1533,6 +1533,10 @@ function observerUsage(write = console.log) {
|
|
|
1533
1533
|
mio observer pipeline Run the full research DAG (collect → trend → research →
|
|
1534
1534
|
insights → world model). Previews by default; --run
|
|
1535
1535
|
executes it (network + LLM). --mode neutral|analytical|creative
|
|
1536
|
+
mio observer serve Keep the research scheduler running in this terminal:
|
|
1537
|
+
collectors on their intervals + one pipeline run per
|
|
1538
|
+
day. Previews with --dry-run (that is what runs it
|
|
1539
|
+
the README/doc gate); Ctrl+C stops it.
|
|
1536
1540
|
|
|
1537
1541
|
Options:
|
|
1538
1542
|
--base-dir DIR Observer data directory (default: <cwd>/.local/observer)
|
|
@@ -1918,6 +1922,66 @@ async function observerPipelineCommand(args, useJson) {
|
|
|
1918
1922
|
printObserverPipeline(result)
|
|
1919
1923
|
}
|
|
1920
1924
|
|
|
1925
|
+
function printObserverServe(result) {
|
|
1926
|
+
if (result.dryRun) {
|
|
1927
|
+
console.log(`Observer research scheduler preview (base-dir=${result.baseDir})`)
|
|
1928
|
+
if (result.llm) {
|
|
1929
|
+
const key = result.llm.hasApiKey ? 'key set' : 'no key'
|
|
1930
|
+
console.log(` LLM: ${result.llm.provider} ${result.llm.model} (${key})`)
|
|
1931
|
+
console.log(` ${result.llm.apiUrl}`)
|
|
1932
|
+
} else {
|
|
1933
|
+
console.log(' LLM: unknown (observer package did not expose ObserverLlmService)')
|
|
1934
|
+
}
|
|
1935
|
+
if (result.schedule) {
|
|
1936
|
+
console.log(` pipeline: first tick ${result.schedule.pipelineFirstTickMs}ms, then every ${result.schedule.pipelineTickMs}ms`)
|
|
1937
|
+
console.log(` gate: ${result.schedule.pipelineGate}`)
|
|
1938
|
+
console.log(` lock: ${result.schedule.lockFile}`)
|
|
1939
|
+
}
|
|
1940
|
+
console.log('\nPreview only. Re-run without --dry-run to keep the scheduler running (Ctrl+C stops it).')
|
|
1941
|
+
return
|
|
1942
|
+
}
|
|
1943
|
+
console.log(`Observer research scheduler running (base-dir=${result.baseDir})`)
|
|
1944
|
+
console.log(` ${result.schedule ? result.schedule.collectors : 'collectors started'}`)
|
|
1945
|
+
console.log(` pipeline: first tick ${result.schedule ? result.schedule.pipelineFirstTickMs : 5000}ms, then every ${result.schedule ? result.schedule.pipelineTickMs : 60000}ms`)
|
|
1946
|
+
console.log(' Stops with Ctrl+C (or `mio observe --stop` when started that way).')
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// D6: `mio observer serve` is the long-lived host for ObserverService.start().
|
|
1950
|
+
// Without it the scheduler inside the service had no caller, so tickPipeline's
|
|
1951
|
+
// daily gate never fired and trends/research/insights only appeared if someone
|
|
1952
|
+
// ran `mio observer pipeline --run` by hand. It stays in the foreground on
|
|
1953
|
+
// purpose -- a scheduler is a process, not a request -- and `--dry-run` exists
|
|
1954
|
+
// both as a preview and as the probe `check:cli-docs` runs, so the documented
|
|
1955
|
+
// command can never hang the gate.
|
|
1956
|
+
async function observerServeCommand(args, useJson) {
|
|
1957
|
+
const flags = args.slice(2)
|
|
1958
|
+
const base = { baseDir: optionValue(flags, '--base-dir') }
|
|
1959
|
+
const dryRun = flagPresent(flags, '--dry-run')
|
|
1960
|
+
let result
|
|
1961
|
+
try {
|
|
1962
|
+
result = await cliObserverStore().serve({ ...base, dryRun })
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
console.error(error.message || error)
|
|
1965
|
+
process.exitCode = 1
|
|
1966
|
+
return
|
|
1967
|
+
}
|
|
1968
|
+
if (useJson && result.dryRun) return jsonOrText(result, true)
|
|
1969
|
+
if (result.dryRun) {
|
|
1970
|
+
printObserverServe(result)
|
|
1971
|
+
return
|
|
1972
|
+
}
|
|
1973
|
+
if (useJson) console.log(JSON.stringify({ serving: true, baseDir: result.baseDir, schedule: result.schedule }))
|
|
1974
|
+
else printObserverServe(result)
|
|
1975
|
+
const stop = typeof result.stop === 'function' ? result.stop : () => {}
|
|
1976
|
+
const shutdown = (signal) => {
|
|
1977
|
+
stop()
|
|
1978
|
+
console.log('Observer research scheduler stopped (' + signal + ')')
|
|
1979
|
+
process.exit(0)
|
|
1980
|
+
}
|
|
1981
|
+
process.once('SIGINT', () => shutdown('SIGINT'))
|
|
1982
|
+
process.once('SIGTERM', () => shutdown('SIGTERM'))
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1921
1985
|
function observerCommand(args, useJson) {
|
|
1922
1986
|
const sub = args[1]
|
|
1923
1987
|
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
@@ -1933,6 +1997,7 @@ function observerCommand(args, useJson) {
|
|
|
1933
1997
|
if (sub === 'collect') return observerCollectCommand(args, useJson)
|
|
1934
1998
|
if (sub === 'ferment') return observerFermentCommand(args, useJson)
|
|
1935
1999
|
if (sub === 'pipeline') return observerPipelineCommand(args, useJson)
|
|
2000
|
+
if (sub === 'serve') return observerServeCommand(args, useJson)
|
|
1936
2001
|
|
|
1937
2002
|
if (!['status', 'world-model', 'trends', 'research', 'insights', 'essays', 'dag'].includes(sub)) {
|
|
1938
2003
|
console.error(`Unknown observer subcommand: ${sub}`)
|
|
@@ -3098,26 +3163,67 @@ function mcpCommand() {
|
|
|
3098
3163
|
require(SERVER_SCRIPT)
|
|
3099
3164
|
}
|
|
3100
3165
|
|
|
3101
|
-
|
|
3166
|
+
// `--research` is opt-in: it adds a second child (mio observer serve) that owns
|
|
3167
|
+
// the collector intervals and the daily pipeline tick. It is deliberately not
|
|
3168
|
+
// on by default -- starting the transcript watcher should not silently start
|
|
3169
|
+
// spending LLM calls in the background (D6).
|
|
3170
|
+
function startResearchScheduler(cwd) {
|
|
3171
|
+
if (!isObserverAvailable()) {
|
|
3172
|
+
return { running: false, started: false, pid: null, skipped: '@akemi-mio/observer not installed' }
|
|
3173
|
+
}
|
|
3174
|
+
return observer.startResearchBackground(MIO_HOME, { cwd })
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
function observeCommand(args, jsonFlag = false) {
|
|
3102
3178
|
const flags = args.slice(1)
|
|
3103
|
-
|
|
3179
|
+
// main() already strips --json from argv and passes it as the second
|
|
3180
|
+
// argument; the local check is kept for direct callers. Without the
|
|
3181
|
+
// parameter `mio observe --json --status` silently printed text.
|
|
3182
|
+
const useJson = jsonFlag || flags.includes('--json')
|
|
3183
|
+
const research = flags.includes('--research')
|
|
3104
3184
|
if (flags.includes('--start')) {
|
|
3105
3185
|
const result = observer.startBackground(MIO_HOME)
|
|
3106
|
-
|
|
3186
|
+
const researchResult = research ? startResearchScheduler(process.cwd()) : null
|
|
3187
|
+
if (useJson) {
|
|
3188
|
+
return jsonOrText({ ...result, research: researchResult || { running: null, started: false, pid: null } }, true)
|
|
3189
|
+
}
|
|
3107
3190
|
console.log(result.started ? 'Observer started.' : 'Observer already running.')
|
|
3191
|
+
if (researchResult) {
|
|
3192
|
+
if (researchResult.skipped) console.log('Research scheduler skipped: ' + researchResult.skipped)
|
|
3193
|
+
else if (researchResult.started) console.log('Research scheduler started (pid ' + researchResult.pid + ', base-dir ' + researchResult.baseDir + ')')
|
|
3194
|
+
else console.log('Research scheduler already running (pid ' + researchResult.pid + ')')
|
|
3195
|
+
}
|
|
3108
3196
|
return
|
|
3109
3197
|
}
|
|
3110
3198
|
if (flags.includes('--stop')) {
|
|
3111
3199
|
const result = observer.stopBackground(MIO_HOME)
|
|
3112
|
-
|
|
3200
|
+
const researchResult = observer.stopResearchBackground(MIO_HOME)
|
|
3201
|
+
if (useJson) {
|
|
3202
|
+
return jsonOrText(
|
|
3203
|
+
{ ...result, research: { running: false, stopped: researchResult.stopped, pid: researchResult.pid } },
|
|
3204
|
+
true
|
|
3205
|
+
)
|
|
3206
|
+
}
|
|
3113
3207
|
console.log(result.stopped ? 'Observer stopped.' : 'Observer was not running.')
|
|
3208
|
+
console.log(researchResult.stopped ? 'Research scheduler stopped.' : 'Research scheduler was not running.')
|
|
3114
3209
|
return
|
|
3115
3210
|
}
|
|
3116
3211
|
if (flags.includes('--status')) {
|
|
3117
3212
|
const running = observer.isRunning(MIO_HOME)
|
|
3118
3213
|
const pid = observer.readPid(MIO_HOME)
|
|
3119
|
-
|
|
3214
|
+
const researchRunning = observer.isResearchRunning(MIO_HOME)
|
|
3215
|
+
const researchPid = observer.readResearchPid(MIO_HOME)
|
|
3216
|
+
const researchInfo = { running: researchRunning, pid: researchPid, logFile: observer.researchLogFile(MIO_HOME) }
|
|
3217
|
+
if (useJson) return jsonOrText({ running, pid, research: researchInfo }, true)
|
|
3120
3218
|
console.log(running ? 'Observer: running (pid ' + pid + ')' : 'Observer: not running')
|
|
3219
|
+
console.log(
|
|
3220
|
+
researchRunning ? 'Research scheduler: running (pid ' + researchPid + ')' : 'Research scheduler: not running'
|
|
3221
|
+
)
|
|
3222
|
+
return
|
|
3223
|
+
}
|
|
3224
|
+
if (flags.includes('--research') && !flags.includes('--start')) {
|
|
3225
|
+
console.error('--research only applies to --start; run `mio observer serve` to run the scheduler in this terminal.')
|
|
3226
|
+
process.exitCode = 1
|
|
3121
3227
|
return
|
|
3122
3228
|
}
|
|
3123
3229
|
if (flags.includes('--once')) {
|
|
@@ -3168,7 +3274,7 @@ Usage:
|
|
|
3168
3274
|
mio evolution migration plan Preview state migration diffs (--legacy-records/--modular-records JSON; required)
|
|
3169
3275
|
mio evolution cutover apply --dry-run Dry-run a cutover plan (--plan JSON and --dry-run are both required)
|
|
3170
3276
|
mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes
|
|
3171
|
-
mio observe --start|--stop|--status|--once Manage the background observer daemon
|
|
3277
|
+
mio observe --start [--research]|--stop|--status|--once Manage the background observer daemon
|
|
3172
3278
|
mio recall "<query>" Search Mio memory from the terminal (same ranking as mio.memory.query)
|
|
3173
3279
|
mio traces Show recent observer traces (--type/--outcome/--agent/--since/--limit/--compact)
|
|
3174
3280
|
mio remember "<content>" Write a memory record from the terminal (same schema as mio.memory.record)
|
|
@@ -3197,6 +3303,7 @@ Usage:
|
|
|
3197
3303
|
mio observer collect Fetch from the configured sources (--sources/--keywords/--limit)
|
|
3198
3304
|
mio observer ferment Run the fermentation engine (--session morning|afternoon|night)
|
|
3199
3305
|
mio observer pipeline Run the research DAG (previews; --run to execute, --mode neutral|analytical|creative)
|
|
3306
|
+
mio observer serve Keep the research scheduler running (previews with --dry-run; Ctrl+C stops it)
|
|
3200
3307
|
mio phase0 report Show the Phase 0 validation report (--project X, --format markdown)
|
|
3201
3308
|
mio host capabilities Show what each host supports and whether it is installed
|
|
3202
3309
|
mio task route "<task>" Which verified experiences apply to this task (--project/--scope/--limit)
|
|
@@ -3232,7 +3339,7 @@ async function main() {
|
|
|
3232
3339
|
case 'agents':
|
|
3233
3340
|
return agentsCommand(args, useJson)
|
|
3234
3341
|
case 'observe':
|
|
3235
|
-
return observeCommand(args)
|
|
3342
|
+
return observeCommand(args, useJson)
|
|
3236
3343
|
case 'recall':
|
|
3237
3344
|
return recallCommand(args, useJson)
|
|
3238
3345
|
case 'traces':
|
package/observe/observer.js
CHANGED
|
@@ -1695,9 +1695,35 @@ function pidFile(home) {
|
|
|
1695
1695
|
return path.join(home, 'observe.pid')
|
|
1696
1696
|
}
|
|
1697
1697
|
|
|
1698
|
+
function researchPidFile(home) {
|
|
1699
|
+
return path.join(home, 'research.pid')
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function researchLogFile(home) {
|
|
1703
|
+
return path.join(home, 'research.log')
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function pidAlive(pid) {
|
|
1707
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
1708
|
+
try {
|
|
1709
|
+
process.kill(pid, 0)
|
|
1710
|
+
return true
|
|
1711
|
+
} catch (_) {
|
|
1712
|
+
return false
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1698
1716
|
function readPid(home) {
|
|
1717
|
+
return readPidFile(pidFile(home))
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function readResearchPid(home) {
|
|
1721
|
+
return readPidFile(researchPidFile(home))
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function readPidFile(file) {
|
|
1699
1725
|
try {
|
|
1700
|
-
const pid = Number(fs.readFileSync(
|
|
1726
|
+
const pid = Number(fs.readFileSync(file, 'utf8').trim())
|
|
1701
1727
|
return Number.isInteger(pid) && pid > 0 ? pid : null
|
|
1702
1728
|
} catch (_) {
|
|
1703
1729
|
return null
|
|
@@ -1705,14 +1731,11 @@ function readPid(home) {
|
|
|
1705
1731
|
}
|
|
1706
1732
|
|
|
1707
1733
|
function isRunning(home) {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
} catch (_) {
|
|
1714
|
-
return false
|
|
1715
|
-
}
|
|
1734
|
+
return pidAlive(readPid(home))
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function isResearchRunning(home) {
|
|
1738
|
+
return pidAlive(readResearchPid(home))
|
|
1716
1739
|
}
|
|
1717
1740
|
|
|
1718
1741
|
function startBackground(home) {
|
|
@@ -1745,6 +1768,69 @@ function stopBackground(home) {
|
|
|
1745
1768
|
return { running: false, stopped: Boolean(pid), pid: pid || null }
|
|
1746
1769
|
}
|
|
1747
1770
|
|
|
1771
|
+
// ---------------------------------------------------------------------------
|
|
1772
|
+
// research scheduler daemon (`mio observe --start --research`)
|
|
1773
|
+
//
|
|
1774
|
+
// D6: ObserverService's own scheduler (collector intervals + the daily
|
|
1775
|
+
// pipeline tick) needed a process to live in. The transcript daemon could have
|
|
1776
|
+
// hosted it in-process, but that daemon is spawned with cwd: home, so its
|
|
1777
|
+
// `<cwd>/.local/observer` would resolve to MIO_HOME -- a second observation
|
|
1778
|
+
// directory that `mio observer status` in the project would never see. Running
|
|
1779
|
+
// `mio observer serve` as its own child with the *launcher's* cwd keeps one
|
|
1780
|
+
// data directory per working directory, exactly like every other observer
|
|
1781
|
+
// command, and keeps a crash in either half from taking down the other.
|
|
1782
|
+
//
|
|
1783
|
+
// spawn/kill are injectable so tests never signal a real pid.
|
|
1784
|
+
// ---------------------------------------------------------------------------
|
|
1785
|
+
|
|
1786
|
+
function startResearchBackground(home, opts = {}) {
|
|
1787
|
+
const cwd = opts.cwd || process.cwd()
|
|
1788
|
+
const spawnImpl = opts.spawn || spawn
|
|
1789
|
+
if (isResearchRunning(home)) {
|
|
1790
|
+
return { running: true, started: false, pid: readResearchPid(home), baseDir: path.join(cwd, '.local', 'observer') }
|
|
1791
|
+
}
|
|
1792
|
+
const script = path.join(__dirname, '..', 'bin', 'mio.js')
|
|
1793
|
+
const baseDir = path.join(cwd, '.local', 'observer')
|
|
1794
|
+
// The scheduler logs to stdout, so a detached child with stdio:'ignore' would
|
|
1795
|
+
// be undiagnosable ("is it running? did it collect?"). Handing it an append
|
|
1796
|
+
// fd instead of a pipe is deliberate: a pipe nobody reads fills up after
|
|
1797
|
+
// ~64KB and the scheduler would block on its own log.
|
|
1798
|
+
fs.mkdirSync(home, { recursive: true })
|
|
1799
|
+
const logFd = fs.openSync(researchLogFile(home), 'a')
|
|
1800
|
+
let child
|
|
1801
|
+
try {
|
|
1802
|
+
child = spawnImpl(
|
|
1803
|
+
process.execPath,
|
|
1804
|
+
[script, 'observer', 'serve', '--base-dir', baseDir],
|
|
1805
|
+
{
|
|
1806
|
+
detached: true,
|
|
1807
|
+
stdio: ['ignore', logFd, logFd],
|
|
1808
|
+
windowsHide: true,
|
|
1809
|
+
cwd,
|
|
1810
|
+
}
|
|
1811
|
+
)
|
|
1812
|
+
} finally {
|
|
1813
|
+
fs.closeSync(logFd)
|
|
1814
|
+
}
|
|
1815
|
+
if (child.unref) child.unref()
|
|
1816
|
+
fs.writeFileSync(researchPidFile(home), String(child.pid), 'utf8')
|
|
1817
|
+
return { running: true, started: true, pid: child.pid, baseDir, logFile: researchLogFile(home) }
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function stopResearchBackground(home, killImpl) {
|
|
1821
|
+
const kill = killImpl || process.kill
|
|
1822
|
+
const pid = readResearchPid(home)
|
|
1823
|
+
if (pid) {
|
|
1824
|
+
try {
|
|
1825
|
+
kill(pid)
|
|
1826
|
+
} catch (_) {}
|
|
1827
|
+
try {
|
|
1828
|
+
fs.unlinkSync(researchPidFile(home))
|
|
1829
|
+
} catch (_) {}
|
|
1830
|
+
}
|
|
1831
|
+
return { running: false, stopped: Boolean(pid), pid: pid || null }
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1748
1834
|
module.exports = {
|
|
1749
1835
|
INTERVAL_MS,
|
|
1750
1836
|
homeDir,
|
|
@@ -1770,6 +1856,11 @@ module.exports = {
|
|
|
1770
1856
|
stopBackground,
|
|
1771
1857
|
isRunning,
|
|
1772
1858
|
readPid,
|
|
1859
|
+
startResearchBackground,
|
|
1860
|
+
stopResearchBackground,
|
|
1861
|
+
isResearchRunning,
|
|
1862
|
+
readResearchPid,
|
|
1863
|
+
researchLogFile,
|
|
1773
1864
|
// exported for tests: the numeric-segment guard here was silently dead for
|
|
1774
1865
|
// several releases because nothing exercised it (see __tests__/observer-project.test.js)
|
|
1775
1866
|
projectFromDirName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
4
4
|
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@akemi-mio/evolution-strategy": "^0.1.0",
|
|
34
34
|
"@akemi-mio/evolution-safety": "^0.1.0",
|
|
35
35
|
"@akemi-mio/evolution-scheduler": "^0.1.0",
|
|
36
|
-
"@akemi-mio/observer": "^0.1.
|
|
36
|
+
"@akemi-mio/observer": "^0.1.3",
|
|
37
37
|
"@akemi-mio/insight": "^0.1.0"
|
|
38
38
|
},
|
|
39
39
|
"main": "index.js",
|
package/server/observer-store.js
CHANGED
|
@@ -332,6 +332,64 @@ function createObserverStore(options = {}) {
|
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
// D6: the scheduler host. ObserverService.start() wires the per-collector
|
|
336
|
+
// intervals, a 60s pipeline tick and a 5s first tick, and tickPipeline skips
|
|
337
|
+
// while today's DAG is COMPLETED -- i.e. the daily research engine already
|
|
338
|
+
// existed, it just had no long-lived caller anywhere in mio-agent-runtime.
|
|
339
|
+
// This is that caller.
|
|
340
|
+
//
|
|
341
|
+
// Deliberately NOT exposed as an MCP tool: every MCP server that connects
|
|
342
|
+
// would start its own scheduler against the same dag file, and a client
|
|
343
|
+
// restart would leave a second one running. The cross-process PipelineLock
|
|
344
|
+
// keeps runs from interleaving, but a scheduler is a process-lifetime
|
|
345
|
+
// concern, so it belongs to `mio observer serve` and `mio observe --research`
|
|
346
|
+
// (both single pid-managed processes), not to a tool call.
|
|
347
|
+
//
|
|
348
|
+
// Like `pipeline`, the dry run never constructs the service: the constructor
|
|
349
|
+
// mkdir's the dag directory and start() begins fetching, and this path is
|
|
350
|
+
// what `check:cli-docs` runs.
|
|
351
|
+
async function serve(args = {}) {
|
|
352
|
+
if (!ObserverService) throw new Error('@akemi-mio/observer not installed')
|
|
353
|
+
const baseDir = baseDirOf(args)
|
|
354
|
+
const llmConfig = observerLlmConfig()
|
|
355
|
+
const schedule = {
|
|
356
|
+
collectors: 'every source collects on start, then on its own interval (30m weibo / 1h rss,hn,douyin,bilibili / 4h github-trending)',
|
|
357
|
+
pipelineTickMs: 60000,
|
|
358
|
+
pipelineFirstTickMs: 5000,
|
|
359
|
+
pipelineGate: "runs while today's DAG is not COMPLETED -- one full run per day, retried until it completes",
|
|
360
|
+
lockFile: path.join(baseDir, 'dag', 'pipeline.lock'),
|
|
361
|
+
}
|
|
362
|
+
let llm = null
|
|
363
|
+
if (ObserverLlmService) {
|
|
364
|
+
try {
|
|
365
|
+
const svc = new ObserverLlmService(llmConfig)
|
|
366
|
+
if (svc && typeof svc.getInfo === 'function') llm = svc.getInfo()
|
|
367
|
+
} catch (_) {
|
|
368
|
+
llm = null
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (args.dryRun) {
|
|
372
|
+
return {
|
|
373
|
+
dryRun: true,
|
|
374
|
+
baseDir,
|
|
375
|
+
observerAvailable: true,
|
|
376
|
+
llm,
|
|
377
|
+
schedule,
|
|
378
|
+
hint: 'Re-run without --dry-run to keep the scheduler in the foreground (Ctrl+C stops it).',
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const service = new ObserverService(baseDir, llmConfig)
|
|
382
|
+
await service.start()
|
|
383
|
+
return {
|
|
384
|
+
dryRun: false,
|
|
385
|
+
serving: true,
|
|
386
|
+
baseDir,
|
|
387
|
+
llm,
|
|
388
|
+
schedule,
|
|
389
|
+
stop: () => service.stop(),
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
335
393
|
return {
|
|
336
394
|
baseDir: defaultBaseDir,
|
|
337
395
|
status,
|
|
@@ -344,6 +402,7 @@ function createObserverStore(options = {}) {
|
|
|
344
402
|
collect,
|
|
345
403
|
ferment,
|
|
346
404
|
pipeline,
|
|
405
|
+
serve,
|
|
347
406
|
}
|
|
348
407
|
}
|
|
349
408
|
|