Hermes Agent Wiki 非公式・日本語wiki

課金のライフサイクル: クライアント側の状態、エラー、回復

目次

このページは、ゲートウェイが(NAS から)返す billing.*/subscription.* のあらゆる状態の形を ターミナルが実際にどう表示するかに対応づけ、さらに型付きの拒否・エラーコードそれぞれを、 ユーザーに見せる正確な文言と回復手順に対応づけたものです。保証していることは次のとおりです。 NAS の課金状態も型付きの拒否も、汎用のトースト通知に流れ込むことはありません。以下のケースはすべて、 ui-tui/src/app/slash/commands/topup.ts、ui-tui/src/components/billingOverlay.tsx、 ui-tui/src/components/subscriptionOverlay.tsx のいずれかにある明示的な分岐です。未知のコードでも 表示は穏やかに縮退します。クラッシュしたり拒否を黙って捨てたりせず、default 分岐に入り、 サーバーのペイロードから取り出した汎用ながら実のあるメッセージを表示します(空のトーストにはなりません)。

1. billing.state の形 → 表示

出典: ui-tui/src/components/billingOverlay.tsx(OverviewScreen、 BuyScreen、AutoReloadScreen)、ui-tui/src/app/slash/commands/topup.ts(/topup の実行)。

状態の形 表示
ログアウト状態(s.logged_in === false) オーバーレイは開きません。sys: 💳 Not logged into Nous Portal — run /portal to log in, then /topup.
billing.state の RPC 取得に失敗(通信エラー・タイムアウト) fail-closed: .catch(ctx.guardedErr) により、オーバーレイは開かず、状態を推測もしません。sys: error: <message or "request failed">。「カードなし」など推測した状態は決して表示しません。ユーザーは /topup をやり直す必要があります。
card: null(保存済みカードなし)、フルメニュー(is_admin && cli_billing_enabled) 概要画面に No saved card on file — "Add funds" walks you through adding one. と表示します。「Add funds」はカード追加の流れを開きます: Add a card on the portal / I've added it — check again / Back(金額選択は開きません。開くと no_payment_method で 403 になるためです)。
card あり、resolved_via が設定済み カードの出どころを反映した display フィールドを使い、Card: {display} と表示します(例: Visa ····4242 — the card on your subscription)。
card あり、resolved_via なし(古い NAS) 汎用の Card: {masked} に切り替わります。確認画面には Your card saved on the portal will be charged. が加わります。
auto_reload: null 自動チャージの行はまったく表示しません(autoReloadLine が null を返す)。この機能は表に出ません。
auto_reload.card.kind: 'canonical' 別カードの警告は出しません。カードの行は登録済みのカードの表示になります。
auto_reload.card.kind: 'distinct' 自動チャージ画面に ⚠ Auto-refill is charging {brand} ••{last4} — not your card on file. を表示します(食い違いの通知)。
auto_reload.card.kind: 'none' 表示は canonical と同じで、別カードの警告は出しません。
monthly_cap あり、limit_usd != null {spent_display} of {limit_display} used this month(is_default_ceiling のときに限り (default ceiling) を付加)。
monthly_cap なし、または limit_usd == null No monthly cap visible (managed on the portal).
課金権限のないロール(!is_admin、メニューが縮小) 注記: Billing actions need someone with billing permissions (owner, admin, or finance admin). メニューは Manage on portal / Cancel に縮小します。
組織の停止スイッチがオフ(is_admin だが !cli_billing_enabled) 注記: Remote spending is off for this org — a billing admin can turn it on from the portal's Hermes Agent page. メニューは同じく縮小します。

注: full = s.is_admin && s.cli_billing_enabled が制御するのは組織単位の スイッチで、ターミナルごとの billing:manage スコープではありません。スコープは事前チェックではなく 事後に判明し(請求が insufficient_scope で 403 になる)、再開できる 権限昇格の画面へ誘導されます。

2. 拒否コード(renderBillingError、コードに書かれた順)

出典: ui-tui/src/app/slash/commands/topup.ts:37-149 の renderBillingError。 「Portal」の行 = sys('Portal: {portal_url}') は、portal_url があれば、どのコードでも(default を含めて)末尾に付きます。

error コード 文言 ポータル URL retry_after
insufficient_scope This needs Remote Spending allowed. Start a top-up to allow it, then retry. あれば —
remote_spending_revoked(CF-4) `{An admin stopped remote spending for this terminal. \ You stopped remote spending for this terminal.}(actor によって切り替え)Reconnect to restore — run /portal to re-authorize this terminal. あわせて billing` オーバーレイの状態をすぐに消去します(トークンの更新を待ちません)。 あれば —
session_revoked Your session was logged out. Run /portal to log in again. あわせて billing オーバーレイの状態を消去します。 あれば —
cli_billing_disabled / remote_spending_disabled(両方が送られる) Remote spending is off for this account — a billing admin can turn it on from the portal's Hermes Agent page. あれば —
role_required Adding funds needs someone with billing permissions (owner, admin, or finance admin), or manage this on the portal. あれば —
consent_required This action needs a one-time card confirmation and consent step on the portal before it can proceed. あれば —
org_access_denied This token isn't bound to an org you can manage. Sign in with the right org, or manage this on the portal. あれば —
upgrade_cap_exceeded 🔴 Daily plan-change limit reached (5 per org) — try again tomorrow, or manage this on the portal. あれば —
auto_top_up_disabled_failures Auto-reload was turned off after repeated charge failures. Fix the card issue, then re-enable it from /topup → Auto-reload. あれば —
idempotency_conflict 🔴 That charge key was already used for a different amount. Start a fresh top-up. あれば —
no_payment_method 💳 No saved card for terminal charges yet. Set one up on the portal (one-time credit buys don't save a reusable card). あれば —
monthly_cap_exceeded payload.remainingUsd があれば 🔴 Monthly spend cap reached — ${remainingUsd} headroom left.、なければ 🔴 Monthly spend cap reached. あれば —
rate_limited / temporarily_unavailable 🟡 Too many charges right now{ (try again in ~N min)}. This isn't a payment failure. あれば あり。分数は max(1, round(retry_after/60)) で計算
stripe_unavailable 🟡 Stripe is having trouble right now — try again shortly{ (try again in ~N min)}. あれば あり(同じ式)
*default(未知・その他)* `🔴 {message \ \ error \ \ 'Billing request failed.'}`。サーバーが返した内容をそのまま表示し、空のトーストにはしません。 あれば —

3. 請求の確定結果(pollCharge / renderChargeFailed)

出典: pollCharge(ui-tui/src/app/slash/commands/topup.ts:170-258)と renderChargeFailed(:260-290)。ポーリング間隔は2秒、上限は5分 (POLL_INTERVAL_MS=2000、POLL_CAP_MS=5*60*1000)で、終端でないすべての経路 (保留中*と*スロットリング中の両方)に適用されます。そのため 429/503 が続いても、 ポーリングが永遠に続くことはありません。

結果 文言 補足
status: 'settled' ✅ ${amount_usd} added.(金額がなければ ✅ Credits added.) 終端の成功。
status: 'failed'、reason: 'authentication_required' 🔴 Your bank requires verification (3DS). Complete it on the portal to finish this purchase. portalUrl があれば Portal: の行を追加。
status: 'failed'、reason: 'payment_method_expired' 🔴 Your card has expired. Update it on the portal. Portal: の行を追加。
status: 'failed'、reason: 'card_declined' 🔴 Your card was declined. Try another card on the portal. Portal: の行を追加。
status: 'failed'、reason: 'processing_error' 🔴 The charge didn't go through (processing_error). Portal: の行を追加。
status: 'failed'、認識できない・欠けている reason `🔴 The charge didn't go through ({reason \ \ 'processing_error'}).` 同じくポータルへ誘導します。cli.py の _billing_portal_hint と同じ挙動です。
ポーリングのタイムアウト(5分の上限を過ぎても pending) 🟡 Still processing after 5 minutes — this is a timeout, not a failure. Check /topup or the portal shortly. portalUrl があれば Portal: の行を追加。明示的に失敗とは呼びません。
ポーリング中の取り消し(ポーリング中に remote_spending_revoked / session_revoked) 対応する §2 の文言を表示し、そのあとに 🟡 Your last charge's outcome is unconfirmed — check your balance/history before retrying. を付け加えます。 CF-7 ルール4: ポーリング中の取り消し後の 403 はどちらとも取れる(請求がすでに確定しているかもしれない)ため、決して「失敗」とは呼びません。
ポーリング中の 429/503(rate_limited/temporarily_unavailable/stripe_unavailable) エラーは表示しません。retry_after に従って間隔を空け(既定5秒、上限30秒)、5分の上限までポーリングを続け、その後はタイムアウトとして扱います。 支払いの失敗ではありません。
それ以外の !ok な状態確認エラー `🔴 Could not check the charge: {message \ \ error \ \ 'error'}`
通信の途絶(ポーリングの RPC が例外を投げる・reject される) 🟡 Your last charge's outcome is unconfirmed — check your balance/history before retrying.(UNCONFIRMED_CHARGE_MESSAGE) ポーリング中の取り消しと同じく「未確認なので残高を確認」という伝え方です。接続が切れたことを「失敗」と読むことは決してありません。

4. サブスクリプションのプレビュー・保留中の変更・アップグレードの結果

出典: ui-tui/src/components/subscriptionOverlay.tsx の previewAndRoute、applyPendingAndRoute、upgradeResult、 stepUpDenialResult。

プレビューの effect の値(確認画面の内容を決めます):

effect 確認画面の文言 主な操作
charge_now Upgrade to {target}. You will be charged {amount} now (prorated).(月間クレジットの差分と、判定処理が確実にわかる場合はどのカードかを追加) Pay {amount} & upgrade now
scheduled Change to {target} — takes effect {date}. No charge now; you keep your current plan until then. Schedule change to {target}
no_op You are already on {target} — nothing to change. なし(戻るのみ)
blocked {preview.reason}、なければ代わりに That change cannot be made here — manage it on the portal. Manage on portal
プレビューの RPC が null を返す・通信に失敗 結果画面へ直行: Could not preview that change. —
プレビューが !ok、insufficient_scope stepup 画面へ({kind:'preview', tierId}) —
プレビューが !ok、それ以外のエラー errorResult(p) で結果画面へ(`message \ \ error \ \ 'Something went wrong. Try again, or manage on the portal.'`) —

保留中の変更を適用した結果(applyPendingAndRoute):

pending.kind 成功時の文言
cancellation Scheduled — your plan stays active until the end of the billing period, then it cancels. Nothing changes today.
tier_change(ダウングレード・予約) Scheduled — your plan doesn't change today. You keep your current plan until the end of the billing period, then it switches.
upgrade upgradeResult を通して処理(下記)
どの種類でも、変更操作が insufficient_scope 権限昇格へ({kind:'apply'})

アップグレードの status × reason の対応表(upgradeResult。この 順で確認し、reason を status *より先に*確認します):

条件 結果
r === null(請求を伴う経路での通信失敗) Couldn't confirm the upgrade — your card may or may not have been charged. Re-run /subscription to check your plan before trying again. どちらとも取れる状態なので、やみくもな再試行はしません。
reason: 'authentication_required' または reason: 'subscription_payment_intent_requires_action' Please verify your card in the portal to finish this upgrade. → recovery_url。どちらの reason も同じ SCA の文言に対応します。クライアントが status ではなく reason で分岐するのは、#711 より前の NAS が SCA のケースを status: 'payment_failed' と誤ってラベル付けする(区別できる reason がまだ無い)場合でも、はっきりした拒否として読まずに正しい「カードを確認してください」の文言へ誘導するためです。
reason: 'card_declined' Your card was declined — try a different card on the portal. → recovery_url。
ok && status: 'already_on_tier' You are already on {target_tier_name}.(成功)
ok && status: 'upgraded' Upgraded to {target_tier_name}. Your new monthly credits land in a moment. 結果整合性を待つ適用ポーリング(下記)を開始します。
status: 'requires_action'(区別できる reason なし) This upgrade needs extra verification (3DS). Finish it on the portal. → recovery_url。
status: 'payment_failed'(区別できる reason なし) Your card was declined. Update your payment method on the portal and try again. → recovery_url。
それ以外 errorResult(r): `message \ \ error \ \ 'Something went wrong. Try again, or manage on the portal.'`

結果整合性を待つ適用ポーリング(ResultScreen。status: 'upgraded' の後だけ): billing/サブスクリプションの状態を2秒ごと (UPGRADE_CONFIRM_INTERVAL_MS)に最大15回 (UPGRADE_CONFIRM_ATTEMPTS、つまり約30秒)ポーリングし、current.tier_id が 目的のプランに切り替わるのを待ちます。待っている間の画面は Applying… と表示し、時間内に切り替わらなければ Still applying / Your upgrade succeeded and is still applying — refresh in a moment. と表示します。NAS の反映が追いついていないだけで、 アップグレードを失敗として報告し直すことは決してありません。

権限昇格が拒否されたときの文言(stepUpDenialResult、サブスクリプションの流れ):

error 文言
session_revoked Your session expired — run /portal to log in again, then retry the change.
remote_spending_revoked {message}、なければ Remote spending was stopped for this terminal — reconnect from the portal, then retry.
rate_limited Too many attempts — wait a moment, then try again.
その他・未知 {message}、なければ Remote Spending was not allowed — someone with billing permissions (owner, admin, or finance admin) must approve it. You can also make this change on the portal.

権限付与後の再実行中にスコープの拒否が再び起きても、権限昇格の画面には 戻りません(すでにその画面が表示されており、再度差し替えると画面が固まるためです)。 代わりに allowStepUp=false で終端の結果を表示します: Remote Spending still isn’t active for this terminal — the authorization didn’t take. Retry, or make this change on the portal.

テキストモード(CLI)での同等性

cli.py の _show_billing / _billing_overview と _show_subscription / _subscription_overview は、同じ状態の形(残高のタイトル、2本のバーによる ドル建ての利用量、自動チャージの行、カードの行、月間上限)を表示し、 「ログアウト時やポータルの一時的な不具合では fail-open とし、決してクラッシュしない」という規律も共有しています。CLI の /subscription は、対話的な環境にいる有料プランの管理者・オーナーに、ターミナル内で完結する 変更の流れ(プラン選択 → プレビュー → 確認 → 適用。TUI オーバーレイと同等)を提供します。 メンバーや非対話的な環境では、_billing_portal_hint による subscription_manage_url へのディープリンクに 切り替わります。/topup の対話的なモーダル(prompt_toolkit)も同じように TUI オーバーレイに合わせてあり、 非対話的な環境では同じテキストとポータルへのリンクの表示に切り替わり、 入力を求めることはありません。

CLI の画面・状態 挙動(TUI・デスクトップとの同等性)
Free プランで管理者・オーナー、かつ対話的な環境での /subscription _subscription_free_catalog が、TUI と同じ tiers[] データからプランの一覧を表示します。有効な有料プランごとに1行、安い順に、name · $/mo · $credits/mo の形です(月間クレジットはドル建てなので $22 credits/mo と表示し、単なる数字にはしません)。番号で選ぶと、plan=<tier_id> を付けた /manage-subscription のディープリンクが開き、ポータル側で選んだプランが選択済みになります。新規のサブスクリプションには新しいカードが必要なので、できる操作はポータルへの受け渡しだけです(ここでターミナルが請求することはありません)。
CLI が組み立てる管理・購読用のすべての URL subscription_manage_url(state, tier_id=…) は、プランが選ばれたときだけ(Free の一覧)plan=<tier_id> を付けます(名前や slug ではなく、tiers[] の安定した ID)。ポータルがサーバー側で検証し、未知のプランは無視するので、CLI は選択があれば無条件に付け、TUI の ?plan= と同じ挙動になります。org_id を先に、plan を後に出力します。
CLI でのダウングレード 通常の変更はネイティブ・アプリ内で完結します(put_subscription_pending_change による請求なしの予約)。ダウングレードが拒否された場合は汎用の管理 URL を表示することがありますが、plan=<tier_id> は決して付けません。プランを指定したディープリンクは、新規のサブスクリプションとアップグレード専用です。
/topup の概要画面の操作文言 1回限りのチャージと自動の補充を分け、その違いをそれぞれの最初の文で先に伝えます: Add funds now — a single charge, added to your balance today. と Refill when low — charges $X automatically when your balance falls below $Y.(ドル建てだけの /topup 画面では「credits」という語を使いません。「Add funds now」だけで1回限りの意味が伝わります)。自動チャージがオフのときは、自動の行に具体的な金額を載せません。

将来の互換性

上の表に無い error/status/reason のコードは、renderBillingError(§2)の default 分岐か、errorResult/upgradeResult の フォールスルー(§4)に入ります。その場合もサーバー自身の message は表示されます(空にもクラッシュにもなりません)。 専用の文言や型付きの回復手段が無いだけです。 NAS W3 ではカードの健全性を表すコード(card_paused、card_expired、 card_mismatch)が導入されますが、ここではまだ型付けされていません。クライアントの更新で 明示的な分岐が追加されるまでは、未知のコードとして届き、この default の経路へ縮退します。