主题
Hermes Activation Complete — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Get new WeChat customers through the full Hermes provisioning flow (payment → QR scan → container spawned → agent ready), fix all blocking gaps, and show LLM model info in the UI.
Architecture: Three layers of fixes: (A) ECS database hot-fixes (no deploy needed), (B) code bug fixes requiring a deploy, (C) feature additions for LLM model display. All changes are backward-compatible with the existing READY instance.
Tech Stack: NestJS, Prisma, PostgreSQL, React, spawn-profile.sh, Docker, cloudflared SSH
Current State (2026-05-17)
What works
- Existing instance
cmp6vr0zq000bp601beipc3nr— READY, WeChat connected, gateway running - ProvisionWorkerService, ServiceRuntimeService, StatusPage — all code shipped
- spawn-profile.sh accepts all required params (WEIXIN_HOME_CHANNEL, WEIXIN_TOKEN, TIER_CPUS, etc.)
- ProvisionTask table — all columns present on ECS ✅
Blocking gaps (new customers cannot onboard)
| Gap | Root cause |
|---|---|
PENDING_BIND write → DB error | ECS InstanceStatus enum missing 6 values |
weixinChannel write → DB error | ECS HermesInstance table missing 7 columns |
| Hermes container spawns with empty LLM key | HERMES_DEFAULT_LLM_KEY not set on ECS |
| LLM key = "" → agent fails at runtime | NewAPI unconfigured (NEWAPI_ADMIN_* unset) |
| ERROR status → user stuck forever | canBind only allows PENDING_BIND/BIND_CONFIRMED |
| ERROR in instanceLabel() → raw string | No case in switch statement |
Feature gaps
| Gap | Impact |
|---|---|
| quotaUsed always 0 | UI shows misleading usage data |
| No LLM model display | User doesn't know which model their agent uses |
| Plan has no modelName field | Can't show tier-to-model mapping |
Files Map
Phase A — ECS hot-fix (no files changed, SSH commands only)
Phase B — Code fixes
| File | Change |
|---|---|
backend/src/modules/service-runtime/service-runtime.service.ts | Allow rebind when status=ERROR |
backend/src/modules/service-runtime/service-runtime.controller.ts | Add ERROR to QR_ELIGIBLE_STATUSES |
frontend/src/pages/StatusPage.tsx | Add ERROR to canBind, instanceLabel |
backend/src/modules/service-runtime/service-runtime.service.ts | Fix getProvisionStatus null return |
Phase C — Feature additions
| File | Change |
|---|---|
backend/prisma/schema.prisma | Add modelName String? to Plan |
backend/prisma/migrations/0002_plan_model_name/migration.sql | ALTER TABLE |
backend/src/modules/service-runtime/service-runtime.controller.ts | Include plan.modelName in response |
frontend/src/pages/StatusPage.tsx | Show modelName + quota card |
backend/src/modules/service-runtime/service-runtime.service.ts | Sync quotaUsed from NewAPI on status fetch |
backend/src/modules/newapi/newapi.service.ts | Add getTokenUsage() method |
Phase A: ECS Database Hot-Fix
These run via SSH. No code deploy required. Run before deploying any new image.
Task A1: Fix InstanceStatus enum — add 6 missing values
Context: ECS enum only has PROVISIONING, READY, ERROR, DECOMMISSIONED. New code writes PENDING_BIND at instance creation → Postgres rejects it.
- [ ] Step 1: Run ALTER TYPE on ECS
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-postgres psql -U postgres -d twilight_drive <<'SQL'
ALTER TYPE \"InstanceStatus\" ADD VALUE IF NOT EXISTS 'CREATED';
ALTER TYPE \"InstanceStatus\" ADD VALUE IF NOT EXISTS 'PENDING_BIND';
ALTER TYPE \"InstanceStatus\" ADD VALUE IF NOT EXISTS 'BIND_CONFIRMED';
ALTER TYPE \"InstanceStatus\" ADD VALUE IF NOT EXISTS 'STOPPED';
ALTER TYPE \"InstanceStatus\" ADD VALUE IF NOT EXISTS 'FAILED';
ALTER TYPE \"InstanceStatus\" ADD VALUE IF NOT EXISTS 'DESTROYED';
SQL"Expected output: ALTER TYPE × 6
- [ ] Step 2: Verify
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-postgres psql -U postgres -d twilight_drive -c \
\"SELECT unnest(enum_range(NULL::\\\"InstanceStatus\\\"))::text;\""Expected: 10 rows — CREATED, PENDING_BIND, BIND_CONFIRMED, PROVISIONING, READY, STOPPED, FAILED, DESTROYED, ERROR, DECOMMISSIONED
Task A2: Add 7 missing columns to HermesInstance
Context: New code reads/writes weixinChannel, weixinToken, profileName, lastError, spawnedAt, destroyedAt — all missing on ECS.
- [ ] Step 1: Run ALTER TABLE on ECS
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-postgres psql -U postgres -d twilight_drive <<'SQL'
ALTER TABLE \"HermesInstance\"
ADD COLUMN IF NOT EXISTS \"weixinChannel\" TEXT,
ADD COLUMN IF NOT EXISTS \"weixinToken\" TEXT,
ADD COLUMN IF NOT EXISTS \"profileName\" TEXT,
ADD COLUMN IF NOT EXISTS \"lastError\" TEXT,
ADD COLUMN IF NOT EXISTS \"spawnedAt\" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS \"destroyedAt\" TIMESTAMP(3);
SQL"Expected output: ALTER TABLE
- [ ] Step 2: Verify columns
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-postgres psql -U postgres -d twilight_drive \
-c \"SELECT column_name FROM information_schema.columns WHERE table_name='HermesInstance' ORDER BY ordinal_position;\""Expected: 17 rows including weixinChannel, weixinToken, profileName, lastError, spawnedAt, destroyedAt.
Task A3: Configure LLM key on ECS
Context: Without this, spawn-profile.sh writes empty LLM_API_KEY to container .env → Hermes starts but LLM calls fail silently.
Two options — pick whichever applies:
Option 1 (preferred): Set HERMES_DEFAULT_LLM_KEY to an existing valid NewAPI key
bash
# Get an existing key from NewAPI admin dashboard at llm.fsagent.cc
# Then add to ECS .env:
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"echo 'HERMES_DEFAULT_LLM_KEY=sk-xxxx' >> /home/twilight/twilight/.env"Option 2: Configure NewAPI admin credentials (enables per-user key issuance)
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"cat >> /home/twilight/twilight/.env <<'EOF'
NEWAPI_ADMIN_USERNAME=admin
NEWAPI_ADMIN_PASSWORD=your-newapi-admin-password
EOF"[ ] Step 1: Add the env var(s) using one of the options above
[ ] Step 2: Restart app-backend to pick up new env
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"cd /home/twilight/twilight && docker compose restart twilight-app-backend"- [ ] Step 3: Verify var is loaded
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-app-backend env | grep -E 'HERMES_DEFAULT_LLM_KEY|NEWAPI_ADMIN'"Expected: at least one of the vars present and non-empty.
Task A4: Remove stray container
- [ ] Step 1:
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker rm -f unruffled_chaum"Phase B: Code Bug Fixes
Branch: feat/v0.4.2-hermes-activation-fixes
Task B1: Fix ERROR status — user can recover
Problem: When NewAPI token creation fails, instance.status=ERROR. Frontend canBind only checks PENDING_BIND/BIND_CONFIRMED → QR button disabled. rebind() checks externalRef (null on ERROR) so it proceeds, but service-runtime controller may block it.
Files:
Modify:
backend/src/modules/service-runtime/service-runtime.controller.tsModify:
backend/src/modules/service-runtime/service-runtime.service.tsModify:
frontend/src/pages/StatusPage.tsx[ ] Step 1: Read the controller to find QR_ELIGIBLE_STATUSES and rebind guard
bash
grep -n "QR_ELIGIBLE\|rebind\|instance?.status" backend/src/modules/service-runtime/service-runtime.controller.ts- [ ] Step 2: Add ERROR to QR_ELIGIBLE_STATUSES in controller
In backend/src/modules/service-runtime/service-runtime.controller.ts, find:
typescript
const QR_ELIGIBLE_STATUSES = ['PENDING_BIND', 'BIND_CONFIRMED', 'READY'];Change to:
typescript
const QR_ELIGIBLE_STATUSES = ['PENDING_BIND', 'BIND_CONFIRMED', 'READY', 'ERROR'];- [ ] Step 3: Allow rebind when status=ERROR in service
In backend/src/modules/service-runtime/service-runtime.service.ts, find the rebind() method. It already handles null externalRef (skips stop-profile.sh). No change needed — ERROR instances have no container, so rebind just resets to PENDING_BIND. ✅
Verify the reset clears status to PENDING_BIND:
bash
grep -n "status.*PENDING_BIND\|PENDING_BIND.*status" backend/src/modules/service-runtime/service-runtime.service.tsExpected: line in rebind() data block sets status: 'PENDING_BIND'. If not found, add it.
- [ ] Step 4: Fix getProvisionStatus to return error on null
In backend/src/modules/service-runtime/service-runtime.service.ts, find getProvisionStatus():
typescript
async getProvisionStatus(instanceId: string) {
const instance = await this.prisma.hermesInstance.findUnique({
where: { id: instanceId },
select: { id: true, status: true, lastError: true, weixinChannel: true, baseUrl: true },
});
return instance;
}Change to:
typescript
async getProvisionStatus(instanceId: string) {
const instance = await this.prisma.hermesInstance.findUnique({
where: { id: instanceId },
select: { id: true, status: true, lastError: true, weixinChannel: true, baseUrl: true },
});
if (!instance) return { error: 'instance_not_found' as const };
return instance;
}- [ ] Step 5: Fix frontend — add ERROR to canBind and instanceLabel
In frontend/src/pages/StatusPage.tsx, find:
typescript
const canBind = instanceStatus === 'PENDING_BIND' || instanceStatus === 'BIND_CONFIRMED';Change to:
typescript
const canBind = instanceStatus === 'PENDING_BIND' || instanceStatus === 'BIND_CONFIRMED' || instanceStatus === 'ERROR';In instanceLabel(), find the case 'FAILED': line, add before it:
typescript
case 'ERROR':
return '配置异常';- [ ] Step 6: Add rebind button when instanceStatus=ERROR
In frontend/src/pages/StatusPage.tsx, the idle phase shows the QR button only when canBind. Since we added ERROR to canBind, the QR button will appear. But also add a prominent rebind prompt for ERROR state above the button section:
In the weixin.phase === 'idle' block, before the button:
tsx
{weixin.phase === 'idle' && instanceStatus === 'ERROR' && (
<p className="text-amber-400 text-sm text-center mb-2">
配置时出现问题,请重新绑定微信以重试。
</p>
)}- [ ] Step 7: Commit
bash
git add backend/src/modules/service-runtime/service-runtime.controller.ts \
backend/src/modules/service-runtime/service-runtime.service.ts \
frontend/src/pages/StatusPage.tsx
git commit -m "fix(hermes): allow ERROR instance recovery via rebind + fix null provisionStatus"Task B2: E2E smoke test for new user flow (manual)
Run this after deploying Phase A + B to ECS.
- [ ] Step 1: Simulate a new paid order via dev endpoint
bash
# Get a valid JWT first from the app
TOKEN="your-jwt-here"
curl -X POST https://backend.fsagent.cc/billing/dev-mark-paid \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"orderId": "test-order-id"}'Expected: { ok: true, subscriptionId: "...", instanceId: "..." }
- [ ] Step 2: Check DB — instance should be PENDING_BIND
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-postgres psql -U postgres -d twilight_drive \
-c \"SELECT id, status FROM \\\"HermesInstance\\\" ORDER BY \\\"createdAt\\\" DESC LIMIT 3;\""Expected: new row with status=PENDING_BIND
- [ ] Step 3: Visit StatusPage in browser
Navigate to https://app.fsagent.cc → login → StatusPage.
Expected: "获取微信绑定二维码" button is enabled.
- [ ] Step 4: Click button — QR should appear
Expected: iLink QR code image loads, instructions shown.
- [ ] Step 5: Scan QR with WeChat
After scan, status should auto-poll to confirmed → then provisioning → eventually ready.
- [ ] Step 6: Verify container spawned
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc "docker ps --filter 'name=hermes-'"Expected: new hermes-<instanceId> container Up and (healthy).
- [ ] Step 7: Send a test message in WeChat
Send "你好" to the bot. Expected: Hermes responds within 30s.
Phase C: Feature Additions
Task C1: Add LLM model name to Plan and StatusPage UI
Context: Users don't know which LLM model their agent uses. Plan model has no modelName field. Fix: add optional modelName field to Plan, seed it, show in StatusPage.
Files:
Modify:
backend/prisma/schema.prismaCreate:
backend/prisma/migrations/0002_plan_model_name/migration.sqlModify:
backend/src/modules/service-runtime/service-runtime.service.tsModify:
frontend/src/pages/StatusPage.tsxModify:
frontend/src/services/api.ts[ ] Step 1: Add modelName to Plan in schema.prisma
In backend/prisma/schema.prisma, inside model Plan, after billingCycleDays Int, add:
prisma
modelName String? // Display name for the LLM model, e.g. "Qwen 72B"- [ ] Step 2: Create migration file
bash
mkdir -p backend/prisma/migrations/0002_plan_model_nameCreate backend/prisma/migrations/0002_plan_model_name/migration.sql:
sql
ALTER TABLE "Plan" ADD COLUMN IF NOT EXISTS "modelName" TEXT;- [ ] Step 3: Run migration on local dev DB (if running)
bash
cd backend && npx prisma migrate deploy- [ ] Step 4: Seed modelName for existing plans
bash
# Check existing plan codes:
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-postgres psql -U postgres -d twilight_drive \
-c \"SELECT id, code, name, \\\"priceCnyFen\\\" FROM \\\"Plan\\\";\""Then update with model name (adjust plan codes to match actual):
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-postgres psql -U postgres -d twilight_drive <<'SQL'
-- Apply migration first
ALTER TABLE \"Plan\" ADD COLUMN IF NOT EXISTS \"modelName\" TEXT;
-- Seed model names (adjust based on actual plan codes)
UPDATE \"Plan\" SET \"modelName\" = 'Qwen2.5 72B' WHERE \"priceCnyFen\" <= 19900;
UPDATE \"Plan\" SET \"modelName\" = 'Qwen2.5 72B Pro' WHERE \"priceCnyFen\" > 19900;
SQL"- [ ] Step 5: Include modelName in getServiceStatus response
In backend/src/modules/service-runtime/service-runtime.service.ts, getCurrentSubscription():
typescript
async getCurrentSubscription(userId: string) {
return await this.prisma.subscription.findFirst({
where: { userId, status: 'ACTIVE' },
orderBy: { createdAt: 'desc' },
include: { plan: true, instance: true },
});
}plan: true already fetches all plan fields including modelName. No change needed in service.
Verify getServiceStatus returns subscription with plan:
bash
grep -n "subscription\|plan" backend/src/modules/service-runtime/service-runtime.service.ts | head -10- [ ] Step 6: Update frontend API type to include modelName
In frontend/src/services/api.ts, find the Subscription/Plan type definition (or type annotation on the response). Add modelName?: string to the plan type:
typescript
// Find the Plan interface/type and add:
modelName?: string;- [ ] Step 7: Show modelName in StatusPage
In frontend/src/pages/StatusPage.tsx, in the status grid (after plan name display), add a row:
tsx
<div>
<p className="text-zinc-500">LLM 模型</p>
<p className="text-white font-medium">
{currentSubscription?.plan?.modelName ?? '标准模型'}
</p>
</div>Place it after:
tsx
<div>
<p className="text-zinc-500">套餐</p>
<p className="text-white font-medium">{currentSubscription?.plan?.name ?? '—'}</p>
</div>- [ ] Step 8: Commit
bash
git add backend/prisma/schema.prisma \
backend/prisma/migrations/0002_plan_model_name/ \
backend/src/modules/service-runtime/ \
frontend/src/pages/StatusPage.tsx \
frontend/src/services/api.ts
git commit -m "feat(ui): show LLM model name in StatusPage, add Plan.modelName field"Task C2: Sync quotaUsed from NewAPI (lightweight approach)
Context: quotaUsed is set to 0 at subscription creation and never updated. NewAPI tracks per-token usage. Fix: on each GET /services/status call, if NewAPI is configured, fetch the token's usage and update the snapshot.
Files:
Modify:
backend/src/modules/newapi/newapi.service.tsModify:
backend/src/modules/service-runtime/service-runtime.service.ts[ ] Step 1: Add getTokenUsage() to NewApiService
In backend/src/modules/newapi/newapi.service.ts, after createUserToken(), add:
typescript
async getTokenUsage(tokenId: number): Promise<number> {
const token = await this.getAdminToken();
const { data } = await axios.get(`${this.baseUrl}/api/token/${tokenId}`, {
headers: { Authorization: `Bearer ${token}` },
timeout: 5_000,
});
// NewAPI returns used_quota in the token object (in units of 0.000001 tokens)
return Math.round((data?.data?.used_quota ?? 0) / 1_000_000);
}Note: NewAPI's
used_quotaunit is0.000001tokens (i.e., divide by 1e6 to get actual tokens). Verify against your NewAPI version's response shape atGET /api/token/:id.
- [ ] Step 2: Update getServiceStatus to sync quota
In backend/src/modules/service-runtime/service-runtime.service.ts, update getServiceStatus():
typescript
async getServiceStatus(userId: string) {
const sub = await this.getCurrentSubscription(userId);
const snap = await this.prisma.serviceStatusSnapshot.findFirst({
where: { userId },
orderBy: { createdAt: 'desc' },
});
let quotaUsed = snap?.quotaUsed ?? 0;
if (sub?.instance?.llmApiTokenId && this.newApi.isConfigured) {
try {
quotaUsed = await this.newApi.getTokenUsage(sub.instance.llmApiTokenId);
if (snap) {
await this.prisma.serviceStatusSnapshot.update({
where: { id: snap.id },
data: { quotaUsed },
});
}
} catch {
// fall through to cached value
}
}
return {
subscription: sub,
status: {
health: snap?.health ?? (sub ? 'healthy' : 'inactive'),
quotaUsed,
quotaCap: snap?.quotaCap ?? sub?.plan.monthlyQuotaTokens ?? null,
expiresAt: snap?.expiresAt ?? sub?.endsAt ?? null,
},
};
}- [ ] Step 3: Inject NewApiService into ServiceRuntimeService
In backend/src/modules/service-runtime/service-runtime.service.ts, add to constructor:
typescript
constructor(
private readonly prisma: PrismaService,
private readonly provisioning: ProvisioningService,
private readonly newApi: NewApiService,
) {}In backend/src/modules/service-runtime/service-runtime.module.ts (or wherever the module is defined), add NewApiModule or NewApiService to imports/providers.
- [ ] Step 4: Check service-runtime.module.ts imports
bash
cat backend/src/modules/service-runtime/service-runtime.module.tsAdd NewApiModule to imports if not present:
typescript
imports: [PrismaModule, ProvisioningModule, NewApiModule],- [ ] Step 5: Verify NewApiModule is exportable
bash
grep -n "exports" backend/src/modules/newapi/newapi.module.tsIf NewApiService is not in exports, add it:
typescript
exports: [NewApiService],- [ ] Step 6: Commit
bash
git add backend/src/modules/newapi/newapi.service.ts \
backend/src/modules/service-runtime/service-runtime.service.ts \
backend/src/modules/service-runtime/service-runtime.module.ts \
backend/src/modules/newapi/newapi.module.ts
git commit -m "feat(quota): sync quotaUsed from NewAPI on status fetch"Phase D: Deploy to ECS
Task D1: Build and push new image
- [ ] Step 1: Ensure on branch with all Phase B+C changes merged to main
bash
git log --oneline -5- [ ] Step 2: Trigger CI build
Push to main or create PR → merge → GH Actions docker-build.yml auto-builds and pushes to ACR.
bash
git push origin main- [ ] Step 3: Wait for CI
bash
gh run watch $(gh run list --workflow docker-build.yml --limit 1 --json databaseId -q '.[0].databaseId')Expected: all jobs green.
Task D2: Deploy to ECS
- [ ] Step 1: Run ECS migration for 0002 (if Phase C included)
bash
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" \
root@ssh-ecs.fsagent.cc \
"docker exec twilight-postgres psql -U postgres -d twilight_drive \
-c \"ALTER TABLE \\\"Plan\\\" ADD COLUMN IF NOT EXISTS \\\"modelName\\\" TEXT;\""- [ ] Step 2: Deploy app-backend
bash
ssh -i ~/.ssh/twilight-ecs-deploy deploy@ssh-ecs.fsagent.cc deploy latestOr use manual.sh:
bash
bash scripts/deploy/manual.sh deploy latest- [ ] Step 3: Verify health gates
bash
curl -fsS https://api.fsagent.cc/healthz
curl -fsS https://backend.fsagent.cc/health
curl -fsS https://app.fsagent.cc/All must return 200.
- [ ] Step 4: Run smoke test (Task B2)
Repeat Task B2 steps 1-7 to verify the full new-user flow works end-to-end.
Acceptance Checklist
- [ ] New user pays → DB creates instance with status=PENDING_BIND (not PROVISIONING)
- [ ] StatusPage shows QR button enabled for PENDING_BIND
- [ ] User scans QR → within 10s, status transitions to BIND_CONFIRMED
- [ ] ProvisionWorker picks up task → spawn-profile.sh runs → container starts
- [ ] Within 60s of scan, instance status=READY
- [ ] Hermes responds to WeChat message within 30s (LLM key is valid)
- [ ] StatusPage shows "微信绑定完成,Agent 已就绪"
- [ ] StatusPage shows correct LLM model name
- [ ] quotaUsed shows non-zero after a few LLM calls
- [ ] ERROR status shows "配置异常" label + QR button enabled (not stuck)
- [ ] rebind resets ERROR instance to PENDING_BIND + user can get new QR
Issue Gap Summary (not in any prior plan)
| Issue | Prior plan coverage | This plan |
|---|---|---|
| ECS InstanceStatus enum missing 6 values | ❌ | Task A1 |
| ECS HermesInstance missing 7 columns | ❌ | Task A2 |
| HERMES_DEFAULT_LLM_KEY not set on ECS | ❌ | Task A3 |
| ERROR status user stuck (no rebind path) | Partially in lifecycle-rebuild | Task B1 |
| instanceLabel() missing ERROR case | ❌ | Task B1 |
| getProvisionStatus returns null not error | ❌ | Task B1 |
| quotaUsed never synced from NewAPI | ❌ | Task C2 |
| No LLM model display in UI | ❌ | Task C1 |
| Plan.modelName field doesn't exist | ❌ | Task C1 |
| Stray unruffled_chaum container | ❌ | Task A4 |