Wave 47 — Dim U (Agent Credit Wallet) migration PR — STATE¶
- Date: 2026-05-12 (Wave 47 Phase 2 永遠ループ tick#7)
- Dim: U — Agent Credit Wallet (per
feedback_agent_credit_wallet_design) - Branch:
feat/jpcite_2026_05_12_wave47_dim_u_migration - Worktree:
/tmp/jpcite-w47-dim-u-mig(lane claim:/tmp/jpcite-w47-dim-u-mig.lane) - Base:
origin/main@a557569f7 - PR: filled at push time
Purpose¶
Storage substrate for the Dim U "Agent Credit Wallet" surface backing the ¥3/req call rail. Designed for CFO/CIO budget-predictability:
- Pre-payment (balance_yen prevents unbounded LLM-side spend)
- Auto-topup (auto_topup_threshold + auto_topup_amount)
- Spending alert at 50% / 80% / 100% of monthly_budget_yen
Per feedback_agent_credit_wallet_design. No LLM API call
operator-side (per feedback_no_operator_llm_api); the ETL is pure
SQL aggregation + threshold evaluation.
Files (4 new + 2 manifest edits)¶
| Path | LOC | Role |
|---|---|---|
scripts/migrations/281_credit_wallet.sql |
119 | schema (3 tables + 1 view) |
scripts/migrations/281_credit_wallet_rollback.sql |
26 | rollback (drops only Dim U surface) |
scripts/etl/process_credit_wallet_alerts.py |
168 | hourly cron — 50/80/100 pct firing |
tests/test_dim_u_credit_wallet.py |
357 | 24 cases (mig + ETL + thresholds + LLM-0) |
scripts/migrations/jpcite_boot_manifest.txt |
+14 | register 281 |
scripts/migrations/autonomath_boot_manifest.txt |
+14 | register 281 mirror |
Schema (migration 281)¶
am_credit_wallet¶
One row per agent owner (token-hashed via sha256, raw never stored).
| Column | Type | Notes |
|---|---|---|
wallet_id |
INTEGER PK AUTOINCREMENT | |
owner_token_hash |
TEXT NOT NULL | UNIQUE, sha256 hex (length=64) |
balance_yen |
INTEGER NOT NULL DEFAULT 0 | >= 0 CHECK |
auto_topup_threshold |
INTEGER NOT NULL DEFAULT 0 | trigger threshold (¥) |
auto_topup_amount |
INTEGER NOT NULL DEFAULT 0 | top-up size (¥) |
monthly_budget_yen |
INTEGER NOT NULL DEFAULT 0 | soft cap for alerts (0 = disabled) |
enabled |
INTEGER NOT NULL DEFAULT 1 | 0/1 CHECK |
created_at, updated_at |
TEXT NOT NULL | ISO8601 strftime default |
am_credit_transaction_log (append-only ledger)¶
| Column | Type | Notes |
|---|---|---|
txn_id |
INTEGER PK AUTOINCREMENT | |
wallet_id |
INTEGER NOT NULL | FK -> am_credit_wallet(wallet_id) |
amount_yen |
INTEGER NOT NULL | sign-rule CHECK |
txn_type |
TEXT NOT NULL | enum: topup | charge | refund |
occurred_at |
TEXT NOT NULL | ISO8601 strftime default |
note |
TEXT | optional human note |
Sign-rule CHECK:
- topup → amount_yen > 0
- refund → amount_yen > 0
- charge → amount_yen < 0
am_credit_spending_alert¶
| Column | Type | Notes |
|---|---|---|
alert_id |
INTEGER PK AUTOINCREMENT | |
wallet_id |
INTEGER NOT NULL | FK -> am_credit_wallet(wallet_id) |
threshold_pct |
INTEGER NOT NULL | enum: 50 | 80 | 100 |
billing_cycle |
TEXT NOT NULL | 'YYYY-MM' (length=7) |
fired_at |
TEXT NOT NULL | ISO8601 strftime default |
spent_yen, budget_yen |
INTEGER NOT NULL | snapshot at firing |
UNIQUE (wallet_id, threshold_pct, billing_cycle) — guarantees a single alert per threshold per cycle. The ETL relies on this for natural idempotency (INSERT OR IGNORE pattern).
v_credit_wallet_topup_due (helper view)¶
Wallets where enabled = 1 AND auto_topup_threshold > 0 AND
auto_topup_amount > 0 AND balance_yen < auto_topup_threshold.
Used by a downstream operator script (out of scope for this PR) to
trigger Stripe top-up charges.
ETL process_credit_wallet_alerts.py (hourly)¶
For each enabled = 1 wallet with monthly_budget_yen > 0:
1. Compute spent_yen = |Σ charge.amount_yen WHERE substr(occurred_at,1,7) = cycle|.
2. Compute pct = spent * 100 / budget.
3. For each threshold in (50, 80, 100):
- If pct >= threshold, attempt INSERT OR IGNORE INTO am_credit_spending_alert.
- New rowcount = 1 → alert just fired; emit in JSON report.
- Duplicate (already fired this cycle) → skip silently.
4. --cycle YYYY-MM overrides the current UTC month bucket.
5. --dry-run reports what would fire without writing.
Re-running within the same cycle = no-op (UNIQUE constraint).
In the next cycle, fresh alerts fire because billing_cycle differs.
3-threshold smoke (verified offline)¶
WALLET budget=¥10,000, charge=¥12,000 (120%), cycle=2026-05
ETL run → alerts_fired = [
{threshold_pct: 50, spent: 12000, budget: 10000},
{threshold_pct: 80, spent: 12000, budget: 10000},
{threshold_pct: 100, spent: 12000, budget: 10000}
]
ETL re-run (same cycle) → alerts_fired = [] # idempotent
Test plan — 24 cases (all green via pytest)¶
- mig applies + idempotent re-apply (2)
- rollback drops all artefacts (1)
- CHECK constraint guards: owner_token_hash length, balance>=0, txn_type enum, txn sign-rule (topup-neg / charge-pos rejected), threshold_pct enum (25 rejected), billing_cycle length (6 rejected) (6)
- UNIQUE constraints: owner_token_hash, (wallet,threshold,cycle) (2)
- ETL threshold firing: 50% only, 50+80, 50+80+100, idempotent, re-fires in next cycle, dry-run, disabled wallet skipped, no-budget skipped (8)
- helper view: includes low-balance enabled, excludes high / disabled (1)
- boot manifest: jpcite + autonomath both list 281 (2)
- LLM-0: no
anthropic|openai|google.generativeaiimport in any new file. legacy brand (税務会計AI/zeimu-kaikei.ai) = 0 (2)
Constraints respected (per task spec)¶
- No Stripe Portal overwrite — Dim U is a parallel ledger; Stripe Portal stays the human-CRM rail. Programmatic call billing reads from the wallet, not Stripe.
- No main worktree — branch created in
/tmp/jpcite-w47-dim-u-migworktree offorigin/main. - No rm / mv — only additive writes (4 new files + 2 manifest appends, no deletes).
- No legacy brand —
税務会計AI/zeimu-kaikei.ainot present. - No LLM API — pure SQL ETL, no
import anthropic/import openai.
Memory references¶
feedback_dual_cli_lane_atomic(lane mkdir + worktree mkdir)feedback_agent_credit_wallet_design(50/80/100 + auto-topup)feedback_no_operator_llm_api(ETL LLM-0)feedback_destruction_free_organization(additive only)
Post-merge follow-ups (out of scope for this PR)¶
- REST
/credit/walletGET (balance + recent transactions) - REST
/credit/topupPOST (Stripe payment intent → topup txn) - MCP tool
credit_wallet_statusfor agents to introspect balance - Cron workflow
credit-wallet-alerts-hourly.yml(calls the ETL) - Telegram/Slack hook on alert firing (out-of-band ops alert)