Skip to content

[bug] checkForUpdate 的缓存短路条件在「已是最新版」时恒为假,导致每次 Stop hook 都执行一次 npm view #671

Closed

Description

@CarlosWonMore

English summary — checkForUpdate() guards its cache with
if (!options?.force && isCacheValid(state.lastUpdateCheck) && state.availableUpdate).
availableUpdate is persisted as null whenever the registry has nothing newer, so that
short-circuit can never fire in the common case, and fetchLatestVersion() —
execFile(npm, ['view', <pkg>, 'version']) — runs on every Stop hook, i.e. once per agent
turn, instead of once per CACHE_TTL_MS (12h).

Verified on main: src/update.ts:422 (the condition is unchanged from 0.24.0).
Two consequences: one npm child process plus one network round-trip per turn; and on the
released 0.24.0 that same call had no windowsHide, which is exactly why it showed up as a
console window on Windows after every turn. #559 added windowsHide there on main, but the
root cause — this condition — is still present, so the npm spawn still happens every turn, just
invisibly now.

Suggested fix: short-circuit on isCacheValid(state.lastUpdateCheck) alone, and return
{ available: false, current, latest: current } when state.availableUpdate is null.

Description

checkForUpdate() 本意是「12 小时内不重复查版本」,但短路条件写成了合取式,把「缓存有效」和「缓存里有新版本」绑在了一起:

// src/update.ts:417-429(main,与 0.24.0 的打包产物一致)
export async function checkForUpdate(options?: { force?: boolean }): Promise<CheckResult> {
  const state = await loadState();
  const current = getCurrentVersion();

  // Use cached result if valid
  if (!options?.force && isCacheValid(state.lastUpdateCheck) && state.availableUpdate) {
    const cmp = compareVersions(current, state.availableUpdate);
    return { available: cmp < 0, current, latest: state.availableUpdate };
  }

  // Fetch latest version from registry
  const latest = await fetchLatestVersion();   // ← 每次都会走到这里
  ...
  await saveState({
    ...state,
    lastUpdateCheck: new Date().toISOString(),
    availableUpdate: available ? latest : null,   // ← 没有新版本时写 null
  });

isCacheValid(state.lastUpdateCheck) 本身已经隐含了「lastUpdateCheck 存在且不超过 TTL」(它的实现对空值返回 false),所以 && state.availableUpdate 是多余的一层,而且方向是反的:

  • 有新版本可升级 → availableUpdate 非空 → 短路生效(此时反而最该提示用户,却可能被缓存住 12 小时);
  • 没有新版本(绝大多数机器、绝大多数时间) → availableUpdate 恒为 null → 短路永不生效 → 每次都真的去查。

CACHE_TTL_MS = 12h 的存在说明这里本意就是要缓存,因此判断这是笔误而非有意设计;不过「负结果该缓存多久」是个产品取舍,留给你们定。

影响

  1. 每轮对话结束都会 spawn 一个 npm 子进程 + 一次 registry 往返。 updateHandler 挂在 Stop hook 上(src/hook-handlers.ts:530,background: true),所以频率 = 每轮回答一次。
  2. 在 0.24.0 上,这正是「每轮弹一次终端窗口」的直接成因。 0.24.0 的 fetchLatestVersion 是
    execFileAsync(npm.cmd, [...npm.args, 'view', pkgName, 'version', `--registry=${...}`],
                  { timeout, encoding: 'utf-8' })   // 没有 windowsHide
    #559(merged 2026-09-15)在 main 上给这一行补了 windowsHide: true,但 0.24.0 发布于 2026-09-14 —— 比该 PR 早一天,之后没有新的稳定版,所以线上装 0.24.0 的 Windows 用户同时吃到「窗口可见」与「每轮都跑」。这条已在下面第 3 节说明。

实测证据

本机 0.24.0 的 ~/.teamai/state.json,在正常使用(不手动执行 teamai 命令)期间被反复改写:

02:03:47.137Z   (一次 Stop hook 之后)
02:11:03.188Z   (又一次 Stop hook 之后)   ← 中间没有调用任何 teamai 命令

给短路条件打上「只按 TTL 判断」的本地补丁后,连续执行两次 teamai update --check:

$ node dist/index.js update --check      # ℹ Already up to date (v0.24.0)
$ node dist/index.js update --check      # ℹ Already up to date (v0.24.0)

修复前 lastUpdateCheck = 2026-09-20T02:11:03.188Z
修复后 lastUpdateCheck = 2026-09-20T02:11:03.188Z   ← 未变,说明已短路、未进入 fetchLatestVersion

建议改法

if (!options?.force && isCacheValid(state.lastUpdateCheck)) {
  if (state.availableUpdate) {
    const cmp = compareVersions(current, state.availableUpdate);
    return { available: cmp < 0, current, latest: state.availableUpdate };
  }
  return { available: false, current, latest: current };
}

如果你们希望「负结果」用更短的 TTL(比如 1 小时)以便更快发现新版本,那就在 saveState 时多记一个标记并给 isCacheValid 传不同的 ttlMs —— 关键是别让 availableUpdate 的取值决定要不要读缓存。

附:为什么这条容易被忽略

fetchLatestVersion() 里的 catch 只 log.error 然后返回 null,调用方拿到 null 后走 return { available: false, current, latest: current } —— 路径很安静,从输出上看不出它每轮都跑过一次。


相关

  • 同一台机器上的另一个 Windows 现象(子进程缺 windowsHide)见配套 issue。
  • #559 fix(update): hide Windows console windows during self-update —— 修了窗口,未修这条根因。

Activity

  1. added a commit that references this issue on Sep 20, 2026
    610ec14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions