@yottameta/yotta-memory 0.13.2 → 0.16.1

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.
@@ -22,10 +22,11 @@ const os = require('os');
22
22
  const crypto = require('crypto');
23
23
  const http = require('http');
24
24
  const child_process = require('child_process');
25
+ const { AsyncLocalStorage } = require('async_hooks');
25
26
 
26
- const VERSION = '0.13.2';
27
+ const VERSION = '0.16.1';
27
28
  // @generated view-html:start
28
- const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>元忆 · 用户查看平台</title><style>\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\nh1{font-size:22px} .card{background:#fff;border:1px solid #e2e2e2;border-radius:10px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\n.owner{display:inline-flex;align-items:center;gap:6px;border:1px solid #ddd;border-radius:8px;padding:5px 10px;margin:4px 6px 4px 0;background:#f6f8fa}\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\n</style></head><body>\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\n<div id=\"lock\" class=\"card\">\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key &lt;钥匙&gt;</code></p>\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\n <button onclick=\"unlock()\">解锁</button>\n <div class=\"err\" id=\"lockerr\"></div>\n</div>\n<div id=\"app\">\n <div class=\"card\">\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\n <div class=\"meta\" style=\"margin-top:6px\">「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 <code>keys/pending/&lt;agent_id&gt;.key</code>,供该 AI 新会话领取,领取成功后自动删除。</div>\n <div id=\"owners\" style=\"margin-top:8px\"></div>\n </div>\n <div class=\"card\">\n <b>记忆</b>\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter'){off=0;load()}\">\n <button onclick=\"off=0;load()\">搜索</button>\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\n <div id=\"meta\" style=\"margin-top:10px;font-size:12px;color:#666\"></div>\n <div id=\"entries\" style=\"margin-top:6px\"></div>\n <div id=\"pager\" style=\"margin-top:10px\">\n <button class=\"ghost\" id=\"prevb\" onclick=\"prevPage()\">上一页</button>\n <span id=\"pageinfo\" style=\"font-size:12px;color:#888;margin:0 8px\"></span>\n <button class=\"ghost\" id=\"nextb\" onclick=\"nextPage()\">下一页</button>\n </div>\n </div>\n <div class=\"card\">\n <b>重设口令</b><br>\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\n <button onclick=\"resetPw()\">重设</button>\n <span id=\"pwout\"></span>\n </div>\n</div>\n<script>\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];});}\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML='(无 owner)';return;}\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){var owner=b.getAttribute('data-a');if(!confirm('确认由你为用户授权 '+owner+' 读取其私密记忆?授权后将生成只显示一次的 agent_key,请立即保存;同时写入待领取文件供该 AI 新会话领取。AI 不应代为执行该授权操作。'))return;b.disabled=true;api('/api/authorize',{owner:owner}).then(function(d){b.disabled=false;if(!d||d.error){alert((d&&d.error)||'授权失败');loadOwners();return;}if(d.agentKey){showKey(d.agentKey);}loadOwners();});};});\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){if(!confirm('确认吊销 '+b.getAttribute('data-r')+' 的 agent_key?吊销后该智能体立即失去私密读写能力。'))return;api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\nfunction showKey(k){var ov=document.createElement('div');ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:99';var box=document.createElement('div');box.className='card';box.style.cssText='max-width:640px;word-break:break-all';var t=document.createElement('div');t.innerHTML='<b>agent_key(只显示一次)</b>';var hint=document.createElement('div');hint.className='meta';hint.textContent='请用户立即单独保存。AI 新会话执行 yotta-memory key claim <agent_id> --to <AI_HOME> 领取,写入 <AI_HOME>/.yotta-memory-agent-key 后删除待领取文件。若 key 丢失,可吊销后重新授权;旧 key 会立即校验失败。';var ta=document.createElement('textarea');ta.readOnly=true;ta.value=k;ta.style.cssText='width:100%;height:72px;margin-top:8px;font-family:monospace;font-size:12px';var close=document.createElement('button');close.textContent='我已保存,关闭';close.onclick=function(){ov.remove();};box.appendChild(t);box.appendChild(hint);box.appendChild(ta);box.appendChild(close);ov.appendChild(box);document.body.appendChild(ov);ta.focus();ta.select();}\n}\nlet off=0,PS=50;\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value,offset:off,limit:PS});const meta=document.getElementById('meta');const pg=document.getElementById('pageinfo');if(meta)meta.textContent='共 '+d.count+' 条';const lim=d.limit||PS;const totalPg=Math.max(1,Math.ceil(d.count/lim));const curPg=Math.floor((d.offset||0)/lim)+1;if(pg)pg.textContent='第 '+curPg+' / '+totalPg+' 页';const box=document.getElementById('entries');box.innerHTML='';if(d.entries)for(const e of d.entries){const div=document.createElement('div');div.className='entry';div.innerHTML='<b>['+esc(e.type)+'] '+esc(e.subject)+'</b><div>'+esc(e.statement)+'</div><div class=\"meta\">'+esc(e.file)+' · owner='+esc(e.owner||'-')+' · '+esc(e.updated||e.created||'')+'</div>';box.appendChild(div);}const pb=document.getElementById('prevb'),nb=document.getElementById('nextb');if(pb)pb.disabled=(d.offset||0)<=0;if(nb)nb.disabled=!d.hasMore;}\nfunction prevPage(){if(off>=PS){off-=PS;load();}}\nfunction nextPage(){off+=PS;load();}\nasync function doExport(){const d=await api('/api/export');if(d.error){alert(d.error);return;}const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='yottamemory-view-export.json';a.click();}\nasync function showRk(){const d=await api('/api/recovery-key');document.getElementById('rkout').textContent=d.recoveryKey?('恢复钥匙: '+d.recoveryKey):(d.error||'');}\nasync function resetPw(){const np1=document.getElementById('np1').value,np2=document.getElementById('np2').value;if(np1!==np2){document.getElementById('pwout').innerHTML='<span class=\"err\">两次新口令不一致</span>';return;}\n const d=await api('/api/reset-password',{currentPassword:document.getElementById('cur').value,newPassword:np1});document.getElementById('pwout').innerHTML=d.error?('<span class=\"err\">'+esc(d.error)+'</span>'):('<span class=\"ok\">'+esc(d.text||'ok')+'</span>');}\nboot();\n</script></body></html>\n";
29
+ const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>元忆 · 用户查看平台</title><style>\r\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\r\nh1{font-size:22px} .card{background:#fff;border:1px solid #e2e2e2;border-radius:10px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}\r\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\r\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\r\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\r\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\r\n.owner{display:inline-flex;align-items:center;gap:6px;border:1px solid #ddd;border-radius:8px;padding:5px 10px;margin:4px 6px 4px 0;background:#f6f8fa}\r\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\r\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\r\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\r\n</style></head><body>\r\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\r\n<div id=\"lock\" class=\"card\">\r\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key &lt;钥匙&gt;</code></p>\r\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\r\n <button onclick=\"unlock()\">解锁</button>\r\n <div class=\"err\" id=\"lockerr\"></div>\r\n</div>\r\n<div id=\"app\">\r\n <div class=\"card\">\r\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\r\n <div class=\"meta\" style=\"margin-top:6px\">「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 <code>keys/pending/&lt;agent_id&gt;.key</code>,供该 AI 新会话领取,领取成功后自动删除。</div>\r\n <div id=\"owners\" style=\"margin-top:8px\"></div>\r\n </div>\r\n <div class=\"card\">\r\n <b>记忆</b>\r\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter'){off=0;load()}\">\r\n <button onclick=\"off=0;load()\">搜索</button>\r\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\r\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\r\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\r\n <div id=\"meta\" style=\"margin-top:10px;font-size:12px;color:#666\"></div>\r\n <div id=\"entries\" style=\"margin-top:6px\"></div>\r\n <div id=\"pager\" style=\"margin-top:10px\">\r\n <button class=\"ghost\" id=\"prevb\" onclick=\"prevPage()\">上一页</button>\r\n <span id=\"pageinfo\" style=\"font-size:12px;color:#888;margin:0 8px\"></span>\r\n <button class=\"ghost\" id=\"nextb\" onclick=\"nextPage()\">下一页</button>\r\n </div>\r\n </div>\r\n <div class=\"card\">\r\n <b>重设口令</b><br>\r\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\r\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\r\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\r\n <button onclick=\"resetPw()\">重设</button>\r\n <span id=\"pwout\"></span>\r\n </div>\r\n</div>\r\n<script>\r\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];});}\r\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\r\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\r\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\r\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\r\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML='(无 owner)';return;}\r\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\r\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){var owner=b.getAttribute('data-a');if(!confirm('确认由你为用户授权 '+owner+' 读取其私密记忆?授权后将生成只显示一次的 agent_key,请立即保存;同时写入待领取文件供该 AI 新会话领取。AI 不应代为执行该授权操作。'))return;b.disabled=true;api('/api/authorize',{owner:owner}).then(function(d){b.disabled=false;if(!d||d.error){alert((d&&d.error)||'授权失败');loadOwners();return;}if(d.agentKey){showKey(d.agentKey);}loadOwners();});};});\r\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){if(!confirm('确认吊销 '+b.getAttribute('data-r')+' 的 agent_key?吊销后该智能体立即失去私密读写能力。'))return;api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\r\nfunction showKey(k){var ov=document.createElement('div');ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:99';var box=document.createElement('div');box.className='card';box.style.cssText='max-width:640px;word-break:break-all';var t=document.createElement('div');t.innerHTML='<b>agent_key(只显示一次)</b>';var hint=document.createElement('div');hint.className='meta';hint.textContent='请用户立即单独保存。AI 新会话先执行 yotta-memory key status <agent_id>,有 pending 再执行 key claim <agent_id>;默认写入 AI_HOME/.yotta-memory-agent-key,需要时用 --to 或 --agent-key-file 指定。若 key 丢失,可吊销后重新授权;旧 key 会立即校验失败。';var ta=document.createElement('textarea');ta.readOnly=true;ta.value=k;ta.style.cssText='width:100%;height:72px;margin-top:8px;font-family:monospace;font-size:12px';var close=document.createElement('button');close.textContent='我已保存,关闭';close.onclick=function(){ov.remove();};box.appendChild(t);box.appendChild(hint);box.appendChild(ta);box.appendChild(close);ov.appendChild(box);document.body.appendChild(ov);ta.focus();ta.select();}\r\n}\r\nlet off=0,PS=50;\r\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value,offset:off,limit:PS});const meta=document.getElementById('meta');const pg=document.getElementById('pageinfo');if(meta)meta.textContent='共 '+d.count+' 条';const lim=d.limit||PS;const totalPg=Math.max(1,Math.ceil(d.count/lim));const curPg=Math.floor((d.offset||0)/lim)+1;if(pg)pg.textContent='第 '+curPg+' / '+totalPg+' 页';const box=document.getElementById('entries');box.innerHTML='';if(d.entries)for(const e of d.entries){const div=document.createElement('div');div.className='entry';div.innerHTML='<b>['+esc(e.type)+'] '+esc(e.subject)+'</b><div>'+esc(e.statement)+'</div><div class=\"meta\">'+esc(e.file)+' · owner='+esc(e.owner||'-')+' · '+esc(e.updated||e.created||'')+'</div>';box.appendChild(div);}const pb=document.getElementById('prevb'),nb=document.getElementById('nextb');if(pb)pb.disabled=(d.offset||0)<=0;if(nb)nb.disabled=!d.hasMore;}\r\nfunction prevPage(){if(off>=PS){off-=PS;load();}}\r\nfunction nextPage(){off+=PS;load();}\r\nasync function doExport(){const d=await api('/api/export');if(d.error){alert(d.error);return;}const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='yottamemory-view-export.json';a.click();}\r\nasync function showRk(){const d=await api('/api/recovery-key');document.getElementById('rkout').textContent=d.recoveryKey?('恢复钥匙: '+d.recoveryKey):(d.error||'');}\r\nasync function resetPw(){const np1=document.getElementById('np1').value,np2=document.getElementById('np2').value;if(np1!==np2){document.getElementById('pwout').innerHTML='<span class=\"err\">两次新口令不一致</span>';return;}\r\n const d=await api('/api/reset-password',{currentPassword:document.getElementById('cur').value,newPassword:np1});document.getElementById('pwout').innerHTML=d.error?('<span class=\"err\">'+esc(d.error)+'</span>'):('<span class=\"ok\">'+esc(d.text||'ok')+'</span>');}\r\nboot();\r\n</script></body></html>\r\n";
29
30
  // @generated view-html:end
30
31
  // MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
31
32
  const MCP_PROTOCOL_MODERN = '2026-07-28';
@@ -89,39 +90,531 @@ function memoryRoots() {
89
90
  }
90
91
  return out;
91
92
  }
93
+ // ---- v0.16.0 M2: runtime stable entry ----
94
+ const RUNTIME_SCHEMA = 1;
95
+ function runtimeRoot() {
96
+ if (process.env.YOTTA_MEMORY_RUNTIME_HOME) return path.resolve(process.env.YOTTA_MEMORY_RUNTIME_HOME);
97
+ return path.join(path.dirname(configPath()), 'runtime');
98
+ }
99
+ function runtimeManifestPath() { return path.join(runtimeRoot(), 'runtime.json'); }
100
+ function runtimeVersionsDir() { return path.join(runtimeRoot(), 'versions'); }
101
+ function runtimeVersionDir(version) { return path.join(runtimeVersionsDir(), String(version)); }
102
+ function runtimeCurrentDir() { return path.join(runtimeRoot(), 'current'); }
103
+ function runtimeCurrentBin() { return path.join(runtimeCurrentDir(), 'bin', 'yotta-memory.js'); }
104
+ function isSafeRuntimeVersion(version) {
105
+ const value = String(version || '').trim();
106
+ if (!value || value === '.' || value === '..') return false;
107
+ if (!/^[0-9A-Za-z][0-9A-Za-z.+-]*$/.test(value)) return false;
108
+ return !/[\\/]/.test(value);
109
+ }
110
+ function runtimePathWithin(parent, child) {
111
+ const p = path.resolve(parent);
112
+ const c = path.resolve(child);
113
+ return c === p || c.indexOf(p + path.sep) === 0;
114
+ }
115
+ function readRuntimeManifest() {
116
+ try {
117
+ const parsed = JSON.parse(fs.readFileSync(runtimeManifestPath(), 'utf8'));
118
+ if (!parsed || typeof parsed !== 'object') return null;
119
+ return parsed;
120
+ } catch (e) {
121
+ return null;
122
+ }
123
+ }
124
+ function writeRuntimeManifest(manifest) {
125
+ fs.mkdirSync(runtimeRoot(), { recursive: true });
126
+ const target = runtimeManifestPath();
127
+ const temp = target + '.tmp-' + process.pid + '-' + Date.now();
128
+ fs.writeFileSync(temp, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
129
+ fs.renameSync(temp, target);
130
+ }
131
+ function runtimeTreeHash(dir) {
132
+ const hash = crypto.createHash('sha256');
133
+ function walk(rel) {
134
+ const full = path.join(dir, rel);
135
+ const entries = fs.readdirSync(full, { withFileTypes: true })
136
+ .sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
137
+ for (const entry of entries) {
138
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
139
+ const childRel = rel ? path.join(rel, entry.name) : entry.name;
140
+ const childFull = path.join(dir, childRel);
141
+ if (entry.isDirectory()) {
142
+ walk(childRel);
143
+ } else if (entry.isFile()) {
144
+ hash.update(childRel.replace(/\\/g, '/') + '\0');
145
+ hash.update(fs.readFileSync(childFull));
146
+ hash.update('\0');
147
+ }
148
+ }
149
+ }
150
+ walk('');
151
+ return hash.digest('hex');
152
+ }
153
+ function runtimePackageVersion(dir) {
154
+ const pkgPath = path.join(dir, 'package.json');
155
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
156
+ const version = String(pkg.version || '').trim();
157
+ if (!isSafeRuntimeVersion(version)) throw new Error('runtime 包版本非法: ' + version);
158
+ return version;
159
+ }
160
+ function runtimeVerifyVersionDir(dir, expected, manifest) {
161
+ if (!fs.existsSync(dir)) return { error: true, text: 'runtime 版本目录不存在: ' + dir };
162
+ let version = '';
163
+ try { version = runtimePackageVersion(dir); } catch (e) { return { error: true, text: e.message }; }
164
+ if (version !== expected) return { error: true, text: 'runtime 版本不一致: 目录为 ' + version + ',请求 ' + expected };
165
+ if (!fs.existsSync(path.join(dir, 'bin', 'yotta-memory.js'))) {
166
+ return { error: true, text: 'runtime 版本缺少 bin/yotta-memory.js: ' + dir };
167
+ }
168
+ const treeHash = runtimeTreeHash(dir);
169
+ const record = manifest && manifest.versions && manifest.versions[expected];
170
+ if (record && record.treeHash && record.treeHash !== treeHash) {
171
+ return { error: true, text: 'runtime 版本内容哈希与 runtime.json 不一致;请重新安装 ' + expected, treeHash: treeHash };
172
+ }
173
+ return { error: false, treeHash: treeHash };
174
+ }
175
+ function compareRuntimeVersions(a, b) {
176
+ const pa = String(a).split('.').map(function (x) { return parseInt(x, 10) || 0; });
177
+ const pb = String(b).split('.').map(function (x) { return parseInt(x, 10) || 0; });
178
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
179
+ const d = (pa[i] || 0) - (pb[i] || 0);
180
+ if (d !== 0) return d;
181
+ }
182
+ return String(a).localeCompare(String(b));
183
+ }
184
+ function runtimeListVersions() {
185
+ const dir = runtimeVersionsDir();
186
+ if (!fs.existsSync(dir)) return [];
187
+ return fs.readdirSync(dir).filter(function (name) {
188
+ if (!isSafeRuntimeVersion(name)) return false;
189
+ return fs.existsSync(path.join(dir, name, 'package.json'));
190
+ }).sort(function (a, b) { return compareRuntimeVersions(b, a); });
191
+ }
192
+ function runtimeManagedScript() {
193
+ const current = runtimeCurrentBin();
194
+ return fs.existsSync(current) ? current : __filename;
195
+ }
196
+ function runtimePatternToRegExp(pattern) {
197
+ const escaped = String(pattern).split('*').map(function (part) {
198
+ return part.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
199
+ }).join('.*');
200
+ return new RegExp('^' + escaped + '$');
201
+ }
202
+ function runtimeMatchExclude(rel, pattern) {
203
+ const r = String(rel).replace(/\\/g, '/');
204
+ const p = String(pattern).replace(/\\/g, '/').replace(/^\.\//, '');
205
+ if (p.indexOf('*') === -1) return r === p || r.indexOf(p.replace(/\/$/, '') + '/') === 0;
206
+ return runtimePatternToRegExp(p).test(r);
207
+ }
208
+ function runtimeCopyTreeFiltered(src, dst, excludes, relBase) {
209
+ const base = relBase ? relBase + '/' : '';
210
+ const entries = fs.readdirSync(src, { withFileTypes: true });
211
+ fs.mkdirSync(dst, { recursive: true });
212
+ for (const entry of entries) {
213
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
214
+ const rel = base + entry.name;
215
+ if (excludes.some(function (pattern) { return runtimeMatchExclude(rel, pattern); })) continue;
216
+ const source = path.join(src, entry.name);
217
+ const target = path.join(dst, entry.name);
218
+ if (entry.isDirectory()) runtimeCopyTreeFiltered(source, target, excludes, rel);
219
+ else if (entry.isFile()) fs.copyFileSync(source, target);
220
+ }
221
+ }
222
+ function runtimeCopyTree(src, dst) {
223
+ const st = fs.statSync(src);
224
+ if (st.isDirectory()) {
225
+ runtimeCopyTreeFiltered(src, dst, [], '');
226
+ } else if (st.isFile()) {
227
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
228
+ fs.copyFileSync(src, dst);
229
+ }
230
+ }
231
+ function runtimeCopyPackageFromRoot(srcRoot, dstRoot) {
232
+ const pkg = JSON.parse(fs.readFileSync(path.join(srcRoot, 'package.json'), 'utf8'));
233
+ const files = Array.isArray(pkg.files) ? pkg.files : [];
234
+ const includes = ['package.json'].concat(files.filter(function (entry) { return entry && entry[0] !== '!'; }));
235
+ const excludes = files.filter(function (entry) { return entry && entry[0] === '!'; }).map(function (entry) { return entry.slice(1); });
236
+ for (const entry of includes) {
237
+ const rel = String(entry).replace(/^\.\//, '');
238
+ const source = path.join(srcRoot, rel);
239
+ if (!fs.existsSync(source)) continue;
240
+ const target = path.join(dstRoot, rel);
241
+ const st = fs.statSync(source);
242
+ if (st.isDirectory()) {
243
+ runtimeCopyTreeFiltered(source, target, excludes, rel);
244
+ } else if (st.isFile() && !excludes.some(function (pattern) { return runtimeMatchExclude(rel, pattern); })) {
245
+ fs.mkdirSync(path.dirname(target), { recursive: true });
246
+ fs.copyFileSync(source, target);
247
+ }
248
+ }
249
+ }
250
+ function runtimeRemoveVersionDir(version) {
251
+ const target = runtimeVersionDir(version);
252
+ if (!runtimePathWithin(runtimeVersionsDir(), target)) throw new Error('拒绝删除 runtime 目录外的路径: ' + target);
253
+ if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
254
+ }
255
+ function runtimeSwitchCurrent(version) {
256
+ const dir = runtimeVersionDir(version);
257
+ if (!fs.existsSync(path.join(dir, 'package.json'))) throw new Error('runtime 版本未安装: ' + version);
258
+ fs.mkdirSync(runtimeRoot(), { recursive: true });
259
+ const temp = path.join(runtimeRoot(), '.current-' + process.pid + '-' + Date.now());
260
+ const current = runtimeCurrentDir();
261
+ try {
262
+ fs.symlinkSync(dir, temp, process.platform === 'win32' ? 'junction' : 'dir');
263
+ if (fs.existsSync(current)) {
264
+ const st = fs.lstatSync(current);
265
+ if (st.isSymbolicLink() || st.isFile()) fs.unlinkSync(current);
266
+ else throw new Error('current 已存在且不是可替换的指针: ' + current);
267
+ }
268
+ fs.renameSync(temp, current);
269
+ } catch (e) {
270
+ try { if (fs.existsSync(temp)) fs.unlinkSync(temp); } catch (ignore) {}
271
+ throw e;
272
+ }
273
+ }
274
+ function runtimeInstallPrepared(pkgRoot, version, opts) {
275
+ opts = opts || {};
276
+ if (!isSafeRuntimeVersion(version)) return { error: true, text: '非法 runtime 版本: ' + version };
277
+ fs.mkdirSync(runtimeVersionsDir(), { recursive: true });
278
+ const finalDir = runtimeVersionDir(version);
279
+ const staging = path.join(runtimeVersionsDir(), '.' + version + '.tmp-' + process.pid + '-' + Date.now());
280
+ try {
281
+ runtimeCopyTree(pkgRoot, staging);
282
+ if (runtimePackageVersion(staging) !== version) throw new Error('安装包版本与目标版本不一致');
283
+ if (!fs.existsSync(path.join(staging, 'bin', 'yotta-memory.js'))) throw new Error('安装包缺少 bin/yotta-memory.js');
284
+ const treeHash = runtimeTreeHash(staging);
285
+ if (fs.existsSync(finalDir)) {
286
+ const existingHash = runtimeTreeHash(finalDir);
287
+ if (existingHash === treeHash) {
288
+ fs.rmSync(staging, { recursive: true, force: true });
289
+ return { error: false, installed: false, version: version, treeHash: treeHash, path: finalDir, text: 'runtime ' + version + ' 已安装且内容一致。' };
290
+ }
291
+ if (!opts.force) throw new Error('runtime ' + version + ' 已安装且内容不同;如需覆盖请加 --force。');
292
+ runtimeRemoveVersionDir(version);
293
+ }
294
+ fs.renameSync(staging, finalDir);
295
+ const previousManifest = readRuntimeManifest();
296
+ const manifest = previousManifest || { schema: RUNTIME_SCHEMA, current: '', previous: '', versions: {}, updatedAt: '' };
297
+ manifest.schema = RUNTIME_SCHEMA;
298
+ manifest.versions = manifest.versions || {};
299
+ manifest.versions[version] = Object.assign({}, manifest.versions[version], {
300
+ version: version,
301
+ path: finalDir,
302
+ treeHash: treeHash,
303
+ installedAt: new Date().toISOString(),
304
+ source: opts.source || 'unknown',
305
+ });
306
+ if (!manifest.current) manifest.current = version;
307
+ manifest.updatedAt = new Date().toISOString();
308
+ writeRuntimeManifest(manifest);
309
+ if (manifest.current === version) {
310
+ try {
311
+ runtimeSwitchCurrent(version);
312
+ } catch (e) {
313
+ if (previousManifest) writeRuntimeManifest(previousManifest);
314
+ return { error: true, text: '切换 current 失败: ' + e.message };
315
+ }
316
+ }
317
+ return { error: false, installed: true, version: version, treeHash: treeHash, path: finalDir, text: '已安装 runtime ' + version + ' 到 ' + finalDir };
318
+ } catch (e) {
319
+ try { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); } catch (ignore) {}
320
+ return { error: true, text: e.message };
321
+ }
322
+ }
323
+ function runtimeValidateTarballEntries(entries) {
324
+ const normalized = (entries || []).map(function (entry) {
325
+ return String(entry || '').replace(/\\/g, '/').replace(/\/+$/, '');
326
+ }).filter(Boolean);
327
+ if (!normalized.length) throw new Error('runtime tarball 为空');
328
+ for (const entry of normalized) {
329
+ if (entry[0] === '/' || /^[A-Za-z]:/.test(entry)) throw new Error('runtime tarball 含绝对路径成员: ' + entry);
330
+ const parts = entry.split('/');
331
+ if (parts.indexOf('..') !== -1) throw new Error('runtime tarball 含路径穿越成员: ' + entry);
332
+ if (parts[0] !== 'package') throw new Error('runtime tarball 含 package/ 之外的成员: ' + entry);
333
+ }
334
+ if (normalized.indexOf('package/package.json') === -1) throw new Error('runtime tarball 缺少 package/package.json');
335
+ }
336
+ function runtimeListTarballEntries(tarball) {
337
+ const tarBin = process.env.YOTTA_RUNTIME_TAR || 'tar';
338
+ const listed = child_process.spawnSync(tarBin, ['-tzf', tarball], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
339
+ if (listed.error) throw new Error('无法执行 tar: ' + listed.error.message);
340
+ if (listed.status !== 0) throw new Error('读取 runtime tarball 失败: ' + String(listed.stderr || listed.stdout || '').trim());
341
+ return String(listed.stdout || '').split(/\r?\n/).filter(Boolean);
342
+ }
343
+ function runtimeExtractTarball(tarball, tempDir) {
344
+ runtimeValidateTarballEntries(runtimeListTarballEntries(tarball));
345
+ const tarBin = process.env.YOTTA_RUNTIME_TAR || 'tar';
346
+ const result = child_process.spawnSync(tarBin, ['-xzf', tarball, '-C', tempDir], { encoding: 'utf8' });
347
+ if (result.error) throw new Error('无法执行 tar: ' + result.error.message);
348
+ if (result.status !== 0) throw new Error('解压 runtime tarball 失败: ' + String(result.stderr || result.stdout || '').trim());
349
+ const packaged = path.join(tempDir, 'package');
350
+ if (fs.existsSync(path.join(packaged, 'package.json'))) return packaged;
351
+ if (fs.existsSync(path.join(tempDir, 'package.json'))) return tempDir;
352
+ throw new Error('runtime tarball 中未找到 package/package.json');
353
+ }
354
+ function runtimeInstallFromCurrent(opts) {
355
+ opts = opts || {};
356
+ const srcRoot = path.join(__dirname, '..');
357
+ const pkgPath = path.join(srcRoot, 'package.json');
358
+ if (!fs.existsSync(pkgPath)) return { error: true, text: '当前运行时不是完整 npm 包(缺少 package.json),无法安装稳定入口。' };
359
+ let version = '';
360
+ try { version = runtimePackageVersion(srcRoot); } catch (e) { return { error: true, text: e.message }; }
361
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-current-'));
362
+ try {
363
+ const staged = path.join(temp, 'package');
364
+ runtimeCopyPackageFromRoot(srcRoot, staged);
365
+ return runtimeInstallPrepared(staged, version, { source: 'current', force: !!opts.force });
366
+ } catch (e) {
367
+ return { error: true, text: e.message };
368
+ } finally {
369
+ try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
370
+ }
371
+ }
372
+ function runtimeInstallTarball(tarball, requestedVersion, opts) {
373
+ opts = opts || {};
374
+ const abs = path.resolve(String(tarball || ''));
375
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return { error: true, text: 'runtime tarball 不存在: ' + abs };
376
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-tarball-'));
377
+ try {
378
+ const pkgRoot = runtimeExtractTarball(abs, temp);
379
+ const version = runtimePackageVersion(pkgRoot);
380
+ if (requestedVersion && String(requestedVersion) !== version) {
381
+ throw new Error('runtime 包版本 ' + version + ' 与请求版本 ' + requestedVersion + ' 不一致');
382
+ }
383
+ return runtimeInstallPrepared(pkgRoot, version, { source: abs, force: !!opts.force });
384
+ } catch (e) {
385
+ return { error: true, text: e.message };
386
+ } finally {
387
+ try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
388
+ }
389
+ }
390
+ function runtimeNpmSpec() {
391
+ const candidates = [];
392
+ if (process.env.npm_execpath) candidates.push(process.env.npm_execpath);
393
+ candidates.push(path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
394
+ candidates.push(path.resolve(path.dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
395
+ for (const candidate of candidates) {
396
+ if (candidate && fs.existsSync(candidate)) return { command: process.execPath, args: [path.resolve(candidate)] };
397
+ }
398
+ return null;
399
+ }
400
+ function runtimeRunNpm(args, opts) {
401
+ const spec = runtimeNpmSpec();
402
+ if (!spec) throw new Error('无法定位 npm CLI;请改用 runtime install <本地 tarball>。');
403
+ const result = child_process.spawnSync(spec.command, spec.args.concat(args), {
404
+ cwd: (opts && opts.cwd) || process.cwd(),
405
+ encoding: 'utf8',
406
+ timeout: 120000,
407
+ });
408
+ if (result.error) throw new Error('npm 执行失败: ' + result.error.message);
409
+ if (result.status !== 0) throw new Error('npm 执行失败: ' + String(result.stderr || result.stdout || '').trim());
410
+ return result;
411
+ }
412
+ function runtimeInstallVersion(version, opts) {
413
+ const target = String(version || '').trim();
414
+ if (!isSafeRuntimeVersion(target)) return { error: true, text: '非法 runtime 版本: ' + target };
415
+ if (target === VERSION) return runtimeInstallFromCurrent(opts);
416
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-pack-'));
417
+ try {
418
+ fs.mkdirSync(runtimeRoot(), { recursive: true });
419
+ runtimeRunNpm(['pack', '@yottameta/yotta-memory@' + target, '--pack-destination', temp], { cwd: runtimeRoot() });
420
+ const tarball = fs.readdirSync(temp).filter(function (name) { return /\.tgz$/i.test(name); })[0];
421
+ if (!tarball) throw new Error('npm pack 未生成 tarball');
422
+ return runtimeInstallTarball(path.join(temp, tarball), target, opts);
423
+ } catch (e) {
424
+ return { error: true, text: e.message };
425
+ } finally {
426
+ try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
427
+ }
428
+ }
429
+ function runtimeUseCore(version, opts) {
430
+ opts = opts || {};
431
+ const target = String(version || '').trim();
432
+ if (!isSafeRuntimeVersion(target)) return { error: true, text: '非法 runtime 版本: ' + target };
433
+ const manifest = readRuntimeManifest();
434
+ if (!manifest) return { error: true, text: 'runtime 未初始化,请先运行 yotta-memory runtime install --from-current。' };
435
+ const dir = runtimeVersionDir(target);
436
+ if (!fs.existsSync(dir)) return { error: true, text: 'runtime 版本未安装: ' + target };
437
+ const check = runtimeVerifyVersionDir(dir, target, manifest);
438
+ if (check.error) return check;
439
+ const previous = manifest.current && manifest.current !== target ? manifest.current : (manifest.previous && manifest.previous !== target ? manifest.previous : '');
440
+ const previousManifest = JSON.parse(JSON.stringify(manifest));
441
+ try {
442
+ runtimeSwitchCurrent(target);
443
+ } catch (e) {
444
+ return { error: true, text: '切换 current 失败: ' + e.message };
445
+ }
446
+ manifest.schema = RUNTIME_SCHEMA;
447
+ manifest.current = target;
448
+ manifest.previous = previous || '';
449
+ manifest.updatedAt = new Date().toISOString();
450
+ manifest.versions = manifest.versions || {};
451
+ manifest.versions[target] = Object.assign({}, manifest.versions[target], { treeHash: check.treeHash, lastUsedAt: new Date().toISOString() });
452
+ try {
453
+ writeRuntimeManifest(manifest);
454
+ } catch (e) {
455
+ try { if (previous) runtimeSwitchCurrent(previous); } catch (ignore) {}
456
+ return { error: true, text: '写入 runtime.json 失败: ' + e.message };
457
+ }
458
+ let tail = ';如需重启受管 server,请加 --restart';
459
+ if (opts.restart) {
460
+ const restart = opts.restartFn ? opts.restartFn() : runtimeRestartManagedServer();
461
+ if (restart && restart.error) {
462
+ let rollbackText = '';
463
+ if (previous) {
464
+ try {
465
+ runtimeSwitchCurrent(previous);
466
+ const rollback = JSON.parse(JSON.stringify(previousManifest));
467
+ rollback.current = previous;
468
+ rollback.previous = target;
469
+ rollback.updatedAt = new Date().toISOString();
470
+ writeRuntimeManifest(rollback);
471
+ (opts.restartFn ? opts.restartFn() : runtimeRestartManagedServer());
472
+ rollbackText = ';已回滚 current 到 ' + previous;
473
+ } catch (e) {
474
+ rollbackText = ';回滚失败: ' + e.message;
475
+ }
476
+ }
477
+ return { error: true, text: '受管 server 重启失败: ' + (restart.text || '') + rollbackText };
478
+ }
479
+ tail = ';受管 server 已重启';
480
+ }
481
+ return { error: false, text: '已切换 current 到 ' + target + tail + '。', version: target, treeHash: check.treeHash };
482
+ }
483
+ function runtimeRollbackCore(opts) {
484
+ const manifest = readRuntimeManifest();
485
+ if (!manifest || !manifest.current) return { error: true, text: 'runtime 未初始化或没有当前版本。' };
486
+ if (!manifest.previous) return { error: true, text: '没有可回滚的 runtime 版本。' };
487
+ const target = manifest.previous;
488
+ const result = runtimeUseCore(target, opts);
489
+ if (result.error) return result;
490
+ return { error: false, text: '已回滚 current 到 ' + target + '。' + result.text, version: target };
491
+ }
492
+ function runtimeStatusCore() {
493
+ const root = runtimeRoot();
494
+ const manifest = readRuntimeManifest();
495
+ const lines = ['runtime root: ' + root];
496
+ const drift = [];
497
+ if (!manifest) {
498
+ lines.push('runtime: 未初始化');
499
+ lines.push('提示: yotta-memory runtime install --from-current');
500
+ return { error: false, text: lines.join('\n'), runtimeRoot: root, drift: ['runtime manifest missing'] };
501
+ }
502
+ lines.push('current: ' + (manifest.current || '(none)'));
503
+ lines.push('current path: ' + runtimeCurrentDir());
504
+ if (manifest.previous) lines.push('previous: ' + manifest.previous);
505
+ if (!manifest.current) drift.push('current version missing in runtime.json');
506
+ else {
507
+ const dir = runtimeVersionDir(manifest.current);
508
+ if (!fs.existsSync(dir)) drift.push('current version dir missing');
509
+ if (!fs.existsSync(runtimeCurrentBin())) drift.push('current launcher missing');
510
+ else {
511
+ try {
512
+ const real = fs.realpathSync(runtimeCurrentDir());
513
+ if (path.resolve(real) !== path.resolve(dir)) drift.push('current pointer mismatch');
514
+ } catch (e) {
515
+ drift.push('current pointer unreadable');
516
+ }
517
+ }
518
+ }
519
+ const versions = runtimeListVersions();
520
+ lines.push('versions: ' + (versions.length ? versions.join(', ') : '(none)'));
521
+ lines.push('drift: ' + (drift.length ? drift.join('; ') : 'none'));
522
+ return { error: false, text: lines.join('\n'), runtimeRoot: root, manifest: manifest, drift: drift };
523
+ }
524
+ function runtimeListCore() {
525
+ const manifest = readRuntimeManifest() || { current: '', versions: {} };
526
+ const versions = runtimeListVersions();
527
+ const lines = ['runtime versions:'];
528
+ if (!versions.length) lines.push(' (none)');
529
+ for (const version of versions) {
530
+ const record = (manifest.versions && manifest.versions[version]) || {};
531
+ lines.push(' ' + version + (manifest.current === version ? ' [current]' : '') + (record.treeHash ? ' ' + record.treeHash.slice(0, 12) + '...' : ''));
532
+ }
533
+ return { error: false, text: lines.join('\n'), versions: versions, current: manifest.current || '' };
534
+ }
535
+ function runtimeExecutionPath() {
536
+ const invoked = process.argv && process.argv[1] ? path.resolve(process.argv[1]) : '';
537
+ if (invoked && path.basename(invoked).toLowerCase() === 'yotta-memory.js') return invoked;
538
+ return path.resolve(__filename);
539
+ }
540
+ function runtimeEnsureForManagedTask() {
541
+ const manifest = readRuntimeManifest();
542
+ if (manifest && manifest.current) {
543
+ const check = runtimeVerifyVersionDir(runtimeVersionDir(manifest.current), manifest.current, manifest);
544
+ if (!check.error && fs.existsSync(runtimeCurrentBin())) {
545
+ return { error: false, text: 'runtime current 已就绪: ' + manifest.current, version: manifest.current };
546
+ }
547
+ }
548
+ return runtimeInstallFromCurrent({ force: false });
549
+ }
550
+ function runtimeRestartManagedServer() {
551
+ const platform = lanPlatform();
552
+ if (platform === 'win32') {
553
+ const query = child_process.spawnSync('schtasks', ['/query', '/tn', LAN_TASK_NAME, '/fo', 'LIST'], { encoding: 'utf8' });
554
+ if (query.status !== 0) return { error: true, text: '未检测到受管计划任务 ' + LAN_TASK_NAME + '。' };
555
+ const end = child_process.spawnSync('schtasks', ['/end', '/tn', LAN_TASK_NAME], { encoding: 'utf8' });
556
+ const run = child_process.spawnSync('schtasks', ['/run', '/tn', LAN_TASK_NAME], { encoding: 'utf8' });
557
+ if (run.status !== 0) return { error: true, text: '计划任务重启失败: ' + String(run.stderr || run.stdout || '').trim() };
558
+ return { error: false, text: '已重启计划任务 ' + LAN_TASK_NAME + (end.status === 0 ? '' : '(原任务未在运行)') };
559
+ }
560
+ if (platform === 'linux') {
561
+ const unit = lanLinuxUnitPath();
562
+ if (fs.existsSync(unit) && lanFileHasMarker(unit)) {
563
+ const result = lanLinuxSystemctl(['restart', LAN_UNIT_NAME]);
564
+ if (result.status !== 0) return { error: true, text: 'systemd 重启失败: ' + String(result.err || '').trim() };
565
+ return { error: false, text: '已重启 systemd 用户单元 ' + LAN_UNIT_NAME };
566
+ }
567
+ if (lanCrontabHasOurLine(lanCrontabRead())) {
568
+ return { error: true, text: '受管 server 由 crontab @reboot 启动,无法在线重启;请手动重启。' };
569
+ }
570
+ }
571
+ return { error: true, text: '未检测到可管理的 server 自启配置,未执行重启。' };
572
+ }
92
573
  function today() {
93
574
  const d = new Date();
94
575
  const p = (n) => String(n).padStart(2, '0');
95
576
  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
96
577
  }
97
- function envAgentTrusted() {
98
- const v = String(process.env.YOTTA_MEMORY_TRUST_ENV_AGENT || '').trim().toLowerCase();
99
- return v === '1' || v === 'true' || v === 'yes';
100
- }
101
- function rawEnvAgent() {
102
- return process.env.YOTTA_AGENT_ID || process.env.AGENT_ID || '';
103
- }
104
578
  let RUNTIME_AGENT = { id: '', agentKey: '' };
579
+ const IDENTITY_CONTEXT = new AsyncLocalStorage();
105
580
  const RUNTIME_OWNER_KEYS = new Map();
106
581
  function setRuntimeAgent(id, agentKey) {
107
582
  RUNTIME_AGENT = { id: String(id || '').trim(), agentKey: String(agentKey || '').trim() };
108
583
  RUNTIME_OWNER_KEYS.clear();
109
584
  }
110
- function trustedEnvAgentKey() {
111
- return envAgentTrusted() ? String(process.env.YOTTA_MEMORY_AGENT_KEY || '').trim() : '';
585
+ function currentRuntimeIdentity() {
586
+ return IDENTITY_CONTEXT.getStore() || RUNTIME_AGENT;
587
+ }
588
+ function runWithIdentity(identity, fn) {
589
+ const store = {
590
+ id: String((identity && identity.id) || '').trim(),
591
+ agentKey: String((identity && identity.agentKey) || '').trim(),
592
+ };
593
+ return IDENTITY_CONTEXT.run(store, fn);
112
594
  }
113
- function currentAgent() {
114
- // Ambient environment identity is only trusted for host-managed MCP processes.
115
- if (RUNTIME_AGENT.id) return RUNTIME_AGENT.id;
116
- return envAgentTrusted() ? rawEnvAgent() : '';
595
+ function legacyIdentityEnvNames() {
596
+ return ['YOTTA_AGENT_ID', 'AGENT_ID', 'YOTTA_MEMORY_AGENT_KEY', 'YOTTA_MEMORY_TRUST_ENV_AGENT']
597
+ .filter(function (name) { return String(process.env[name] || '').trim() !== ''; });
598
+ }
599
+ function legacyIdentityEnvError() {
600
+ const names = legacyIdentityEnvNames();
601
+ if (!names.length) return '';
602
+ return [
603
+ '[YTM_IDENTITY_ENV_REMOVED] 检测到旧身份环境变量: ' + names.join(', '),
604
+ '0.16.0 已删除身份 env:HTTP / 远程 MCP 只用请求头 Authorization + X-Agent-Id + X-Agent-Key;stdio MCP 只用显式参数 --agent-id <id> + --agent-key-file <path>。',
605
+ 'CLI 仍使用 --agent <id> + --agent-key/--agent-key-file。请从宿主 MCP 配置中移除身份 env 后重试。',
606
+ ].join('\n');
117
607
  }
118
608
  function resolveIdentity(opts) {
119
609
  opts = opts || {};
120
- const explicit = String(opts.agent || opts.selfAgent || '').trim();
610
+ const explicitAgent = String(opts.agent || opts.selfAgent || '').trim();
611
+ const explicitAgentId = String(opts.agentId || '').trim();
612
+ if (explicitAgent && explicitAgentId && explicitAgent !== explicitAgentId) {
613
+ return { id: '', agentKey: '', source: 'conflict', error: '身份冲突:--agent ' + explicitAgent + ' 与 --agent-id ' + explicitAgentId + ' 不一致,请只保留一个身份参数。' };
614
+ }
615
+ const explicit = explicitAgent || explicitAgentId;
121
616
  let explicitKey = String(opts.agentKey || '').trim();
122
617
  const keyFile = String(opts.agentKeyFile || '').trim();
123
- const env = String(rawEnvAgent() || '').trim();
124
- const trusted = envAgentTrusted();
125
618
  if (keyFile) {
126
619
  let fileKey = '';
127
620
  try { fileKey = fs.readFileSync(path.resolve(keyFile), 'utf8').trim(); } catch (e) {
@@ -134,26 +627,8 @@ function resolveIdentity(opts) {
134
627
  if (explicit && !isSafeAgentId(explicit)) {
135
628
  return { id: '', agentKey: '', source: 'invalid', error: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
136
629
  }
137
- if (env && !isSafeAgentId(env)) {
138
- return { id: '', agentKey: '', source: 'invalid-env', error: '环境中的 agent ID 非法:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
139
- }
140
- if (explicit && env && explicit !== env) {
141
- return {
142
- id: '',
143
- agentKey: '',
144
- source: 'conflict',
145
- error: '身份冲突:显式身份 ' + explicit + ' 与环境身份 ' + env + ' 不一致。请只保留一个明确身份。',
146
- };
147
- }
148
- if (explicit) return { id: explicit, agentKey: explicitKey, source: 'explicit', error: '' };
149
- if (env && trusted) return { id: env, agentKey: trustedEnvAgentKey(), source: 'trusted-env', error: '' };
150
- if (env && !trusted) {
151
- return {
152
- id: '',
153
- agentKey: '',
154
- source: 'untrusted-env',
155
- error: '检测到未受信任的环境身份 ' + env + '。请显式传 --agent <id>,或在 MCP 配置中设置 YOTTA_MEMORY_TRUST_ENV_AGENT=1 后重试。',
156
- };
630
+ if (explicit) {
631
+ return { id: explicit, agentKey: explicitKey, source: 'explicit', error: '' };
157
632
  }
158
633
  return { id: '', agentKey: '', source: 'none', error: '' };
159
634
  }
@@ -624,6 +1099,48 @@ function encAgentBindingPath(root, id) { return path.join(encBindingsDir(root),
624
1099
  function encPendingDir(root) { return path.join(encKeysDir(root), AGENT_PENDING_DIR); }
625
1100
  function encPendingPath(root, id) { return path.join(encPendingDir(root), id + '.key'); }
626
1101
  function agentKeyFilePath(agentHome) { return path.join(path.resolve(agentHome), AGENT_KEY_FILENAME); }
1102
+ function detectAgentHost(owner) {
1103
+ const hint = String(process.env.YOTTA_MEMORY_AGENT_HOST || '').trim().toLowerCase();
1104
+ if (hint === 'codex' || hint === 'opencode') return hint;
1105
+ const id = String(owner || '').toLowerCase();
1106
+ if (/(^|[-_.])opencode([-_.]|$)/.test(id)) return 'opencode';
1107
+ if (id === 'codex' || id.indexOf('codex-') === 0) return 'codex';
1108
+ if (process.env.OPENCODE_API_KEY || process.env.OPENCODE_CONFIG) return 'opencode';
1109
+ if (process.env.CODEX_HOME) return 'codex';
1110
+ return 'generic';
1111
+ }
1112
+ function resolveAgentKeyLocation(owner, opts) {
1113
+ opts = opts || {};
1114
+ const explicitHome = String(opts.to || '').trim();
1115
+ const explicitFile = String(opts.agentKeyFile || '').trim();
1116
+ if (explicitHome && explicitFile) {
1117
+ return { ok: false, error: '--to 与 --agent-key-file 不能同时使用。' };
1118
+ }
1119
+ if (explicitFile) {
1120
+ return { ok: true, file: path.resolve(explicitFile), source: 'option:--agent-key-file' };
1121
+ }
1122
+ if (explicitHome) {
1123
+ return { ok: true, file: agentKeyFilePath(explicitHome), source: 'option:--to' };
1124
+ }
1125
+ const envFile = String(process.env.YOTTA_MEMORY_AGENT_KEY_FILE || '').trim();
1126
+ if (envFile) {
1127
+ return { ok: true, file: path.resolve(envFile), source: 'env:YOTTA_MEMORY_AGENT_KEY_FILE' };
1128
+ }
1129
+ const envHome = String(process.env.YOTTA_MEMORY_AGENT_HOME || '').trim();
1130
+ if (envHome) {
1131
+ return { ok: true, file: agentKeyFilePath(envHome), source: 'env:YOTTA_MEMORY_AGENT_HOME' };
1132
+ }
1133
+ const host = detectAgentHost(owner);
1134
+ if (host === 'codex') {
1135
+ const home = process.env.CODEX_HOME ? path.resolve(process.env.CODEX_HOME) : path.join(os.homedir(), '.codex');
1136
+ return { ok: true, file: agentKeyFilePath(home), source: 'host:codex' };
1137
+ }
1138
+ if (host === 'opencode') {
1139
+ const base = process.env.XDG_CONFIG_HOME ? path.resolve(process.env.XDG_CONFIG_HOME) : path.join(os.homedir(), '.config');
1140
+ return { ok: true, file: agentKeyFilePath(path.join(base, 'opencode')), source: 'host:opencode' };
1141
+ }
1142
+ return { ok: true, file: agentKeyFilePath(path.join(os.homedir(), '.' + owner)), source: 'host:generic' };
1143
+ }
627
1144
  function isEncrypted(root) { return fs.existsSync(encKeysDir(root)); }
628
1145
  function isEncFile(fp) { return String(fp).slice(-ENC_SUFFIX.length) === ENC_SUFFIX; }
629
1146
  function ownerFromPrivatePath(root, fp) {
@@ -816,24 +1333,32 @@ function unwrapAgentBinding(root, owner, agentKey) {
816
1333
  if (buf.slice(0, AGENT_MAGIC.length).toString('utf8') !== AGENT_MAGIC) throw new Error('agent binding 文件格式错误: ' + p);
817
1334
  return aesDecryptBytes(agentKey, buf.slice(AGENT_MAGIC.length), Buffer.from('agent:' + owner, 'utf8'));
818
1335
  }
819
- function getOwnerKeyFor(root, owner) {
1336
+ function getOwnerKeyFor(root, owner, identity) {
820
1337
  if (!owner) return null;
821
1338
  if (!isEncrypted(root)) return null;
822
- if (!RUNTIME_AGENT.id || RUNTIME_AGENT.id !== owner || !RUNTIME_AGENT.agentKey) return null;
823
- const agentKey = Buffer.from(RUNTIME_AGENT.agentKey, 'base64');
1339
+ const current = identity || currentRuntimeIdentity();
1340
+ if (!current.id || current.id !== owner || !current.agentKey) return null;
1341
+ const agentKey = Buffer.from(current.agentKey, 'base64');
824
1342
  return unwrapAgentBinding(root, owner, agentKey);
825
1343
  }
826
1344
  function validatePrivateIdentity(root, ident, owner) {
827
1345
  if (!ident || !ident.id) return '私密操作必须先声明显式身份。';
828
1346
  if (!isSafeAgentId(owner || ident.id)) return '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。';
829
1347
  if (!isEncrypted(root)) return '';
830
- const agentKey = ident.agentKey || (RUNTIME_AGENT.id === ident.id ? RUNTIME_AGENT.agentKey : '');
1348
+ const current = currentRuntimeIdentity();
1349
+ const agentKey = ident.agentKey || (current.id === ident.id ? current.agentKey : '');
831
1350
  if (!agentKey) {
832
- return '记忆库已加密,但缺少 agent_key。请使用 --agent-key <key>,或在 MCP 配置中设置 YOTTA_MEMORY_AGENT_KEY + YOTTA_MEMORY_TRUST_ENV_AGENT=1。若该 agent 尚未绑定,请执行 yotta-memory key bind ' + (owner || ident.id) + '。';
1351
+ return '记忆库已加密,但缺少 agent_key。CLI 请使用 --agent-key <key> 或 --agent-key-file <path>;stdio MCP 请使用 --agent-id <id> + --agent-key-file <path>;HTTP MCP 请发送 X-Agent-Key 请求头。若该 agent 尚未绑定,请执行 yotta-memory key bind ' + (owner || ident.id) + '。';
1352
+ }
1353
+ const context = IDENTITY_CONTEXT.getStore();
1354
+ if (context) {
1355
+ context.id = ident.id;
1356
+ context.agentKey = agentKey;
1357
+ } else {
1358
+ setRuntimeAgent(ident.id, agentKey);
833
1359
  }
834
- setRuntimeAgent(ident.id, agentKey);
835
1360
  try {
836
- if (!getOwnerKeyFor(root, owner || ident.id)) {
1361
+ if (!getOwnerKeyFor(root, owner || ident.id, { id: ident.id, agentKey: agentKey })) {
837
1362
  return 'agent_key 校验失败:未找到 ' + (owner || ident.id) + ' 的 agent binding(可能已吊销)。请由用户重新授权,或由用户执行 yotta-memory key bind ' + (owner || ident.id) + '。';
838
1363
  }
839
1364
  } catch (e) {
@@ -1315,7 +1840,7 @@ function hasGrant(userAgent, ownerAgent) {
1315
1840
  return false;
1316
1841
  }
1317
1842
  // 三态读取判定:'read' | 'denied'
1318
- // selfAgent 为可信身份(env 声明 / 调用方上下文);--agent 仅供授权与展示,不得授予跨智能体私密读取。
1843
+ // selfAgent 为调用方显式声明的可信身份(CLI 参数 / stdio 参数 / HTTP 请求头);--owner 仅供授权与展示,不得授予跨智能体私密读取。
1319
1844
  function classifyRead(entry, agent, ownerFilter, unsafe, selfAgent) {
1320
1845
  if (entry.scope === 'public') return 'read';
1321
1846
  const owner = entry.owner || '';
@@ -1507,7 +2032,7 @@ function backupWindowsTaskXml(opts) {
1507
2032
  opts = opts || {};
1508
2033
  const time = backupTimeParts(opts.time || '03:30');
1509
2034
  const nodePath = opts.nodePath || process.execPath;
1510
- const scriptPath = opts.scriptPath || __filename;
2035
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1511
2036
  const userId = opts.userId || '';
1512
2037
  const principal = userId
1513
2038
  ? '<Principal id="Author"><UserId>' + xmlEscape(userId) + '</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal>'
@@ -1540,7 +2065,7 @@ function backupWindowsTaskXml(opts) {
1540
2065
  function backupSystemdServiceContent(opts) {
1541
2066
  opts = opts || {};
1542
2067
  const nodePath = opts.nodePath || process.execPath;
1543
- const scriptPath = opts.scriptPath || __filename;
2068
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1544
2069
  return [
1545
2070
  '[Unit]',
1546
2071
  'Description=Yotta Memory daily backup',
@@ -1573,7 +2098,7 @@ function backupCronLine(opts) {
1573
2098
  opts = opts || {};
1574
2099
  const time = backupTimeParts(opts.time || '03:30');
1575
2100
  const nodePath = opts.nodePath || process.execPath;
1576
- const scriptPath = opts.scriptPath || __filename;
2101
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1577
2102
  const logPath = opts.logPath || '';
1578
2103
  const redirect = logPath ? ' >> ' + shQuote(logPath) + ' 2>&1' : '';
1579
2104
  return time.minute + ' ' + time.hour + ' * * * ' + shQuote(nodePath) + ' ' + shQuote(scriptPath) + ' backup ensure-daily' + redirect + ' ' + BACKUP_CRON_MARKER;
@@ -1582,7 +2107,7 @@ function backupLaunchdPlist(opts) {
1582
2107
  opts = opts || {};
1583
2108
  const time = backupTimeParts(opts.time || '03:30');
1584
2109
  const nodePath = opts.nodePath || process.execPath;
1585
- const scriptPath = opts.scriptPath || __filename;
2110
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1586
2111
  const logPath = opts.logPath || '';
1587
2112
  const lines = [
1588
2113
  '<?xml version="1.0" encoding="UTF-8"?>',
@@ -1633,7 +2158,7 @@ function backupScheduleEnableCore(opts) {
1633
2158
  const time = opts.time || '03:30';
1634
2159
  try { backupTimeParts(time); } catch (error) { return { error: true, text: error.message }; }
1635
2160
  const nodePath = opts.nodePath || process.execPath;
1636
- const scriptPath = opts.scriptPath || __filename;
2161
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1637
2162
  try {
1638
2163
  if (platform === 'win32') {
1639
2164
  const userId = opts.userId || (process.env.USERDOMAIN && process.env.USERNAME ? process.env.USERDOMAIN + '\\' + process.env.USERNAME : '');
@@ -1752,7 +2277,7 @@ function backupScheduleStatusCore(opts) {
1752
2277
  return { error: false, registered: false, scheduler: '', text: '当前平台不支持自动备份调度: ' + platform };
1753
2278
  }
1754
2279
  function backupFallbackCommand() {
1755
- return { command: process.execPath, args: [__filename, 'backup', 'ensure-daily'] };
2280
+ return { command: process.execPath, args: [runtimeManagedScript(), 'backup', 'ensure-daily'] };
1756
2281
  }
1757
2282
  function startBackupFallback() {
1758
2283
  const cfg = loadConfig();
@@ -2033,6 +2558,348 @@ function backupHealthCore(opts) {
2033
2558
  }
2034
2559
  return { level: 'none', needsSetup: false, text: '' };
2035
2560
  }
2561
+ // ---- v0.16.0 M3: runtime drift diagnosis ----
2562
+ function runtimePathList(value) {
2563
+ const pattern = path.delimiter === ':' ? /[;:\n]/ : /[;\n]/;
2564
+ return String(value || '').split(pattern).map(function (item) { return item.trim(); }).filter(Boolean);
2565
+ }
2566
+ function runtimeVersionFromDir(dir) {
2567
+ let current = path.resolve(String(dir || ''));
2568
+ for (let i = 0; i < 8; i++) {
2569
+ const pkgPath = path.join(current, 'package.json');
2570
+ if (fs.existsSync(pkgPath)) {
2571
+ try {
2572
+ const version = String(JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '').trim();
2573
+ if (version) return version;
2574
+ } catch (e) {}
2575
+ }
2576
+ const skillPath = path.join(current, 'SKILL.md');
2577
+ if (fs.existsSync(skillPath)) {
2578
+ try {
2579
+ const text = fs.readFileSync(skillPath, 'utf8');
2580
+ const match = text.match(/^---\s*\n([\s\S]*?)\n---/);
2581
+ if (match) {
2582
+ const versionLine = match[1].split(/\r?\n/).find(function (line) { return /^version\s*:/.test(line); });
2583
+ if (versionLine) {
2584
+ const version = versionLine.slice(versionLine.indexOf(':') + 1).trim().replace(/^["']|["']$/g, '');
2585
+ if (version) return version;
2586
+ }
2587
+ }
2588
+ } catch (e) {}
2589
+ }
2590
+ const parent = path.dirname(current);
2591
+ if (parent === current) break;
2592
+ current = parent;
2593
+ }
2594
+ return '';
2595
+ }
2596
+ function runtimeVersionFromPath(target) {
2597
+ const value = String(target || '').trim();
2598
+ if (!value) return '';
2599
+ const normalized = value.replace(/\\/g, '/');
2600
+ const versionMatch = normalized.match(/\/versions\/([^/]+)\/bin\/yotta-memory\.js$/i);
2601
+ if (versionMatch) return versionMatch[1];
2602
+ const abs = path.resolve(value);
2603
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return runtimeVersionFromDir(abs);
2604
+ const base = path.basename(abs).toLowerCase();
2605
+ const dir = path.dirname(abs);
2606
+ return runtimeVersionFromDir(base === 'yotta-memory.js' ? path.dirname(dir) : dir);
2607
+ }
2608
+ function runtimeExtractLauncherPaths(text) {
2609
+ const found = new Set();
2610
+ const add = function (value) {
2611
+ const raw = String(value || '').trim().replace(/^['"]|['"]$/g, '');
2612
+ if (!raw) return;
2613
+ const parts = splitCommandArgv(raw);
2614
+ for (const part of parts) {
2615
+ const token = part.replace(/^['"]|['"]$/g, '');
2616
+ if (/yotta-memory\.js$/i.test(token.replace(/[;,]$/, ''))) found.add(token.replace(/[;,]$/, ''));
2617
+ }
2618
+ };
2619
+ const parsed = [];
2620
+ try {
2621
+ const value = JSON.parse(String(text || ''));
2622
+ const walk = function (node) {
2623
+ if (typeof node === 'string') parsed.push(node);
2624
+ else if (Array.isArray(node)) node.forEach(walk);
2625
+ else if (node && typeof node === 'object') Object.keys(node).forEach(function (key) { walk(node[key]); });
2626
+ };
2627
+ walk(value);
2628
+ } catch (e) {}
2629
+ for (const value of parsed) if (/yotta-memory\.js/i.test(value)) add(value);
2630
+ const raw = String(text || '').replace(/\\\\/g, '\\');
2631
+ const patterns = [
2632
+ /[A-Za-z]:[\\/][^"'\r\n|;]+?yotta-memory\.js/gi,
2633
+ /(?:\.{0,2}[\\/])?[^"'\s|;]+?yotta-memory\.js/gi,
2634
+ ];
2635
+ for (const pattern of patterns) {
2636
+ let match;
2637
+ while ((match = pattern.exec(raw)) !== null) add(match[0]);
2638
+ }
2639
+ return Array.from(found);
2640
+ }
2641
+ function runtimeConfigIdentityMode(text) {
2642
+ const raw = String(text || '');
2643
+ if (/YOTTA_AGENT_ID|YOTTA_MEMORY_AGENT_KEY|YOTTA_MEMORY_TRUST_ENV_AGENT/.test(raw)) return 'legacy-env';
2644
+ if (/--agent-id/.test(raw) && /--agent-key-file/.test(raw)) return 'stdio-args';
2645
+ if (/X-Agent-Id|Authorization/i.test(raw)) return 'headers';
2646
+ return 'unknown';
2647
+ }
2648
+ function runtimeDiscoverProcesses() {
2649
+ try {
2650
+ if (process.platform === 'win32') {
2651
+ const script = "$p=Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*yotta-memory*' }; if ($p) { $p | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress }";
2652
+ const result = child_process.spawnSync('powershell.exe', ['-NoProfile', '-Command', script], {
2653
+ encoding: 'utf8',
2654
+ timeout: 15000,
2655
+ windowsHide: true,
2656
+ });
2657
+ if (result.status !== 0 || !String(result.stdout || '').trim()) return [];
2658
+ const parsed = JSON.parse(String(result.stdout).trim());
2659
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
2660
+ return rows.map(function (row) {
2661
+ return { pid: row.ProcessId, commandLine: row.CommandLine || '' };
2662
+ });
2663
+ }
2664
+ const result = child_process.spawnSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf8', timeout: 10000 });
2665
+ if (result.status !== 0) return [];
2666
+ return String(result.stdout || '').split(/\r?\n/).filter(Boolean).map(function (line) {
2667
+ const match = line.trim().match(/^(\d+)\s+(.*)$/);
2668
+ return match ? { pid: parseInt(match[1], 10), commandLine: match[2] } : null;
2669
+ }).filter(Boolean);
2670
+ } catch (e) {
2671
+ return [];
2672
+ }
2673
+ }
2674
+ function runtimeDiscoverSkillDirs() {
2675
+ const explicit = runtimePathList(process.env.YOTTA_MEMORY_SKILL_DIRS);
2676
+ if (explicit.length) return explicit;
2677
+ const candidates = [];
2678
+ const add = function (base) {
2679
+ if (!base) return;
2680
+ candidates.push(path.join(base, 'skills', 'yotta-memory'));
2681
+ candidates.push(path.join(base, 'yotta-memory'));
2682
+ };
2683
+ add(process.env.CODEX_HOME);
2684
+ if (process.env.XDG_CONFIG_HOME) candidates.push(path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'skills', 'yotta-memory'));
2685
+ add(process.env.HERMES_HOME);
2686
+ add(path.join(os.homedir(), '.codex'));
2687
+ candidates.push(path.join(os.homedir(), '.config', 'opencode', 'skills', 'yotta-memory'));
2688
+ return Array.from(new Set(candidates)).filter(function (dir) { return fs.existsSync(dir); });
2689
+ }
2690
+ function runtimeDoctorCore(opts) {
2691
+ opts = opts || {};
2692
+ const drifts = [];
2693
+ const warnings = [];
2694
+ const mcpConfigPaths = opts.mcpConfigPaths !== undefined
2695
+ ? opts.mcpConfigPaths
2696
+ : runtimePathList(process.env.YOTTA_MEMORY_MCP_CONFIGS);
2697
+ const skillDirs = opts.skillDirs !== undefined ? opts.skillDirs : runtimeDiscoverSkillDirs();
2698
+ const processes = opts.processes !== undefined ? opts.processes : runtimeDiscoverProcesses();
2699
+ const manifest = readRuntimeManifest();
2700
+ const currentDir = runtimeCurrentDir();
2701
+ const currentBin = runtimeCurrentBin();
2702
+ const manifestVersion = manifest && manifest.current ? String(manifest.current) : '';
2703
+ const installedCurrentVersion = fs.existsSync(path.join(currentDir, 'package.json'))
2704
+ ? runtimeVersionFromDir(currentDir)
2705
+ : '';
2706
+
2707
+ const checks = {
2708
+ cli: { path: runtimeExecutionPath(), version: VERSION },
2709
+ current: {
2710
+ path: currentDir,
2711
+ version: manifestVersion || installedCurrentVersion || '',
2712
+ installedVersion: installedCurrentVersion,
2713
+ manifestVersion: manifestVersion,
2714
+ },
2715
+ runtimeRoot: runtimeRoot(),
2716
+ mcpConfigs: [],
2717
+ runningServers: [],
2718
+ skillCopies: [],
2719
+ identityModes: [],
2720
+ drifts: drifts,
2721
+ };
2722
+
2723
+ if (!manifest || !manifest.current) {
2724
+ drifts.push({
2725
+ kind: 'current-runtime',
2726
+ source: runtimeManifestPath(),
2727
+ actual: 'missing',
2728
+ expected: VERSION,
2729
+ fix: 'yotta-memory runtime install --from-current',
2730
+ blocking: true,
2731
+ });
2732
+ } else {
2733
+ const versionDir = runtimeVersionDir(manifest.current);
2734
+ if (!fs.existsSync(versionDir)) {
2735
+ drifts.push({
2736
+ kind: 'current-runtime',
2737
+ source: runtimeManifestPath(),
2738
+ actual: manifest.current,
2739
+ expected: VERSION,
2740
+ fix: 'yotta-memory runtime install --from-current && yotta-memory runtime use ' + VERSION + ' --restart',
2741
+ blocking: true,
2742
+ });
2743
+ } else if (manifest.current !== VERSION) {
2744
+ drifts.push({
2745
+ kind: 'current-runtime',
2746
+ source: runtimeManifestPath(),
2747
+ actual: manifest.current,
2748
+ expected: VERSION,
2749
+ fix: 'yotta-memory runtime install --from-current && yotta-memory runtime use ' + VERSION + ' --restart',
2750
+ blocking: true,
2751
+ });
2752
+ }
2753
+ if (fs.existsSync(versionDir)) {
2754
+ if (!fs.existsSync(currentBin)) {
2755
+ drifts.push({
2756
+ kind: 'current-runtime',
2757
+ source: currentDir,
2758
+ actual: 'launcher-missing',
2759
+ expected: VERSION,
2760
+ fix: 'yotta-memory runtime use ' + manifest.current + ' --force',
2761
+ blocking: true,
2762
+ });
2763
+ } else {
2764
+ try {
2765
+ const real = fs.realpathSync(currentDir);
2766
+ if (path.resolve(real) !== path.resolve(versionDir)) {
2767
+ drifts.push({
2768
+ kind: 'current-runtime',
2769
+ source: currentDir,
2770
+ actual: 'pointer-mismatch',
2771
+ expected: manifest.current,
2772
+ fix: 'yotta-memory runtime use ' + manifest.current + ' --restart',
2773
+ blocking: true,
2774
+ });
2775
+ }
2776
+ } catch (e) {
2777
+ drifts.push({
2778
+ kind: 'current-runtime',
2779
+ source: currentDir,
2780
+ actual: 'pointer-unreadable',
2781
+ expected: manifest.current,
2782
+ fix: 'yotta-memory runtime use ' + manifest.current + ' --restart',
2783
+ blocking: true,
2784
+ });
2785
+ }
2786
+ }
2787
+ }
2788
+ }
2789
+
2790
+ const legacyEnv = legacyIdentityEnvNames();
2791
+ if (legacyEnv.length) {
2792
+ drifts.push({
2793
+ kind: 'identity-mode',
2794
+ source: 'process-env',
2795
+ actual: 'legacy-env',
2796
+ expected: 'headers|stdio-args',
2797
+ fix: '移除 ' + legacyEnv.join(', ') + ',HTTP 改用请求头,stdio 改用 --agent-id + --agent-key-file',
2798
+ blocking: true,
2799
+ });
2800
+ }
2801
+
2802
+ for (const configPath of mcpConfigPaths) {
2803
+ const abs = path.resolve(String(configPath));
2804
+ let text = '';
2805
+ try {
2806
+ text = fs.readFileSync(abs, 'utf8');
2807
+ } catch (e) {
2808
+ warnings.push('MCP 配置无法读取: ' + abs);
2809
+ checks.mcpConfigs.push({ path: abs, readable: false, version: 'unknown', identityMode: 'unknown' });
2810
+ continue;
2811
+ }
2812
+ const launchers = runtimeExtractLauncherPaths(text);
2813
+ const identityMode = runtimeConfigIdentityMode(text);
2814
+ const version = launchers.length ? (runtimeVersionFromPath(launchers[0]) || 'unknown') : 'unknown';
2815
+ checks.mcpConfigs.push({ path: abs, readable: true, launcher: launchers[0] || '', version: version, identityMode: identityMode });
2816
+ checks.identityModes.push({ source: abs, mode: identityMode });
2817
+ if (identityMode === 'legacy-env') {
2818
+ drifts.push({
2819
+ kind: 'identity-mode',
2820
+ source: abs,
2821
+ actual: 'legacy-env',
2822
+ expected: 'headers|stdio-args',
2823
+ fix: '从 MCP 配置移除身份 env,HTTP 改用请求头,stdio 改用 --agent-id + --agent-key-file',
2824
+ blocking: true,
2825
+ });
2826
+ }
2827
+ if (version !== 'unknown' && version !== VERSION) {
2828
+ drifts.push({
2829
+ kind: 'mcp-config',
2830
+ source: abs,
2831
+ actual: version,
2832
+ expected: VERSION,
2833
+ fix: '把 MCP 配置中的运行时路径改为 ' + runtimeCurrentBin() + ' 后重启该 MCP',
2834
+ blocking: true,
2835
+ });
2836
+ }
2837
+ }
2838
+
2839
+ for (const processInfo of processes || []) {
2840
+ const row = typeof processInfo === 'string' ? { pid: '', commandLine: processInfo } : (processInfo || {});
2841
+ const commandLine = String(row.commandLine || '');
2842
+ if (!commandLine || commandLine.indexOf('yotta-memory') === -1 || commandLine.indexOf('serve') === -1) continue;
2843
+ const launchers = runtimeExtractLauncherPaths(commandLine);
2844
+ const launcher = launchers[0] || '';
2845
+ const version = launcher ? (runtimeVersionFromPath(launcher) || 'unknown') : 'unknown';
2846
+ checks.runningServers.push({ pid: row.pid || '', launcher: launcher, version: version });
2847
+ if (version !== 'unknown' && version !== VERSION) {
2848
+ drifts.push({
2849
+ kind: 'running-server',
2850
+ source: 'pid=' + String(row.pid || 'unknown'),
2851
+ actual: version,
2852
+ expected: VERSION,
2853
+ fix: 'yotta-memory runtime use ' + VERSION + ' --restart 或重启对应 MCP 客户端',
2854
+ blocking: false,
2855
+ });
2856
+ }
2857
+ }
2858
+
2859
+ for (const skillDir of skillDirs) {
2860
+ const abs = path.resolve(String(skillDir));
2861
+ if (!fs.existsSync(path.join(abs, 'SKILL.md'))) continue;
2862
+ const version = runtimeVersionFromDir(abs) || 'unknown';
2863
+ checks.skillCopies.push({ path: abs, version: version });
2864
+ if (version !== 'unknown' && version !== VERSION) {
2865
+ drifts.push({
2866
+ kind: 'skill-copy',
2867
+ source: abs,
2868
+ actual: version,
2869
+ expected: VERSION,
2870
+ fix: '用官方安装器更新该 yotta-memory 技能副本',
2871
+ blocking: false,
2872
+ });
2873
+ }
2874
+ }
2875
+
2876
+ const lines = [
2877
+ '## 运行时一致性',
2878
+ '- CLI: ' + VERSION + ' (' + runtimeExecutionPath() + ')',
2879
+ '- current: ' + (checks.current.version || '(missing)') + ' (' + currentDir + ')',
2880
+ '- runtime.json: ' + runtimeManifestPath(),
2881
+ ];
2882
+ if (checks.mcpConfigs.length) {
2883
+ for (const item of checks.mcpConfigs) lines.push('- MCP 配置: ' + item.path + ' -> ' + item.version + ' / ' + item.identityMode);
2884
+ } else lines.push('- MCP 配置: (未提供或未发现)');
2885
+ if (checks.runningServers.length) {
2886
+ for (const item of checks.runningServers) lines.push('- 运行中 server: pid=' + item.pid + ' -> ' + item.version);
2887
+ } else lines.push('- 运行中 server: (未发现)');
2888
+ if (checks.skillCopies.length) {
2889
+ for (const item of checks.skillCopies) lines.push('- 技能副本: ' + item.path + ' -> ' + item.version);
2890
+ } else lines.push('- 技能副本: (未提供或未发现)');
2891
+ if (!drifts.length) lines.push('- 漂移: 无');
2892
+ for (const drift of drifts) {
2893
+ lines.push('- [漂移] ' + drift.kind + ': actual=' + drift.actual + ' expected=' + drift.expected + ' fix=' + drift.fix + ' blocking=' + (drift.blocking ? 'yes' : 'no'));
2894
+ }
2895
+ return {
2896
+ ok: !drifts.some(function (drift) { return drift.blocking; }),
2897
+ checks: checks,
2898
+ drifts: drifts,
2899
+ warnings: warnings,
2900
+ text: lines.join('\n'),
2901
+ };
2902
+ }
2036
2903
  // 开工可靠性检查:只读检查根目录、密钥库、索引、身份登记与最近备份。
2037
2904
  function doctorCore(opts) {
2038
2905
  opts = opts || {};
@@ -2131,6 +2998,18 @@ function doctorCore(opts) {
2131
2998
  if (cfg.backup_scheduler_error) warnings.push('每日备份调度异常: ' + cfg.backup_scheduler_error);
2132
2999
  }
2133
3000
 
3001
+ let runtimeReport = null;
3002
+ if (opts.runtime) {
3003
+ runtimeReport = runtimeDoctorCore(opts);
3004
+ checks.runtime = runtimeReport.checks;
3005
+ for (const drift of runtimeReport.drifts) {
3006
+ const message = '运行时漂移 [' + drift.kind + ']: actual=' + drift.actual + ' expected=' + drift.expected + ';修复: ' + drift.fix + ';阻断: ' + (drift.blocking ? '是' : '否');
3007
+ if (drift.blocking) critical.push(message);
3008
+ else warnings.push(message);
3009
+ }
3010
+ for (const warning of runtimeReport.warnings || []) warnings.push(warning);
3011
+ }
3012
+
2134
3013
  const level = critical.length ? 'critical' : (warnings.length ? 'warning' : 'ok');
2135
3014
  const lines = [
2136
3015
  '# yotta-memory doctor(开工可靠性检查)',
@@ -2143,6 +3022,10 @@ function doctorCore(opts) {
2143
3022
  for (const message of critical) lines.push('- [严重] ' + message);
2144
3023
  for (const message of warnings) lines.push('- [警告] ' + message);
2145
3024
  if (!critical.length && !warnings.length) lines.push('- 检查项: 全部通过');
3025
+ if (runtimeReport) {
3026
+ lines.push('');
3027
+ lines.push(runtimeReport.text);
3028
+ }
2146
3029
  if (level === 'critical') {
2147
3030
  lines.push('');
2148
3031
  lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
@@ -2505,7 +3388,7 @@ function rememberCore(type, subject, statement, opts) {
2505
3388
  if (keyError) return { error: true, text: keyError };
2506
3389
  }
2507
3390
  if (scope === 'private' && !owner) {
2508
- return { error: true, text: '私密记忆必须显式声明归属智能体:请传 --agent <id>;MCP 场景需由宿主在配置中设置 YOTTA_AGENT_ID=<id> + YOTTA_MEMORY_TRUST_ENV_AGENT=1。公共记忆(FACT)不受影响。' };
3391
+ return { error: true, text: '私密记忆必须显式声明归属智能体:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。公共记忆(FACT)不受影响。' };
2509
3392
  }
2510
3393
  if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
2511
3394
  return { error: true, text: '拒绝: 当前显式身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请传正确的 --agent <id>,或加 --unsafe(用户显式授权)。' };
@@ -2627,7 +3510,7 @@ function recallCore(query, opts) {
2627
3510
  return {
2628
3511
  error: true,
2629
3512
  exitCode: 3,
2630
- text: '显式跨智能体读取必须先声明身份:请传 --agent <id>;MCP 场景需设置 YOTTA_AGENT_ID=<id> + YOTTA_MEMORY_TRUST_ENV_AGENT=1。',
3513
+ text: '显式跨智能体读取必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
2631
3514
  };
2632
3515
  }
2633
3516
  if (selfAgent && !allSafe) {
@@ -2813,7 +3696,7 @@ function forgetCore(fileRef, opts) {
2813
3696
  if (seg[0] === 'private') {
2814
3697
  const owner = seg[1] || '';
2815
3698
  if (!opts.unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
2816
- return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
3699
+ return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。' };
2817
3700
  }
2818
3701
  }
2819
3702
  const trashFile = path.join(
@@ -2910,7 +3793,7 @@ function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
2910
3793
  if (seg[0] === PRIVATE_DIR) {
2911
3794
  const owner = seg[1] || '';
2912
3795
  if (!unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
2913
- return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。';
3796
+ return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。';
2914
3797
  }
2915
3798
  }
2916
3799
  return null;
@@ -3483,7 +4366,7 @@ function keyBindCore(root, owner, opts) {
3483
4366
  error: false,
3484
4367
  agentKey: agentKey.toString('base64'),
3485
4368
  pendingFile: pendingFile,
3486
- text: '已绑定 ' + owner + ' 的 agent_key,并删除旧明文 cache。\nagent_key: ' + agentKey.toString('base64') + '\n待领取文件: ' + pendingFile + '\nAI 在新会话执行 yotta-memory key claim ' + owner + ' --to <AI_HOME> 领取;用户请另外保存此 key;此 key 只显示一次。',
4369
+ text: '已绑定 ' + owner + ' 的 agent_key,并删除旧明文 cache。\nagent_key: ' + agentKey.toString('base64') + '\n待领取文件: ' + pendingFile + '\nAI 在新会话执行 yotta-memory key status ' + owner + ',有 pending 再执行 yotta-memory key claim ' + owner + ';用户请另外保存此 key;此 key 只显示一次。',
3487
4370
  };
3488
4371
  }
3489
4372
 
@@ -3498,9 +4381,10 @@ function keyRevokeCore(root, owner) {
3498
4381
  return { error: false, text: '已吊销 ' + owner + ' 的 agent_key binding;该 agent 将无法解密其私密。' };
3499
4382
  }
3500
4383
 
3501
- function keyClaimCore(root, owner, targetHome) {
4384
+ function keyClaimCore(root, owner, opts) {
3502
4385
  if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
3503
- if (!targetHome) return { error: true, text: '请用 --to <AI_HOME> 指定 AI 宿主目录。' };
4386
+ const location = resolveAgentKeyLocation(owner, opts);
4387
+ if (!location.ok) return { error: true, text: location.error };
3504
4388
  const pending = encPendingPath(root, owner);
3505
4389
  if (!fs.existsSync(pending)) {
3506
4390
  return { error: true, text: '未找到待领取 agent_key:' + pending + '。请先在 yotta-memory view 中授权,或执行 yotta-memory key bind ' + owner + '。' };
@@ -3519,9 +4403,9 @@ function keyClaimCore(root, owner, targetHome) {
3519
4403
  let targetFile;
3520
4404
  let tempFile;
3521
4405
  try {
3522
- targetDir = path.resolve(String(targetHome));
4406
+ targetFile = location.file;
4407
+ targetDir = path.dirname(targetFile);
3523
4408
  fs.mkdirSync(targetDir, { recursive: true });
3524
- targetFile = agentKeyFilePath(targetDir);
3525
4409
  tempFile = path.join(targetDir, AGENT_KEY_FILENAME + '.tmp-' + process.pid + '-' + crypto.randomBytes(4).toString('hex'));
3526
4410
  fs.writeFileSync(tempFile, agentKey.toString('base64') + '\n', { encoding: 'utf8', mode: 0o600 });
3527
4411
  try { fs.chmodSync(tempFile, 0o600); } catch (e) {}
@@ -3538,31 +4422,36 @@ function keyClaimCore(root, owner, targetHome) {
3538
4422
  return { error: true, text: '写入宿主 key 文件失败:' + e.message + '。pending 文件保留,可重试。' };
3539
4423
  }
3540
4424
  removePendingAgentKey(root, owner);
3541
- return { error: false, targetFile: targetFile, text: '已领取 ' + owner + ' 的 agent_key。\n目标文件: ' + targetFile + '\npending 文件已删除。' };
4425
+ return {
4426
+ error: false,
4427
+ targetFile: targetFile,
4428
+ source: location.source,
4429
+ text: '已领取 ' + owner + ' 的 agent_key。\n目标文件: ' + targetFile + '\n发现规则: ' + location.source + '\npending 文件已删除。',
4430
+ };
3542
4431
  }
3543
4432
 
3544
- function keyStatusCore(root, owner, targetHome) {
4433
+ function keyStatusCore(root, owner, opts) {
3545
4434
  if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
4435
+ const location = resolveAgentKeyLocation(owner, opts);
4436
+ if (!location.ok) return { error: true, text: location.error };
3546
4437
  const binding = fs.existsSync(encAgentBindingPath(root, owner));
3547
4438
  const pending = fs.existsSync(encPendingPath(root, owner));
3548
4439
  let hostKey = 'missing';
3549
- if (targetHome) {
3550
- const fp = agentKeyFilePath(targetHome);
3551
- if (fs.existsSync(fp)) {
3552
- hostKey = 'invalid';
3553
- try {
3554
- const key = readAgentKeyFile(fp);
3555
- if (binding && unwrapAgentBinding(root, owner, key)) hostKey = 'valid';
3556
- } catch (e) {}
3557
- }
4440
+ if (fs.existsSync(location.file)) {
4441
+ hostKey = 'invalid';
4442
+ try {
4443
+ const key = readAgentKeyFile(location.file);
4444
+ if (binding && unwrapAgentBinding(root, owner, key)) hostKey = 'valid';
4445
+ } catch (e) {}
3558
4446
  }
3559
4447
  return {
3560
4448
  error: false,
3561
4449
  binding: binding,
3562
4450
  pending: pending,
3563
4451
  hostKey: hostKey,
3564
- targetFile: targetHome ? agentKeyFilePath(targetHome) : '',
3565
- text: 'agent: ' + owner + '\nbinding: ' + (binding ? 'yes' : 'no') + '\npending: ' + (pending ? 'yes' : 'no') + '\nhost_key: ' + hostKey + (targetHome ? '\ntarget: ' + agentKeyFilePath(targetHome) : ''),
4452
+ targetFile: location.file,
4453
+ source: location.source,
4454
+ text: 'agent: ' + owner + '\nbinding: ' + (binding ? 'yes' : 'no') + '\npending: ' + (pending ? 'yes' : 'no') + '\nhost_key: ' + hostKey + '\nchecked: ' + location.file + '\ndiscovery: ' + location.source,
3566
4455
  };
3567
4456
  }
3568
4457
 
@@ -3620,16 +4509,16 @@ function cmdKeyRevoke(id) {
3620
4509
 
3621
4510
  function cmdKeyClaim(id, opts) {
3622
4511
  const root = userRoot();
3623
- if (!id) { console.error('请指定 <id>:yotta-memory key claim <id> --to <AI_HOME>'); process.exit(2); }
3624
- const r = keyClaimCore(root, id, opts.to);
4512
+ if (!id) { console.error('请指定 <id>:yotta-memory key claim <id> [--to <AI_HOME> | --agent-key-file <文件>]'); process.exit(2); }
4513
+ const r = keyClaimCore(root, id, opts);
3625
4514
  console.log(r.text);
3626
4515
  if (r.error) process.exit(2);
3627
4516
  }
3628
4517
 
3629
4518
  function cmdKeyStatus(id, opts) {
3630
4519
  const root = userRoot();
3631
- if (!id) { console.error('请指定 <id>:yotta-memory key status <id> [--to <AI_HOME>]'); process.exit(2); }
3632
- const r = keyStatusCore(root, id, opts.to);
4520
+ if (!id) { console.error('请指定 <id>:yotta-memory key status <id> [--to <AI_HOME> | --agent-key-file <文件>]'); process.exit(2); }
4521
+ const r = keyStatusCore(root, id, opts);
3633
4522
  console.log(r.text);
3634
4523
  if (r.error) process.exit(2);
3635
4524
  }
@@ -3960,7 +4849,15 @@ function cmdBackupEnsureDaily(opts) {
3960
4849
  }
3961
4850
  function cmdBackupSchedule(action, opts) {
3962
4851
  let r;
3963
- if (action === 'enable') r = backupScheduleEnableCore(opts);
4852
+ if (action === 'enable') {
4853
+ const runtime = runtimeEnsureForManagedTask();
4854
+ if (runtime.error) {
4855
+ console.error('runtime 稳定入口未就绪,拒绝注册备份任务: ' + runtime.text);
4856
+ process.exit(1);
4857
+ }
4858
+ opts = Object.assign({}, opts, { scriptPath: runtimeManagedScript() });
4859
+ r = backupScheduleEnableCore(opts);
4860
+ }
3964
4861
  else if (action === 'disable') r = backupScheduleDisableCore(opts);
3965
4862
  else if (action === 'status') r = backupScheduleStatusCore(opts);
3966
4863
  else {
@@ -4272,8 +5169,8 @@ function cmdWhoami(opts) {
4272
5169
  }
4273
5170
  const id = ident.id;
4274
5171
  if (!id) {
4275
- console.log('当前未声明可信智能体身份(未传 --agent,且没有受信任的 MCP 环境身份)。');
4276
- console.log('CLI:请显式传 --agent <唯一ID>。MCP:在客户端配置中同时设置 YOTTA_AGENT_ID=<唯一ID> 与 YOTTA_MEMORY_TRUST_ENV_AGENT=1。');
5172
+ console.log('当前未声明显式智能体身份(身份不再从环境变量读取)。');
5173
+ console.log('CLI:请传 --agent <唯一ID>。stdio MCP:使用 --agent-id <唯一ID> + --agent-key-file <path>。HTTP MCP:发送 X-Agent-Id + X-Agent-Key。');
4277
5174
  console.log('远端:token + X-Agent-Id + X-Agent-Key(token new --agent <id>;agent_key 由 view 授权生成)。');
4278
5175
  return;
4279
5176
  }
@@ -4330,14 +5227,14 @@ function profileCore(opts) {
4330
5227
  return {
4331
5228
  error: true,
4332
5229
  exitCode: 3,
4333
- text: '生成私密画像必须先声明身份:请传 --agent <id>;MCP 场景需设置 YOTTA_AGENT_ID=<id> + YOTTA_MEMORY_TRUST_ENV_AGENT=1。',
5230
+ text: '生成私密画像必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
4334
5231
  };
4335
5232
  }
4336
5233
  const selfAgent = ident.id;
4337
5234
  const owner = opts.owner || selfAgent;
4338
5235
  const keyError = validatePrivateIdentity(root, ident, owner);
4339
5236
  if (keyError) return { error: true, exitCode: 3, text: keyError };
4340
- if (!owner) return { error: true, exitCode: 2, text: '请先声明身份(YOTTA_AGENT_ID / AGENT_ID)或传 --owner <id>,再生成画像。' };
5237
+ if (!owner) return { error: true, exitCode: 2, text: '请先声明身份(--agent / --agent-id)或传 --owner <id>,再生成画像。' };
4341
5238
  if (selfAgent && owner !== selfAgent && owner !== 'user' && !opts.unsafe && !hasGrant(selfAgent, owner)) {
4342
5239
  return { error: true, exitCode: 3, text: '拒绝: 不能生成其它智能体 ' + owner + ' 的画像(private/' + owner + '/ 为私密区)。如需读取请 --owner user 或 --unsafe(用户显式授权)。' };
4343
5240
  }
@@ -4383,19 +5280,35 @@ function selfProfileKv(root, id) {
4383
5280
  try { text = readMemoryText(root, path.join(selfPrefsDir(root, id), profile), id); } catch (e) { return {}; }
4384
5281
  return parseKvBody(parseFrontmatter(text).body);
4385
5282
  }
5283
+ // v0.14.0:长期摘要优先 / 近期走廊 / 会话闭环上下文编排。
5284
+ // 摘要只认 consolidate 产物,不新增第二套摘要格式。
5285
+ function isConsolidatedSummary(entry) {
5286
+ if (!entry) return false;
5287
+ if (entry.source === 'consolidate') return true;
5288
+ const tags = Array.isArray(entry.tags) ? entry.tags : [];
5289
+ return tags.indexOf('consolidate') !== -1 && tags.indexOf('summary') !== -1;
5290
+ }
5291
+ function contextRecentKey(entry) {
5292
+ return String((entry && (entry.updated || entry.created)) || '');
5293
+ }
5294
+ function compareContextRecent(a, b) {
5295
+ const byTime = contextRecentKey(b).localeCompare(contextRecentKey(a));
5296
+ if (byTime !== 0) return byTime;
5297
+ return (b.access_count || 0) - (a.access_count || 0);
5298
+ }
4386
5299
  function contextCore(opts) {
4387
5300
  opts = opts || {};
4388
5301
  const roots = memoryRoots();
4389
5302
  if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
4390
5303
  const root = userRoot();
4391
- const limit = opts.limit || 10;
5304
+ const limit = parseInt(opts.limit || '10', 10) || 10;
4392
5305
  const ident = resolveIdentity(opts);
4393
5306
  if (ident.error) return { error: true, exitCode: 3, text: ident.error };
4394
5307
  if (!ident.id) {
4395
5308
  return {
4396
5309
  error: true,
4397
5310
  exitCode: 3,
4398
- text: '开工上下文包含私密画像 / 边界 / 承诺,必须先声明身份:请传 --agent <id>;MCP 场景需设置 YOTTA_AGENT_ID=<id> + YOTTA_MEMORY_TRUST_ENV_AGENT=1。',
5311
+ text: '开工上下文包含私密画像 / 边界 / 承诺,必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
4399
5312
  };
4400
5313
  }
4401
5314
  const selfAgent = ident.id;
@@ -4410,14 +5323,32 @@ function contextCore(opts) {
4410
5323
  const embeddingTimeout = effectiveEmbeddingTimeout(opts);
4411
5324
  const trace = [];
4412
5325
  const lines = [];
5326
+ const shownFiles = new Set();
4413
5327
  const FENCE = String.fromCharCode(96, 96, 96);
4414
5328
  function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
5329
+ function appendContextEntry(entry, reason, options) {
5330
+ options = options || {};
5331
+ if (!entry) return false;
5332
+ if (shownFiles.has(entry.file)) {
5333
+ trace.push('[dropped] ' + entry.file + ' reason: duplicate');
5334
+ return false;
5335
+ }
5336
+ const line = '- [' + entry.type + '] ' + entry.subject + ': ' + entry.statement;
5337
+ if (options.budget !== false && budget > 0 && usedChars() + line.length > budget) {
5338
+ trace.push('[dropped] ' + entry.file + ' reason: budget_exceeded');
5339
+ return false;
5340
+ }
5341
+ lines.push(line);
5342
+ shownFiles.add(entry.file);
5343
+ trace.push('[included] ' + entry.file + ' reason: ' + reason);
5344
+ return true;
5345
+ }
4415
5346
  lines.push('# 开工上下文包(yotta-memory context)');
4416
5347
  lines.push('');
4417
5348
  lines.push('## 1. 身份');
4418
5349
  lines.push('');
4419
5350
  if (!owner) {
4420
- lines.push('- 未声明智能体身份(无 YOTTA_AGENT_ID / --owner)。私密记忆与画像不可用;请先 whoami / iam。');
5351
+ lines.push('- 未声明智能体身份(无 --agent / --agent-id / X-Agent-Id)。私密记忆与画像不可用;请先 whoami / iam。');
4421
5352
  } else {
4422
5353
  lines.push('- agent_id: ' + owner);
4423
5354
  const kv = selfProfileKv(root, owner);
@@ -4462,8 +5393,26 @@ function contextCore(opts) {
4462
5393
  }
4463
5394
  lines.push('');
4464
5395
 
5396
+ const readableEntries = [];
5397
+ for (const r of roots) {
5398
+ for (const e of ensureIndex(r)) {
5399
+ if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
5400
+ readableEntries.push(e);
5401
+ }
5402
+ }
5403
+
5404
+ lines.push('## 2.5 长期理解摘要');
5405
+ lines.push('');
5406
+ const summaries = readableEntries.filter(isConsolidatedSummary).sort(compareContextRecent);
5407
+ const summaryLimit = Math.min(3, Math.max(0, limit));
5408
+ if (!summaries.length) lines.push('(暂无周期摘要;可用 yotta-memory consolidate --apply 生成)');
5409
+ for (const e of summaries.slice(0, summaryLimit)) {
5410
+ appendContextEntry(e, 'summary_priority', { budget: false });
5411
+ }
5412
+ lines.push('');
5413
+
4465
5414
  if (focus) {
4466
- lines.push('## 2.5 任务相关记忆(--focus)');
5415
+ lines.push('## 2.6 任务相关记忆(--focus)');
4467
5416
  lines.push('');
4468
5417
  const focused = recallCore(focus, {
4469
5418
  limit: limit,
@@ -4476,70 +5425,51 @@ function contextCore(opts) {
4476
5425
  const focusedEntries = focused.entries || [];
4477
5426
  if (!focusedEntries.length) lines.push('(无匹配记忆)');
4478
5427
  for (const e of focusedEntries) {
4479
- const line = '- [' + e.type + '] ' + e.subject + ': ' + e.statement;
4480
- if (budget > 0 && usedChars() + line.length > budget) {
4481
- trace.push('[dropped] ' + e.file + ' reason: budget_exceeded');
4482
- continue;
4483
- }
4484
- lines.push(line);
4485
- trace.push('[included] ' + e.file + ' reason: focus_match score: ' + round3(e.score));
5428
+ appendContextEntry(e, 'focus_match score: ' + round3(e.score));
4486
5429
  }
4487
5430
  lines.push('');
4488
5431
  }
4489
5432
 
4490
- lines.push('## 3. 近期记忆(按活跃度前 ' + limit + ' 条)');
5433
+ lines.push('## 3. 近期走廊(按时间)');
4491
5434
  lines.push('');
4492
- const recent = [];
4493
- for (const r of roots) {
4494
- for (const e of ensureIndex(r)) {
4495
- if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
4496
- // v0.8.0 排序融合:0.5×importance(旧) + 0.5×utility(新盖棺分)
4497
- recent.push({ e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) });
4498
- }
4499
- }
4500
- recent.sort(function (a, b) { return b.s - a.s; });
4501
- const recentShown = recent.slice(0, limit);
4502
- if (!recentShown.length) lines.push('(暂无记忆)');
4503
- for (const h of recentShown) {
4504
- const line = '- [' + h.e.type + '] ' + h.e.subject + ': ' + h.e.statement;
4505
- if (budget > 0 && usedChars() + line.length > budget) {
4506
- trace.push('[dropped] ' + h.e.file + ' reason: budget_exceeded');
4507
- break;
4508
- }
4509
- lines.push(line);
4510
- trace.push('[included] ' + h.e.file + ' reason: recent_match');
4511
- }
5435
+ const corridor = readableEntries
5436
+ .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT'; })
5437
+ .sort(compareContextRecent)
5438
+ .filter(function (e) { return !shownFiles.has(e.file); })
5439
+ .slice(0, limit);
5440
+ if (!corridor.length) lines.push('(暂无近期记忆)');
5441
+ for (const e of corridor) appendContextEntry(e, 'recent_corridor');
4512
5442
  lines.push('');
4513
- lines.push('## 4. 边界提醒(BOUND)');
5443
+
5444
+ lines.push('## 4. 近期高价值记忆(补位)');
4514
5445
  lines.push('');
4515
- const bounds = [];
4516
- for (const r of roots) {
4517
- for (const e of ensureIndex(r)) {
4518
- if (e.type !== 'BOUND') continue;
4519
- if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
4520
- bounds.push(e);
4521
- }
4522
- }
5446
+ const highValue = readableEntries
5447
+ .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file); })
5448
+ .map(function (e) { return { e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) }; })
5449
+ .sort(function (a, b) { return b.s - a.s; })
5450
+ .slice(0, limit);
5451
+ if (!highValue.length) lines.push('(暂无补位条目)');
5452
+ for (const h of highValue) appendContextEntry(h.e, 'high_value_backfill');
5453
+ lines.push('');
5454
+ lines.push('## 5. 边界提醒(BOUND)');
5455
+ lines.push('');
5456
+ const bounds = readableEntries.filter(function (e) { return e.type === 'BOUND'; }).sort(compareContextRecent);
4523
5457
  if (!bounds.length) lines.push('(无)');
4524
5458
  for (const e of bounds) lines.push('- ' + (e.subject || '边界') + ': ' + e.statement);
4525
5459
  lines.push('');
4526
- lines.push('## 5. 承诺 / 锚点(COMMIT)');
5460
+ lines.push('## 6. 承诺 / 锚点(COMMIT)');
4527
5461
  lines.push('');
4528
- const commits = [];
4529
- for (const r of roots) {
4530
- for (const e of ensureIndex(r)) {
4531
- if (e.type !== 'COMMIT') continue;
4532
- if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
4533
- commits.push(e);
4534
- }
4535
- }
5462
+ const commits = readableEntries.filter(function (e) { return e.type === 'COMMIT'; }).sort(compareContextRecent);
4536
5463
  if (!commits.length) lines.push('(无)');
4537
5464
  for (const e of commits) lines.push('- ' + (e.subject || '承诺') + ': ' + e.statement);
4538
5465
  lines.push('');
4539
- lines.push('## 6. 收工纪律');
5466
+ lines.push('## 7. 本会话闭环契约');
5467
+ lines.push('');
5468
+ lines.push('- 开工:本上下文包已加载;身份、长期摘要、边界与承诺以本包为准。');
5469
+ lines.push('- 进行中:出现事实 / 偏好 / 边界 / 纠正 / 承诺信号时立即 `remember <type> <subject> <statement> --verify`,不攒到收工。');
5470
+ lines.push('- 收工前复盘:检查本轮是否留下 COMMIT / 会话小结;有关键结论但未落盘时补写并 `recall` 回读,无长期价值不硬凑。');
4540
5471
  lines.push('');
4541
- const allEntries = [];
4542
- for (const r of roots) for (const e of ensureIndex(r)) allEntries.push(e);
5472
+ const allEntries = readableEntries;
4543
5473
  const myCommits = allEntries.filter(function (e) { return e.type === 'COMMIT' && classifyRead(e, owner, '', unsafe, selfAgent) !== 'denied'; });
4544
5474
  if (!myCommits.length) {
4545
5475
  lines.push('- 最近承诺: 无(收工请补 COMMIT / 交接锚点)');
@@ -4552,7 +5482,7 @@ function contextCore(opts) {
4552
5482
  if (oldCount > 0) lines.push('- 归档提醒: ' + oldCount + ' 条超 180 天,建议 archive');
4553
5483
  if (explain) {
4554
5484
  lines.push('');
4555
- lines.push('## 7. 选择解释(--explain)');
5485
+ lines.push('## 8. 选择解释(--explain)');
4556
5486
  lines.push('');
4557
5487
  if (!trace.length) lines.push('(无选择记录)');
4558
5488
  for (const t of trace) lines.push(t);
@@ -4619,8 +5549,12 @@ function cmdConfigGet() {
4619
5549
  }
4620
5550
 
4621
5551
  // ---- MCP serve(局域网 streamable HTTP 记忆引擎,零依赖)----
4622
- function mcpTools() {
4623
- return [
5552
+ const MCP_CORE_TOOL_NAMES = ['context', 'recall', 'search', 'remember'];
5553
+ function normalizeMcpToolProfile(profile) {
5554
+ return String(profile || 'full').trim().toLowerCase() === 'core' ? 'core' : 'full';
5555
+ }
5556
+ function mcpTools(profile) {
5557
+ const tools = [
4624
5558
  { name: 'remember', description: '写入一条记忆。参数 type(FACT/PREF/BOUND/COMMIT)、subject、statement 必填;owner 可选,默认当前智能体', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'FACT/PREF/BOUND/COMMIT' }, subject: { type: 'string' }, statement: { type: 'string' }, owner: { type: 'string' } }, required: ['type', 'subject', 'statement'] } },
4625
5559
  { name: 'recall', description: '检索记忆。query 可选;type 可选;limit 可选(默认 20);explain 可选。只返回当前智能体可读记忆;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
4626
5560
  { name: 'search', description: '检索记忆(同 recall)。query 可选;type 可选;limit 可选(默认 20);explain 可选;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
@@ -4631,16 +5565,32 @@ function mcpTools() {
4631
5565
  { name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
4632
5566
  { name: 'export', description: '导出全部记忆到记忆库内的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json;仅限记忆库内路径)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
4633
5567
  { name: 'import', description: '从记忆库内的 JSON 文件导入记忆。src 为文件路径(相对记忆库目录,或记忆库内绝对路径;仅限记忆库内)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
4634
- { name: 'agent_info', description: '查看当前智能体身份与登记状态(远端读经 token 校验的 X-Agent-Id;本机读 YOTTA_AGENT_ID)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
5568
+ { name: 'agent_info', description: '查看当前智能体身份与登记状态(HTTP 读经 token 校验的 X-Agent-Id;stdio 读 --agent-id 显式参数)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
4635
5569
  { name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
4636
5570
  { name: 'feedback', description: '显式使用反馈(自我学习):useful/useless 调整记忆 weight/confidence/feedback_net。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' }, useful: { type: 'boolean' }, useless: { type: 'boolean' }, reason: { type: 'string' } }, required: ['file'] } },
4637
5571
  { name: 'maintain', description: '记忆自组织(自我进化):规则层归档/遗忘/去重预览。默认 dry-run;apply 才执行,purge 才真删', inputSchema: { type: 'object', properties: { apply: { type: 'boolean' }, purge: { type: 'boolean' }, dedup: { type: 'boolean' }, threshold: { type: 'number' }, age: { type: 'number' } } } },
4638
5572
  { name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体(MCP 不支持 --model 外部命令)', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' } } } },
4639
5573
  { name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
4640
5574
  ];
5575
+ if (normalizeMcpToolProfile(profile) !== 'core') return tools;
5576
+ return MCP_CORE_TOOL_NAMES
5577
+ .map((name) => tools.find((tool) => tool.name === name))
5578
+ .filter(Boolean);
4641
5579
  }
4642
5580
  function callTool(name, args, ctx) {
4643
- const agent = ctx.agent || '';
5581
+ return runWithIdentity({
5582
+ id: ctx && ctx.agent,
5583
+ agentKey: ctx && ctx.agentKey,
5584
+ }, function () {
5585
+ return callToolInner(name, args, ctx);
5586
+ });
5587
+ }
5588
+ function callToolInner(name, args, ctx) {
5589
+ const agent = (ctx && ctx.agent) || '';
5590
+ const toolProfile = normalizeMcpToolProfile(ctx && ctx.toolProfile);
5591
+ if (toolProfile === 'core' && MCP_CORE_TOOL_NAMES.indexOf(name) === -1) {
5592
+ return { text: '工具 ' + name + ' 属于 full 分组,当前 MCP 以 core 模式启动。需要完整工具集时请使用 --tools full 重新启动。', error: true };
5593
+ }
4644
5594
  if (agent && !isSafeAgentId(agent)) return { text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。', error: true };
4645
5595
  try {
4646
5596
  if (name === 'remember') {
@@ -4710,7 +5660,7 @@ function callTool(name, args, ctx) {
4710
5660
  if (name === 'agent_info') {
4711
5661
  const root = userRoot();
4712
5662
  const id = agent || '';
4713
- if (!id) return { text: '当前未声明智能体身份(无 X-Agent-Id / YOTTA_AGENT_ID)。本机请在 MCP 配置 env 设 YOTTA_AGENT_ID=<唯一ID> 并 iam 登记。', error: false };
5663
+ if (!id) return { text: '当前未声明智能体身份(无 X-Agent-Id / --agent-id)。stdio 请使用 --agent-id <唯一ID> + --agent-key-file <path>;HTTP 请发送 X-Agent-Id + X-Agent-Key。', error: false };
4714
5664
  const agents = loadAgents(root).agents || {};
4715
5665
  const tokens = loadTokens(root).tokens || {};
4716
5666
  let reg = '未登记';
@@ -4757,18 +5707,32 @@ function reqVersion(params) {
4757
5707
  const meta = (params && params._meta) || {};
4758
5708
  return meta['io.modelcontextprotocol/protocolVersion'] || null;
4759
5709
  }
4760
- function modernOk(payload, ttl) {
5710
+ function mcpIdentityMode(ctx) {
5711
+ const mode = String((ctx && ctx.identityMode) || '').trim().toLowerCase();
5712
+ if (mode === 'headers' || mode === 'stdio-args') return mode;
5713
+ if (ctx && ctx.agent) return 'stdio-args';
5714
+ return 'unknown';
5715
+ }
5716
+ function mcpServerInfo(ctx) {
5717
+ return {
5718
+ name: 'yotta-memory',
5719
+ version: VERSION,
5720
+ runtimePath: runtimeExecutionPath(),
5721
+ identityMode: mcpIdentityMode(ctx),
5722
+ toolProfile: normalizeMcpToolProfile(ctx && ctx.toolProfile),
5723
+ };
5724
+ }
5725
+ function modernOk(payload, ttl, ctx) {
4761
5726
  const out = { resultType: 'complete' };
4762
5727
  Object.assign(out, payload);
4763
5728
  if (ttl) { out.ttlMs = ttl[0]; out.cacheScope = ttl[1]; }
4764
- out._meta = { 'io.modelcontextprotocol/serverInfo': { name: 'yotta-memory', version: VERSION } };
5729
+ out._meta = { 'io.modelcontextprotocol/serverInfo': mcpServerInfo(ctx) };
4765
5730
  return out;
4766
5731
  }
4767
5732
  function unsupportedVersion(id, pv) {
4768
5733
  return { jsonrpc: '2.0', id: id, error: { code: -32022, message: 'Unsupported protocol version', data: { supported: [MCP_PROTOCOL_MODERN], requested: pv } } };
4769
5734
  }
4770
5735
  function handleMessage(msg, ctx) {
4771
- setRuntimeAgent(ctx && ctx.agent, ctx && ctx.agentKey);
4772
5736
  if (!msg || msg.jsonrpc !== '2.0') return { jsonrpc: '2.0', id: msg && msg.id, error: { code: -32600, message: 'invalid request' } };
4773
5737
  const id = msg.id;
4774
5738
  if (id === undefined || id === null) return null;
@@ -4778,17 +5742,18 @@ function handleMessage(msg, ctx) {
4778
5742
  if (pv !== null) {
4779
5743
  // ---- modern(2026-07-28 无状态)----
4780
5744
  if (pv !== MCP_PROTOCOL_MODERN) return unsupportedVersion(id, pv);
5745
+ const toolProfile = normalizeMcpToolProfile(ctx && ctx.toolProfile);
4781
5746
  if (method === 'server/discover') {
4782
5747
  return { jsonrpc: '2.0', id: id, result: modernOk({
4783
5748
  supportedVersions: [MCP_PROTOCOL_MODERN],
4784
5749
  capabilities: { tools: {} },
4785
- instructions: '元忆 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):文件式智能体记忆 remember/recall/search/context/forget/archive/maintain 等读写检索与维护;私密按 owner 物理隔离,数据不出本机。'
4786
- }, [3600000, 'public']) };
5750
+ instructions: '元忆 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):当前工具分组 ' + toolProfile + ';文件式智能体记忆 remember/recall/search/context/forget/archive/maintain 等读写检索与维护;私密按 owner 物理隔离,数据不出本机。'
5751
+ }, [3600000, 'public'], ctx) };
4787
5752
  }
4788
- if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: modernOk({ tools: mcpTools() }, [300000, 'public']) };
5753
+ if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: modernOk({ tools: mcpTools(toolProfile) }, [300000, 'public'], ctx) };
4789
5754
  if (method === 'tools/call') {
4790
5755
  const out = callTool(params.name || '', params.arguments || {}, ctx);
4791
- return { jsonrpc: '2.0', id: id, result: modernOk({ content: [{ type: 'text', text: out.text }], isError: !!out.error }) };
5756
+ return { jsonrpc: '2.0', id: id, result: modernOk({ content: [{ type: 'text', text: out.text }], isError: !!out.error }, null, ctx) };
4792
5757
  }
4793
5758
  if (method === 'initialize') {
4794
5759
  return { jsonrpc: '2.0', id: id, error: { code: -32601, message: "initialize removed in MCP 2026-07-28; use server/discover. supported: ['2026-07-28']" } };
@@ -4797,10 +5762,10 @@ function handleMessage(msg, ctx) {
4797
5762
  }
4798
5763
  // ---- legacy(<=2025-11-25,initialize 握手;响应保持旧形状)----
4799
5764
  if (method === 'initialize') {
4800
- return { jsonrpc: '2.0', id: id, result: { protocolVersion: MCP_PROTOCOL_LEGACY, capabilities: { tools: {} }, serverInfo: { name: 'yotta-memory', version: VERSION } } };
5765
+ return { jsonrpc: '2.0', id: id, result: { protocolVersion: MCP_PROTOCOL_LEGACY, capabilities: { tools: {} }, serverInfo: mcpServerInfo(ctx) } };
4801
5766
  }
4802
5767
  if (method === 'ping') return { jsonrpc: '2.0', id: id, result: {} };
4803
- if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: { tools: mcpTools() } };
5768
+ if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: { tools: mcpTools(ctx && ctx.toolProfile) } };
4804
5769
  if (method === 'tools/call') {
4805
5770
  const out = callTool(params.name || '', params.arguments || {}, ctx);
4806
5771
  return { jsonrpc: '2.0', id: id, result: { content: [{ type: 'text', text: out.text }], isError: !!out.error } };
@@ -4808,23 +5773,34 @@ function handleMessage(msg, ctx) {
4808
5773
  return { jsonrpc: '2.0', id: id, error: { code: -32601, message: 'Method not found: ' + method } };
4809
5774
  }
4810
5775
  function cmdServe(opts) {
4811
- if (opts.stdio) { cmdServeStdio(); return; }
5776
+ if (opts.stdio) { cmdServeStdio(opts); return; }
5777
+ const legacyEnvError = legacyIdentityEnvError();
5778
+ if (legacyEnvError) {
5779
+ console.error(legacyEnvError);
5780
+ process.exit(2);
5781
+ }
5782
+ if (opts.agent || opts.agentId) {
5783
+ console.error('HTTP MCP 不接受 --agent/--agent-id 启动参数:身份只从请求头 Authorization + X-Agent-Id + X-Agent-Key 读取。');
5784
+ process.exit(2);
5785
+ }
4812
5786
  const host = opts.host || '0.0.0.0';
4813
5787
  const port = opts.port || 8787;
4814
5788
  const noAuth = !!opts.noAuth;
4815
5789
  const root = userRoot();
4816
5790
  ensureInit(root);
4817
5791
  function authorize(req) {
4818
- const agentId = String(req.headers['x-agent-id'] || '');
5792
+ const agentId = String(req.headers['x-agent-id'] || '').trim();
4819
5793
  const agentKey = String(req.headers['x-agent-key'] || '').trim();
4820
- if (noAuth) return { agent: agentId, agentKey: agentKey };
5794
+ const toolProfile = normalizeMcpToolProfile(opts.toolProfile);
5795
+ if (noAuth) return { agent: agentId, agentKey: agentKey, toolProfile: toolProfile, identityMode: 'headers' };
5796
+ if (!agentId || !agentKey) return null;
4821
5797
  const auth = req.headers['authorization'] || '';
4822
5798
  const m = /^Bearer\s+(.+)$/i.exec(auth);
4823
5799
  if (!m) return null;
4824
5800
  const token = m[1].trim();
4825
5801
  const agent = agentId;
4826
5802
  const tokenMap = (loadTokens(root).tokens) || {};
4827
- if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent, agentKey: agentKey };
5803
+ if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent, agentKey: agentKey, toolProfile: toolProfile, identityMode: 'headers' };
4828
5804
  return null;
4829
5805
  }
4830
5806
  function originAllowed(req) {
@@ -4894,6 +5870,7 @@ function cmdServe(opts) {
4894
5870
  console.log('URL: http://' + host + ':' + port + '/mcp');
4895
5871
  console.log('记忆库: ' + root);
4896
5872
  console.log('MCP 协议: 2026-07-28(modern,server/discover)/ 2025-11-25(legacy 握手兼容)');
5873
+ console.log('工具分组: ' + normalizeMcpToolProfile(opts.toolProfile));
4897
5874
  if (noAuth) console.log('鉴权: 已关闭(--no-auth,仅限可信内网)');
4898
5875
  else console.log('鉴权: Bearer token + X-Agent-Id + X-Agent-Key(token 由 token new 生成,agent_key 由 view 授权生成)');
4899
5876
  console.log('按 Ctrl+C 停止');
@@ -4901,10 +5878,33 @@ function cmdServe(opts) {
4901
5878
  });
4902
5879
  }
4903
5880
  // ---- stdio 本地零进程模式(客户端按需拉起 CLI)----
4904
- function cmdServeStdio() {
5881
+ function cmdServeStdio(opts) {
5882
+ const legacyEnvError = legacyIdentityEnvError();
5883
+ if (legacyEnvError) {
5884
+ console.error(legacyEnvError);
5885
+ process.exit(2);
5886
+ }
5887
+ if (opts.agent) {
5888
+ console.error('stdio MCP 不接受 --agent:请使用显式参数 --agent-id <id> + --agent-key-file <path>。');
5889
+ process.exit(2);
5890
+ }
5891
+ if (opts.agentKey) {
5892
+ console.error('stdio MCP 不接受 --agent-key:命令行会暴露 key;请使用 --agent-key-file <path>。');
5893
+ process.exit(2);
5894
+ }
5895
+ const ident = resolveIdentity(opts);
5896
+ if (ident.error) {
5897
+ console.error(ident.error);
5898
+ process.exit(2);
5899
+ }
4905
5900
  const root = userRoot();
4906
5901
  ensureInit(root);
4907
- const ctx = { agent: currentAgent(), agentKey: trustedEnvAgentKey() };
5902
+ const ctx = {
5903
+ agent: ident.id,
5904
+ agentKey: ident.agentKey,
5905
+ toolProfile: normalizeMcpToolProfile(opts && opts.toolProfile),
5906
+ identityMode: 'stdio-args',
5907
+ };
4908
5908
  let buf = '';
4909
5909
  process.stdin.setEncoding('utf8');
4910
5910
  process.stdin.on('data', function (chunk) {
@@ -4939,7 +5939,7 @@ function lanTaskRunCmd(opts) {
4939
5939
  const port = opts.port || 8787;
4940
5940
  // schtasks /tr 不接受多余内嵌引号:路径无空格不加引号,含空格/引号用 \" 转义(0.5.2 修复)
4941
5941
  const q = (p) => /[\s"]/.test(p) ? '\\"' + p.replace(/"/g, '\\"') + '\\"' : p;
4942
- return q(process.execPath) + ' ' + q(__filename) + ' serve --host ' + host + ' --port ' + port;
5942
+ return q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port;
4943
5943
  }
4944
5944
 
4945
5945
  function lanServeArgs(opts) {
@@ -4993,7 +5993,7 @@ function lanAutostartCmdContent(opts) {
4993
5993
  return '@echo off\r\n'
4994
5994
  + 'chcp 65001 >nul\r\n'
4995
5995
  + 'rem ' + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable (English only, keep ASCII)\r\n'
4996
- + q(process.execPath) + ' ' + q(__filename) + ' serve --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
5996
+ + q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
4997
5997
  }
4998
5998
  function lanVbsContent(opts) {
4999
5999
  // v0.6.3 自愈:VBS 内联 autostart.cmd 内容,启动时若 .cmd 缺失/被清理即就地重建,
@@ -5070,7 +6070,7 @@ function lanLinuxUnitPath() { return path.join(lanLinuxSystemdUserDir(), LAN_UNI
5070
6070
  function lanLinuxSystemctlBin() { return process.env.YOTTA_LAN_SYSTEMCTL_BIN || 'systemctl'; }
5071
6071
  function lanLinuxLoginctlBin() { return process.env.YOTTA_LAN_LOGINCTL_BIN || 'loginctl'; }
5072
6072
  function lanLinuxExecStart(opts) {
5073
- return [systemdEscapeArg(process.execPath), systemdEscapeArg(__filename)].concat(lanServeArgs(opts)).join(' ');
6073
+ return [systemdEscapeArg(process.execPath), systemdEscapeArg(runtimeManagedScript())].concat(lanServeArgs(opts)).join(' ');
5074
6074
  }
5075
6075
  function lanLinuxUnitContent(opts) {
5076
6076
  // keep comments ASCII (avoid locale/encoding issues); ExecStart uses systemd's own quote rules
@@ -5093,7 +6093,7 @@ function lanLinuxUnitContent(opts) {
5093
6093
  }
5094
6094
  function lanCrontabBin() { return process.env.YOTTA_LAN_CRONTAB_BIN || 'crontab'; }
5095
6095
  function lanCrontabLine(opts) {
5096
- const parts = ['@reboot', shQuote(process.execPath), shQuote(__filename)].concat(lanServeArgs(opts));
6096
+ const parts = ['@reboot', shQuote(process.execPath), shQuote(runtimeManagedScript())].concat(lanServeArgs(opts));
5097
6097
  return parts.join(' ') + ' >> ' + shQuote(lanLogPath(opts)) + ' 2>&1 ' + LAN_CRONTAB_MARKER;
5098
6098
  }
5099
6099
  function lanCrontabRead() {
@@ -5334,6 +6334,11 @@ function cmdLanWinStatus() {
5334
6334
  }
5335
6335
 
5336
6336
  function cmdLanEnable(opts) {
6337
+ const runtime = runtimeEnsureForManagedTask();
6338
+ if (runtime.error) {
6339
+ console.error('runtime 稳定入口未就绪,拒绝注册开机自启: ' + runtime.text);
6340
+ process.exit(1);
6341
+ }
5337
6342
  const p = lanPlatform();
5338
6343
  if (p === 'win32') return cmdLanWinEnable(opts);
5339
6344
  if (p === 'linux') return cmdLanLinuxEnable(opts);
@@ -5855,17 +6860,47 @@ function appendDedupBlock(lines, root, opts) {
5855
6860
 
5856
6861
 
5857
6862
  // ---- usage / main ----
6863
+ function cmdRuntimeInstall(arg, opts) {
6864
+ let result;
6865
+ if (opts.fromCurrent) result = runtimeInstallFromCurrent(opts);
6866
+ else if (!arg) {
6867
+ console.error('runtime install 需要 <tarball|版本>,或使用 --from-current。');
6868
+ process.exit(2);
6869
+ } else if (/\.tgz$/i.test(arg) || fs.existsSync(path.resolve(arg))) {
6870
+ result = runtimeInstallTarball(arg, '', opts);
6871
+ } else {
6872
+ result = runtimeInstallVersion(arg, opts);
6873
+ }
6874
+ console.log(result.text);
6875
+ if (result.error) process.exit(2);
6876
+ }
6877
+ function cmdRuntimeUse(version, opts) {
6878
+ const result = runtimeUseCore(version, opts);
6879
+ console.log(result.text);
6880
+ if (result.error) process.exit(2);
6881
+ }
6882
+ function cmdRuntimeRollback(opts) {
6883
+ const result = runtimeRollbackCore(opts);
6884
+ console.log(result.text);
6885
+ if (result.error) process.exit(2);
6886
+ }
6887
+ function cmdRuntimeStatus() {
6888
+ console.log(runtimeStatusCore().text);
6889
+ }
6890
+ function cmdRuntimeList() {
6891
+ console.log(runtimeListCore().text);
6892
+ }
5858
6893
  function usage() {
5859
6894
  const banner = 'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆';
5860
6895
  const sections = [
5861
6896
  ['核心记忆', [
5862
6897
  ['init', '初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--attach 接入已有库;--no-encrypt 降级明文)'],
5863
- ['remember', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
5864
- ['recall', '检索记忆(--type/--limit/--agent/--owner/--all/--unsafe)'],
6898
+ ['remember <type> <subject> <statement>', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
6899
+ ['recall [关键词]', '检索记忆(--type/--limit/--agent/--owner/--all/--unsafe)'],
5865
6900
  ['forget', '删除一条记忆'],
5866
6901
  ['archive', '归档(--days/--threshold 盖棺分+年龄)'],
5867
6902
  ['backup', '备份记忆库(volumes / setup / status / ensure-daily / schedule / create / list / doctor / restore <id> --to <目录> / drill)'],
5868
- ['doctor', '开工可靠性检查(根目录/密钥库/索引/身份/最近备份;严重异常时锁定破坏性写入)'],
6903
+ ['doctor', '开工可靠性检查(根目录/密钥库/索引/身份/最近备份;--runtime 加查 CLI/current/MCP/进程/技能副本漂移)'],
5869
6904
  ['maintain', '记忆自组织(归档/遗忘候选/去重/合并;默认 dry-run;--dedup 查重+置信度,--dedup --apply 自动合并高置信组)'],
5870
6905
  ['consolidate', '周期摘要压缩(默认 dry-run;--apply 执行;--undo <batch> 回滚;--batches 查批次;候选=超龄+闲置+低效用,immutable/BOUND 豁免)'],
5871
6906
  ['distill', '心理日志蒸馏(统计摘要/主题画像/知识地图;--model 可选外部模型)'],
@@ -5886,11 +6921,12 @@ function usage() {
5886
6921
  ['migrate', '把明文私密区迁移为密文(需主口令;迁移后打印恢复钥匙)'],
5887
6922
  ['view', '启动用户查看平台(--port/--host;口令解锁浏览/授权/吊销 AI)'],
5888
6923
  ['reset-password', '重设主口令(忘口令用恢复钥匙)'],
5889
- ['key', '管理 agent_key binding(list / bind <id> / rotate <id> / claim <id> --to <AI_HOME> / status <id> / revoke <id>;bind/rotate 需主口令或恢复钥匙)'],
6924
+ ['key', '管理 agent_key binding(list / bind <id> / rotate <id> / claim <id> [--to <AI_HOME> | --agent-key-file <文件>] / status <id> [--to <AI_HOME> | --agent-key-file <文件>] / revoke <id>;bind/rotate 需主口令或恢复钥匙)'],
5890
6925
  ['config', '查看/设置配置(get;set memory_home <目录> / backup_dir <目录> / embedding_cmd <命令> / embedding_timeout <毫秒> / maintain_* 阈值与半衰 / consolidate_* 参数)']
5891
6926
  ]],
5892
6927
  ['平台与服务', [
5893
- ['serve', '启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程)'],
6928
+ ['serve', '启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程;--tools core|full 控制工具分组)'],
6929
+ ['runtime', '运行时稳定入口(list / install <tarball|版本> [--from-current] [--force] / use <版本> [--restart] / rollback [--restart] / status)'],
5894
6930
  ['lan', '开机自启管理(enable/disable/status;Windows 计划任务 / Linux systemd/crontab)'],
5895
6931
  ['--version', '版本']
5896
6932
  ]]
@@ -5905,7 +6941,7 @@ function usage() {
5905
6941
  lines.push('');
5906
6942
  }
5907
6943
  lines.push('类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)');
5908
- lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; MCP 身份必须同时设置 YOTTA_AGENT_ID + YOTTA_MEMORY_AGENT_KEY + YOTTA_MEMORY_TRUST_ENV_AGENT=1;CLI 用 --agent + --agent-key/--agent-key-file');
6944
+ lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; 身份不再读取 env;CLI 用 --agent + --agent-key/--agent-key-file,stdio MCP 用 --agent-id + --agent-key-file,HTTP MCP 用请求头');
5909
6945
  lines.push('隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令');
5910
6946
  lines.push('远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id> + X-Agent-Key: <agent_key>');
5911
6947
  console.log(lines.join('\n'));
@@ -5915,7 +6951,7 @@ async function main() {
5915
6951
  if (!args.length) { usage(); return; }
5916
6952
  const opts = {};
5917
6953
  const positional = [];
5918
- const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-key', '--agent-key-file', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time']);
6954
+ const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir']);
5919
6955
  for (let i = 0; i < args.length; i++) {
5920
6956
  const a = args[i];
5921
6957
  if (a === '--version' || a === '-v') { console.log(VERSION); return; }
@@ -5926,6 +6962,8 @@ async function main() {
5926
6962
  else if (a === '--no-auth') opts.noAuth = true;
5927
6963
  else if (a === '--stdio') opts.stdio = true;
5928
6964
  else if (a === '--onstart') opts.onstart = true;
6965
+ else if (a === '--from-current') opts.fromCurrent = true;
6966
+ else if (a === '--restart') opts.restart = true;
5929
6967
  else if (a === '--force') opts.force = true;
5930
6968
  else if (a === '--attach') opts.attach = true;
5931
6969
  else if (a === '--allow-same-volume') opts.allowSameVolume = true;
@@ -5946,6 +6984,7 @@ async function main() {
5946
6984
  else if (a === '--batches') opts.batches = true;
5947
6985
  else if (a === '--explain') opts.explain = true;
5948
6986
  else if (a === '--semantic') opts.semantic = true;
6987
+ else if (a === '--runtime') opts.runtime = true;
5949
6988
  else if (valueOpts.has(a)) {
5950
6989
  const v = args[++i];
5951
6990
  if (a === '--type') opts.type = v;
@@ -5955,6 +6994,7 @@ async function main() {
5955
6994
  else if (a === '--out') opts.out = v;
5956
6995
  else if (a === '--owner') opts.owner = v;
5957
6996
  else if (a === '--agent') opts.agent = v;
6997
+ else if (a === '--agent-id') opts.agentId = v;
5958
6998
  else if (a === '--agent-key') opts.agentKey = v;
5959
6999
  else if (a === '--agent-key-file') opts.agentKeyFile = v;
5960
7000
  else if (a === '--scope') opts.scope = v;
@@ -5985,15 +7025,37 @@ async function main() {
5985
7025
  else if (a === '--to') opts.to = v;
5986
7026
  else if (a === '--id') opts.id = v;
5987
7027
  else if (a === '--time') opts.time = v;
7028
+ else if (a === '--tools') opts.toolProfile = v;
7029
+ else if (a === '--mcp-config') opts.mcpConfigPaths = (opts.mcpConfigPaths || []).concat(v);
7030
+ else if (a === '--skill-dir') opts.skillDirs = (opts.skillDirs || []).concat(v);
5988
7031
  } else if (a.startsWith('--')) {
7032
+ if (a === '--query') {
7033
+ console.error('未知选项: --query。recall/search 的关键词是位置参数:yotta-memory recall [关键词]');
7034
+ process.exit(2);
7035
+ }
5989
7036
  console.error('未知选项: ' + a);
5990
7037
  process.exit(2);
5991
7038
  } else {
5992
7039
  positional.push(a);
5993
7040
  }
5994
7041
  }
7042
+ if (opts.toolProfile !== undefined) {
7043
+ opts.toolProfile = String(opts.toolProfile).trim().toLowerCase();
7044
+ if (opts.toolProfile !== 'core' && opts.toolProfile !== 'full') {
7045
+ console.error('工具分组必须是 core 或 full(例如:yotta-memory serve --stdio --tools core)。');
7046
+ process.exit(2);
7047
+ }
7048
+ }
5995
7049
  const first = positional[0];
5996
7050
  const rest = positional.slice(1);
7051
+ if (first === 'remember' && opts.type) {
7052
+ console.error('位置参数用法:yotta-memory remember <type> <subject> <statement>。--type 仅用于 recall/maintain/consolidate 等筛选,不是 remember 的类型参数。');
7053
+ process.exit(2);
7054
+ }
7055
+ if (first === 'remember' && rest.length < 3) {
7056
+ console.error('位置参数不足:yotta-memory remember <type> <subject> <statement>');
7057
+ process.exit(2);
7058
+ }
5997
7059
  const runtimeIdentity = resolveIdentity(opts);
5998
7060
  setRuntimeAgent(runtimeIdentity.error ? '' : runtimeIdentity.id, runtimeIdentity.error ? '' : runtimeIdentity.agentKey);
5999
7061
  switch (first) {
@@ -6060,6 +7122,16 @@ async function main() {
6060
7122
  else { console.error('key 子命令: list / bind <id> / rotate <id> / claim <id> --to <AI_HOME> / status <id> / revoke <id>'); process.exit(2); }
6061
7123
  break;
6062
7124
  }
7125
+ case 'runtime': {
7126
+ const sub = rest[0];
7127
+ if (sub === 'install') cmdRuntimeInstall(rest[1], opts);
7128
+ else if (sub === 'use') cmdRuntimeUse(rest[1], opts);
7129
+ else if (sub === 'rollback') cmdRuntimeRollback(opts);
7130
+ else if (sub === 'status') cmdRuntimeStatus();
7131
+ else if (sub === 'list') cmdRuntimeList();
7132
+ else { console.error('runtime 子命令: list / install <tarball|版本> [--from-current] [--force] / use <版本> [--restart] / rollback [--restart] / status'); process.exit(2); }
7133
+ break;
7134
+ }
6063
7135
  case 'serve': cmdServe(opts); break;
6064
7136
  case 'lan': {
6065
7137
  const sub = rest[0];
@@ -6107,6 +7179,28 @@ module.exports = {
6107
7179
  collectEntryFiles: collectEntryFiles,
6108
7180
  migrateLayout: migrateLayout,
6109
7181
  typeSubdir: typeSubdir,
7182
+ runtimeRoot: runtimeRoot,
7183
+ runtimeManifestPath: runtimeManifestPath,
7184
+ runtimeVersionsDir: runtimeVersionsDir,
7185
+ runtimeVersionDir: runtimeVersionDir,
7186
+ runtimeCurrentDir: runtimeCurrentDir,
7187
+ runtimeCurrentBin: runtimeCurrentBin,
7188
+ runtimeExecutionPath: runtimeExecutionPath,
7189
+ runtimeManagedScript: runtimeManagedScript,
7190
+ runtimeTreeHash: runtimeTreeHash,
7191
+ runtimeListVersions: runtimeListVersions,
7192
+ runtimeSwitchCurrent: runtimeSwitchCurrent,
7193
+ runtimeInstallFromCurrent: runtimeInstallFromCurrent,
7194
+ runtimeInstallTarball: runtimeInstallTarball,
7195
+ runtimeInstallVersion: runtimeInstallVersion,
7196
+ runtimeValidateTarballEntries: runtimeValidateTarballEntries,
7197
+ runtimeUseCore: runtimeUseCore,
7198
+ runtimeRollbackCore: runtimeRollbackCore,
7199
+ runtimeStatusCore: runtimeStatusCore,
7200
+ runtimeListCore: runtimeListCore,
7201
+ runtimeDoctorCore: runtimeDoctorCore,
7202
+ runtimeEnsureForManagedTask: runtimeEnsureForManagedTask,
7203
+ runtimeRestartManagedServer: runtimeRestartManagedServer,
6110
7204
  lanTaskRunCmd: lanTaskRunCmd,
6111
7205
  lanStartupDir: lanStartupDir,
6112
7206
  lanAutostartDir: lanAutostartDir,