evolcore 0.0.27 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -15
- package/dist/agents/baseagent.js +10 -5
- package/dist/agents/codex/app-server-client.js +7 -0
- package/dist/agents/codex/catalog.js +7 -1
- package/dist/agents/codex-runner.js +48 -11
- package/dist/aun/menu/client.js +1 -0
- package/dist/channels/aun.js +23 -7
- package/dist/cli/daemon-commands.js +77 -140
- package/dist/cli/diag.js +1 -1
- package/dist/cli/menu-client.js +4 -0
- package/dist/cli/restart-monitor.js +65 -17
- package/dist/core/command/command-handler.js +29 -1
- package/dist/core/command/diag-service.js +292 -27
- package/dist/core/command/menu-handler.js +16 -19
- package/dist/core/command/menu-protocol.js +3 -0
- package/dist/core/command/slash-handler.js +24 -15
- package/dist/core/message/im-renderer.js +21 -4
- package/dist/core/message/message-bridge.js +47 -3
- package/dist/core/message/response-engine.js +29 -10
- package/dist/core/permission/ec-command-parser.js +15 -7
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/core/session/session-manager.js +4 -0
- package/dist/index.js +14 -153
- package/dist/product.js +1 -0
- package/dist/utils/atomic-write.js +98 -0
- package/dist/utils/cross-platform.js +87 -16
- package/dist/utils/npm-ops.js +119 -4
- package/dist/utils/package-version-info.js +244 -0
- package/dist/utils/windows-shell-trust.js +37 -0
- package/kits/docs/channels/aun.md +8 -0
- package/package.json +1 -1
|
@@ -9,22 +9,23 @@ import { loadDefaults, loadAllAgents, loadDaemonConfig, saveDaemonConfig } from
|
|
|
9
9
|
import { resolveEffective } from '../config/config-manager.js';
|
|
10
10
|
import { resolveAnthropicConfig } from '../agents/baseagent.js';
|
|
11
11
|
import { migrateProject } from '../config-store.js';
|
|
12
|
-
import { ipcQuery } from '../ipc.js';
|
|
12
|
+
import { ipcQuery, IpcTransportError } from '../ipc.js';
|
|
13
13
|
import { isHelpFlag } from './help.js';
|
|
14
14
|
import { cmdInit, needsControlAidInit, initTail } from './init.js';
|
|
15
15
|
import * as platform from '../utils/cross-platform.js';
|
|
16
16
|
import { EventBus } from '../core/event-bus.js';
|
|
17
|
-
import {
|
|
17
|
+
import { tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-ops.js';
|
|
18
|
+
import { ensureFreshPackageVersionInfo } from '../utils/package-version-info.js';
|
|
18
19
|
import { ecwebVersionRequirementError, resolveEcwebLaunchCommand, fetchRemotePairingCode } from '../utils/ecweb-utils.js';
|
|
19
20
|
import { isHostChinese } from '../utils/locale.js';
|
|
20
|
-
import { scanInstances, cleanupInstances, findOrphanProcesses
|
|
21
|
+
import { scanInstances, cleanupInstances, findOrphanProcesses } from '../utils/instance-registry.js';
|
|
21
22
|
import { filterLogFiles, deriveLogTypes, computePreChecked, validateLogTypes, shortLogName as shortLogNameLocal, isWatchLogFile } from './watch-logs.js';
|
|
22
23
|
import { displaySessionTitle } from '../core/session/session-title.js';
|
|
23
24
|
import { isNonAgentSessionChannel } from '../core/system-channels.js';
|
|
24
25
|
import { printCodeStats } from './code-stats.js';
|
|
25
26
|
import { getProcessStartTime, startTimeMatches } from '../utils/process-introspect.js';
|
|
26
|
-
import { readDelegationCredential } from '../core/auth/agent-delegation.js';
|
|
27
|
-
import { WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME } from '../product.js';
|
|
27
|
+
import { AGENT_DELEGATION_TOKEN_ENV, readDelegationCredential } from '../core/auth/agent-delegation.js';
|
|
28
|
+
import { RUNTIME_HOME_ENV, WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME } from '../product.js';
|
|
28
29
|
import { rotateStdoutLog } from '../utils/log-writer.js';
|
|
29
30
|
import { inspectDataMigrationRequirement } from '../core/data-migration.js';
|
|
30
31
|
import { isInitCancelledError, printInitCancelHint } from './init-cancel.js';
|
|
@@ -250,27 +251,46 @@ function runSourceBuild() {
|
|
|
250
251
|
});
|
|
251
252
|
});
|
|
252
253
|
}
|
|
253
|
-
/**
|
|
254
|
-
* Spawn the restart monitor and confirm it actually reached its own code.
|
|
255
|
-
*
|
|
256
|
-
* The daemon is already stopped at this point, so a monitor that dies on
|
|
257
|
-
* startup leaves nothing running and nobody watching — the exact silent
|
|
258
|
-
* failure this mechanism exists to prevent.
|
|
259
|
-
*/
|
|
260
|
-
async function spawnRestartMonitorAndConfirm(monitorEntry, reportLine) {
|
|
261
|
-
const result = await platform.launchMonitorAndConfirm([...monitorEntry.args, 'restart-monitor'], { EVOLCORE_HOME: resolvePaths().root, ...(monitorEntry.env ?? {}) }, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid === pid), pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid));
|
|
262
|
-
if (result.ok)
|
|
263
|
-
return { ok: true };
|
|
264
|
-
for (const line of (result.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
|
|
265
|
-
reportLine(` | ${line}`);
|
|
266
|
-
}
|
|
267
|
-
return { ok: false, reason: result.reason ?? 'unknown' };
|
|
268
|
-
}
|
|
269
254
|
async function requireFreshSourceBuild() {
|
|
270
255
|
if (!sourceBuildNeedsRefresh())
|
|
271
256
|
return true;
|
|
272
257
|
return await runSourceBuild();
|
|
273
258
|
}
|
|
259
|
+
async function launchPackageLifecycle(source, log, options = {}) {
|
|
260
|
+
if (!options.prepared)
|
|
261
|
+
await ensureFreshPackageVersionInfo(undefined, { source, log });
|
|
262
|
+
const root = resolvePaths().root;
|
|
263
|
+
const launch = platform.resolveMonitorLaunch(getPackageRoot(), path.join(root, 'data', 'monitor-stage'));
|
|
264
|
+
if (launch.note)
|
|
265
|
+
log(launch.note);
|
|
266
|
+
if (!fs.existsSync(launch.entry)) {
|
|
267
|
+
if (launch.stageDir)
|
|
268
|
+
platform.cleanupMonitorStage(launch.stageDir);
|
|
269
|
+
return { ok: false, reason: 'RESTART_ENTRY_MISSING' };
|
|
270
|
+
}
|
|
271
|
+
const args = [...launch.args, 'restart-monitor', ...(options.clear ? ['--clear'] : [])];
|
|
272
|
+
const env = { ...options.env, [RUNTIME_HOME_ENV]: root, ...launch.env, [AGENT_DELEGATION_TOKEN_ENV]: '' };
|
|
273
|
+
if (options.foreground) {
|
|
274
|
+
return new Promise(resolve => {
|
|
275
|
+
const child = spawn(process.execPath, [...args, '--foreground'], {
|
|
276
|
+
stdio: 'inherit', detached: false, windowsHide: true, env: { ...process.env, ...env },
|
|
277
|
+
});
|
|
278
|
+
child.once('error', () => {
|
|
279
|
+
if (launch.stageDir)
|
|
280
|
+
platform.cleanupMonitorStage(launch.stageDir);
|
|
281
|
+
resolve({ ok: false, reason: 'Foreground monitor could not start' });
|
|
282
|
+
});
|
|
283
|
+
child.once('exit', code => {
|
|
284
|
+
process.exitCode = code ?? 1;
|
|
285
|
+
resolve({ ok: code === 0, ...(code !== 0 ? { reason: 'Foreground lifecycle exited unsuccessfully' } : {}) });
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const result = await platform.launchMonitorAndConfirm(args, env, pid => scanInstances().restartMonitors.some(item => item.alive && item.record.pid === pid), pid => scanInstances().restartMonitors.some(item => item.alive && item.record.pid !== pid));
|
|
290
|
+
if (!result.ok && launch.stageDir)
|
|
291
|
+
platform.cleanupMonitorStage(launch.stageDir);
|
|
292
|
+
return { ok: result.ok, ...(!result.ok ? { reason: result.reason } : {}) };
|
|
293
|
+
}
|
|
274
294
|
function requireCompletedDataMigration(root, action = 'start') {
|
|
275
295
|
const requirement = inspectDataMigrationRequirement(root);
|
|
276
296
|
if (!requirement.required)
|
|
@@ -291,7 +311,11 @@ function requireCompletedDataMigration(root, action = 'start') {
|
|
|
291
311
|
* A failed upgrade is reported, but the currently installed version remains
|
|
292
312
|
* usable and the caller continues with its normal lifecycle.
|
|
293
313
|
*/
|
|
294
|
-
export async function runUpgradeChecks(upgradeFn
|
|
314
|
+
export async function runUpgradeChecks(upgradeFn, ecwebUpgradeFn, timeoutMs = 60_000) {
|
|
315
|
+
if (!upgradeFn || !ecwebUpgradeFn) {
|
|
316
|
+
await ensureFreshPackageVersionInfo(undefined, { source: 'start', log: console.log });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
295
319
|
console.log('📦 Checking for updates...');
|
|
296
320
|
const runWithTimeout = async (label, fn) => {
|
|
297
321
|
const controller = new AbortController();
|
|
@@ -470,7 +494,9 @@ export async function cmdStart(opts = {}) {
|
|
|
470
494
|
return;
|
|
471
495
|
// A stopped container may miss the daily upgrade trigger. Check once on
|
|
472
496
|
// every real start so the service does not stay on an old installation.
|
|
473
|
-
await (
|
|
497
|
+
const versionInfo = await ensureFreshPackageVersionInfo(undefined, { source: 'start', log: console.log });
|
|
498
|
+
if (opts.upgradeChecks)
|
|
499
|
+
await opts.upgradeChecks();
|
|
474
500
|
// 旧配置自动迁移(daemon.json → 新结构)
|
|
475
501
|
const { autoMigrateIfNeeded } = await import('../config-store.js');
|
|
476
502
|
autoMigrateIfNeeded();
|
|
@@ -595,6 +621,20 @@ export async function cmdStart(opts = {}) {
|
|
|
595
621
|
return;
|
|
596
622
|
}
|
|
597
623
|
reportOrphans(orphans);
|
|
624
|
+
if (versionInfo.some(result => result.hasUpdate)) {
|
|
625
|
+
cleanEnv();
|
|
626
|
+
const launched = await launchPackageLifecycle('start', console.log, {
|
|
627
|
+
prepared: true, foreground: opts.foreground, env: buildDaemonEnv(p, opts),
|
|
628
|
+
});
|
|
629
|
+
if (!launched.ok) {
|
|
630
|
+
console.error(`❌ 启动事务未能建立:${launched.reason}`);
|
|
631
|
+
process.exitCode = 1;
|
|
632
|
+
}
|
|
633
|
+
else if (!opts.foreground) {
|
|
634
|
+
console.log('📦 已交由 restart-monitor 应用缓存版本并启动服务');
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
598
638
|
console.log('🚀 Starting EvolCore...');
|
|
599
639
|
const stdoutRotation = rotateStdoutLog(p.logs);
|
|
600
640
|
if (stdoutRotation.rotatedPath) {
|
|
@@ -883,126 +923,18 @@ export async function cmdRestart(opts = {}) {
|
|
|
883
923
|
if (initialRuntimeOrphans.length === 0 && rejectPidIsolatedLifecycle('restart', initialStatus, ping))
|
|
884
924
|
return;
|
|
885
925
|
}
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
// normal restart to perform every registry lookup twice, and in installed
|
|
890
|
-
// mode could race the monitor while it was preparing its rollback point.
|
|
926
|
+
// The shared launchPackageLifecycle path delegates to launchMonitorAndConfirm;
|
|
927
|
+
// the monitor retains shutdown, orphan checks, build and rollback ownership.
|
|
928
|
+
// Network preflight must finish before a confirmed monitor stops the daemon.
|
|
891
929
|
if (!requireCompletedDataMigration(resolveRoot(), 'restart'))
|
|
892
930
|
return;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if (aliveMains.length > 0) {
|
|
897
|
-
if (aliveMains.length > 1) {
|
|
898
|
-
console.log(`⚠ 检测到 ${aliveMains.length} 个 main 实例,将一并停止: ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
899
|
-
}
|
|
900
|
-
const mainPids = new Set(aliveMains.map(entry => entry.record.pid));
|
|
901
|
-
const currentPing = await probeDaemon(socketPath);
|
|
902
|
-
const gracefulPid = currentPing && mainPids.has(currentPing.pid)
|
|
903
|
-
? await requestDaemonShutdown(socketPath, DAEMON_SHUTDOWN_TIMEOUT_MS, 'ec restart', mainPids)
|
|
904
|
-
: null;
|
|
905
|
-
const stopped = await Promise.all(aliveMains.map(m => stopPid(m.record.pid, m.record.pid === gracefulPid, m.record.startedAt)));
|
|
906
|
-
if (stopped.some(result => !result)) {
|
|
907
|
-
console.error('❌ Restart aborted because an existing EvolCore process could not be stopped');
|
|
908
|
-
process.exitCode = 1;
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
await sleep(500);
|
|
912
|
-
}
|
|
913
|
-
cleanupInstances();
|
|
914
|
-
const remainingMainProcesses = scanInstances().mains.filter(entry => entry.alive);
|
|
915
|
-
if (remainingMainProcesses.length > 0) {
|
|
916
|
-
console.error(`❌ Restart aborted: EvolCore process still registered and alive (PID: ${remainingMainProcesses.map(entry => entry.record.pid).join(', ')})`);
|
|
917
|
-
process.exitCode = 1;
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
// 孤儿处理:同 HOME/同安装包的孤儿属于本运行时,先确认退出再替换;
|
|
921
|
-
// 跨 HOME 的孤儿只在 --clear 时 kill,否则仅警告。findOrphanProcesses
|
|
922
|
-
// 在 Windows 上需要一次 CIM 查询;保留一个快照,避免同一阶段重复扫描。
|
|
923
|
-
{
|
|
924
|
-
const orphans = findOrphanProcesses();
|
|
925
|
-
const confirmedTestDaemons = orphans.filter(o => o.confirmedTestDaemon);
|
|
926
|
-
if (confirmedTestDaemons.length > 0) {
|
|
927
|
-
const killed = killOrphans(confirmedTestDaemons);
|
|
928
|
-
console.log(`☠ 已清理 ${killed.length} 个泄漏的测试 daemon: ${killed.join(', ')}`);
|
|
929
|
-
await sleep(500);
|
|
930
|
-
}
|
|
931
|
-
const runtimeOrphans = orphans.filter(o => isRuntimeOrphan(o));
|
|
932
|
-
if (runtimeOrphans.length > 0) {
|
|
933
|
-
console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(o => o.pid).join(', ')}`);
|
|
934
|
-
const orphanPids = new Set(runtimeOrphans.map(orphan => orphan.pid));
|
|
935
|
-
const currentPing = await probeDaemon(socketPath);
|
|
936
|
-
const gracefulPid = currentPing && orphanPids.has(currentPing.pid)
|
|
937
|
-
? await requestDaemonShutdown(socketPath, DAEMON_SHUTDOWN_TIMEOUT_MS, 'ec restart', orphanPids)
|
|
938
|
-
: null;
|
|
939
|
-
const stopped = await Promise.all(runtimeOrphans.map(o => stopPid(o.pid, o.pid === gracefulPid)));
|
|
940
|
-
if (stopped.some(result => !result)) {
|
|
941
|
-
console.error('❌ Restart aborted because an unregistered EvolCore process could not be stopped');
|
|
942
|
-
process.exitCode = 1;
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
await sleep(500);
|
|
946
|
-
}
|
|
947
|
-
// stopPid() already waited for every runtime orphan above, so avoid a
|
|
948
|
-
// second scan before handling cross-HOME records.
|
|
949
|
-
const otherHome = orphans.filter(o => !o.confirmedTestDaemon && !isRuntimeOrphan(o));
|
|
950
|
-
if (opts.clear && otherHome.length > 0) {
|
|
951
|
-
const killed = killOrphans(otherHome);
|
|
952
|
-
console.log(`☠ 已 SIGKILL ${killed.length} 个跨 HOME 孤儿进程: ${killed.join(', ')}`);
|
|
953
|
-
await sleep(500);
|
|
954
|
-
}
|
|
955
|
-
else if (otherHome.length > 0) {
|
|
956
|
-
reportOrphans(otherHome);
|
|
957
|
-
}
|
|
958
|
-
// Keep the post-stop ownership guard: a process that appeared during the
|
|
959
|
-
// cleanup window must still block replacement.
|
|
960
|
-
const remainingRuntimeOrphans = findOrphanProcesses().filter(o => isRuntimeOrphan(o));
|
|
961
|
-
if (remainingRuntimeOrphans.length > 0) {
|
|
962
|
-
console.error(`❌ Restart aborted: EvolCore process still running (PID: ${remainingRuntimeOrphans.map(o => o.pid).join(', ')})`);
|
|
963
|
-
process.exitCode = 1;
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
// 只清理 EvolCore 自己登记的 codex app-server,不能影响桌面端实例。
|
|
968
|
-
{
|
|
969
|
-
const killed = stopCodexAppServerOrphans();
|
|
970
|
-
if (killed > 0) {
|
|
971
|
-
console.log(`☠ 已清理 ${killed} 个 EvolCore codex app-server 孤儿进程`);
|
|
972
|
-
await sleep(500);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
console.log(`⏱ restart prep done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s, starting...`);
|
|
976
|
-
// 通过 restart-monitor 启动,避免在 agent 内调用时自杀。
|
|
977
|
-
// 开发仓下 monitor 从源码跑;安装包下先复制到 stage 目录,两种情况都让看门狗
|
|
978
|
-
// 不住在会被替换的产物里。
|
|
979
|
-
const monitorEntry = platform.resolveMonitorLaunch(getPackageRoot(), path.join(resolvePaths().root, 'data', 'monitor-stage'));
|
|
980
|
-
if (monitorEntry.note)
|
|
981
|
-
console.log(`ℹ ${monitorEntry.note}`);
|
|
982
|
-
if (!fs.existsSync(monitorEntry.entry)) {
|
|
983
|
-
console.error(`❌ 重启中止:找不到 restart-monitor 入口 ${monitorEntry.entry}`);
|
|
984
|
-
console.error(' 请先执行 npm run build 重建产物');
|
|
931
|
+
const launched = await launchPackageLifecycle('restart', console.log, { clear: opts.clear });
|
|
932
|
+
if (!launched.ok) {
|
|
933
|
+
console.error(`❌ 重启中止:restart-monitor 未能启动(${launched.reason});当前服务未主动停止`);
|
|
985
934
|
process.exitCode = 1;
|
|
986
935
|
return;
|
|
987
936
|
}
|
|
988
|
-
|
|
989
|
-
if (!monitorStarted.ok) {
|
|
990
|
-
if (monitorEntry.stageDir)
|
|
991
|
-
platform.cleanupMonitorStage(monitorEntry.stageDir);
|
|
992
|
-
console.error(`❌ 重启中止:restart-monitor 未能启动(${monitorStarted.reason})`);
|
|
993
|
-
if (fs.existsSync(path.join(getPackageRoot(), 'src', 'cli', 'index.ts'))) {
|
|
994
|
-
console.error(' 源码可能存在语法错误导致 tsx 无法加载。请执行 npx tsc --noEmit 检查。');
|
|
995
|
-
}
|
|
996
|
-
console.error(' 服务已停止,请修复后执行 ec start。');
|
|
997
|
-
process.exitCode = 1;
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
console.log('🔄 Restart monitor started; service is coming back online in the background...');
|
|
1001
|
-
// 代码统计(开发环境)
|
|
1002
|
-
if (path.normalize(resolveRoot()) === path.normalize(getPackageRoot())) {
|
|
1003
|
-
console.log('');
|
|
1004
|
-
printCodeStats(getPackageRoot(), resolvePaths().logs);
|
|
1005
|
-
}
|
|
937
|
+
console.log(`🔄 Restart monitor started (${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s); service is restarting...`);
|
|
1006
938
|
}
|
|
1007
939
|
function normalizeRuntimePath(value) {
|
|
1008
940
|
return value.replace(/[\\/]+/g, '/').toLowerCase();
|
|
@@ -1310,8 +1242,13 @@ export async function cmdStatus() {
|
|
|
1310
1242
|
console.log(' 请稍后再次执行 ec status。');
|
|
1311
1243
|
}
|
|
1312
1244
|
else {
|
|
1313
|
-
|
|
1314
|
-
|
|
1245
|
+
// With no live main/monitor, a missing Windows pipe is the normal stopped
|
|
1246
|
+
// state. Preserve uncertainty for permissions, timeouts, unknown errors,
|
|
1247
|
+
// and Unix callers that may be isolated from the daemon's PID namespace.
|
|
1248
|
+
const stopped = !ipcFailure || (platform.isWindows && ipcFailure instanceof IpcTransportError
|
|
1249
|
+
&& ['ENOENT', 'ECONNREFUSED'].includes(ipcFailure.code));
|
|
1250
|
+
console.log(stopped ? '⚠ EvolCore is not running' : '⚠ 无法确认 EvolCore 运行状态:IPC 不可用,且没有可见的存活 PID。');
|
|
1251
|
+
if (!stopped && ipcFailure)
|
|
1315
1252
|
console.log(` ${ipcFailure instanceof Error ? ipcFailure.message : String(ipcFailure)}`);
|
|
1316
1253
|
}
|
|
1317
1254
|
// Session & Project statistics (从文件系统读)
|
package/dist/cli/diag.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseDiagArgs } from '../core/command/diag-service.js';
|
|
|
5
5
|
import { wantsHelp } from './help.js';
|
|
6
6
|
export async function cmdDiag(args) {
|
|
7
7
|
if (wantsHelp(args)) {
|
|
8
|
-
console.log('用法:ec diag <问题描述> [--owner <daemonOwner-aid>] [--format json]\n ec diag stop [--format json]\n未指定 --owner 时联系本机 daemon.json.owners[0]。');
|
|
8
|
+
console.log('用法:ec diag <问题描述> [--owner <daemonOwner-aid>] [--format json]\n ec diag stop [--format json]\n未指定 --owner 时联系本机 daemon.json.owners[0];有效诊断期间再次执行会追加到原 case。');
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
const json = args.at(-2) === '--format' && args.at(-1) === 'json';
|
package/dist/cli/menu-client.js
CHANGED
|
@@ -111,4 +111,8 @@ export async function cmdMenuClient(args) {
|
|
|
111
111
|
else
|
|
112
112
|
response = await executeMenuClientRequest(parseMenuClientArgs(args));
|
|
113
113
|
console.log(JSON.stringify(response, null, 2));
|
|
114
|
+
// Transport success does not imply the requested operation succeeded.
|
|
115
|
+
// Preserve the complete wire response for callers while failing the CLI.
|
|
116
|
+
if (response?.error)
|
|
117
|
+
process.exitCode = 1;
|
|
114
118
|
}
|
|
@@ -6,19 +6,27 @@ import { resolvePaths, getPackageRoot, daemonControlDir } from '../paths.js';
|
|
|
6
6
|
import { loadDaemonConfig } from '../config-store.js';
|
|
7
7
|
import * as platform from '../utils/cross-platform.js';
|
|
8
8
|
import { EventBus } from '../core/event-bus.js';
|
|
9
|
-
import {
|
|
9
|
+
import { applyCachedPackageUpgrades, recordPackageUpgrade } from '../utils/package-version-info.js';
|
|
10
10
|
import { isValidAid } from '../aun/aid/index.js';
|
|
11
11
|
import { msgSend } from '../aun/msg/index.js';
|
|
12
12
|
import { scanInstances, cleanupInstances, writeRestartMonitor, removeRestartMonitor, isRestartMonitorWinner, findOrphanProcesses, killOrphans } from '../utils/instance-registry.js';
|
|
13
|
-
import { WEB_PACKAGE_NAME } from '../product.js';
|
|
13
|
+
import { MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME } from '../product.js';
|
|
14
14
|
import { shouldSuppressRealRestart } from '../utils/restart-safety.js';
|
|
15
15
|
import { rotateStdoutLog } from '../utils/log-writer.js';
|
|
16
16
|
import { inspectDataMigrationRequirement } from '../core/data-migration.js';
|
|
17
|
-
import { DAEMON_SHUTDOWN_TIMEOUT_MS, requestDaemonShutdown } from './daemon-commands.js';
|
|
17
|
+
import { DAEMON_SHUTDOWN_TIMEOUT_MS, requestDaemonShutdown, stopCodexAppServerOrphans } from './daemon-commands.js';
|
|
18
18
|
import { getProcessStartTime, startTimeMatches } from '../utils/process-introspect.js';
|
|
19
19
|
const execFileAsync = promisify(execFile);
|
|
20
20
|
// Bound network notifications so they cannot hold up recovery after shutdown.
|
|
21
21
|
const OWNER_NOTIFICATION_TIMEOUT_MS = 5000;
|
|
22
|
+
let foregroundDaemonExit;
|
|
23
|
+
async function finishSuccessfulRestart() {
|
|
24
|
+
if (foregroundDaemonExit) {
|
|
25
|
+
removeRestartMonitor();
|
|
26
|
+
process.exit(await foregroundDaemonExit);
|
|
27
|
+
}
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
22
30
|
/**
|
|
23
31
|
* The package this monitor operates on, which is not always the one it runs
|
|
24
32
|
* from.
|
|
@@ -144,13 +152,14 @@ export async function cmdRestartMonitor() {
|
|
|
144
152
|
// still alive. Refuse to create a second daemon when an unregistered process
|
|
145
153
|
// from this package is found; a duplicate would connect AUN and kick the
|
|
146
154
|
// existing session before failing on IPC/port ownership.
|
|
147
|
-
const
|
|
155
|
+
const belongsToRuntime = (o) => {
|
|
148
156
|
if (o.confirmedTestDaemon)
|
|
149
157
|
return false;
|
|
150
158
|
const homeMatches = o.evolcoreHome && normalizePath(o.evolcoreHome) === normalizePath(p.root);
|
|
151
159
|
const packageMatches = !o.evolcoreHome && normalizePath(o.cmdline).includes(normalizePath(targetPackageRoot()));
|
|
152
160
|
return Boolean(homeMatches || packageMatches);
|
|
153
|
-
}
|
|
161
|
+
};
|
|
162
|
+
const runtimeOrphans = orphanSnapshot.filter(belongsToRuntime);
|
|
154
163
|
if (runtimeOrphans.length > 0) {
|
|
155
164
|
const pids = runtimeOrphans.map(o => o.pid).join(', ');
|
|
156
165
|
log(`Stopping unregistered EvolCore process(es): ${pids}`);
|
|
@@ -238,15 +247,32 @@ export async function cmdRestartMonitor() {
|
|
|
238
247
|
// for instance-file cleanup.
|
|
239
248
|
cleanupInstances();
|
|
240
249
|
}
|
|
241
|
-
//
|
|
242
|
-
|
|
250
|
+
// Preserve CLI restart's final ownership guards and optional orphan cleanup
|
|
251
|
+
// here, after election/shutdown, so no launcher stops a daemon before a
|
|
252
|
+
// watchdog has successfully loaded.
|
|
253
|
+
const remaining = findOrphanProcesses();
|
|
254
|
+
if (scanInstances().mains.some(item => item.alive) || remaining.some(belongsToRuntime)) {
|
|
255
|
+
log('Restart aborted: a runtime process appeared or survived shutdown');
|
|
256
|
+
cleanupPendingFile(pendingFile, log);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
const otherHome = remaining.filter(o => !o.confirmedTestDaemon && !belongsToRuntime(o));
|
|
260
|
+
if (process.argv.includes('--clear') && otherHome.length) {
|
|
261
|
+
log(`Cleaned cross-HOME orphans: ${killOrphans(otherHome).join(', ')}`);
|
|
262
|
+
}
|
|
263
|
+
else if (otherHome.length) {
|
|
264
|
+
log(`Other HOME orphan processes left running: ${otherHome.map(o => o.pid).join(', ')}`);
|
|
265
|
+
}
|
|
266
|
+
const stoppedCodex = stopCodexAppServerOrphans();
|
|
267
|
+
if (stoppedCodex)
|
|
268
|
+
log(`Cleaned ${stoppedCodex} EvolCore codex app-server orphan(s)`);
|
|
269
|
+
// Apply only the preflight cache. There is no registry VERSION query after
|
|
270
|
+
// shutdown (npm still downloads the exact cached package when needed).
|
|
271
|
+
log('Applying cached version information...');
|
|
243
272
|
// 升级会原地替换整包,先留一个可回退的产物副本。备份放 EVOLCORE_HOME 而非包目录:
|
|
244
273
|
// Linux/macOS 的全局目录常常不可写,而 HOME 一定可写。
|
|
245
274
|
const upgradeBackup = await backupDistForUpgrade(p, log);
|
|
246
|
-
const [upgrade, ecwebUpgrade] = await
|
|
247
|
-
tryUpgrade(),
|
|
248
|
-
tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME),
|
|
249
|
-
]);
|
|
275
|
+
const [upgrade, ecwebUpgrade] = await applyCachedPackageUpgrades(undefined, { log });
|
|
250
276
|
let upgradeRolledBack;
|
|
251
277
|
switch (upgrade.status) {
|
|
252
278
|
case 'upgraded':
|
|
@@ -287,8 +313,7 @@ export async function cmdRestartMonitor() {
|
|
|
287
313
|
await notifyDaemonOwners(p, `⚠️ evolcore 升级失败,使用当前版本继续\n${upgrade.error ?? ''}`.trim(), log);
|
|
288
314
|
break;
|
|
289
315
|
}
|
|
290
|
-
// EC Web
|
|
291
|
-
// 查询并行执行,npm 安装阶段由 npm-ops 的全局锁串行化。
|
|
316
|
+
// EC Web independently applies a valid cached result under the install lock.
|
|
292
317
|
switch (ecwebUpgrade.status) {
|
|
293
318
|
case 'upgraded':
|
|
294
319
|
log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
|
|
@@ -322,6 +347,13 @@ export async function cmdRestartMonitor() {
|
|
|
322
347
|
// 启动并检测 ready signal
|
|
323
348
|
let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
324
349
|
if (started) {
|
|
350
|
+
if (upgrade.status === 'upgraded' || upgrade.status === 'failed') {
|
|
351
|
+
await recordPackageUpgrade(MAIN_PACKAGE_NAME, {
|
|
352
|
+
from: upgrade.from, to: upgrade.to, at: new Date().toISOString(),
|
|
353
|
+
status: upgradeRolledBack ? 'rolled-back' : upgrade.status === 'upgraded' ? 'succeeded' : 'failed',
|
|
354
|
+
error: upgradeRolledBack ?? upgrade.error,
|
|
355
|
+
}, log);
|
|
356
|
+
}
|
|
325
357
|
if (upgradeRolledBack) {
|
|
326
358
|
markRestartOutcome(pendingFile, 'rolled-back', upgradeRolledBack, log);
|
|
327
359
|
log(`✓ Service restarted after upgrade rollback`);
|
|
@@ -341,15 +373,21 @@ export async function cmdRestartMonitor() {
|
|
|
341
373
|
}
|
|
342
374
|
archiveSelfHealLog(p, log);
|
|
343
375
|
// 发起会话内的“重启成功”通知由新进程自行发送,此处只负责失败/自愈告警。
|
|
344
|
-
|
|
376
|
+
await finishSuccessfulRestart();
|
|
345
377
|
}
|
|
346
378
|
// 升级后的新版本起不来:回退到升级前的产物再试一次。
|
|
347
379
|
if (upgradeVersions?.to && !upgradeRolledBack) {
|
|
380
|
+
await recordPackageUpgrade(MAIN_PACKAGE_NAME, {
|
|
381
|
+
...upgradeVersions, at: new Date().toISOString(), status: 'failed', error: 'Upgraded daemon failed its ready check',
|
|
382
|
+
}, log);
|
|
348
383
|
if (await restoreDistBackup(upgradeBackup, log)) {
|
|
349
384
|
await notifyDaemonOwners(p, `⚠️ 升级到 ${upgradeVersions.to} 后启动失败,正在回滚到 ${upgradeVersions.from} 重试...`, log);
|
|
350
385
|
started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
351
386
|
if (started) {
|
|
352
387
|
const reason = `升级到 ${upgradeVersions.to} 后启动失败,已回滚到 ${upgradeVersions.from}`;
|
|
388
|
+
await recordPackageUpgrade(MAIN_PACKAGE_NAME, {
|
|
389
|
+
...upgradeVersions, at: new Date().toISOString(), status: 'rolled-back', error: reason,
|
|
390
|
+
}, log);
|
|
353
391
|
log(`✓ ${reason}`);
|
|
354
392
|
markRestartOutcome(pendingFile, 'rolled-back', reason, log);
|
|
355
393
|
await notifyDaemonOwners(p, `⚠️ ${reason}。当前运行旧版本,请检查后重试。`, log);
|
|
@@ -858,9 +896,10 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
858
896
|
const out = fs.openSync(stdoutLog, 'a');
|
|
859
897
|
const err = fs.openSync(stdoutLog, 'a');
|
|
860
898
|
const appMain = path.join(targetPackageRoot(), 'dist', 'index.js');
|
|
899
|
+
const foreground = process.argv.includes('--foreground');
|
|
861
900
|
const child = spawn(process.execPath, ['--no-warnings=ExperimentalWarning', appMain], {
|
|
862
|
-
detached:
|
|
863
|
-
stdio: ['ignore', out, err],
|
|
901
|
+
detached: !foreground,
|
|
902
|
+
stdio: foreground ? 'inherit' : ['ignore', out, err],
|
|
864
903
|
windowsHide: true,
|
|
865
904
|
env: {
|
|
866
905
|
...process.env,
|
|
@@ -872,7 +911,16 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
872
911
|
}
|
|
873
912
|
});
|
|
874
913
|
const childPid = child.pid;
|
|
875
|
-
|
|
914
|
+
if (foreground) {
|
|
915
|
+
foregroundDaemonExit = new Promise(resolve => {
|
|
916
|
+
child.once('error', () => resolve(1));
|
|
917
|
+
child.once('exit', code => resolve(code ?? 1));
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
else
|
|
921
|
+
child.unref();
|
|
922
|
+
fs.closeSync(out);
|
|
923
|
+
fs.closeSync(err);
|
|
876
924
|
log(`Spawned new process PID: ${childPid}, waiting for ready signal...`);
|
|
877
925
|
// 轮询等待 ready.signal 出现
|
|
878
926
|
const start = Date.now();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseagentRunnerUnavailableError } from '../../agents/runner-types.js';
|
|
2
2
|
import { loadedBaseagentIds } from '../../agents/baseagent-availability.js';
|
|
3
|
+
import { closeDiagnosticSession, diagnosticTokenHash, isDiagnosticSessionActive } from './diag-service.js';
|
|
3
4
|
import { parseMenuClientArgs } from '../../cli/menu-client.js';
|
|
4
5
|
import { ensureSessionRuntimeDir } from '../../cli/task-context.js';
|
|
5
6
|
import { renderCommandCardAsText } from '../interaction-router.js';
|
|
@@ -166,7 +167,34 @@ export class CommandHandler {
|
|
|
166
167
|
throw new Error('Menu request file must be a regular file of at most 128 KiB');
|
|
167
168
|
return fs.readFileSync(target, 'utf8');
|
|
168
169
|
});
|
|
169
|
-
|
|
170
|
+
const binding = session.metadata?.diagnostic;
|
|
171
|
+
if (binding || session.metadata?.diagnosticStatus === 'cancelled') {
|
|
172
|
+
if (!isDiagnosticSessionActive(session, this.sessionManager.getChatDir(session))) {
|
|
173
|
+
return { ok: false, code: 'DIAG_CLOSED', error: '诊断授权已结束,禁止旧会话继续访问' };
|
|
174
|
+
}
|
|
175
|
+
if (binding && (options.targetAid !== binding.controlAid
|
|
176
|
+
|| diagnosticTokenHash(options.remoteAccessToken ?? options.ecwebToken ?? '') !== binding.tokenHash)) {
|
|
177
|
+
return { ok: false, code: 'DIAG_SCOPE_MISMATCH', error: '诊断请求必须使用本会话绑定的主机和 Token' };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const guardedOptions = binding
|
|
181
|
+
? {
|
|
182
|
+
...options,
|
|
183
|
+
beforeSend: () => {
|
|
184
|
+
if (!isDiagnosticSessionActive(session, this.sessionManager.getChatDir(session))) {
|
|
185
|
+
const error = new Error('诊断授权已结束,禁止旧会话继续访问');
|
|
186
|
+
error.code = 'DIAG_CLOSED';
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
: options;
|
|
192
|
+
const response = await send(guardedOptions);
|
|
193
|
+
if (binding && response?.error?.code === 'REMOTE_ACCESS_TOKEN_INVALID') {
|
|
194
|
+
closeDiagnosticSession(session, this.sessionManager.getChatDir(session));
|
|
195
|
+
this.messageQueue?.clearBySession(session.id);
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, response };
|
|
170
198
|
}
|
|
171
199
|
catch (error) {
|
|
172
200
|
return { ok: false, code: error.code ?? 'MENU_REQUEST_FAILED', error: error.message };
|