Skip to content

v0.5.17

Choose a tag to compare

@ggshr9 ggshr9 released this 11 May 15:18
· 445 commits to master since this release
ed34cc3

v0.5.17 — AI provider failure modes: real-user "Not logged in" leak, codex version trap, /reset recovery

Date: 2026-05-11
Tag: v0.5.17 + desktop-v0.5.17
Scope: Patch release driven by a single user-reported screenshot — the WeChat bot was replying with the literal string "Not logged in · Please run /login" as if it were the AI's answer. Excavation surfaced two distinct failure modes plus the absence of any user-facing recovery affordance. Both modes now have structural fixes; users get a recovery surface for the residual cases.
Predecessor: v0.5.16 — four real-user reports, four surgical fixes


TL;DR

Three classes of failure addressed, plus the missing recovery surface:

  1. claude provider leaks the OAuth-stale sentinel as a chat reply — long-running stream-json subprocess holds tokens from startup, the user's interactive claude rotates keychain credentials underneath, the daemon's subprocess can't refresh and emits the binary's "Please run /login" text as assistant content. Verified by inspecting the binary's string table. Provider now intercepts and routes a structured errorCode: 'auth_failed' to the coordinator; coordinator suppresses the leak and self-heals via session release. Covers all four dispatch paths (solo, parallel, chatroom speaker, chatroom moderator).
  2. codex CLI ↔ SDK protocol mismatch silent no-reply — a globally-installed codex CLI at a version different from the bundled SDK silently emits events the SDK can't decode, every dispatch returns empty assistantText, no reply ever reaches the user. The fix in find-codex-binary from v0.5.6 only worked for plugin installs; the desktop installer still hit this. Boot now compares codex --version against the bundled @openai/codex version and refuses to register the provider on mismatch — loud [BOOT] log instead of silent failure.
  3. SessionManager.sweepIdle() was dead config — the method existed but was never called from production paths, so idleEvictMs: 30 * 60_000 did nothing. Wired to a 60 s setInterval. Long turns are protected by an in-flight counter so a mid-stream dispatch isn't killed by the timer.
  4. Recovery affordance/reset (/重置) drops every registered provider's session for the current chat; next message starts fresh. /health ai shows per-provider session age. wechat-cc doctor now reports the installed claude --version and codex --version.

What changed

