Skip to content

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)

GapRoot cause
PENDING_BIND write → DB errorECS InstanceStatus enum missing 6 values
weixinChannel write → DB errorECS HermesInstance table missing 7 columns
Hermes container spawns with empty LLM keyHERMES_DEFAULT_LLM_KEY not set on ECS
LLM key = "" → agent fails at runtimeNewAPI unconfigured (NEWAPI_ADMIN_* unset)
ERROR status → user stuck forevercanBind only allows PENDING_BIND/BIND_CONFIRMED
ERROR in instanceLabel() → raw stringNo case in switch statement

Feature gaps

GapImpact
quotaUsed always 0UI shows misleading usage data
No LLM model displayUser doesn't know which model their agent uses
Plan has no modelName fieldCan't show tier-to-model mapping

Files Map

Phase A — ECS hot-fix (no files changed, SSH commands only)

Phase B — Code fixes

FileChange
backend/src/modules/service-runtime/service-runtime.service.tsAllow rebind when status=ERROR
backend/src/modules/service-runtime/service-runtime.controller.tsAdd ERROR to QR_ELIGIBLE_STATUSES
frontend/src/pages/StatusPage.tsxAdd ERROR to canBind, instanceLabel
backend/src/modules/service-runtime/service-runtime.service.tsFix getProvisionStatus null return

Phase C — Feature additions

FileChange
backend/prisma/schema.prismaAdd modelName String? to Plan
backend/prisma/migrations/0002_plan_model_name/migration.sqlALTER TABLE
backend/src/modules/service-runtime/service-runtime.controller.tsInclude plan.modelName in response
frontend/src/pages/StatusPage.tsxShow modelName + quota card
backend/src/modules/service-runtime/service-runtime.service.tsSync quotaUsed from NewAPI on status fetch
backend/src/modules/newapi/newapi.service.tsAdd 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.ts

  • Modify: backend/src/modules/service-runtime/service-runtime.service.ts

  • Modify: 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.ts

Expected: 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.prisma

  • Create: backend/prisma/migrations/0002_plan_model_name/migration.sql

  • Modify: backend/src/modules/service-runtime/service-runtime.service.ts

  • Modify: frontend/src/pages/StatusPage.tsx

  • Modify: 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_name

Create 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.ts

  • Modify: 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_quota unit is 0.000001 tokens (i.e., divide by 1e6 to get actual tokens). Verify against your NewAPI version's response shape at GET /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.ts

Add 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.ts

If 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 latest

Or 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)

IssuePrior plan coverageThis plan
ECS InstanceStatus enum missing 6 valuesTask A1
ECS HermesInstance missing 7 columnsTask A2
HERMES_DEFAULT_LLM_KEY not set on ECSTask A3
ERROR status user stuck (no rebind path)Partially in lifecycle-rebuildTask B1
instanceLabel() missing ERROR caseTask B1
getProvisionStatus returns null not errorTask B1
quotaUsed never synced from NewAPITask C2
No LLM model display in UITask C1
Plan.modelName field doesn't existTask C1
Stray unruffled_chaum containerTask A4

团队内部文档