evolcore 0.0.35 → 0.0.37

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.
Files changed (224) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +177 -85
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +115 -36
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { getPackageRoot, resolvePaths } from '../paths.js';
4
4
  import { logger } from './logger.js';
5
- import { isSandboxInitializationFailure } from '../core/permission/sandbox-runtime.js';
5
+ import { CODEX_SANDBOX_USER_NAMESPACE_DISABLED_BY_APPARMOR, CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE, isClaudeSandboxDependencyFailure, isCodexSandboxUserNamespaceFailure, isSandboxInitializationFailure, } from '../core/permission/sandbox-runtime.js';
6
6
  /** A deadline imposed by an outer executor, handled by the ordinary task timeout path. */
7
7
  export class ExecutionTimeoutError extends Error {
8
8
  reason;
@@ -87,6 +87,8 @@ export var ErrorType;
87
87
  ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
88
88
  ErrorType["SESSION_UNAVAILABLE"] = "session_unavailable";
89
89
  ErrorType["SANDBOX_INITIALIZATION_FAILED"] = "sandbox_initialization_failed";
90
+ ErrorType["SANDBOX_DEPENDENCIES_MISSING"] = "sandbox_dependencies_missing";
91
+ ErrorType["CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE"] = "codex_sandbox_userns_unavailable";
90
92
  ErrorType["CLAUDE_STARTUP_ARGUMENTS_TOO_LONG"] = "claude_startup_arguments_too_long";
91
93
  ErrorType["CLAUDE_CODE_STARTUP_FAILED"] = "claude_code_startup_failed";
92
94
  ErrorType["UNKNOWN"] = "unknown";
@@ -440,6 +442,16 @@ function hasRetryableHttpStatus(text) {
440
442
  export function classifyError(error) {
441
443
  if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
442
444
  return ErrorType.SESSION_UNAVAILABLE;
445
+ if (isClaudeSandboxDependencyFailure(error))
446
+ return ErrorType.SANDBOX_DEPENDENCIES_MISSING;
447
+ if (error?.code === CODEX_SANDBOX_USER_NAMESPACE_DISABLED_BY_APPARMOR
448
+ || error?.code === CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE
449
+ || (isCodexSandboxUserNamespaceFailure(error)
450
+ && /\b(?:bwrap|bubblewrap)\b/i.test(String(error?.message ?? error)))) {
451
+ return ErrorType.CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE;
452
+ }
453
+ if (isSandboxInitializationFailure(error))
454
+ return ErrorType.SANDBOX_INITIALIZATION_FAILED;
443
455
  const msg = (error?.message || '').toLowerCase();
444
456
  // A damaged ecagent JSONL transcript is deterministic local state. Retrying
445
457
  // the provider request cannot repair it and can append more entries to the
@@ -481,9 +493,6 @@ export function classifyError(error) {
481
493
  || /api error: 404\b/.test(msg)) {
482
494
  return ErrorType.MODEL_UNAVAILABLE;
483
495
  }
484
- if (isSandboxInitializationFailure(error)) {
485
- return ErrorType.SANDBOX_INITIALIZATION_FAILED;
486
- }
487
496
  if (msg.includes('401') || msg.includes('authentication_error') || msg.includes('unauthorized')) {
488
497
  return ErrorType.AUTH_ERROR;
489
498
  }
@@ -509,6 +518,8 @@ export function classifyError(error) {
509
518
  export function isRetryableError(error) {
510
519
  if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
511
520
  return false;
521
+ if (isClaudeSandboxDependencyFailure(error) || isSandboxInitializationFailure(error))
522
+ return false;
512
523
  // Replaying the same request does not increase its output budget.
513
524
  if (isOutputTokenLimitError(error))
514
525
  return false;
@@ -530,6 +541,28 @@ export function isRetryableError(error) {
530
541
  return true;
531
542
  return false;
532
543
  }
544
+ function sandboxInitializationErrorMessage(error, prefix) {
545
+ const stderr = typeof error?.stderr === 'string'
546
+ ? [error.stderr]
547
+ : Array.isArray(error?.stderr) ? error.stderr.filter((line) => typeof line === 'string') : [];
548
+ const diagnostic = [error?.message ?? String(error ?? ''), ...stderr].join('\n')
549
+ .match(/bwrap:\s*[^\r\n]+: read-only file system/i)?.[0];
550
+ if (error?.code === CODEX_SANDBOX_USER_NAMESPACE_DISABLED_BY_APPARMOR
551
+ || /codex_sandbox_userns_disabled_by_apparmor/i.test(String(error?.message ?? ''))
552
+ || (isCodexSandboxUserNamespaceFailure({ message: error?.message, stderr })
553
+ && /apparmor_restrict_unprivileged_userns\s*=\s*1|apparmor/i.test([error?.message ?? '', ...stderr].join('\n')))) {
554
+ return `${prefix}Codex Linux 沙箱无法创建 user namespace(${CODEX_SANDBOX_USER_NAMESPACE_DISABLED_BY_APPARMOR}),当前操作未执行;请让宿主为该 EvolCore/Codex 服务配置受限的 AppArmor 允许规则,或使用等价隔离后端。不要关闭沙箱或自动降级为全权限,并运行 ec diagnose 获取探针结果`;
555
+ }
556
+ if (error?.code === CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE
557
+ || isCodexSandboxUserNamespaceFailure({ message: error?.message, stderr })) {
558
+ return `${prefix}Codex Linux 沙箱无法创建 user namespace(${CODEX_SANDBOX_USER_NAMESPACE_UNAVAILABLE}),当前操作未执行;请检查内核/AppArmor/容器策略并运行 ec diagnose。不会关闭沙箱或自动降级为全权限`;
559
+ }
560
+ if (diagnostic) {
561
+ const target = /mkdir parents for/i.test(diagnostic) ? '挂载目标的父目录' : '挂载目标';
562
+ return `${prefix}执行沙箱文件系统挂载失败(sandbox_initialization_failed),当前操作未执行;只读文件系统阻止创建${target},请检查文件系统挂载规则`;
563
+ }
564
+ return `${prefix}执行沙箱启动失败(sandbox_initialization_failed),当前操作未执行;请查看原始诊断并运行 ec diagnose 排查`;
565
+ }
533
566
  export function getErrorMessage(error, terminalReason, includeEmoji = true) {
534
567
  if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
535
568
  return `${includeEmoji ? '⚠️ ' : ''}${error.message}`;
@@ -566,7 +599,9 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
566
599
  case 'permission_denied':
567
600
  return `${prefix}权限被拒绝,操作已取消`;
568
601
  case 'sandbox_initialization_failed':
569
- return `${warnPrefix}执行沙箱启动失败(sandbox_initialization_failed),当前操作未执行;请检查文件系统挂载规则及 user namespace/seccomp 配置`;
602
+ return sandboxInitializationErrorMessage(error, warnPrefix);
603
+ case 'sandbox_dependencies_missing':
604
+ return `${warnPrefix}Claude 执行沙箱缺少系统依赖(sandbox_dependencies_missing),当前操作未执行;请安装 bubblewrap 和 socat 后运行 ec diagnose。不会自动关闭沙箱或安装系统包`;
570
605
  case 'claude_startup_arguments_too_long':
571
606
  return `${warnPrefix}当前 Claude 启动参数过长,任务未执行`;
572
607
  case 'claude_code_startup_failed':
@@ -580,6 +615,9 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
580
615
  const msg = resolveErrorMessage(error);
581
616
  const warnPrefix = includeEmoji ? '⚠️ ' : '';
582
617
  const errPrefix = includeEmoji ? '❌ ' : '';
618
+ if (isClaudeSandboxDependencyFailure(error)) {
619
+ return `${warnPrefix}Claude 执行沙箱缺少系统依赖(sandbox_dependencies_missing),当前操作未执行;请安装 bubblewrap 和 socat 后运行 ec diagnose。不会自动关闭沙箱或安装系统包`;
620
+ }
583
621
  if (/invalid (?:jsonl )?session file/i.test(msg)) {
584
622
  return `${errPrefix}ecagent 会话文件损坏,已停止自动重试;请使用 /repair 或 /new 恢复会话`;
585
623
  }
@@ -605,7 +643,7 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
605
643
  : `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
606
644
  }
607
645
  if (isSandboxInitializationFailure(error)) {
608
- return `${warnPrefix}执行沙箱启动失败(sandbox_initialization_failed),当前操作未执行;请检查文件系统挂载规则及 user namespace/seccomp 配置`;
646
+ return sandboxInitializationErrorMessage(error, warnPrefix);
609
647
  }
610
648
  if (msg.includes('401') || msg.includes('authentication_error') || msg.toLowerCase().includes('unauthorized')) {
611
649
  return `${errPrefix}API Key 无效,请检查密钥配置。使用 /status 查看当前配置`;
@@ -16,6 +16,7 @@ import { isProcessRunning, killProcess, isWindows, findProcessesWithCommandLine
16
16
  import { getProcessStartTime, startTimeMatches } from './process-introspect.js';
17
17
  import { isConfirmedLeakedTestDaemon, runtimeHomeFromEnv } from './restart-safety.js';
18
18
  import { decodeWindowsOutput } from './windows-output.js';
19
+ import { atomicWriteJson } from './atomic-write.js';
19
20
  // ── Helpers ──
20
21
  function instanceDir() {
21
22
  return resolvePaths().instanceDir;
@@ -58,7 +59,7 @@ export function writeMain(launchedBy) {
58
59
  runMode: isDevMode() ? 'dev' : 'install',
59
60
  };
60
61
  const filePath = path.join(dir, mainFileName(process.pid));
61
- writeAtomic(filePath, JSON.stringify(record, null, 2));
62
+ atomicWriteJson(filePath, record);
62
63
  return filePath;
63
64
  }
64
65
  export function removeMain(pid) {
@@ -99,7 +100,7 @@ export function isMainWinner() {
99
100
  function restartMonitorFileName(pid) {
100
101
  return `restart-monitor-${pid}.json`;
101
102
  }
102
- export function writeRestartMonitor() {
103
+ export function writeRestartMonitor(requestId, phase) {
103
104
  const dir = instanceDir();
104
105
  fs.mkdirSync(dir, { recursive: true });
105
106
  const startedAt = getProcessStartTime(process.pid) ?? Date.now();
@@ -108,11 +109,20 @@ export function writeRestartMonitor() {
108
109
  startedAt,
109
110
  startedAtIso: new Date(startedAt).toISOString(),
110
111
  launchedBy: 'restart-monitor',
112
+ ...(requestId ? { requestId } : {}),
113
+ ...(phase ? { phase } : {}),
111
114
  };
112
115
  const filePath = path.join(dir, restartMonitorFileName(process.pid));
113
116
  writeAtomic(filePath, JSON.stringify(record, null, 2));
114
117
  return filePath;
115
118
  }
119
+ export function updateRestartMonitorPhase(pid, phase) {
120
+ const filePath = path.join(instanceDir(), restartMonitorFileName(pid));
121
+ const record = safeParseJson(filePath);
122
+ if (!record)
123
+ return;
124
+ atomicWriteJson(filePath, { ...record, phase });
125
+ }
116
126
  export function removeRestartMonitor(pid) {
117
127
  const target = pid ?? process.pid;
118
128
  const filePath = path.join(instanceDir(), restartMonitorFileName(target));
@@ -64,6 +64,33 @@ function resolveNpmInvocation() {
64
64
  }
65
65
  return { command: fs.existsSync(sibling) ? sibling : 'npm', argsPrefix: [] };
66
66
  }
67
+ function npmConfigArgs(config = {}) {
68
+ return [
69
+ ...(config.cacheDir ? ['--cache', config.cacheDir] : []),
70
+ ...(config.userConfig ? ['--userconfig', config.userConfig] : []),
71
+ ...(config.globalConfig ? ['--globalconfig', config.globalConfig] : []),
72
+ ];
73
+ }
74
+ function normalizeNpmPath(value) {
75
+ if (!value || /^(?:https?|file):/i.test(value) || ['false', 'undefined', 'null'].includes(value))
76
+ return undefined;
77
+ return path.resolve(value);
78
+ }
79
+ async function readNpmConfigValue(key, signal) {
80
+ const npm = resolveNpmInvocation();
81
+ try {
82
+ const result = await execFileAsync(npm.command, [...npm.argsPrefix, 'config', 'get', key === 'cacheDir' ? 'cache' : key === 'userConfig' ? 'userconfig' : 'globalconfig'], {
83
+ timeout: 5000,
84
+ windowsHide: isWindows,
85
+ signal,
86
+ });
87
+ const value = String(result.stdout ?? '').trim();
88
+ return normalizeNpmPath(value);
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ }
67
94
  /**
68
95
  * Read the effective registry from the npm belonging to the current Node.
69
96
  * An empty value means npm has no explicit registry and uses the public
@@ -72,6 +99,8 @@ function resolveNpmInvocation() {
72
99
  */
73
100
  export async function resolveUpgradeRegistry(signal) {
74
101
  const npm = resolveNpmInvocation();
102
+ let registry = DEFAULT_NPM_REGISTRY;
103
+ let useNpmCli = false;
75
104
  try {
76
105
  const result = await execFileAsync(npm.command, [...npm.argsPrefix, 'config', 'get', 'registry'], {
77
106
  timeout: 5000,
@@ -79,22 +108,27 @@ export async function resolveUpgradeRegistry(signal) {
79
108
  signal,
80
109
  });
81
110
  const value = String(result.stdout ?? '').trim();
82
- if (!value || value === 'undefined' || value === 'null') {
83
- return { registry: DEFAULT_NPM_REGISTRY, useNpmCli: false };
111
+ if (value && value !== 'undefined' && value !== 'null') {
112
+ registry = normalizeRegistry(value);
113
+ useNpmCli = true;
84
114
  }
85
- return { registry: normalizeRegistry(value), useNpmCli: true };
86
- }
87
- catch {
88
- return { registry: DEFAULT_NPM_REGISTRY, useNpmCli: false };
89
115
  }
116
+ catch { }
117
+ const [cacheDir, userConfig, globalConfig] = await Promise.all([
118
+ readNpmConfigValue('cacheDir', signal),
119
+ readNpmConfigValue('userConfig', signal),
120
+ readNpmConfigValue('globalConfig', signal),
121
+ ]);
122
+ const npmConfig = { ...(cacheDir ? { cacheDir } : {}), ...(userConfig ? { userConfig } : {}), ...(globalConfig ? { globalConfig } : {}) };
123
+ return { registry, useNpmCli, npmConfig };
90
124
  }
91
- async function checkLatestVersionViaNpm(pkg, timeoutMs, signal, registry) {
125
+ async function checkLatestVersionViaNpm(pkg, timeoutMs, signal, registry, distTag = 'latest') {
92
126
  const npm = resolveNpmInvocation();
93
127
  try {
94
128
  const result = await execFileAsync(npm.command, [
95
129
  ...npm.argsPrefix,
96
130
  'view',
97
- `${pkg}@latest`,
131
+ `${pkg}@${distTag}`,
98
132
  'version',
99
133
  '--json',
100
134
  '--registry',
@@ -160,7 +194,7 @@ async function canSudoNonInteractively() {
160
194
  export async function npmInstallGlobal(pkg, options = {}) {
161
195
  const npm = resolveNpmInvocation();
162
196
  const registry = options.registry ? normalizeRegistry(options.registry) : undefined;
163
- const npmArgs = ['install', '-g', pkg, ...(registry ? ['--registry', registry] : [])];
197
+ const npmArgs = ['install', '-g', pkg, ...(registry ? ['--registry', registry] : []), ...npmConfigArgs(options.npmConfig), ...(options.preferOffline ? ['--prefer-offline'] : [])];
164
198
  try {
165
199
  await execFileAsync(npm.command, [...npm.argsPrefix, ...npmArgs], {
166
200
  timeout: options.timeoutMs ?? 180000,
@@ -179,7 +213,8 @@ export async function npmInstallGlobal(pkg, options = {}) {
179
213
  const registryArg = registry ? ` --registry ${registry}` : '';
180
214
  throw new Error(`权限不足,且 sudo 需要密码(当前进程无法交互输入)。请使用当前 Node 对应的 npm 手动执行: sudo npm install -g ${pkg}${registryArg}`);
181
215
  }
182
- await execFileAsync('sudo', ['-n', npm.command, ...npm.argsPrefix, ...npmArgs], {
216
+ const preserveEnv = ['NPM_TOKEN', 'NODE_AUTH_TOKEN'].filter(key => process.env[key]);
217
+ await execFileAsync('sudo', ['-n', ...(preserveEnv.length ? [`--preserve-env=${preserveEnv.join(',')}`] : []), npm.command, ...npm.argsPrefix, ...npmArgs], {
183
218
  timeout: options.timeoutMs ?? 180000,
184
219
  windowsHide: false,
185
220
  signal: options.signal,
@@ -295,7 +330,7 @@ export function getLocalVersion() {
295
330
  }
296
331
  /**
297
332
  * 解析全局安装包的已装版本(通过 `npm root -g` 定位全局 node_modules)。
298
- * 用于 EC Web 这类独立全局命令包——它们不在主包的依赖树里。
333
+ * EC Web 从 EvolCore 固定依赖解析;开发链接时使用同仓 ecweb 子项目。
299
334
  * 未安装或查询失败返回 null。
300
335
  */
301
336
  export function resolveGlobalPkg(pkgName) {
@@ -304,8 +339,15 @@ export function resolveGlobalPkg(pkgName) {
304
339
  const globalRoot = execFileSync(npm.command, [...npm.argsPrefix, 'root', '-g'], {
305
340
  encoding: 'utf-8', timeout: 10000, windowsHide: true,
306
341
  }).trim();
307
- const pkgPath = path.join(globalRoot, ...pkgName.split('/'), 'package.json');
308
- if (fs.existsSync(pkgPath)) {
342
+ const packagePaths = pkgName === WEB_PACKAGE_NAME
343
+ ? [
344
+ ...(isLinkedInstall() ? [path.join(getPackageRoot(), 'ecweb', 'package.json')] : []),
345
+ path.join(globalRoot, MAIN_PACKAGE_NAME, 'node_modules', ...pkgName.split('/'), 'package.json'),
346
+ ]
347
+ : [path.join(globalRoot, ...pkgName.split('/'), 'package.json')];
348
+ for (const pkgPath of packagePaths) {
349
+ if (!fs.existsSync(pkgPath))
350
+ continue;
309
351
  const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
310
352
  return { version: data.version, path: pkgPath };
311
353
  }
@@ -318,7 +360,7 @@ export function resolveGlobalPkg(pkgName) {
318
360
  * 使用 HTTP fetch 直接查 registry API,不依赖 npm CLI。
319
361
  * 超时 10 秒,失败返回 null。
320
362
  */
321
- async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, registry = DEFAULT_NPM_REGISTRY) {
363
+ async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, registry = DEFAULT_NPM_REGISTRY, distTag = 'latest') {
322
364
  const controller = new AbortController();
323
365
  const abort = () => controller.abort();
324
366
  if (signal?.aborted)
@@ -327,7 +369,7 @@ async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, r
327
369
  signal?.addEventListener('abort', abort, { once: true });
328
370
  const timer = setTimeout(abort, timeoutMs);
329
371
  try {
330
- const res = await fetch(`${normalizeRegistry(registry)}/${encodeURIComponent(pkg)}/latest`, {
372
+ const res = await fetch(`${normalizeRegistry(registry)}/${encodeURIComponent(pkg)}/${distTag}`, {
331
373
  signal: controller.signal,
332
374
  headers: { 'Accept': 'application/json' },
333
375
  });
@@ -344,18 +386,49 @@ async function fetchLatestVersion(pkg = 'evolcore', timeoutMs = 10000, signal, r
344
386
  signal?.removeEventListener('abort', abort);
345
387
  }
346
388
  }
389
+ export async function preparePackageCache(target, registry, options = {}) {
390
+ if (!isPackageVersion(target) || normalizeVersionRegistry(registry) !== registry)
391
+ throw new Error('Invalid cache target');
392
+ const npm = resolveNpmInvocation();
393
+ const spec = `${MAIN_PACKAGE_NAME}@${target}`;
394
+ const run = (args) => new Promise((resolve, reject) => {
395
+ let failure = null;
396
+ let output = '';
397
+ const child = execFile(npm.command, [...npm.argsPrefix, ...args, '--registry', registry, ...npmConfigArgs(options.npmConfig), '--ignore-scripts'], {
398
+ timeout: 180_000, windowsHide: isWindows, signal: options.signal, maxBuffer: 8 * 1024 * 1024,
399
+ }, (error, stdout) => { failure = error; output = String(stdout ?? ''); });
400
+ child.once('error', error => { failure = error; });
401
+ child.once('close', code => {
402
+ if (failure || code !== 0 || options.signal?.aborted)
403
+ reject(new Error(options.signal?.aborted
404
+ ? 'Package cache preparation timed out or was cancelled' : 'npm cache preparation failed; check registry connectivity, credentials, disk space and permissions'));
405
+ else
406
+ resolve(output);
407
+ });
408
+ });
409
+ options.onPhase?.('downloading');
410
+ await run(['cache', 'add', spec, '--prefer-offline']);
411
+ options.onPhase?.('tarball-check');
412
+ const metadata = JSON.parse(await run(['view', spec, 'name', 'version', 'dist', '--json', '--offline']));
413
+ const [packed] = JSON.parse(await run(['pack', spec, '--dry-run', '--json', '--offline']));
414
+ if (metadata?.name !== MAIN_PACKAGE_NAME || metadata.version !== target || packed?.name !== MAIN_PACKAGE_NAME || packed.version !== target
415
+ || !(typeof metadata.dist?.integrity === 'string' ? packed.integrity === metadata.dist.integrity
416
+ : typeof metadata.dist?.shasum === 'string' && packed.shasum === metadata.dist.shasum)) {
417
+ throw new Error('Cached package identity or integrity does not match the selected EvolCore version');
418
+ }
419
+ }
347
420
  /** Query an already selected source, retaining npm proxy/CA/auth behavior. */
348
- export async function queryPackageVersion(pkg, selection, signal) {
421
+ export async function queryPackageVersion(pkg, selection, signal, distTag = 'latest') {
349
422
  const registry = normalizeVersionRegistry(selection.registry);
350
- if (!registry)
423
+ if (!registry || !['latest', 'test'].includes(distTag))
351
424
  return null;
352
425
  const version = selection.useNpmCli
353
- ? await checkLatestVersionViaNpm(pkg, 10000, signal, registry)
354
- : await fetchLatestVersion(pkg, 10000, signal, registry);
355
- return isPackageVersion(version) ? { version, registry } : null;
426
+ ? await checkLatestVersionViaNpm(pkg, 10000, signal, registry, distTag)
427
+ : await fetchLatestVersion(pkg, 10000, signal, registry, distTag);
428
+ return isPackageVersion(version) ? { version, registry, npmConfig: selection.npmConfig } : null;
356
429
  }
357
430
  /** No registry lookup: callers must validate cache freshness before each install. */
358
- export async function installExactPackageVersion(pkg, target, registry, resolveInstalled) {
431
+ export async function installExactPackageVersion(pkg, target, registry, resolveInstalled, npmConfig) {
359
432
  if (![MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME].includes(pkg)
360
433
  || !isPackageVersion(target) || normalizeVersionRegistry(registry) !== registry) {
361
434
  return { status: 'failed', error: 'Invalid cached install target' };
@@ -374,7 +447,7 @@ export async function installExactPackageVersion(pkg, target, registry, resolveI
374
447
  }
375
448
  for (let attempt = 0; attempt < 2; attempt++) {
376
449
  try {
377
- await npmInstallGlobal(`${pkg}@${target}`, { registry });
450
+ await npmInstallGlobal(`${pkg}@${target}`, { registry, preferOffline: true, npmConfig });
378
451
  const actual = resolveInstalled()?.version;
379
452
  if (!actual || !isPackageVersion(actual) || comparePackageVersions(actual, target) < 0) {
380
453
  throw new Error('Installed version did not reach the cached target');
@@ -407,7 +480,7 @@ async function checkLatestVersionWithFallback(pkg, timeoutMs, signal, explicitRe
407
480
  const version = selection.useNpmCli
408
481
  ? await checkLatestVersionViaNpm(pkg, timeoutMs, signal, selection.registry)
409
482
  : await fetchLatestVersion(pkg, timeoutMs, signal, selection.registry);
410
- return version ? { version, registry: selection.registry } : null;
483
+ return version ? { version, registry: selection.registry, npmConfig: selection.npmConfig } : null;
411
484
  }
412
485
  /**
413
486
  * Query the registry selected from the running Node/npm configuration, or the
@@ -434,7 +507,7 @@ export async function tryUpgrade(options = {}) {
434
507
  }
435
508
  const { version: remoteVer, registry } = checked;
436
509
  // 版本比较
437
- if (compareVersions(localVer, remoteVer) >= 0) {
510
+ if (comparePackageVersions(localVer, remoteVer) >= 0) {
438
511
  return { status: 'no-update', from: localVer };
439
512
  }
440
513
  // 有新版本,执行升级(失败重试一次)
@@ -447,10 +520,11 @@ export async function tryUpgrade(options = {}) {
447
520
  timeoutMs: options.installTimeoutMs,
448
521
  signal: options.signal,
449
522
  registry,
523
+ npmConfig: checked.npmConfig,
450
524
  });
451
525
  installedVer = getLocalVersion();
452
526
  });
453
- if (!installedVer || compareVersions(installedVer, remoteVer) < 0) {
527
+ if (!installedVer || comparePackageVersions(installedVer, remoteVer) < 0) {
454
528
  throw new Error(`npm install completed but evolcore is still ${installedVer ?? 'not installed'} (target ${remoteVer})`);
455
529
  }
456
530
  return { status: 'upgraded', from: localVer, to: installedVer };
@@ -484,7 +558,7 @@ export async function tryUpgradeGlobalPkg(resolveInstalled, pkgName, options = {
484
558
  : { status: 'skipped', error: 'Failed to check remote version' };
485
559
  }
486
560
  const { version: remoteVer, registry } = checked;
487
- if (compareVersions(localVer, remoteVer) >= 0) {
561
+ if (comparePackageVersions(localVer, remoteVer) >= 0) {
488
562
  return { status: 'no-update', from: localVer };
489
563
  }
490
564
  let lastError;
@@ -496,10 +570,11 @@ export async function tryUpgradeGlobalPkg(resolveInstalled, pkgName, options = {
496
570
  timeoutMs: options.installTimeoutMs,
497
571
  signal: options.signal,
498
572
  registry,
573
+ npmConfig: checked.npmConfig,
499
574
  });
500
575
  installedVer = resolveInstalled()?.version;
501
576
  });
502
- if (!installedVer || compareVersions(installedVer, remoteVer) < 0) {
577
+ if (!installedVer || comparePackageVersions(installedVer, remoteVer) < 0) {
503
578
  throw new Error(`npm install completed but ${pkgName} is still ${installedVer ?? 'not installed'} (target ${remoteVer})`);
504
579
  }
505
580
  return { status: 'upgraded', from: localVer, to: installedVer };
@@ -2,13 +2,20 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { getPackageRoot, resolvePaths } from '../paths.js';
4
4
  import { MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME, TARGET_PACKAGE_ROOT_ENV } from '../product.js';
5
- import { getLocalVersion, isLinkedInstall, resolveGlobalPkg, resolveUpgradeRegistry, queryPackageVersion, installExactPackageVersion, } from './npm-ops.js';
5
+ import { DEFAULT_NPM_REGISTRY, getLocalVersion, isLinkedInstall, resolveGlobalPkg, resolveUpgradeRegistry, queryPackageVersion, installExactPackageVersion, } from './npm-ops.js';
6
6
  import { comparePackageVersions, isPackageVersion, normalizeVersionRegistry } from './npm-ops.js';
7
7
  import { atomicReplaceJson, withFileLock } from './atomic-write.js';
8
+ import { loadDaemonConfig } from '../config-store.js';
8
9
  export const VERSION_INFO_TTL_MS = 24 * 60 * 60 * 1000;
10
+ export const VERSION_INFO_SCHEMA_VERSION = 2;
9
11
  export const VERSION_INFO_PACKAGES = [MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME, '@agentunion/fastaun'];
10
- export const AUTO_UPGRADE_PACKAGES = [MAIN_PACKAGE_NAME, WEB_PACKAGE_NAME];
11
- const empty = () => ({ $schema_version: 1, packages: {} });
12
+ export const AUTO_UPGRADE_PACKAGES = [MAIN_PACKAGE_NAME];
13
+ export function resolveUpgradeDistTag() {
14
+ return loadDaemonConfig().upgrade?.testPackages === true ? 'test' : 'latest';
15
+ }
16
+ const linkedInstallSelection = { registry: DEFAULT_NPM_REGISTRY, useNpmCli: false, npmConfig: {} };
17
+ const snapshotKey = (name, registry, distTag) => JSON.stringify([name, registry, distTag]);
18
+ const empty = () => ({ $schema_version: VERSION_INFO_SCHEMA_VERSION, packages: {} });
12
19
  const timestamp = (value) => typeof value === 'string' ? Date.parse(value) : NaN;
13
20
  const allowed = (name) => VERSION_INFO_PACKAGES.includes(name);
14
21
  const filePath = () => path.join(resolvePaths().dataDir, 'package-version-info.json');
@@ -17,10 +24,22 @@ const writeLockPath = () => `${filePath()}.lock`;
17
24
  export function readPackageVersionInfo() {
18
25
  try {
19
26
  const raw = JSON.parse(fs.readFileSync(filePath(), 'utf8'));
20
- if (raw?.$schema_version !== 1 || !raw.packages || Array.isArray(raw.packages)
27
+ if (raw?.$schema_version !== VERSION_INFO_SCHEMA_VERSION || !raw.packages || Array.isArray(raw.packages)
21
28
  || typeof raw.packages !== 'object' || Object.keys(raw.packages).some(name => !allowed(name)))
22
29
  return empty();
23
30
  const result = empty();
31
+ if (raw.snapshots && typeof raw.snapshots === 'object' && !Array.isArray(raw.snapshots)) {
32
+ result.snapshots = {};
33
+ for (const [key, value] of Object.entries(raw.snapshots)) {
34
+ const entry = value;
35
+ if (entry && allowed(entry.packageName) && entry.registry && normalizeVersionRegistry(entry.registry) === entry.registry
36
+ && ['latest', 'test'].includes(entry.distTag ?? 'latest')
37
+ && key === snapshotKey(entry.packageName, entry.registry, entry.distTag ?? 'latest')
38
+ && (entry.currentVersion === null || isPackageVersion(entry.currentVersion))
39
+ && (entry.latestVersion == null || isPackageVersion(entry.latestVersion)))
40
+ result.snapshots[key] = entry;
41
+ }
42
+ }
24
43
  for (const [name, value] of Object.entries(raw.packages)) {
25
44
  const entry = value;
26
45
  if (!entry || entry.packageName !== name || !['ok', 'unavailable', 'skipped', 'not-installed'].includes(entry.lastAttemptStatus)
@@ -46,11 +65,12 @@ export async function readPackageVersionInfoConsistent(timeoutMs = 30000) {
46
65
  }
47
66
  return readPackageVersionInfo();
48
67
  }
49
- export function isPackageVersionInfoFresh(entry, now = Date.now(), ttlMs = VERSION_INFO_TTL_MS, registry) {
68
+ export function isPackageVersionInfoFresh(entry, now = Date.now(), ttlMs = VERSION_INFO_TTL_MS, registry, distTag = 'latest') {
50
69
  if (!entry || !allowed(entry.packageName) || !isPackageVersion(entry.currentVersion)
51
70
  || !isPackageVersion(entry.latestVersion) || !entry.registry
52
71
  || normalizeVersionRegistry(entry.registry) !== entry.registry
53
- || (registry !== undefined && entry.registry !== registry))
72
+ || (registry !== undefined && entry.registry !== registry)
73
+ || (entry.distTag ?? 'latest') !== distTag)
54
74
  return false;
55
75
  const checked = timestamp(entry.checkedAt);
56
76
  const observed = timestamp(entry.currentVersionObservedAt);
@@ -69,7 +89,9 @@ export async function mergePackageVersionInfo(patch) {
69
89
  const file = filePath();
70
90
  return withFileLock(writeLockPath(), async () => {
71
91
  const data = readPackageVersionInfo();
72
- const previous = data.packages[patch.packageName];
92
+ const key = patch.registry ? snapshotKey(patch.packageName, patch.registry, patch.distTag ?? 'latest') : undefined;
93
+ const previous = key ? data.snapshots?.[key] ?? ((data.packages[patch.packageName]?.distTag ?? 'latest') === (patch.distTag ?? 'latest')
94
+ && data.packages[patch.packageName]?.registry === patch.registry ? data.packages[patch.packageName] : undefined) : data.packages[patch.packageName];
73
95
  const entry = previous ? { ...previous } : {
74
96
  packageName: patch.packageName, currentVersion: null, lastAttemptStatus: patch.lastAttemptStatus,
75
97
  };
@@ -83,6 +105,8 @@ export async function mergePackageVersionInfo(patch) {
83
105
  entry.latestVersion = patch.latestVersion;
84
106
  entry.checkedAt = patch.checkedAt;
85
107
  entry.registry = patch.registry;
108
+ entry.distTag = patch.distTag ?? 'latest';
109
+ entry.npmConfig = patch.npmConfig;
86
110
  }
87
111
  if (newer(patch.lastAttemptAt, entry.lastAttemptAt)) {
88
112
  entry.lastAttemptStatus = patch.lastAttemptStatus;
@@ -95,6 +119,15 @@ export async function mergePackageVersionInfo(patch) {
95
119
  if (patch.lastUpgrade && newer(patch.lastUpgrade.at, entry.lastUpgrade?.at))
96
120
  entry.lastUpgrade = patch.lastUpgrade;
97
121
  data.packages[patch.packageName] = entry;
122
+ if (key) {
123
+ entry.registry = patch.registry;
124
+ entry.distTag = patch.distTag ?? 'latest';
125
+ data.snapshots ??= {};
126
+ data.snapshots[key] = entry;
127
+ const keys = Object.keys(data.snapshots);
128
+ for (const expired of keys.sort((left, right) => timestamp(data.snapshots[left].lastAttemptAt) - timestamp(data.snapshots[right].lastAttemptAt)).slice(0, Math.max(0, keys.length - 48)))
129
+ delete data.snapshots[expired];
130
+ }
98
131
  await atomicReplaceJson(file, data);
99
132
  return entry;
100
133
  });
@@ -115,8 +148,8 @@ export function resolveVersionInfoPackage(name) {
115
148
  return null;
116
149
  }
117
150
  }
118
- function hasUpdate(entry, registry, now = Date.now()) {
119
- return isPackageVersionInfoFresh(entry, now, VERSION_INFO_TTL_MS, registry)
151
+ function hasUpdate(entry, registry, now = Date.now(), distTag = 'latest') {
152
+ return isPackageVersionInfoFresh(entry, now, VERSION_INFO_TTL_MS, registry, distTag)
120
153
  && comparePackageVersions(entry.currentVersion, entry.latestVersion) < 0;
121
154
  }
122
155
  async function checkPackage(name, force, options) {
@@ -124,37 +157,44 @@ async function checkPackage(name, force, options) {
124
157
  throw new Error('Unmanaged package');
125
158
  const now = options.now ?? Date.now();
126
159
  const at = new Date(now).toISOString();
127
- const old = (await readPackageVersionInfoConsistent()).packages[name];
160
+ const distTag = options.distTag ?? resolveUpgradeDistTag();
161
+ const selection = options.selection ?? (isLinkedInstall() && !options.includeUninstalled
162
+ ? linkedInstallSelection
163
+ : await resolveUpgradeRegistry(options.signal));
164
+ const registry = normalizeVersionRegistry(selection.registry);
165
+ const data = await readPackageVersionInfoConsistent();
166
+ const legacy = data.packages[name];
167
+ const old = registry ? data.snapshots?.[snapshotKey(name, registry, distTag)]
168
+ ?? (legacy?.registry === registry && (legacy.distTag ?? 'latest') === distTag ? legacy : undefined) : undefined;
128
169
  const observed = {
129
170
  packageName: name, currentVersion: null, currentVersionObservedAt: at,
130
171
  lastAttemptStatus: 'unavailable',
172
+ distTag, npmConfig: selection.npmConfig, ...(registry ? { registry } : {}),
131
173
  };
132
174
  const log = (message) => options.log?.(`[${options.source ?? 'version-check'}] ${name}: ${message}`);
133
175
  let source = 'refresh';
134
176
  try {
135
177
  const linked = isLinkedInstall();
136
- const local = linked && name !== MAIN_PACKAGE_NAME ? null : resolveVersionInfoPackage(name);
178
+ const local = resolveVersionInfoPackage(name);
137
179
  if (local && !isPackageVersion(local.version))
138
180
  throw new Error('Invalid installed package version');
139
181
  observed.currentVersion = local?.version ?? null;
140
- if (linked)
182
+ if (linked && !options.includeUninstalled)
141
183
  observed.lastAttemptStatus = 'skipped';
142
- else if (!local)
184
+ else if (!local && !options.includeUninstalled)
143
185
  observed.lastAttemptStatus = 'not-installed';
144
186
  else {
145
- const selection = options.selection ?? await resolveUpgradeRegistry(options.signal);
146
- const registry = normalizeVersionRegistry(selection.registry);
147
187
  if (!registry)
148
188
  throw new Error('Registry configuration is not safe for version caching');
149
- if (!force && isPackageVersionInfoFresh(old, now, VERSION_INFO_TTL_MS, registry)) {
189
+ if (!force && isPackageVersionInfoFresh(old, now, VERSION_INFO_TTL_MS, registry, distTag)) {
150
190
  source = 'cache';
151
191
  observed.lastAttemptStatus = 'ok';
152
192
  const entry = await mergePackageVersionInfo(observed);
153
193
  log(`Using cached version information (age ${Math.floor((now - timestamp(old.checkedAt)) / 60000)}m)`);
154
- return { packageName: name, status: 'ok', source, entry, hasUpdate: hasUpdate(entry, registry, now) };
194
+ return { packageName: name, status: 'ok', source, entry, hasUpdate: hasUpdate(entry, registry, now, distTag), npmConfig: selection.npmConfig };
155
195
  }
156
196
  log(force ? 'Refreshing version information' : 'Version information missing/invalid/expired; refreshing');
157
- const checked = await queryPackageVersion(name, selection, options.signal);
197
+ const checked = await queryPackageVersion(name, selection, options.signal, distTag);
158
198
  if (!checked)
159
199
  throw new Error('Version registry unavailable or returned an invalid version');
160
200
  observed.latestVersion = checked.version;
@@ -167,7 +207,7 @@ async function checkPackage(name, force, options) {
167
207
  }
168
208
  catch {
169
209
  observed.lastAttemptStatus = 'unavailable';
170
- observed.lastError = 'Version refresh failed; continuing with current version';
210
+ observed.lastError = `Cannot resolve ${name}@${distTag}; registry unavailable, tag missing or invalid version`;
171
211
  log(observed.lastError);
172
212
  }
173
213
  observed.lastAttemptAt = at;
@@ -181,20 +221,25 @@ async function checkPackage(name, force, options) {
181
221
  }
182
222
  return {
183
223
  packageName: name, source, entry, status: observed.lastAttemptStatus,
184
- hasUpdate: observed.lastAttemptStatus === 'ok' && hasUpdate(entry, observed.registry, now),
224
+ hasUpdate: observed.lastAttemptStatus === 'ok' && hasUpdate(entry, observed.registry, now, distTag),
185
225
  ...(observed.lastError ? { error: observed.lastError } : {}),
226
+ npmConfig: selection.npmConfig,
186
227
  };
187
228
  }
188
229
  export async function refreshPackageVersionInfo(name, options = {}) {
189
230
  return checkPackage(name, true, options);
190
231
  }
191
232
  export async function ensureFreshPackageVersionInfo(names = AUTO_UPGRADE_PACKAGES, options = {}) {
192
- const selection = options.selection ?? (isLinkedInstall() ? undefined : await resolveUpgradeRegistry(options.signal));
193
- return Promise.all(names.map(name => checkPackage(name, false, { ...options, selection })));
233
+ const selection = options.selection ?? (isLinkedInstall() && !options.includeUninstalled
234
+ ? linkedInstallSelection
235
+ : await resolveUpgradeRegistry(options.signal));
236
+ const distTag = options.distTag ?? resolveUpgradeDistTag();
237
+ return Promise.all(names.map(name => checkPackage(name, false, { ...options, selection, distTag })));
194
238
  }
195
239
  export async function refreshManagedPackageVersions(names = AUTO_UPGRADE_PACKAGES, options = {}) {
196
- const selection = options.selection ?? (isLinkedInstall() ? undefined : await resolveUpgradeRegistry(options.signal));
197
- return Promise.all(names.map(name => checkPackage(name, true, { ...options, selection })));
240
+ const selection = options.selection ?? await resolveUpgradeRegistry(options.signal);
241
+ const distTag = options.distTag ?? resolveUpgradeDistTag();
242
+ return Promise.all(names.map(name => checkPackage(name, true, { ...options, selection, distTag })));
198
243
  }
199
244
  export async function recordPackageUpgrade(name, upgrade, log) {
200
245
  try {
@@ -219,14 +264,14 @@ export async function applyCachedPackageUpgrades(names = AUTO_UPGRADE_PACKAGES,
219
264
  if (!AUTO_UPGRADE_PACKAGES.includes(name))
220
265
  throw new Error('Package is not automatically managed');
221
266
  try {
222
- const entry = (await readPackageVersionInfoConsistent(0)).packages[name];
267
+ const entry = options.target ?? (await readPackageVersionInfoConsistent(0)).packages[name];
223
268
  const registry = entry?.registry ? normalizeVersionRegistry(entry.registry) : null;
224
- if (!registry || !isPackageVersionInfoFresh(entry, Date.now(), VERSION_INFO_TTL_MS, registry)) {
269
+ if (!registry || entry.packageName !== name || !isPackageVersionInfoFresh(entry, Date.now(), VERSION_INFO_TTL_MS, registry, options.distTag ?? resolveUpgradeDistTag())) {
225
270
  result = { status: 'skipped', error: 'No valid cached version information' };
226
271
  }
227
272
  else {
228
273
  options.log?.(`Applying cached version information: ${name}@${entry.latestVersion}`);
229
- result = await installExactPackageVersion(name, entry.latestVersion, registry, () => resolveVersionInfoPackage(name));
274
+ result = await installExactPackageVersion(name, entry.latestVersion, registry, () => resolveVersionInfoPackage(name), options.npmConfig ?? entry.npmConfig);
230
275
  }
231
276
  }
232
277
  catch {