1. claude auth-fail sentinel interception

  • 7a52114 fix(provider): stop leaking claude "Not logged in" as chat reply + wire idle sweep (PR #32 first commit)
    Provider intercepts assistant text matching Please run \/login or Not logged in and emits { kind: 'error', code: 'auth_failed', message } instead. Coordinator's dispatchSolo branches on summary.errorCode === 'auth_failed' to suppress the fallback-reply path and send one neutral notice (AI 暂时不可用,请在 wechat-cc 桌面端检查并重新连接) throttled to ≤ 1/hour/chat. SessionManager.sweepIdle() finally wired to a 60s setInterval (the method existed but was never called from production paths). .unref() so the timer never holds the event loop.

  • 65660ac fix(p1.5): four review findings — self-heal, in-flight safety, sentinel coverage, codex version gate
    Round-1 review surfaced four real issues. Coordinator now releases the session on auth_failed so busy chats self-heal automatically (without this, a chat that keeps bumping lastUsedAt never goes idle and stays broken for the full window). SessionManager.sweepIdle skips sessions with an in-flight dispatch (long turns no longer killed mid-stream). AUTH_FAIL_RE broadened to catch the split-chunk case where the SDK emits "Not logged in" and "/login" as separate assistant events. Bootstrap refuses to register codex when codex --version ≠ bundled @openai/codex version.

  • 2793404 fix(p1.6): two round-2 review findings — semver prerelease + close-propagates contract
    Round-2 review caught the SEMVER regex silently mangling 0.128.0-rc.1 to 0.128.0. Widened to capture optional -prerelease. Added a regression test pinning the close()-propagates-to-iterator contract that the in-flight counter relies on.

2. /both, /chat, and moderator coverage

  • 94c7bcb fix: plug auth_failed in /both + /chat paths, harden moderator, Tauri bundle probe (PR #33)
    Post-merge audit of PR #32 found only dispatchSolo was reading errorCode. /both (parallel) and /chat (chatroom) had the same gap — provider correctly intercepted the leakage but coordinator didn't release the failing session, so a stale-credential provider stayed cached for 30 min with every dispatch silently producing no reply from that side. The chatroom moderator's own haiku-4-5 call shared the same credential chain and silently degraded to alternation when stale. All four paths (solo, parallel, chatroom speaker, chatroom moderator) now intercept consistently. find-codex-binary derives the macOS .app/Contents/Resources/ path from process.execPath and probes a bundled codex shim first when available — code-level wiring ready for the build-system follow-up that ships @openai/codex inside the Tauri bundle.

3. Admin recovery commands

  • e9698bc feat(admin/doctor): /reset + /health ai in-chat, plus CLI version probe (PR #32 second commit)
    In-chat operator affordances. /reset (and the Chinese alias /重置) iterates the registry and calls sessionManager.release(alias, provider) + sessionStore.delete(alias) so the next dispatch starts from clean state. /health ai lists per-provider session age — pure bookkeeping inspection, no token. wechat-cc doctor adds a probeBinaryVersion helper that runs claude --version and codex --version via spawnSync (3s cap) and surfaces both in the report — gives support flows a one-glance signal for the SDK↔CLI mismatch trap.

4. Acceptance script

  • 2ced13e + 213f2dc test(acceptance): end-to-end harness for the four manual-acceptance checks + typecheck fix
    Self-contained smoke at scripts/acceptance-p0p1.ts. Boots an isolated daemon (does not touch production state), exercises each user-facing path: auth_failed sentinel never leaks; coordinator releases session + sends throttled notice; /reset / /health ai operate against real bootstrap-wired SessionManager; codex version mismatch refusal at boot. PR #33 extended to seven scenarios covering /both, /chat speaker, and chatroom moderator. Not wired into CI — operator-grade smoke for re-verification when refactoring this area.

How to apply

After updating to v0.5.17:

  • You don't need to do anything for the common case. The daemon now auto-recycles a stale session on the next message; most users won't see any user-visible signal of the underlying recovery.
  • If you see ⚠ AI 暂时不可用... — most likely transient; the daemon is mid-recycle. Send /reset if it doesn't resolve on retry, or check wechat-cc doctor for the underlying status.
  • If wechat-cc doctor shows codex at a version different from the bundled SDK — the BOOT log will refuse to register codex. Run npm i -g @openai/codex@<expected-version> (the boot log includes the exact version), or remove the older codex from PATH. Restart the daemon.

Behind the scenes

A look at the iteration depth: this release went through four review rounds (P0 → P1.5 → P1.6 → P1.7) before reaching merge, with one Claude code-reviewer and one Codex independent reviewer agreeing on findings at each step. The cycle surfaced:

  • Round 1 (P0 → P1.5): four issues the P0 fix didn't cover — most critically, the coordinator wasn't releasing the session on auth_failed, so a busy chat could never self-heal (lastUsedAt keeps bumping → sweepIdle never fires).
  • Round 2 (P1.5 → P1.6): SEMVER regex silently mangling prerelease versions, plus a contract-level concern about the in-flight counter that turned out to hold in practice for our providers (regression test pins the contract for future provider rewrites).
  • Round 3 (P1.6 → P1.7): only the solo dispatch path was covered — /both, /chat, and the moderator's own query had the same structural gap.

The "cc-connect reset_on_idle_mins" pattern was the design north star — explicitly chose idle-reset + reactive interception over spawn-per-dispatch after surveying how chenhg5/cc-connect and similar projects on GitHub handle the same class of problem. Keeps warm-start performance; reactive interception + auto-release covers the busy-chat edge case.

What's still pending

  • Desktop dashboard "AI 状态" tile + Reset / Sign-in buttons — separate UI-focused PR. The internal-api routes will land alongside the Vue rendering.
  • Tauri build-config to ship @openai/codex in Resources — the find-codex probe is ready; needs a per-platform bundle entry in tauri.conf.json.
  • WarmQuery pool — only if measured cold-start ever shows up in real usage.
  • haiku-eval empty-response handling — not auth-related; rate-limit/network-empty case currently silently degrades to alternation. Reviewer flagged as a separate class of failure to address later.

Tests

  • 17 new unit tests across the release line, all TDD'd (RED before GREEN, except one tests-after regression-pinning test in P1.6 which is flagged in its commit message).
  • 1654/1655 vitest pass (the 1 send-reply preflight network timeout fails on master too — pre-existing flake, unrelated).
  • typecheck clean.
  • 9/9 playwright e2e green (apps/desktop/playwright).
  • 23/23 acceptance harness assertions pass against the host's live codex 0.125 ↔ SDK 0.128 mismatch.