fix(cloud): apex console never boots the agent app — /dashboard home + standalone account routes - #13410
Conversation
…+ standalone account routes (#13406) elizacloud.ai (apex control-plane host) has no same-origin agent backend; landing authenticated visitors on /settings#cloud-billing booted the agent app, whose failed /api/first-run/status probe threw the first-run onboarding chooser over the console (the prod bug shaw + nubs hit). - AppCatchAllRoute: on apex hosts the agent app never renders — unauth → /login, authed → /dashboard, not-ready → blank fallback (never the app). - New /dashboard console home: live credit balance + add-funds, directory cards for all 13 console surfaces. - Standalone console routes mounting the existing section bodies: dashboard/billing, api-keys, account, security, security/permissions, monetization, connectors. Same bodies stay mounted in in-app Settings. - Legacy redirects: earnings/affiliates → /dashboard/monetization; dashboard/settings?tab=<x> → matching console page (works on every host). Tests: shell apex behavior re-pinned (8), route registry invariants inverted to require the standalone mounts, new home-page coverage (5). Live-verified against the real signed-in session + real api.elizacloud.ai (evidence in .github/issue-evidence/13406-apex-console/).
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Reviewed current head No blocking code finding from static review. The routing shape matches the intended host split: registered
Local checks:
Local checks blocked by environment:
I would wait for the remote focused tests + aesthetic audit to finish before auto-merge. |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Patched one blocker I found while reviewing the standalone console routes: the new Validation run:
I also attempted the required shared-UI app audit with |
…elete + stale-row fixes (#13406) - ConsoleShell: the old cloud-frontend layout (left sidebar Run/Observe/ Money/Account + top bar w/ captured page titles) rebuilt on the surviving cloud-ui layout kit, wrapped around every dashboard/admin route. Kit fixed for body-scroll-locked hosts: viewport-height sidebar with internally scrolling nav, content pane owns its scroll (nubs: 'cant scroll the sidebar'). - Host-aware post-login landing: apex console -> /dashboard, app domains keep /join drop-into-chat. Fixes sign-in dumping console users into the agent-provisioning join flow. - Bulk multi-select delete on Instances + Apps (one confirm dialog serves row + bulk paths) with optimistic cache removal. - Stale-row-after-delete root causes fixed: agents-table merge resurrected any row the API list omitted (dropped), delete tombstones absorb the eventually-consistent list API, actions invalidate the parent useAgents cache; apps list gets optimistic removal + delayed re-sync. - ApiKeysSurface: canonical useSessionAuth gate (raw-context read skeletoned forever when no provider was mounted; signed-out now renders a designed state). - Monetization clients: deep primitive/brand imports per packages/ui rules. Tests: ConsoleShell chrome, login-return-to host matrix, DashboardHomePage, AppsTable bulk delete (3), MonetizationPage render smoke. 12 files, all green; typecheck clean on every touched file.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
…brand fallback, buy-credits disabled state (#13437) Fixes 2 of 3 visual findings from QA tracker #13406, and root-causes the third. Finding 2 (billing selected payment-method = blank white box) — OWNED, our regression: The token sweep #13073 (a6de6d6) converted the selected toggle from `bg-[#FF5800] ... text-white` to `bg-[var(--accent)] ... text-white`. That regressed on the cloud dashboard: `dashboard-shell.tsx` wraps everything in `.theme-cloud`, where `--accent` resolves to `--brand-white`. Result: bg-white + text-white = an invisible label on a white fill = the blank white box. Fix pairs the accent fill with `text-accent-foreground` (black under `.theme-cloud`) so the label is readable in every theme scope, and swaps the raw white-opacity ladder on the unselected state for theme-aware border/muted tokens. Finding 3 (Buy credits disabled reads as broken gray): `BrandButton variant="primary"` is `bg-accent` (white under `.theme-cloud`); `disabled:opacity-50` ghosted the white pill into an accidental muddy gray. Made the disabled state intentional per the design system — muted token surface (`disabled:bg-bg-muted disabled:text-muted`) + the existing reduced opacity — mirroring the proven `icon-primary` disabled pattern. Fixes the disabled look for all BrandButton primary call sites, not just billing. Finding 1 (dashboard/apps white rectangle top-center) — root-caused, NOT a standalone cloud-ui component bug: The develop apps render path (ApplicationsPage → DashboardStatGrid) has no <img> and no white-block element. Pixel analysis of the prod evidence shows a flat pure-#fff 289x79 rectangle flanked by pure #000 — the agent-app-boot artifact from the apex post-signin bug ([qa-agent]'s #13410 scope: agent app boots on the apex host with no same-origin backend, assets 404 / shell chrome mis-renders). Same white-accent theme class as finding 2 is the likely mechanism. Left to #13410; not touching routing. Evidence: tsc clean on touched files (pre-existing core/shared codegen errors only), all 4 anti-slop gates unaffected (the pre-existing ChatSurface.tsx backdrop-blur regression is untouched by this PR), biome clean, 5 new unit tests pass, codex-reviewed (no correctness issues). Prod verification routes through [qa-agent]'s signed-in session per the tracker protocol. — [sol-orch] Co-authored-by: wakesync <shadow@shad0w.xyz>
|
Maintainer review note: this branch must be rebased before further review or merge. After refreshing I attempted to disable auto-merge, but GitHub returned: I did not run the claimed cloud/UI test matrix because the branch shape itself is unsafe. |
…able (#13707) * fix(cloud): agents status-poll must not blank a populated Instances table (#13406) The background status poll's merge was made membership-authoritative in #13410 — it replaced localSandboxes with ONLY the rows that fetch returned. A poll that came back short or empty (transient, paging blip, a 200 with an empty list during container state flux) then blanked the whole table while the authoritative useAgents() count still read >0 (nubs saw '2 running' with an empty list). mergeApiData now updates + adds but NEVER removes on a refresh. Membership removal stays owned by the two authoritative paths that already existed: the useAgents() refetch (the initialSandboxes resync replaces the list with server truth) and explicit-delete tombstones. Extracted the merge into a pure mergeAgentList() with direct coverage of the invariant (empty poll keeps all rows; partial poll keeps omitted rows + updates returned ones; new rows append; tombstoned ids excluded). 4/4 green. Re-verify against a live '2 running' session on staging once it redeploys current develop — the deployed staging predates this. * fix(ui): retire agent tombstones after merge * fix(ui): retire agent tombstones outside the state updater (StrictMode purity) Shepherd review on 97f3b19 was right: retiring tombstones inside the setLocalSandboxes updater mutates shared state in a function React StrictMode double-invokes — the second invocation sees the smaller set and can resurrect a tombstoned row the eventually-consistent API still returns. mergeApiData now retires ids the API stopped returning BEFORE the state update and hands the pure mergeAgentList an immutable snapshot. The impure mergeAgentListAndRetireTombstones helper is deleted. Tests: double-invocation idempotence (tombstoned row excluded on both runs, identical results) + retire-only-outside-the-updater semantics. 6/6 green. --------- Co-authored-by: NubsCarson <claude3@nubs.site> Co-authored-by: Shaw <shawgotbags@gmail.com>
* fix: harden advertising app credit numerics
* fix(#13415): fail closed on corrupt pending-charge estimate in inference sweep
The cron sweep (`sweepStalePendingInferenceChargesDb`) read each stale
pending charge's persisted `estimated_cost_usd` (a NOT NULL numeric(12,6),
so a driver string) via `Number(row.estimated_cost_usd)` then coerced with
`Number.isFinite(estimate) ? estimate : 0`. A corrupt value ('NaN'::numeric
is a valid Postgres NUMERIC that reads back as the string "NaN", or an empty/
non-finite value from DB corruption / a migration artifact / a manual edit)
therefore SETTLED the charge at $0 — a fabricated-default free-inference
collection that clears the pending row as if the cost were legitimately zero
(#13415 fallback-slop: a failed read must not become a success-shaped value).
Fix: new `parseSweepEstimate` fail-closed boundary + `CorruptPendingChargeEstimateError`
throws on missing/empty/non-finite (an explicit domain $0 free request is
allowed through). On a corrupt estimate the sweep no longer settles at $0 —
it logs at error and transitions the row OUT of `pending` to an auditable
`corrupt` terminal state via `markSweepPendingCorrupt` (no debit, no fabricated
collection), counted in a new `LedgerSweepStats.corrupt` field; the GC clause
now reclaims `corrupt` rows too so the table stays bounded. `markSweepPendingCorrupt`
never throws — a failed transition leaves the row `pending` for the next sweep
(still fail-closed, never a fabricated success). A corrupt row does not block
healthy rows in the same batch.
Tests (packages/cloud/shared/.../inference-billing-ledger.test.ts, real PGlite):
- REGRESSION GUARD: a 'NaN'::numeric estimate is NOT settled at $0 — fails
closed to a 'corrupt' row, zero debit, balance untouched (old code would
have settled at $0).
- corrupt row leaves 'pending' (no forever re-scan) + GCs like other terminals.
- corrupt rows do not block a healthy $3 charge in the same batch.
- an explicit $0 estimate stays a legitimate free request (settled, not corrupt).
- exports the fail-closed error type.
26/26 ledger suite green (5 new), cron route test green (baseUrl-primed, quirk
restored not committed), tsc 0 errors in touched file (6 pre-existing baseline
in unrelated node-redis/plugin-mcp/anthropic/plugin-elizacloud files, identical
on develop), biome clean, error-policy-ratchet 'no new fallback-slop'.
Refs #13415
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(#13415): reject negative pending-charge estimates in fail-closed sweep (addresses review)
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix: flip wake readiness when the agent can actually answer (#14038)
three stacked layers made the launcher show 'taking longer than usual to
wake' while the agent already answered chat:
- runtime: model-provider plugins now load in the BLOCKING boot phase
(they are first-turn capability), so agentState=running, canRespond,
/api/health ready, and the warming-gate release all flip at the same
instant a TEXT_GENERATION handler exists — also kills the 'no LLM
provider configured' wrong answer served to real turns during warm-up
- runtime: broadcast WS status on MODEL_REGISTERED so a late-registering
provider flips the ws lane immediately instead of the 5s statusInterval
- cloud: provision()/executeWake persist status='running' + bridge
metadata right after health check + runtime-agent start, BEFORE the
backup restore — the dedicated-agent proxy stops synthesizing 202
'starting' for /api/status as soon as the container actually serves;
restore failure still exits 'running' via the existing catch
probe evidence (real server lane, before/after timelines + harness) in
.github/issue-evidence/14038-*
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agent): gate blocking-phase provider claim on mobile loadability (#14039)
lalalune's review (CHANGES_REQUESTED) on #14039 flagged that the
force-include path at plugin-resolver.ts added env-selected model
providers to `blockingPhaseClaimedProviderNames` WITHOUT the
mobile-loadability check the non-force-include claim loop applies.
On mobile (no node_modules), a provider whose module is not baked into
the static bundle and could not be pre-registered by
ensureStaticPluginsRegisteredByName() would be claimed by the blocking
pass but then fail to load (no STATIC_ELIZA_PLUGINS / loader entry). The
deferred pass then excluded it via the shared claimed set, dropping the
configured provider from BOTH phases and deadlocking readiness on a
provider that can never register.
Fix (minimal, keeps NubsCarson's design):
- Extract the "loadable now" predicate (`isProviderLoadableNow`) and
apply it to the force-include branch too. An unloadable-on-mobile
forced provider is neither claimed nor force-kept in the blocking set;
it falls through to the normal phase partition so the deferred pass
(once the static wave registers its module) can still own it, and
readiness never waits on a provider that cannot load at blocking time.
- Off-mobile and statically-bundled behavior is unchanged.
Tests (packages/agent/src/runtime/plugin-resolver.test.ts):
- new: a force-included provider absent from the static bundle at
blocking time is NOT claimed (blocking excludes it, no deadlock) and,
once a static loader is registered, the deferred pass owns it.
- new control: a statically-bundled env-selected provider IS claimed in
the blocking phase and excluded from the deferred phase.
Local run: bunx vitest run packages/agent/src/runtime/plugin-resolver.test.ts
=> 6 passed (6) — the 4 pre-existing + 2 new.
Co-authored-by: wakesync <shadow@shad0w.xyz>
* chore: format wake-status evidence scripts
* fix(cloud): warn on restricted Stripe test keys in prod
* docs(cloud): refresh Stripe warning evidence
Co-authored-by: wakesync <shadow@shad0w.xyz>
* docs: end Stripe rk test-key evidence with newline
* fix(ios): make ASC profile provisioning device-safe
* fix(ios): reuse valid ASC device profiles
* fix: refuse expired ios development profile reuse
* test(electrobun): cover packaged desktop notifications
* test(electrobun): harden packaged notification e2e
Co-authored-by: wakesync <shadow@shad0w.xyz>
* ci: preflight stale PR bases before test fanout
* ci: guard stale preflight fanout contract
* fix(cloud): accept Stripe restricted keys in env validation
* fix: thread of a muted parent reports muted in inbox and channel listings
a discord thread whose parent channel is muted showed muted:false in the
inbox and list_channels surfaces while the connector's inbound gate was
dropping its messages via the [room, parent] mute chain. the display now
inherits the parent's mute everywhere the flag is computed:
- TargetInfo gains parentChannelId; the discord connector stamps it on
listed channel/thread targets from the same parentId the inbound gate
drops on
- resolveMutedTargetFlags checks the parent room's mute after the
target's own, so list_channels / list_connections match the drop path
- the inbox mute state passes the parent room as an ancestor to
resolveEffectiveMuteState, deriving the linkage from the cached live
channel lookup the chat list already uses for titles
real tests on all three surfaces fail on the old behavior: core resolver
(thread target of muted parent), real DiscordService.listConnectorRooms
target linkage + flags, and GET /api/inbox/chats through the real route
handler.
* fix(ui): preserve console page title ownership
* fix(#14083): freshness guard on cloud-cf-deploy — skip stale zombie-run deploys
Zombie CI runs stuck `queued` through a runner freeze eventually execute and
deploy their OLD ref OVER a newer build. #14082 hit this: staging regressed to
a pre-#13410 bundle (8deb9cbd07) hours after newer builds were live, because a
stale queued cloud-cf-deploy run finally ran and clobbered newer deploys.
Adds a freshness guard step to both Pages deploy jobs (deploy-console
eliza-cloud @ elizacloud.ai / staging.elizacloud.ai, deploy-app eliza-app @
app.elizacloud.ai / app-staging.elizacloud.ai) that runs BEFORE wrangler pages
deploy:
1. fetches the currently-served build stamp (the deployed
eliza-renderer-build.json, whose `commit` field records the ref that built
it — the same manifest #9309 ships on every renderer build);
2. SKIPs the deploy when this run's SHA is an ancestor of the served commit
(`git merge-base --is-ancestor <runSha> <servedCommit>` — the served build
is strictly newer => this run is stale);
3. a `workflow_dispatch` `force` input bypasses the guard for intentional
rollbacks to an older ref.
FAIL-OPEN by construction: the guard only SKIPs on a definitive stale signal
(run SHA provably an ancestor of a known-newer served commit). Every ambiguous
state — served stamp unreachable/unparseable/unstamped, no commit recorded,
unrelated histories, ancestry undeterminable, same-commit redeploy, git error,
unexpected crash — DEPLOYS. A freshness guard must never turn a transient
signal-fetch failure into an undeployable state (that would block the exact fix
that needs to ship). The CLI always exits 0 and signals only via
`should_deploy`; the deploy step gates on it.
Split into a pure/tested decision core (decideDeployFreshness + parseServedCommit
+ fetchServedCommit in deploy-freshness-guard.mjs) and a thin git/IO CLI
(deploy-freshness-guard-cli.mjs). 19 unit tests cover the narrow SKIP case + every
fail-open branch + the force bypass + the network boundary; verified end-to-end
against a real 2-commit git repo (stale->skip, newer->deploy, force->deploy).
Fixes #14083
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(ci): guard worker deploy freshness
* fix(ci): make stale preflight runner self-contained
* fix(ci): harden deploy freshness guard
* fix(#13772): close admission-queue drain races found in #13841 post-merge audit
Three defects in the merged admission queue:
1. Double dispatch via direct spawn: a successful spawnAgentForTask never
cleared a pre-existing admission record, so spawning a parked task
directly (slot freed silently, user beats the reconcile tick) left the
stale record behind and the next drain replayed it — a duplicate agent
for the same goal. The spawn success path now dequeues the parked state.
2. Paused parked tasks stranded by restart: pauseTask retains the durable
admission record (clearMetadata=false) so resume can replay the original
spawn, but rebuildAdmissionQueueFromStore re-seeded paused tasks and
drainOnce then CLEARED their retained record, making a later resume a
silent no-op (task stuck open forever). Rebuild now skips paused tasks
and the drain keeps a paused head's record.
3. Dead cap-race re-park in drainOnce: the drain-replayed spawn self-parked
on SessionCapError (fresh enqueuedAt, pushed to the back), so the
drain's re-park-at-head handler never ran and a raced head lost its
seniority + accumulated aging — and could even be dropped entirely via
AdmissionQueueFullError when the queue refilled mid-dispatch. The drain
now passes parkOnCap:false so the cap error rethrows and the original
record is restored at the head.
Regression coverage for all three in admission-integration.test.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(#13775): mint traceId at the first MESSAGE_RECEIVED touchpoint so emit-first paths join the trace
The agent API chat route (packages/agent chat-routes.ts) and connectors
(iMessage, Signal, Matrix, Feishu, autonomy) emit MESSAGE_RECEIVED BEFORE
calling messageService.handleMessage. On those paths the trajectories plugin
handler started the DB row while message.metadata carried no traceId — the row
persisted trace_id NULL — and handleMessage then minted a different traceId
that only the file recorder saw. The #13871 join key was silently broken on
the most-used paths.
Mint at the first touchpoint instead: the trajectories MESSAGE_RECEIVED
handler mints (env-inherited, else fresh) and stamps message.metadata when no
traceId is present; message.ts now reuses a pre-stamped metadata traceId
instead of unconditionally overwriting it. Whichever of the two runs first
mints; both stores agree.
Real-PGLite regression test drives the actual plugin handler with a
metadata-less message (the chat-routes shape) and asserts the row's trace_id
matches the id stamped on message.metadata.
Found by post-merge audit of #13871.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(android): wait for assistant route logs
* style: biome format on audit fix files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(#13775): redact secrets in the sub-agent stdout tee before persisting
The raw sub-agent stdout tee (subagent-stdout-log.ts appendSubagentStdout)
wrote every ACP chunk to a per-session NDJSON file VERBATIM. Sub-agent stdout
routinely echoes the model key / Bearer token in use, and that file outlives
the session, so a raw provider credential there was a durable on-disk leak
(#13811 post-merge review of #13775).
Fix: mask credentials at write time using core's canonical value-shape
redactor `redactSensitiveText` (packages/core/src/security/redact.ts) — the
same pattern set the log sink and runtime.redactSecrets already apply — rather
than duplicating patterns in the plugin. The persisted file can no longer
contain a raw secret; the in-memory live tail (outputBuffers/appendOutput) is a
separate path and is unchanged.
Gap closed in the shared redactor: its DEFAULT_REDACT_PATTERNS covered OpenAI
`sk-`, Stripe, GitHub, Slack, Google, Bearer, PEM — but not Cerebras `csk-`
keys, which sub-agents echo as the active model key. Added the `csk-` pattern
next to the `sk-` one (the word boundary keeps `sk-` from partial-matching
inside `csk-`).
Tests:
- packages/core/src/security/redact.test.ts: csk- key is masked and the sk-
variant is not eaten (mutation-checked: fails without the new pattern).
- plugin-agent-orchestrator subagent-stdout-log.test.ts: a MODEL-USED-style
line with sk-/Bearer tokens lands masked in the persisted NDJSON;
non-secret content is byte-identical; the rotation path is unaffected.
Real filesystem writes to a temp trajectory dir — no mocks of the unit
under test.
Part of #13775
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(#13696): cover packaged desktop notification e2e
* test(#13772): make the cap-race re-park test wait for the stolen dispatch
Polling for record-presence raced ahead of the fire-and-forget drain and
vacuously observed the original record; wait for the steal to be consumed
and the pass to settle instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(ci): document deploy guard action output env
* fix(orchestrator): drop post-completion teardown-race errors; route auto-verify retry through the transition table (#13830 audit)
A late session `error` arriving after the same session already posted
`task_complete` is a teardown race — the router already suppresses its
respawn for exactly this case (the completion claim in router-loop-guard) —
but the task event bridge treated it as a fresh crash: it overwrote the
`completed` session record with `errored`, counted it against the
crash-retry budget, and (via the #13830 `validating -> retrying -> active`
edge) knocked a `validating` task back to `active` mid-verification. That
aborts validateTask (status is no longer `validating`), so a task whose
deliverable already shipped wedges at `active` with no live worker — the
same non-terminal-hang class #13771/#13909 exist to prevent.
Also routes the auto-verify re-engage write (validating -> active on a
failed verdict) through advanceTaskStatus('validation_failed') instead of a
direct store.updateTask status write, so an operator archive/pause landing
while the judge runs is a legal no-op instead of being stomped.
Test hardening: the task_complete bridge path performs real async IO
(change-set mirror, trajectory ingest #13775) before the
completion_reported advance, so the single-macrotask drive() flush the
#13830 tests rely on is racy — 'moves the task to validating on
completion' and 'never mutates a terminal task from a session event' fail
when run in isolation on develop. Added a settleStatus() poll helper and
used it at the racy assertions, plus regression tests for the teardown
race.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: avoid expect.poll in orchestrator task suite
* test(#13696): expose notification store packaged test bridge
* test(#13620): seed lane coverage allowlist
Seed the default lint-lane-coverage allowlist from the current develop baseline so the gate can run in enforced mode without failing historical deterministic-lane debt. New unsuppressed findings still fail, and stale allowlist entries remain errors.
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(ui,training): close the two #14068 capability-grant residuals
Split-layout panes mounted granted views through the second
DynamicViewLoader instantiation without the surface prop, so the broker
resolved zero grants and denied mutating capabilities that the tabbed
mount of the same view allowed. plugin-training's FineTuningView is
instrumented with useAgentElement but was the one first-party view the
#14068 migration missed.
* test(#13610): enable runtime chooser in packaged first-run lane
* test(#13620): refresh lane coverage allowlist baseline
* fix(#13415): fail closed on corrupt credit_balance in container deploy/quota
ContainersRepository read organizations.credit_balance (Postgres NUMERIC ->
driver string) via a bare Number(...) at three sites, failing OPEN on a corrupt
value ('NaN'::numeric is a valid Postgres NUMERIC / migration artifact / manual
DB edit):
- createContainerWithCreditDeduction (money-out spend gate): the insufficient-
balance guard `Number(credit_balance) < deploymentCost` is FALSE for NaN, so
the deploy+debit was AUTHORIZED against a corrupt balance -- the container
deployed FREE and String(NaN - cost) = "NaN" was written back, permanently
poisoning the balance column. A money-out gate failing open is the worst class.
- createWithQuotaCheck / checkQuota (container-quota tier): NaN fed
getMaxContainersForOrg, silently dropping the org into the FREE quota tier.
All three now delegate to the merged, exported fail-closed boundary
parseOrganizationCreditBalance (#13416) -- the same helper OrganizationsRepository
already uses for this exact column -- so a corrupt read throws a field-named
error inside the mutation transaction (money path rolls back atomically) or
denies the pre-flight check (read-only path) instead of bypassing the guard or
fabricating a free-tier max.
Tests: containers-credit-balance-numeric.test.ts -- 8/8, reversion-proven
(reverting any site fails the grep-guard), pins the reused parser against the
money-out class + proves all three sites delegate + no bare Number(org.credit_balance)
survives. tsgo 0 errors in touched files (8 pre-existing baseline: node-redis/
plugin-mcp/anthropic + 4 fresh-worktree i18n), biome clean, error-policy-ratchet
no new fallback-slop, codex clean (no discrete correctness issues).
Co-authored-by: wakesync <shadow@shad0w.xyz>
* docs(cloud): document corrupt pending-charge lifecycle
* refactor(ui): share eliza agents row view model
* fix(#13431): add local conversation importer (#13506)
* fix(#13431): add local conversation importer
* fix(ui): parse plain text conversation imports
* fix(ui): use canonical conversation document importer
* fix(ui): finish conversation import document provenance
Add component coverage for the Memories conversation importer flow and preserve import provenance through the document upload path.
Co-authored-by: wakesync <shadow@shad0w.xyz>
---------
Co-authored-by: moon <stupidlybadadvice@gmail.com>
Co-authored-by: Sol <sol@shad0w.xyz>
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(ui): format push registration listener
* fix(cloud): split advertising cap parser from app-credit slice
* fix(#13415): fail closed on corrupt NUMERIC in ad-account spend-cap enforcement (#14076)
* fix(#13415): fail closed on corrupt NUMERIC in ad-account spend-cap enforcement
The two race-safe (advisory-lock transaction) spend-cap enforcement points in
AdCampaignsRepository — createWithAccountSpendCapCheck and
claimAllocationChangeWithAccountSpendCapCheck — read adAccounts.spend_cap_credits
and SUM(adCampaigns.credits_allocated) (both numeric(12,2) -> driver string) via
a bare Number(). A corrupt 'NaN'::numeric cap or allocated row (valid Postgres
NUMERIC, migration artifact, or manual DB edit) makes Number()=NaN, and the cap
gate 'allocated > cap + 1e-9' is FALSE for NaN, so the ad-account spend cap is
silently BYPASSED = unbounded ad spend (fail-OPEN money gate).
New colocated fail-closed boundary ad-campaigns-spend-cap-numeric.ts:
parseAdAccountSpendCapCredits (throws on empty/non-finite/non-NUMERIC, allows
domain 0) + parseAdCampaignsAllocatedTotal (absent SUM = legit domain 0, corrupt
SUM throws). Wired into both enforcement sites.
The repo returns a new cap_error status (not a raw throw) so the callers'
existing compensation runs: in updateCampaign the credit debit + provider budget
increase have ALREADY been applied when the cap check runs, and compensation
(refund + provider revert) previously only ran for RETURNED cap_exceeded/conflict
statuses. A raw throw would skip compensation and leave the third-party campaign at
the larger budget with credits debited but the DB denied. Both createCampaign and
updateCampaign now handle cap_error: create's surrounding catch compensates;
update's cap_error branch refunds + reverts the provider then fails closed.
(codex round-1 P1.)
Tests: 15/15 parser bun test (boundary incl 'NaN'/Infinity/1e3/0x10 + fail-open
regression proving bare Number() bypass + healthy-gate control) + a new
credit-reconciliation cap_error compensation test (reversion-proven: FAILS when
the caller's cap_error handling is reverted). Full credit-reconciliation suite
15/15 + ad-account-approval spend-caps 20/20 no-regression. tsgo 0-errors-in-touched
(pre-existing baseline node-redis/plugin-mcp/anthropic only), biome clean,
error-policy-ratchet no-new-slop. Mirrors merged #13454/#13474/#13482/#13486/
#13503. Distinct file from open #14037 (advertising service-layer spend-cap).
-- [sol-orch]
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(cloud): fail closed before ad spend side effects
* fix(cloud): reject negative ad spend-cap numerics
---------
Co-authored-by: wakesync <shadow@shad0w.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ci: supersede stale develop-push runs — per-workflow concurrency (#14125)
The PR-scoped concurrency groups shipped in #14069 fall back to
github.run_id on push events, so every develop push gets a unique
concurrency group and no run ever supersedes the previous one. Combined
with cancel-in-progress gated to pull_request only, develop pushes never
cancel. A 37-merge wave this morning queued ~2,545 push runs on develop,
starving the 46-runner self-hosted fleet: PR-required legs starved,
auto-merge stalled, admin-merges pushed develop again and amplified the
fan-out. Death spiral.
Fix (20 pure-CI workflows): change the push fallback from github.run_id
to github.ref so all develop pushes share one workflow-scoped group, and
extend cancel-in-progress to fire on push events too. A newer develop
push now cancels the superseded older run of the same workflow.
Safety:
- PR behavior unchanged: pull_request.number still wins the group key, so
each PR dedups exactly as before #14069.
- Groups stay workflow-scoped (each keeps its own prefix / github.workflow),
never a shared repo-wide group.
- merge_group and schedule events are NOT in the cancel set, so the merge
queue and nightly runs always complete (test.yml verified).
- Deploy/publish/build-push workflows (cloud-*-deploy, deploy-*, publish-*,
build-agent-image, build-example-app-images, tee-build-deploy, release*)
are untouched — they keep cancel-in-progress: false so an in-flight
deploy is never interrupted.
- gateway-discord/webhook already dedup correctly on push (ref-keyed) and
were left as-is.
Validated: actionlint clean on all 20 touched files; ci-workflow-dedup,
ci-turbo-cache, windows-ci, and scenario-pr workflow contract tests pass
(real-repo assertions included).
Co-authored-by: wakesync <shadow@shad0w.xyz>
* feat(trace): per-task usage roll-up across traceId (#13775 item 5) (#14070)
* feat(trace): per-task trajectory usage roll-up across traceId (#13775 item 5)
Add rollUpTrajectoryUsage (core) summing RecordedTrajectory metrics grouped by
the shared traceId, plus OrchestratorTaskService.getTraceUsage reading the
ingested sub-agent trajectory artifacts + a GET /tasks/:taskId/trace-usage
route. Kept separate from the ACP-frame TaskUsageSummary so the two spend
sources are never double-summed. Additive; no existing DTO/contract change.
Co-authored-by: wakesync <shadow@shad0w.xyz>
* test(trace): getTraceUsage per-trace roll-up over ingested trajectory artifacts (#13775 item 5)
Real InMemoryTaskStore + on-disk child-trajectory files exercised through the
real ingest path: sums metrics grouped by traceId, skips a corrupt artifact
without throwing, empty-roll-up (not null) for no artifacts, null for unknown
task. Guarded with a stale-dist skip (same rationale as
child-trajectory-ingest.test.ts) so a local prebuilt-core run degrades instead
of red-ing.
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(trace): dedupe trajectory artifacts by path in getTraceUsage (#13775)
Codex review [P2]: ingestChildTrajectories rescans the task-wide child dir on
every task_complete, so a multi-session/retried task can hold multiple artifact
rows pointing at the SAME trajectory file. The roll-up read each row, inflating
tokens/cost. Dedupe by distinct path before reading/summing; add a regression
test asserting a file counts once despite duplicate rows.
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(orchestrator): surface partial trace usage
---------
Co-authored-by: wakesync <shadow@shad0w.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(core): per-project memory scoping helpers (#13776 item 4) — [sol-orch] (#14078)
* feat(core): per-project memory scoping helpers (#13776 item 4)
Add worldId-mapping memory scoping per design D3: each Project maps to a
dedicated worldId so agents in project A never retrieve project B's
memories. No memory schema change (the rejected projectId-column path);
Memory.worldId already exists and getMemories/searchMemories already
filter by it.
New core util packages/core/src/utils/project-memory-scope.ts:
- projectWorldId(agentId, projectId): deterministic per-agent world for a
project. Verified identical to createUniqueUuid(runtime, 'project:'+id),
so the agent-package ensureProjectWorld helper and these store-layer
helpers derive the same world.
- scopeMemoryFilterToProject / scopeMemoryToProject: inject/stamp the
project world on reads/writes. No projectId => unchanged (global,
backward-compatible). Fail-closed on a conflicting caller worldId.
- assertMemoriesInProject: fail-closed retrieval guard for paths that
bypass the store worldId filter (e.g. id-batch fetches); legacy
unscoped memories (no worldId) pass through and stay retrievable.
Backward compatibility: projectId omitted anywhere => zero behavior
change. Legacy memories without a worldId remain retrievable under an
unscoped read and are not treated as cross-project leaks.
Tests (17 total, real store, no mock-of-subject):
- project-memory-scope.test.ts (14): derivation determinism/uniqueness,
scoped write->read roundtrip, cross-project isolation, fail-closed
guards, legacy backward-compat, #13948-consistent filter normalization
(empty projectId == absent, single idempotent worldId predicate).
- inMemoryAdapter.project-memory-scope.test.ts (3): end-to-end roundtrip
against the real InMemoryDatabaseAdapter via world->room->memory
partition (getMemoriesByWorldId) proving isolation + legacy retrieval.
Part of #13776. Item 4 store-layer surface only; agent-runtime
ensureProjectWorld wiring + task-room worldId binding deferred (noted on
the issue). UI switcher (item 5) untouched.
Author: Sol <sol@shad0w.xyz>
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(core): honor project world filters in memory fallback
* fix(core): use ElizaError for project memory scope
* chore(feed): declare audited runtime env keys
---------
Co-authored-by: wakesync <shadow@shad0w.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(app-core): repair runtime-mode.disk test import (module moved to @elizaos/agent)
`src/runtime/mode/runtime-mode.disk.test.ts` is RED on develop — it fails to
even load: `Error: Cannot find module './runtime-mode'`. Its only sibling in
`src/runtime/mode/` is the test itself; `getRuntimeModeSnapshot` now lives in
`@elizaos/agent` (app-core's own `api/runtime-mode-routes.ts` and `index.ts`
already import/re-export it from there). The test's import path was never
updated when the module moved, so vitest can't resolve it → "no tests" / file
failure.
Fix: import `getRuntimeModeSnapshot` from `@elizaos/agent`. The disk-backed
resolution the test exercises (real loadElizaConfig over a temp state dir) is
unchanged. Full file: fail-to-load → `4 passed (4)`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auth): scope shared Steward cookie reads
* style(auth): format scoped steward cookie reads
* test(auth): format steward cookie fallback coverage
* fix(app-core): import runtime mode resolver from agent subpath
* feat(#14112): project switcher UI + per-project task filter + single-project no-chrome guard (#14135)
* feat(#13776): project switcher UI + registry list/switch API (item 5)
Part of #13776 (first-class Project entity, design D3). Item 5 only: the
per-project UI switcher, backed by the already-merged core project registry
(#13851/#13815). Items 1-3 (registry + task.projectId + workdir binding) are
merged; item 4 (per-project memory scoping) is owned by a separate lane and is
untouched here.
Agent API (thin projection over @elizaos/core project-registry):
- GET /api/projects -> { projects, activeProjectId }
- POST /api/projects/:id/activate -> switch active project, return the record
- Read + switch verbs only (no register/delete), so the switcher can never mint
or destroy projects behind the user's back; absent registry -> empty list.
- Lazy-route wrapper + server dispatch mirror the existing bug-report pattern.
- Handler takes injectable readRegistry/activate deps for hermetic tests.
Client:
- listProjects()/activateProject() on ElizaClient; 404 -> empty registry so the
switcher self-hides on mobile/web where the surface isn't hosted.
- listCodingAgentTaskThreads gains an optional projectId filter param (wired to
#13948's /api/orchestrator/tasks?projectId=).
UI (ProjectSwitcher):
- Compact dropdown in the CodingAgentTasksPanel header showing the active
project; selecting a row switches it and re-filters the task list by the new
projectId. Design tokens only (no raw hex / purple / blue / backdrop-blur),
single lucide FolderGit2 glyph + Check on the active row. Self-hides when the
registry is empty (dead chrome guard).
Tests (real, not green-by-skip):
- packages/agent project-routes.test.ts: 7 pass (list shape, activate 200,
unknown id 404, slashy id 400, absent-registry empty, read-throw 500).
- plugin-task-coordinator ProjectSwitcher.test.tsx: 3 pass (renders per-project
rows w/ active marker, switch calls activateProject + fires callback, empty
registry self-hides).
Verification:
- bun run --cwd packages/agent test src/api/project-routes.test.ts --run -> 7/7
- bun run --cwd plugins/plugin-task-coordinator test src/ProjectSwitcher.test.tsx --run -> 3/3
- agent typecheck: no new errors from these files (only a pre-existing
@elizaos/plugin-streaming unbuilt-optional-plugin resolution error).
- ui typecheck: no errors reference the touched client files.
- codex review: hung on this host (>100s, zero output) as flagged; self-review
done instead.
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(task-coordinator): gate task fetch on project scope
* fix(project-switcher): satisfy import ordering
* fix(projects): reject malformed project ids
* fix(projects): stabilize legacy workspace ids
* fix(projects): clean develop rebase
* feat(#14112): hide project switcher in the degenerate single-project case
The #13776 item-5 switcher (from PR #14080, merged into this branch) rendered
whenever >=1 project was registered, so a user with a single workspace folder
saw a new dropdown in the Tasks panel header. Issue #14112 requires the
single-project case to look exactly like pre-switcher builds.
Hide the switcher when projects.length <= 1 and report a null projectId to the
host in that case, so the task list stays unfiltered (project-unbound tasks
stay visible) — byte-identical to today. The switcher only appears once a
second project exists to switch between.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Sol <sol@shad0w.xyz>
Co-authored-by: wakesync <shadow@shad0w.xyz>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Shaw <shawgotbags@gmail.com>
* fix(orchestrator): broker-first subagent Cloud access; gate raw ELIZAOS_CLOUD forwarding behind opt-in (#14118) (#14134)
* feat(orchestrator): broker-first subagent Cloud access; gate raw ELIZAOS_CLOUD forwarding behind opt-in (#14118)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(orchestrator): cover broker-first Cloud guidance + gated cloud-key forwarding (#14118)
- should-forward-env: raw ELIZAOS_CLOUD* stripped by default, restored under opt-in
- app-deploy-guidance: register/deploy render as broker apps.create/containers.create; edad container-runtime key stays on the credential bridge
- sub-agent-identity: broker section states broker-first Cloud + credential-bridge escape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Shaw <shawgotbags@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(plugin-training): annotate trajectory map param to unblock develop typecheck (#14131)
TS7006 implicit-any on trajectoryList.trajectories.map((trajectory)=>) at
fine-tuning-panels.tsx:461 failed every PR's typecheck lane. Annotate with the
existing TrajectorySummary type (declared at :176). Verified: plugin-training
tsc --noEmit clean.
Co-authored-by: NubsCarson <nubsontopgang@gmail.com>
* refactor(ui): Character — section strip via app-shell group, delete overview CTA grid, collapse dual render paths (#13591) (#14123)
* refactor(ui): Character — app-shell group section strip, delete overview CTA grid, collapse dual render paths (#13591)
Fold the Character family (Personality/Relationships/Skills/Experience) into
one group under a shared "Character" ViewHeader + secondary section strip
(CharacterSectionNav), mirroring the Wallet section-nav idiom. The strip reuses
the shared presentational SectionTabStrip + the extracted navigateToSectionPath
helper; members are a fixed host-owned set so the strip is static (no registry
round-trip, no route hijack). Threaded through App.tsx as `characterNav` next to
`walletNav`.
Delete CharacterOverviewSection (+ stories) — the banned EmptyCta recommendation
grid — and collapse CharacterHubView's dual render path: it once rendered all six
hub sections internally (overview + the four now-promoted top-level views); it now
owns Personality only. That kills the dead useCharacterHubData 5-fetch hook, the
overview widget builder, and the section-list helpers. Personality drops its inline
Save button for pure debounced autosave (step 7).
Knowledge decision (documented on #13591): it stays a standalone multimedia PEER
hub (#13594), NOT a Character sub-tab — excluded from the strip, keeps its own
"Knowledge" header. Companion scene-overlay tablist loses its Knowledge editor
tab; its remaining personality/style/examples tabs are editor sub-panels, a
distinct axis from the family sections (one section-tab definition per axis).
Section views (Skills/Experience/Relationships/Personality) render headerless
bodies under the strip. Relationships gains `hideHeader` so its mobile sidebar
trigger falls back to the workspace inline drawer button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(ui): Character section strip + collapsed hub; fix stale wallet strip testid (#13591)
- CharacterSectionNav.test.tsx: header/back geometry, the four ordered family
tabs with no Knowledge tab, active resolution from route + the legacy
/character/relationships alias, click navigation, and isCharacterSectionPath
(accepts every section + alias, rejects Knowledge and unrelated routes).
- CharacterHubView.test.tsx: the Personality-only collapse — panels render, no
own ViewHeader/back, no overview EmptyCta chips, and none of the removed
embedded sub-view surfaces (documents/experience/relationships), proving the
dual render path is gone; no manual Save button.
- WalletSectionNav.test.tsx: repair a stale `wallet-section-tabs` testid (the
shared SectionNav has emitted `section-nav-<group>` since #13586) — 4
pre-existing red tests in a file adjacent to the shared SectionNav this PR
touches, now green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): format character redesign files
---------
Co-authored-by: Shaw <shawgotbags@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): wrap fine-tuning route in agent surface (#14120)
* test(ui): cover fine-tuning agent surface grants
* test(ui): isolate fine-tuning surface fixture
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(#14110): dedupe child-trajectory ingest against already-recorded artifacts
ingestChildTrajectories rescanned the per-TASK child-trajectory dir on
every per-SESSION task_complete with no dedupe. A follow-up prompt
re-completing the same session re-attached every file as a brand-new
artifact (fresh randomUUID id) and appended duplicate childTrajectoryIds;
a respawned second session re-ingested session A's files and stamped
session B's correlation (sessionId/parentStepId/traceId) onto trajectories
A actually recorded, corrupting the file<->DB trace join #13871 provides.
Fix: dedupe against the task's already-recorded trajectory artifact paths
(read via store.getTask) before ingest, skipping files already attached.
The recorded-artifact set is the persistent dedupe key (survives restart
with the task document) and skipping already-attached files preserves the
original ingesting session's correlation. The MAX_CHILD_TRAJECTORY_ARTIFACTS
cap now applies to genuinely-new files only so a large ingested backlog
can't starve fresh trajectories out of the window. childTrajectoryIds
append is union-deduped defensively.
Tests (child-trajectory-ingest.test.ts, reversion-proven: both fail on
pristine source): same-session re-completion attaches no duplicate
artifacts/ids; a respawned session B ingests only its own new file and
leaves session A's file + correlation untouched.
-- [sol-orch]
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(ui): finish the #13914 sweep — stale 'Instances' skeleton + last dead PageHeaderProvider (#14138)
Two residuals the ultracode adversarial verification surfaced (#13914 core
fix was correct; the sweep missed these):
- ContainersSkeleton (the agents-table loading skeleton) still declared
Name/Port/Instances/Deployed columns — none of which the real Agents table
has anymore. It flashed 'Instances' + stale headers on every
/dashboard/agents load before the live Agent/Status/Runtime/Web UI/Created
headers painted. Columns realigned to the real table.
- McpsRoute mounted a local PageHeaderProvider that shadowed ConsoleShell's
(the standalone route is group:dashboard → ConsoleShell already supplies
one), so /dashboard/mcps rendered with no top-bar title — the same
dead-context bug #13914 fixed on the other pages, missed here. Removed;
the settings-section entry keeps its CloudSettingsSectionShell provider.
Typecheck clean, biome clean.
Co-authored-by: NubsCarson <claude3@nubs.site>
* fix(orchestrator): reclaimIdleSession resolves session→task via store fallback (#14106)
The keepAlive starvation guard (`reclaimIdleSession`) resolved a live session
to its owning task by reading the in-memory `sessionTaskIndex` directly. After
a parent restart that index is empty, but pre-restart keepAlive sessions are
still live and their session→task mapping survives only in the durable store.
The guard therefore could not map any pre-restart session to its (terminal)
owning task, so it reclaimed nothing — queued tasks starved behind zombie
sessions whose owning tasks were already done/failed/archived.
Route the lookup through `resolveTaskId`, which already falls back to
`store.findSession` on an index miss and re-primes the index — the same
fallback its sibling resolvers use.
Regression test drives the REAL orchestrator + REAL AcpService worker cap:
spawns a keepAlive session, marks its task terminal, clears the in-memory index
to simulate a restart, then parks a queued task at full capacity. Against the
old index-only code the drain never reclaims the zombie (poll times out); with
the store fallback the zombie is stopped and the queued task is admitted.
Fixes #14106
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cloud): add ElizaError to the Worker core stub so the API bundle builds (#14128)
* fix(orchestrator): route operator status writes through the transition table (#14105 audit)
Operator lifecycle paths wrote task status directly instead of going through
the legal-transition table #13830 introduced, so two related gaps existed:
Defect 1 — direct `interrupted` writes could move a terminal task. `stopTaskAgent`
and `stopActiveSessions` (x2) wrote `updateTask(id, { status: "interrupted" })`
on the ACP-unavailable / stop-failure paths. A `done`/`failed`/`archived` task
still holding a live keepAlive session whose stop failed was stomped to
`interrupted` — `done → interrupted` is not a table edge, violating the
terminal-immutability invariant #13830 exists to enforce (same class as #14099).
Defect 2 — `nextTaskStatus` was dead on the production path: exported but consumed
only by tests, so the `interrupted`/`archived` table rows asserted a legality
nothing enforced at the write sites.
Fix: route the three `interrupted` writes through `advanceTaskStatus` (table
lookup → no-op on an illegal edge) and resolve `archiveTask`'s target through
`nextTaskStatus(from, "archived")`. Illegal terminal edges become legal no-ops
instead of silent stomps; the table rows stop being dead documentation.
Tests: terminal `done` task with a live session survives a failed/unavailable
stop as `done` (both stopTaskAgent and archive paths); a non-terminal task still
advances to `interrupted`; archive of a terminal task with no live session lands
via the table edge.
Fixes #14105
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(orchestrator): unify workdir precedence across action + service layers (#14108) (#14145)
The two workdir-binding mechanisms (#13801 boundWorkdir, #13851 projectId)
resolved precedence differently at the action layer vs the service layer, so
the same operator input produced different workdirs depending on entry point:
- Action layer (tasks.ts): a project-bound task discarded an explicit caller
workdir *silently* and forced lockWorkdir.
- Service layer (spawnAgentForTask): honored the explicit workdir over the
bound pin, and ignored the project binding (projectId) entirely — so a
project-bound task spawned via the /agents API could land in boundWorkdir
or an explicit workdir instead of the project localPath.
Fix: introduce a single shared resolver resolveTaskSpawnWorkdir() that both
entry points delegate to, enforcing ONE precedence order:
project localPath > explicit caller workdir > boundWorkdir
documented on the resolver where the binding precedence is defined. When an
explicit caller workdir loses to a project binding we now log a LOUD warning
instead of silently substituting (per the #14108 audit recommendation). The
first-spawn re-pin no longer treats an ignored explicit workdir as a rebind.
Tests (real vitest, in-worktree):
- project-binding.test.ts: +7 precedence/loud-warn cases (12 total, pass)
- task-workdir-binding.test.ts: +1 end-to-end case proving a project-bound
task spawns in its project localPath even when the service caller passes a
conflicting explicit workdir (9 total, pass)
Fixes #14108
— [sol-orch]
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(orchestrator): isolate acp git indexes per session (#14042)
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(#13773): measure free disk against nearest existing ancestor in workspace backpressure
enforceWorkspaceDiskBudget runs on the scratch/clone root BEFORE that root (or
its task-* child) is created. statfs throws ENOENT on a not-yet-created path, so
freeBytesFor returned undefined and — with the default 2 GiB free-disk floor —
the very first isolated spawn on a cold machine (where $TMPDIR/eliza-acp has
never been created) was refused with "disk budget exceeded (free-disk-floor)
used=0 free=0", defeating the whole spawn path. Walk up to the nearest existing
ancestor (the same filesystem that will host the new dir); a genuinely
unreadable filesystem still yields undefined and fails closed, preserving the
floor's fail-closed contract. Adds a regression test that a not-yet-created root
is measured against its existing parent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#14109): age-gated GC for the child-trajectory state dir (#14151)
The per-task `<stateDir>/orchestrator/child-trajectories/<taskId>` dir was
never garbage-collected. Ingest attaches trajectory JSON by reference and never
deletes it, and no workspace-GC path reaches the state dir (`AcpService`'s
scratch GC only scans workspace roots), so every trajectory-gated eliza-backend
task run leaked per-task JSON under the state dir forever — the same disk-leak
class as the 3.6TB worktree-farm incident (#13773), relocated into the state dir.
Add `gcChildTrajectoryDirs`, a startup sweep (mirroring
`AcpService.cleanOrphanedScratchWorkdirs`) of the child-trajectories root. A
per-task dir is reclaimed only when BOTH hold:
- the owning task is terminal in the store, OR has no task doc (an orphan); and
- the dir has been idle (newest entry's mtime) past a retention window.
The age gate is the load-bearing safety: a not-yet-ingested or in-flight
trajectory is recent by construction, so it is never deleted — even if its task
looks terminal (e.g. a respawned session still writing). A store-read failure
keeps the dir (data-loss guard), never treats it as an orphan. Window defaults
to 24h, overridable via `ELIZA_ORCHESTRATOR_CHILD_TRAJECTORY_GC_MAX_AGE_MS`.
Deletes are best-effort; a locked/vanished dir is skipped and retried next boot.
This is purely additive to the #14110 ingest-dedupe fix: GC only touches dirs
that are terminal/absent AND aged, so the intentional 'leave files on disk for
cross-session dedupe' invariant (recent files) is preserved.
Also fixes the stale comment at `childTrajectoryDir` that claimed #13773's
workspace-GC registry could reclaim it — it cannot; the dir is under the state
dir, which that registry never scans.
Tests: `child-trajectory-gc.test.ts` — aged terminal dir reclaimed, recent
un-ingested dir preserved, live-task dir never reclaimed, aged orphan reclaimed,
recent orphan preserved, window override honored, absent-root no-op. The three
safety cases are reversion-proven (they FAIL when the age gate is removed).
Fixes #14109
Co-authored-by: wakesync <shadow@shad0w.xyz>
* feat(browser): Safari/Arc-style folded tabs — completes #13596 (#14149)
* feat(#13596): folded browser tab switcher component
Safari/Arc-mobile-style fold: a compact count control + a stacked
tab-card switcher overlay, replacing the permanent tab sidebar strip.
Pure foldBrowserTabs helper for deterministic grouping/count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(#13596): fold browser tabs into the switcher, drop the sidebar strip
Wire BrowserWorkspaceView to the folded-tab model: a toolbar count control
opens the switcher (no permanent sidebar), agent tabs stay distinct, active
tab always visible. Removes the AppPageSidebar tab strip + collapsed-section
persistence. 16 real component tests cover fold/switch/close/re-seed and the
44px touch targets.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(#13596): declare browser's native-webview surface manifest
Make the isolation catalogue's documented native-webview level authoritative
on the browser tab (background: opaque, the default made explicit). Policy
declaration only — the native embedding itself lives in the tab renderers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(#13596): drive folded-tab switcher in browser-workspace e2e
Rewrite the browser-workspace ui-smoke spec for the folded UX: open tabs via
the fold-control switcher, assert the tab count + cards there instead of the
removed sidebar strip. Covers create/navigate/switch/close + close-last re-seed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(#13596): narrow browser surface manifest to fix strict typecheck
resolveSurfaceManifest wants a SurfaceManifest; narrow away the {shared}
path-predicate variant so tsc is clean (runtime behavior unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Shaw <shawwwwwwwwww@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(app): clouds sunset wallpaper as default background (#14157)
Ship the curated 'Ember Night' sunset-in-the-clouds wallpaper as the boot
default background, replacing the generated ember-night gradient data URL.
The photo is a generic clouds/sunset image (no person, character, branding,
logo, or text) served as a same-origin static asset from packages/app/public.
- add packages/app/public/bg-sunset.jpg (served, cacheable; not bundled into JS)
- ui-preferences: DEFAULT_BACKGROUND_CONFIG.imageUrl -> /bg-sunset.jpg, and the
matching ember-night catalog tile resolves to the same asset so the boot
default and its gallery tile stay in sync (#13538). The shader field remains
the fallback when the image is cleared or fails to load.
- drop the now-unused EMBER_NIGHT_DATA_URL gradient constant; gradientDataUrl
still backs every non-default image entry (they stay code-free data URLs).
The curated default is a compile-time constant same-origin path, so the
apply-channel confinement invariants (#11088 / #13523) and the grant-gated
wallpaper resolver (#14068) are unaffected: this changes WHAT the default
image is, not WHO may paint the shared wallpaper.
Note: this intentionally relaxes the strict #13538 'zero committed binary'
rule for the ONE curated default asset (846KB served, not inlined); all other
catalog image entries remain code-free gradient data URLs. Test invariant
updated to permit exactly the default served asset.
Co-authored-by: wakesync <shadow@shad0w.xyz>
* docs(bench): live multitask-bench evidence for #13777 (eliza/hermes/openclaw, N=1/5/10)
Clears the live-model-keys gate on #13777. Ran multitask-bench live on
gpt-oss-120b (Cerebras) for all three harnesses at N∈{1,5,10} on the frozen
10-scenario LifeOps sample:
- eliza (shared_runtime): N10 mean_score 0.310, jain 1.000→0.934→0.909
- hermes (process_per_turn): N10 mean_score 0.430, interference ≈0
- openclaw (direct-compat, partial, not CLI-native): N10 mean_score 0.475
Score-interference is ≈0 on every lane; the shared-runtime cost surfaces as
eliza's monotonic Jain-fairness drop, while process-isolated lanes stay ≥0.98.
hermes/openclaw reports pass the registry scorer as publishable. Trajectories
read by hand (one clean completion + one failure per lane). No secrets in any
artifact. Part of epic #13766.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(#14107): reconcile project registries — core projects.json is canonical, port worldId, delete dead plugin ProjectRegistry
The core `<stateDir>/projects.json` registry (packages/core) is the canonical,
live store wired into workspace resolution, the desktop picker, the plugin bind
seam, and GET /tasks?projectId=. The plugin-agent-orchestrator ProjectRegistry
(async InMemory/File/facade writing ~/.eliza/plugin-acp/orchestrator-projects.json)
had zero production consumers — only barrel exports and its own test.
Changes:
- Port `worldId` onto core `ProjectRecord` (validated, persisted, preserved on
re-upsert) and add `PROJECT_WORLD_ID_PREFIX` — the `project:<id>` stringToUuid
convention, kept in core so the string lives with the record it stamps.
- Implement #13776 design D3 on the LIVE path: `OrchestratorTaskService.bindProject`
now stamps `task.worldId = deriveProjectWorldId(projectId)` from the bound
project (an explicit caller worldId still wins). Previously nothing set the
task's worldId from a project, so the per-project memory partition was
unimplemented.
- Delete the dead plugin ProjectRegistry (classes + barrel exports + its test).
No on-disk migration: nothing ever wrote orchestrator-projects.json in prod.
Hardening (same files):
- `writeProjectRegistry` refuses to overwrite a present, newer-schema
projects.json (a future version reads as null via isProjectRegistry; a naive
v1 rewrite would silently drop the user's projects) — throws instead.
- `upsertProject` canonicalizes localPath (realpath) at write AND match time so
/tmp/x vs /private/tmp/x register one project, not a duplicate.
- Documented that cross-process read-modify-write of projects.json is unlocked
(atomic rename prevents torn writes, not interleaved updates) — the accepted
workspace-folder-config.ts precedent.
Tests (real on-disk registry + real OrchestratorTaskService, no mocks):
- core: worldId persist/preserve, symlink canonicalization dedup, refuse-clobber-v2.
- plugin: deriveProjectWorldId determinism + stringToUuid convention; end-to-end
createTask worldId stamping for bound/unbound/explicit-worldId cases.
Fixes #14107
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agent): preserve incumbent view-scoped action owners
* fix(ci): guarantee Node on PATH in setup-bun-workspace + stale-base-guard
Self-hosted (hetzner-robot) runners are not guaranteed to expose `node`
on the step PATH. Two CI surfaces relied on a pre-installed system Node
and failed deterministically with "node: command not found" on the
eliza-prod-robot-* fleet:
1. setup-bun-workspace composite — its very first step
(turbo-cache-github -> turbo-cache-key.mjs) invokes `node` directly.
Jobs that use this composite as their only toolchain setup
(coverage-gate, feed-env-audit / Env Audit, scenario-matrix,
voice-workbench) hit the failure. Fix: add a setup-node step
(default on, toggleable via setup-node input) before turbo-cache.
Idempotent for callers that already run actions/setup-node
(keyless-harness, the aesthetic audits) — they just re-select 24.
2. stale-base-guard.yml — self-test + detector steps call `node` with
no setup-node and no use of the composite. Fix: add setup-node
after checkout.
Root cause was runner-side (node absent on prod-robot fleet) but the
robust fix is to make the workflows self-sufficient rather than depend
on undocumented runner state. This is the same class as the app/cloud
audits, which already carry their own setup-node.
Evidence (develop, last 24h): Env Audit 66 non-cancelled failures,
Stale Base Guard 58, coverage-gate 34 — dominated by this signature.
Contract tests pass: ci-bun-version-contract, ci-workflow-dedup-contract,
turbo-cache-key (16), scenario-pr-workflow (11). actionlint clean on both
touched workflows; composite YAML validated.
[sol-orch] ci-medic
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(trajectories): document gate tiers, treat blank env as unset, hoist scenario opt-in (#14111) (#14143)
* fix(trajectories): document gate tiers, treat blank env as unset, hoist scenario opt-in (#14111)
Three residual gaps from the #13871 trajectory-recording gate (epic #13766):
1. Docs: the file-recorder default flip to opt-in for NODE_ENV=production|test
was undocumented and training/SECURITY.md:33 still described capture as
default-on gated only by ELIZA_DISABLE_TRAJECTORY_LOGGING. Documented the
full resolveTrajectoryGate precedence in SECURITY.md and training
CLAUDE.md/AGENTS.md.
2. Blank env silently opted out: coerceFlag treated ELIZA_TRAJECTORY_LOGGING=""
(an empty .env line) as an explicit opt-out, disabling dev recording. Now
blank/whitespace-only = unset, falling through to the NODE_ENV default, per
the repo's blank-is-unset env contract (presentEnvValue, boot-env.ts; #13802).
3. Scenario CLI only opted in with a run dir: the ELIZA_TRAJECTORY_LOGGING="1"
set lived inside the effectiveRunDir branch, so a bare `eliza-scenarios run`
under NODE_ENV=test|production captured nothing despite scenario trajectories
being required PR evidence. Hoisted the opt-in out of the branch into a pure,
unit-tested helper (trajectory-opt-in.ts) that respects an operator-set knob.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs and format trajectory logging gate
---------
Co-authored-by: Shaw <shawgotbags@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ci(android): add scheduled host emulator cadence (#13580) (#13860)
Rebased shaw's draft onto current develop and resolved the workflow
conflict: preserved develop's Hetzner-fleet runs-on expression
(HETZNER_FLEET_ONLINE gate) while keeping the draft's schedule support.
- Weekly hosted x86_64 emulator run (cron 17 9 * * 1), backend=host,
API 34 by default since schedule events carry no dispatch inputs.
- android-e2e now runs on workflow_dispatch OR schedule. Never on PR:
no per-PR runner load added (aligns with the fleet-throughput
direction in #14051 / #14057). PRs stay label-gated via pr-device-smoke.
- test:e2e:android:routes hard-gated for host/cloud, signal-only for the
known-broken local backend on hosted x86_64.
Validation: actionlint clean, python yaml load ok, git diff --check clean,
ci-workflow-dedup-contract + ci-path-gate self-test pass, cron slot checked
against all scheduled workflows (weekly Monday slot, no hard collision).
Co-authored-by: Sol <sol@shad0w.xyz>
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(test-harness): resolve nested cloud/* workspace packages to source (#14165)
`buildHarnessSourceAliases()` only scanned the top level of `plugins/` and
`packages/`, so `@elizaos/*` packages nested deeper were never given a
source alias. The eliza monorepo nests several published packages:
@elizaos/cloud-routing -> packages/cloud/routing
@elizaos/cloud-api -> packages/cloud/api
@elizaos/cloud-shared -> packages/cloud/shared
@elizaos/cloud-sdk -> packages/cloud/sdk
@elizaos/gateway-* -> packages/cloud/services/*
...
Without a source alias, Vite falls back to each package's `exports`
(`./dist/index.js`). Under the keyless harness install
(`bun install --ignore-scripts`) those dist files are never built, so the
per-plugin harness proofs (`plugins/plugin-*/vitest.harness.config.ts`,
which reuse this builder) failed deterministically with:
Failed to resolve entry for package "@elizaos/cloud-routing".
File: packages/core/src/cloud-routing.ts
(core re-exports the cloud routing surface, so every plugin harness that
boots a real runtime through @elizaos/core hit it.) This was the #2 CI
failure signature on develop — 65 non-cancelled Keyless Harness E2E
failures in 24h.
Fix: replace the flat `readdirSync` with a bounded recursive descent
(`collectWorkspacePackageDirs`) that walks through grouping directories
(dirs with no package.json) and records the first package dir it finds,
pruning node_modules/dist/.turbo/.git/coverage. Purely additive: base
produced 477 aliases, now 615 (+46 nested packages x 3 rules). All 207
exact-name aliases resolve to an on-disk source file (verified). Top-level
packages are unchanged (recorded, not descended into).
[sol-orch] ci-medic
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(pwa): dark host-chrome (theme-color + launch bg) + shell cache bump (#14166)
The PWA painted the launch orange (#ef5a1f) as a PERSISTENT surface, not a
boot flash, so any strip the app's fixed background layers don't reach (most
visibly the iOS home-indicator safe-area under the composer) rendered as a
glowing orange band against the dark app.
Three-part root cause, three-part fix:
1. app.config.ts themeColor/backgroundColor were #ef5a1f. The build
substitutes __APP_THEME_COLOR__ from this, and on iOS standalone PWAs
<meta name="theme-color"> paints the home-indicator inset. Now #160d07
(= DEFAULT_BACKGROUND_COLOR, the ember-night home base). The
__APP_THEME_COLOR__ substitution contract is preserved; only the config
value changes.
2. index.html html/body/#root + .eliza-preboot-shell backgrounds used
var(--launch-bg, #ef5a1f). That's the page background under the app
forever, not FOUC-only. Now #160d07 (also the renderer root CSS
fallbacks in packages/ui styles.css).
3. sw.js SHELL_CACHE_NAME bumped v2 -> v3 so installed PWAs drop the stale
orange shell on activate instead of re-serving it from cache.
Native boot-splash surfaces (capacitor config, Android splash resources,
iOS LaunchScreen) intentionally KEEP the splash orange: they are true
boot-flash surfaces fully covered once the app paints. brand-surface.test.ts
now encodes the persistent-vs-splash split (LAUNCH_DARK vs SPLASH_ORANGE);
its first assertion was already red on develop (it pinned
DEFAULT_BACKGROUND_COLOR to orange, but develop moved it to #160d07 in
#13438) and is green again with the realigned expectations.
Ported from feat/ui-overhaul (canon series PR1).
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(scenario-runner): treat blank trajectory knob as unset
* feat(agent): Character view-scoped actions (FILL_BIO / style / examples) (#14167)
Declare scopedActions on the builtin `character` view (#14155, the
deferred step 8 of #13591/#14123): VIEW_CHARACTER_FILL_BIO,
VIEW_CHARACTER_ADD_STYLE_RULE, VIEW_CHARACTER_ADD_MESSAGE_EXAMPLE.
Each reuses the #13589 view-scoped-action mechanism 1:1 — no parallel
DOM path — expanding into the same agent-fill/agent-click interact
sequence the element-level protocol drives, gated on the Character view
being foreground. Targets only always-mounted CharacterEditor ids
(identity-bio, style-add-input-all/style-add-all,
example-add-conversation); row-level index-dependent ids are omitted so
no declaration targets an unmounted element. The boot path already
registers BUILTIN_VIEWS scoped actions; this makes the character view
non-empty.
Tests: 5 new cases drive the real BUILTIN_VIEWS declarations through the
mechanism (registration, active-view gating, per-action dispatch, and a
loud VIEW_SCOPED_ACTION_ELEMENT_MISSING on an unmounted target). Full
view-scoped-actions suite: 14 passed.
Fixes #14155
Co-authored-by: wakesync <shadow@shad0w.xyz>
* fix(orchestrator): single-source the state-lost retry budget — no 4th orphan respawn against a failed task (#14104) (#14150)
* fix(orchestrator): single-source the state-lost retry budget so no 4th orphan worker spawns against a failed task (#14104)
The task service and the sub-agent router kept independent retry budgets for
the same crash-retry event stream and disagreed: the task went terminal
`failed` at the 3rd errored session (`erroredSessions >= 3`) while the router's
own cap (`count <= 3`) still respawned a 4th orphan worker on the same
`session_state_lost` event, burning tokens against an already-terminal task.
- Derive the router's default state-lost respawn cap from the shared
`MAX_SESSION_RETRY_ATTEMPTS` budget (`stateLostRespawnCapFor`), and route both
the router's loop-guard reducer and the task service's terminal decision
through one shared gate (`stateLostRespawnUnderCap`) resolving the SAME
effective cap (`resolveStateLostRespawnCap`, honoring the
ACPX_STATE_LOST_RESPAWN_CAP operator override on both sides). The task now goes
terminal on exactly the error the router refuses to respawn — no orphan.
- Reset the crash-retry budget on operator `restartTask` via a per-run budget
epoch stamped in task metadata, so a restarted previously-failed task no longer
re-fails on its first recoverable blip while the router respawns anyway.
Tests: new reconciliation suite drives BOTH subsystems over the same state-lost
stream and asserts they agree (no o…
…ombie-run deploys Zombie CI runs stuck `queued` through a runner freeze eventually execute and deploy their OLD ref OVER a newer build. elizaOS#14082 hit this: staging regressed to a pre-elizaOS#13410 bundle (8deb9cbd07) hours after newer builds were live, because a stale queued cloud-cf-deploy run finally ran and clobbered newer deploys. Adds a freshness guard step to both Pages deploy jobs (deploy-console eliza-cloud @ elizacloud.ai / staging.elizacloud.ai, deploy-app eliza-app @ app.elizacloud.ai / app-staging.elizacloud.ai) that runs BEFORE wrangler pages deploy: 1. fetches the currently-served build stamp (the deployed eliza-renderer-build.json, whose `commit` field records the ref that built it — the same manifest elizaOS#9309 ships on every renderer build); 2. SKIPs the deploy when this run's SHA is an ancestor of the served commit (`git merge-base --is-ancestor <runSha> <servedCommit>` — the served build is strictly newer => this run is stale); 3. a `workflow_dispatch` `force` input bypasses the guard for intentional rollbacks to an older ref. FAIL-OPEN by construction: the guard only SKIPs on a definitive stale signal (run SHA provably an ancestor of a known-newer served commit). Every ambiguous state — served stamp unreachable/unparseable/unstamped, no commit recorded, unrelated histories, ancestry undeterminable, same-commit redeploy, git error, unexpected crash — DEPLOYS. A freshness guard must never turn a transient signal-fetch failure into an undeployable state (that would block the exact fix that needs to ship). The CLI always exits 0 and signals only via `should_deploy`; the deploy step gates on it. Split into a pure/tested decision core (decideDeployFreshness + parseServedCommit + fetchServedCommit in deploy-freshness-guard.mjs) and a thin git/IO CLI (deploy-freshness-guard-cli.mjs). 19 unit tests cover the narrow SKIP case + every fail-open branch + the force bypass + the network boundary; verified end-to-end against a real 2-commit git repo (stale->skip, newer->deploy, force->deploy). Fixes elizaOS#14083 Co-authored-by: wakesync <shadow@shad0w.xyz>
Thirteen specs were red on develop. Ten were stale assertions (a billing route moved in elizaOS#13410, a deploy form that no longer exists, a cookie behaviour elizaOS#13728 deliberately removed). One was a harness defect: the mock control plane settled lifecycle jobs with a bare updateStatus, manufacturing a DB state the delete quiescence fence correctly refuses. One was a product config defect: the base wrangler vars left the inference flags off, so the shared tier could not complete a single turn locally. Three specs are deliberately NOT made green here, because they carry real signal: the dedicated-agent bridge regression, the app-chat turn-0 path that has no coverage at all, and snapshot restore, which needs to move to a daemon job before it is exercisable.
Thirteen specs were red on develop. Ten were stale assertions (a billing route moved in #13410, a deploy form that no longer exists, a cookie behaviour #13728 deliberately removed). One was a harness defect: the mock control plane settled lifecycle jobs with a bare updateStatus, manufacturing a DB state the delete quiescence fence correctly refuses. One was a product config defect: the base wrangler vars left the inference flags off, so the shared tier could not complete a single turn locally. Three specs are deliberately NOT made green here, because they carry real signal: the dedicated-agent bridge regression, the app-chat turn-0 path that has no coverage at all, and snapshot restore, which needs to move to a daemon job before it is exercisable.
Closes the first item on the launch QA tracker #13406. Design approved live by shaw + nubs (2026-07-04): elizacloud.ai = pure cloud console, the agent app never boots there; app.elizacloud.ai stays the app.
The bug (prod, reproduced live)
Signing in on elizacloud.ai landed on
/settings#cloud-billing(the #11031 console home)./settingsis an agent-app view, so the full agent app booted on a host with no same-origin agent backend: nine/api/*boot calls 404'd, and the failed/api/first-run/statusprobe made the app treat the signed-in user as brand-new — throwing the "where should your agent run?" first-run chooser over everything, with an eternal "Waking Eliza…" spinner. Trace + screenshot captured from the real session:The fix
AppCatchAllRoutehardening — on an apex control-plane host the agent app never renders: unauthenticated →/login?returnTo=…(unchanged), authenticated →/dashboard(any path:/,/settings,/chat…), auth-not-ready → blank fallback (previously the app rendered during this window and could strand the visitor)./dashboardconsole home (cloud/home/) — live credit balance + Add funds, and directory cards for all 13 console surfaces.dashboard/billing(Stripe cancel URL lands here natively now),dashboard/api-keys,dashboard/account,dashboard/security,dashboard/security/permissions,dashboard/monetization,dashboard/connectors./dashboard/monetization; backend-issueddashboard/settings?tab=<x>URLs → the matching console page (these previously pointed into the agent-app settings and were broken on the apex).Evidence (real signed-in session, local bundle served over the real domain, real api.elizacloud.ai underneath)
/dashboard, real balance/settingsdeep-load →/dashboard(the exact bug path; no first-run chooser)dashboard/billingstandalone: live balance, add-credits, hosting-from-earningsbuild:webbundle forelizacloud.airequests (service worker bypassed), everything else — includingapi.elizacloud.ai— real network + real session. Redirect assertions:/→/dashboard,/settings→/dashboard, zero first-run chooser on any visited path.Tests
CloudRouterShell.test.tsxre-pinned (8): unauth apex → login; authed apex root and/settingsand arbitrary deep paths →/dashboard; per-agent subdomains + localhost untouched; redirect-map invariants (no same-path redirect may shadow a standalone route; earnings/affiliates → monetization).register-all.test.tsinverted: standalone mounts required for home/billing/api-keys/account/security/permissions/monetization/connectors; legacy-only spellings must stay redirect-only.DashboardHomePage.test.tsx(5): three-state balance (loading em dash / designed error / live amount — never fabricated $0), all 13 directory links, Add-funds target, session-not-ready skeleton.bunx vitest runon the four touched suites: 21/21 green.biome checkclean.packages/uitypecheck: fails on develop tip pre-existing in a fresh checkout (useAccounts.ts×3,useWalletState.ts×1 — verified identical with my diff stashed); ledgered on Launch QA tracker: Eliza app + Eliza Cloud all-surface QA/UX ([qa-agent] × [cloud-agent]) #13406. My files contribute zero typecheck errors.Notes for review
OrganizationSectionalready was.dashboardroute registration lives incloud/home/routes.ts, wired viaregister-all.tsside-effect imports like every other domain.Signed: [qa-agent] (with nubs live-driving the human half of the QA)