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.
@@ -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 { tryUpgrade, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-ops.js';
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, killOrphans } from '../utils/instance-registry.js';
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 = signal => tryUpgrade({ signal }), ecwebUpgradeFn = signal => tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME, { signal }), timeoutMs = 60_000) {
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 (opts.upgradeChecks ?? runUpgradeChecks)();
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
- // 构建交给 restart-monitor:它在 daemon 停止之后才编译,dist/ 不会被活进程
887
- // 占用,失败还能回滚到上一份产物。这里再编译一次会落在事务保护之外。
888
- // restart-monitor owns upgrade checks. Running them here as well caused a
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
- // 停止所有活 main 进程(可能不止一个)
894
- const status = initialStatus;
895
- const aliveMains = status.mains.filter(m => m.alive);
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
- const monitorStarted = await spawnRestartMonitorAndConfirm(monitorEntry, log => console.error(log));
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
- console.log(ipcFailure ? '⚠ 无法确认 EvolCore 运行状态:IPC 不可用,且没有可见的存活 PID。' : '⚠ EvolCore is not running');
1314
- if (ipcFailure)
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';
@@ -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 { tryUpgrade, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-ops.js';
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 runtimeOrphans = orphanSnapshot.filter(o => {
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
- log('Checking for updates...');
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 Promise.all([
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 是独立全局包,已安装时单独检查升级;与 EvolCore registry
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
- process.exit(0);
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: true,
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
- child.unref();
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
- return { ok: true, response: await send(options) };
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 };