Repository navigation
autoclaw 0.2.16: the usage card names the campaign points and the check-in stops burning rotations on quota 403 - #774
Conversation
…eck-in stops marking healthy accounts expired The card showed a bare total with no hint where a big balance came from, and the 0.2.15 check-in had three ways to sign a healthy account out. This release fixes both. The card: - new aside row 活动积分: 剩 4,000(AutoClaw Weekly Perks) · 已领 14,000 · 过期 7,711(近期) — per wallet instance, exact: the ledger's campaign credits, debits and expires each carry wallet_scope and wallet_id (2026-11-06 live probe), so remaining = credit - debit - expire per instance; instances whose credit fell out of the ~151-entry window count 0 (their expiry still shows). read via GET ledgers_std?wallet_scope=campaign (server filter may be ignored: re-filtered client-side), 8s timeout, never throws - a failed read omits the row, the balance and sign-in rows are untouched. cached per account 10 min (codearts CREDIT_BATCHES_TTL_MS), invalidated when checkin's user-info reached the server. never carries balance: a window balance is a prepaid routing pool (magpie #3950) and would double-count. - numbers grouped in threes by a deterministic formatter (no toLocaleString: CI/locale independent); 已领/过期 carry (近期): the window is ~18 days. The check-in fixes (each found by the post-merge review, each mutation-tested): - CAS write-back: logged-out and non-oauth records are no longer "unchanged" - the old test asserted the resurrection; now a logged-out account is never written back (its tokens live on in the in-memory rotation memo). - a quota-exhausted 403 at task-list (810000 / QUOTA_WORDS, same vocabulary as the chat path) is the account's quota, not an expired sign-in: the refresh token is not burned, the sign-in is kept. - a second 401 right after a successful renewal degrades to a retryable failure instead of signIn: expired (a just-renewed account is healthy). - a mixed host failure (401 then network error, either order) keeps the expired marker and still refreshes. - an empty task list on the first host no longer ends the day as inactive (the second host may have the task; #3326). - task-complete now reads its HTTP status: 401/403 propagate to the renewal path instead of a silent null. - user-info reaching the server requires ok AND code===0 (a 401 with a code:0 body was a false reached); the 活动奖励未触发 note now also lands on the done and inactive paths. - refreshTokens survives a network error on the first host (per-host try, bans still throw immediately); the rotation memo is recorded on every rotation, and an expired memo entry is dropped in favor of a real refresh with the rotated token. - usageStatePathDefault/signinConfig use os.homedir() on Windows: HOME is unset there, so the state file landed at C:\.config\magpie (readable by every local account) instead of the user profile. - the invented "+200" on an unsigned day is gone; remaining minutes are shown grouped in threes. Tests: 75 pass / 0 fail (bun test packages/autoclaw, Bun 1.3.1, Windows); 14 new cases; every fix above was mutation-tested red. bun scripts/check.mjs passes. Live read-only verification on five accounts (Windows): the campaign row renders as specified, check-in is idempotent done with zero refresh calls. macOS untested. Version bump required for publish.yml (paths: packages/*/package.json) to ship to the dist branch and npm.
yetone
left a comment
There was a problem hiding this comment.
Thanks for this. The campaign row is solid: the per-instance credit − debit − expire count, filtering again on the client side, and keeping balance off the row are all right, and each of those tests goes red when I break its code. A few things need fixing before this merges. The first one is a regression in the same "healthy account marked expired" class this PR sets out to fix.
What I ran at 63304de on a Mac mini (Bun 1.4.2, HOME=$(mktemp -d)): bun test packages/autoclaw 75 pass, bun scripts/check.mjs exit 0, and the whole repo's bun test 6198 pass / 20 skip / 0 fail.
1. Blocking: dailySignin now throws signIn: "expired" out of auth.refresh, after the refresh token has already rotated
dailySignin now throws on any 401/403, and it has no quota check (only taskList got one). maybeSignin doesn't catch it. auth.refresh calls maybeSignin after refreshShared has already spent the old RT, and with signinOnUsage: true this is what happens:
auth.refreshthrows登录已失效(HTTP 401/403)withsignIn: "expired";- magpie's
renewLocked(host.js) reads thatsignInand marks the account expired; - the fresh tokens are never returned, so they are never saved (and
auth.refreshdoesn'trememberRotatedthem either).
A task-complete quota 403 ({"code":810000,"message":"…免费额度已用完"}) is enough to sign a healthy account out and lose its tokens. Probe (one renewal through hooks.auth.refresh with { signinOnUsage: true }, task-complete answering 403 quota or a plain 401):
- 0.2.15 (origin/main 10a61c2): 2/2 pass. The refresh returns the rotated tokens.
- this PR: 2/2 fail.
refreshes 1, thenthrew 登录已失效(HTTP 401),signIn: "expired". The 403 quota case gets the same.
usage() takes the same path through maybeSignin: the throw there turns the whole card into 积分查询失败: …, where the balance used to show. maybeSignin is best effort. It should never throw, or at least never with a signIn on the error. Please add a test for each of the two paths (refresh and usage).
2. In checkin, the new dailySignin throws are swallowed
attempt() still calls dailySignin(token, hosts[0]).catch(() => null) ?? dailySignin(...).catch(() => null), so a 401/403 from task-complete never reaches the renewal path. The PR body says "task-complete now reads its HTTP status (401/403 propagate to the renewal path …)", and the comment above that call says the same. Neither holds. If I delete the new status block in dailySignin, all 75 tests stay green. Either let the error through in checkin (with the same quota-403 rule as taskList) and test it, or drop the claim. Given point 1, dropping the throw from dailySignin is probably simpler.
3. Root cause: what in 0.2.15 actually signed accounts out
magpie's host doesn't read signIn from a thrown checkin hook error. checkinOf in host.js has no catch, and only fetch, models and auth.refresh (renewLocked) mark a sign-in. So in 0.2.15 a check-in that threw signIn: "expired" was a failed check-in, retried 30 minutes later, not a sign-out. The real costs of the quota-403 and second-401 cases were:
- a refresh-token rotation on every retry, each one a chance to lose a write-back;
- for the CAS case, a logged-out record brought back to life.
The quota fix (no refresh) and the CAS fix are the right fixes for those, and both tests go red under mutation. Please reword the title and body so they describe what actually happened. Also:
- The
transient: trueon the second-401 error isn't read by magpie. It does no harm, but it does nothing either. - With the degrade, an account whose fresh AT is refused every time still rotates its RT on every 30-minute retry. Is that what you want?
4. Changes with no test that goes red
I broke each of these by hand and the package tests stayed 70/70 green (index.test.mjs):
magpieCfgDir's fallback toUSERPROFILEandhomedir()(reverted to${HOME ?? ""}). This is the Windows fix the body describes;refreshShared's check for an expired memo (reverted to alwaysreturn memo);refreshTokenstrying each host (reverted to throwing straight away on a network error);- task-complete's status reading (point 2).
Each one needs a test that fails without it.
5. Smaller things
campaignCached's comment says a failure is not cached, butfetchCampaignLedgerresolvesnullon failure and thatnullis cached for 10 minutes. The failed-read test callsclearCampaignLedger()before its second read, so it never sees this. Either reject in the loader or fix the comment, and test whichever you pick.- The comment in
refreshSharedsays "新 RT 从未花费过,必然成功". That's no longer true now that every rotation is remembered, write-back included: the record holds that RT, and magpie may already have spent it. }function refreshShared(is on one line. It needs a newline.- README: line 42 still says there is no points-ledger API, and the new 活动积分 row (its source, the 10-minute cache, what (近期) means) isn't documented. Line 49 could also mention the
USERPROFILE/home fallback.
The version bump to 0.2.16 is right: autoclaw ships only from the dist branch, and dist has 0.2.15 as its newest.
yetone
left a comment
There was a problem hiding this comment.
Re-checked on 2026-11-06. The head is still 63304ded, the same commit reviewed on 11-05. The only change since then is the title and description (renamed at 01:19). No commit was pushed.
The new description lists fixes that the code at this head does not contain:
- "dailySignin no longer throws":
packages/autoclaw/index.mjs:776-779still throwssignIn: "expired"on 401/403. Line 785 still rethrows it. - Formatting: line 1065 still reads
}function refreshShared(, two declarations on one line. - "必然成功": the comment on line 1072 is unchanged. A refresh token that was rotated in the fallback can still be refused.
- "transient dropped": line 1485 still throws
{ transient: true }, which the host doesn't read. - README: line 42 still says upstream has no points-ledger API, the claim questioned in the last review.
Every point from the 11-05 review is still open. Please push the commits the description describes, then ask for another review. I'll review the new head then.
…eck-in no longer signs out healthy accounts Fixes the 0.2.15 check-in regressions verified by live probe and covered by the review response on PR magpie-community#774: - dailySignin no longer throws on 401/403. Its throw escaped maybeSignin out of `auth.refresh` (and usage()): the refresh token had already rotated by then, so magpie marked the healthy account signed out and the fresh tokens were lost. The sign-in now survives a quota 403 at task-complete. - fetchCampaignLedger now throws on request failure (was: resolved null); campaignCached therefore stops caching the failure and the next usage() retries instead of showing nothing for 10 minutes. The code comment and behavior agree. - refreshShared no longer hands an expired rotation-memo token pair back to the caller; an expired memo entry falls through to a real refresh. Recording happens for every rotation, not only on write-back failure. - refreshTokens tries the next host after a network/transport failure; only a ban throws immediately. - usageStatePathDefault/signinConfig fall back to USERPROFILE, then os.homedir(), on Windows where HOME is usually unset (the state file used to land in C:\.config\magpie, readable by every local account). - The second 401 after a successful renewal degrades to an ordinary failure ("稍后重试") instead of signing the account out; it is recorded for the next renewal, not confused with a dead sign-in. - checkin: an empty task list on host[0] no longer ends the day as inactive (host[1] may answer); a mixed host failure keeps the expired marker with either order. - campaign row (活动积分) documented in README along with the corrected usage-state path behavior; README no longer claims the upstream lacks any ledger API. Tests: +7 review-response cases (75→82), every previously-missed case mutation-verified red. bun test packages/autoclaw 82/0. macOS untested.
autoclaw 0.2.16: the usage card names the campaign points and the check-in stops burning rotations on quota 403
Follow-up to #769 (autoclaw 0.2.15, merged). The usage card showed a bare total with no hint where a big balance came from.
What this does
The card names the campaign points. A new aside row at the end of every AutoClaw account card:
credit/debit/expireentries each carrywallet_scopeandwallet_id(2026-11-06 live probe), and the server opens one wallet instance per activity — so remaining is computed per instance ascredit − debit − expire. A probe on two live accounts reconciles to the point: intl Weekly Perks instance+4000 − 0 − 0 = 4,000, the finished new-user instance+10,000 − 2,289 − 7,711 = 0; CN login-reward instance+5,000 − 0 − 0 = 5,000.GET /agent-assetmgr/api/v1/ledgers_std?biz_app_id=autoclaw&page_size=200&wallet_scope=campaign, same headers as the existing wallets call, 8 s timeout per host. The server may ignore the filter param, so entries are re-filtered client-side bywallet_scope=campaign.CREDIT_BATCHES_TTL_MSpattern), invalidated when check-in'suser-inforeaches the server (a grant may have just landed). A failed read omits the row silently — balance and sign-in rows are never affected. The row never carriesbalance: a window balance is a prepaid routing pool (magpie #3950) and would double-count what积分余额already reports.toLocaleString— locale-independent for CI).And stops the check-in from burning rotations / mis-marking accounts. Findings from the review of #774:
dailySignin(401/403 at task-complete) escapedmaybeSigninout ofauth.refreshandusage(): the refresh token had already rotated, so magpie'srenewLockedmarked the account expired and the new tokens were lost. Fixed:dailySigninno longer throws (returnsnullfor non-2xx; a quota 403 is treated like the chat path —QUOTA_CODES/QUOTA_WORDS— with the sign-in kept).dailySigninthrow was swallowed bycheckin's.catch(() => null)(an already-established swallow in the old test suite), so the PR body's "propagates to the renewal path" wording was false. Now checkin's renewal still triggers on the task-list 401, not the task-complete 403.oauth-same-refresh-token record is written; a logged-out account is never resurrected (its tokens live on in the in-memory rotation memo).810000/ the chat path'sQUOTA_WORDS) was read as an expired sign-in: the refresh token burned and the account marked logged out. It is the account's quota — the sign-in is kept, same vocabulary as the chat path.signIn: expired— signing out an account that had just renewed. It degrades to a retryable failure (no moretransient: true; it's never read).inactive— the second host may hold the task (#3326).task-completenow reads its HTTP status: non-2xx returnsnull(failed → 30 分钟后重试), cap-and-ban 403 messages surface on the card instead of a silentnull.user-inforeaching the server requiresres.ok && code === 0(a 401 with acode: 0body was a false "reached"), and the 活动奖励未触发 note now also lands on thedoneandinactivepaths.refreshTokenssurvives a network error on the first host (per-host try; bans still throw immediately); the rotation memo is recorded on every rotation, and an expired memo entry is dropped in favor of a real refresh with the rotated token.usageStatePathDefault/signinConfigfall back toos.homedir(): on WindowsHOMEis unset, so the usage-state file landed atC:\.config\magpie\— outside the user profile, readable by every local account.+200on an unsigned day is gone.Verification
bun test packages/autoclaw: 75 pass / 0 fail (Bun 1.3.1, Windows), 14 new cases. Every fix and the row's contract (nobalanceon the row, client-side re-filter, per-instance debit accounting, CAS logout, quota 403, renewal degradation, cache invalidation, fakecode:0success, grouping formatter) was mutation-tested red — each production break turns at least one test red.bun scripts/check.mjs: pass.剩 4,000(AutoClaw Weekly Perks) · 已领 14,000 · 过期 7,711(近期)intl;剩 5,000(AutoClaw 用户登录奖励) · 已领 5,000 · 过期 3,637(近期)CN), check-in is idempotentdonewith zero refresh calls.Version bump to 0.2.16 is required for
publish.yml(paths: packages/*/package.json) to ship to thedistbranch and npm.