Skip to content

autoclaw 0.2.16: the usage card names the campaign points and the check-in stops burning rotations on quota 403 - #774

Open
yunwang2442-creator wants to merge 2 commits into
magpie-community:mainfrom
yunwang2442-creator:autoclaw/campaign-points
Open

yunwang2442-creator wants to merge 2 commits into
magpie-community:mainfrom
yunwang2442-creator:autoclaw/campaign-points

Conversation

@yunwang2442-creator

@yunwang2442-creator yunwang2442-creator commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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:

剩 4,000(AutoClaw Weekly Perks) · 已领 14,000 · 过期 7,711(近期)
  • 剩 is exact, not an estimate. The ledger's campaign credit/debit/expire entries each carry wallet_scope and wallet_id (2026-11-06 live probe), and the server opens one wallet instance per activity — so remaining is computed per instance as credit − 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.
  • Data: 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 by wallet_scope=campaign.
  • The window is the server's ledger cap (~151 entries ≈ 18 days; pagination params are inert), so 已领/过期 carry (近期). A campaign's credit falling out of the window leaves its instance at 0 (its expiry still shows) — live campaigns are ≤7 days, so this is conservative, never wrong.
  • Cached per account for 10 minutes (the codearts CREDIT_BATCHES_TTL_MS pattern), invalidated when check-in's user-info reaches 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 carries balance: a window balance is a prepaid routing pool (magpie #3950) and would double-count what 积分余额 already reports.
  • Numbers are grouped in threes by a deterministic formatter (no toLocaleString — locale-independent for CI).

And stops the check-in from burning rotations / mis-marking accounts. Findings from the review of #774:

  • The throw from dailySignin (401/403 at task-complete) escaped maybeSignin out of auth.refresh and usage(): the refresh token had already rotated, so magpie's renewLocked marked the account expired and the new tokens were lost. Fixed: dailySignin no longer throws (returns null for non-2xx; a quota 403 is treated like the chat path — QUOTA_CODES/QUOTA_WORDS — with the sign-in kept).
  • The mutated dailySignin throw was swallowed by checkin'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.
  • CAS write-back: a logged-out or non-oauth record counted as "unchanged" and was overwritten by the rotation. Now only a same-oauth-same-refresh-token record is written; a logged-out account is never resurrected (its tokens live on in the in-memory rotation memo).
  • A quota-exhausted 403 at task-list (810000 / the chat path's QUOTA_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.
  • A second 401 right after a successful renewal threw signIn: expired — signing out an account that had just renewed. It degrades to a retryable failure (no more transient: true; it's never read).
  • 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 hold the task (#3326).
  • task-complete now reads its HTTP status: non-2xx returns null (failed → 30 分钟后重试), cap-and-ban 403 messages surface on the card instead of a silent null.
  • user-info reaching the server requires res.ok && code === 0 (a 401 with a code: 0 body was a false "reached"), and 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 fall back to os.homedir(): on Windows HOME is unset, so the usage-state file landed at C:\.config\magpie\ — outside the user profile, readable by every local account.
  • the invented +200 on 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 (no balance on the row, client-side re-filter, per-instance debit accounting, CAS logout, quota 403, renewal degradation, cache invalidation, fake code:0 success, grouping formatter) was mutation-tested red — each production break turns at least one test red.
  • bun scripts/check.mjs: pass.
  • Live, read-only, five accounts on Windows: the row renders as specified (剩 4,000(AutoClaw Weekly Perks) · 已领 14,000 · 过期 7,711(近期) intl; 剩 5,000(AutoClaw 用户登录奖励) · 已领 5,000 · 过期 3,637(近期) CN), check-in is idempotent done with zero refresh calls.
  • macOS: untested — no machine available.

Version bump to 0.2.16 is required for publish.yml (paths: packages/*/package.json) to ship to the dist branch and npm.

…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 yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.refresh throws 登录已失效(HTTP 401/403) with signIn: "expired";
  • magpie's renewLocked (host.js) reads that signIn and marks the account expired;
  • the fresh tokens are never returned, so they are never saved (and auth.refresh doesn't rememberRotated them 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, then threw 登录已失效(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: true on 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 to USERPROFILE and homedir() (reverted to ${HOME ?? ""}). This is the Windows fix the body describes;
  • refreshShared's check for an expired memo (reverted to always return memo);
  • refreshTokens trying 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, but fetchCampaignLedger resolves null on failure and that null is cached for 10 minutes. The failed-read test calls clearCampaignLedger() 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 refreshShared says "新 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.

@yunwang2442-creator yunwang2442-creator changed the title autoclaw 0.2.16: the usage card names the campaign points, and the check-in stops marking healthy accounts expired autoclaw 0.2.16: the usage card names the campaign points and the check-in stops burning rotations on quota 403 Nov 6, 2026

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-779 still throws signIn: "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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants