Skip to content

fix(cloud): apex console never boots the agent app — /dashboard home + standalone account routes - #13410

Merged
0xSolace merged 3 commits into
developfrom
fix/apex-console-standalone-dashboard
Jul 4, 2026
Merged

0xSolace merged 3 commits into
developfrom
fix/apex-console-standalone-dashboard

Conversation

@NubsCarson

Copy link
Copy Markdown
Member

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). /settings is 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/status probe 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:

before

The fix

  1. AppCatchAllRoute hardening — 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).
  2. New /dashboard console home (cloud/home/) — live credit balance + Add funds, and directory cards for all 13 console surfaces.
  3. Standalone console routes mounting the existing section bodies (they stay mounted in in-app Settings too): dashboard/billing (Stripe cancel URL lands here natively now), dashboard/api-keys, dashboard/account, dashboard/security, dashboard/security/permissions, dashboard/monetization, dashboard/connectors.
  4. Redirect map updated — earnings/affiliates → /dashboard/monetization; backend-issued dashboard/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)

Authed apex root → /dashboard, real balance after-root
/settings deep-load → /dashboard (the exact bug path; no first-run chooser) after-settings
dashboard/billing standalone: live balance, add-credits, hosting-from-earnings billing
Mobile 390px mobile
  • Harness: attached to the real signed-in Chrome session, Playwright-served the branch's build:web bundle for elizacloud.ai requests (service worker bypassed), everything else — including api.elizacloud.ai — real network + real session. Redirect assertions: //dashboard, /settings/dashboard, zero first-run chooser on any visited path.
  • Video: N/A — the flow is single-redirect landings; the four full-page screenshots + the JSONL navigation trace cover every transition (trace in tracker Launch QA tracker: Eliza app + Eliza Cloud all-surface QA/UX ([qa-agent] × [cloud-agent]) #13406 first comment).
  • Real-LLM trajectories: N/A — no agent/action/prompt/model behavior touched (routing + console pages only).

Tests

  • CloudRouterShell.test.tsx re-pinned (8): unauth apex → login; authed apex root and /settings and 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.ts inverted: 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 run on the four touched suites: 21/21 green. biome check clean.
  • packages/ui typecheck: 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

  • The in-app Settings sections are untouched — the bodies are now mounted twice by design (app settings + console page), exactly like OrganizationSection already was.
  • dashboard route registration lives in cloud/home/routes.ts, wired via register-all.ts side-effect imports like every other domain.
  • Deep links that used to "keep working" on the apex by rendering the app were an illusion — the app 404-walled there. They now land on the console home.

Signed: [qa-agent] (with nubs live-driving the human half of the QA)

…+ 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/).

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b48ea62-5d9f-44c3-8ba2-4b5e7163fd74

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/apex-console-standalone-dashboard

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

Reviewed current head a1958350670cad6f5c2dad89811e907c45f1bec8 locally.

No blocking code finding from static review. The routing shape matches the intended host split: registered dashboard/* console routes win before the catch-all, and AppCatchAllRoute prevents the agent app from booting on apex control-plane hosts while leaving per-agent subdomains / localhost untouched. I also manually inspected the submitted evidence images:

  • .github/issue-evidence/13406-apex-console/after-apex-root-dashboard.png
  • .github/issue-evidence/13406-apex-console/after-apex-root-dashboard-mobile.png
  • .github/issue-evidence/13406-apex-console/after-billing-standalone.png
  • .github/issue-evidence/13406-apex-console/after-settings-path-redirects.png

Local checks:

  • bunx biome check <touched ui files> - passed
  • git diff --check origin/develop...HEAD && git diff --check - passed
  • node packages/shared/scripts/generate-keywords.mjs --target ts - generated missing worktree keyword data

Local checks blocked by environment:

  • bun run --cwd packages/ui test src/cloud/shell/CloudRouterShell.test.tsx src/cloud/home/DashboardHomePage.test.tsx src/cloud/register-all.test.ts could not produce a meaningful result in this worktree. Before keyword generation, route tests failed on missing generated validation keyword data; after restoring the workspace dependency symlink, React packages resolve through /home/shaw/milady/eliza/dist/node_modules, causing duplicate-React invalid-hook failures in jsdom.
  • bun run --cwd packages/ui typecheck failed in unrelated workspace declaration resolution through /home/shaw/milady/eliza/dist/node_modules (drizzle/pg/etc.).
  • Required bun run --cwd packages/app audit:app was attempted and failed before screenshots/manual review during @elizaos/core#build declaration generation with the same dist/node_modules type-resolution issue.

I would wait for the remote focused tests + aesthetic audit to finish before auto-merge.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

Patched one blocker I found while reviewing the standalone console routes: the new /dashboard/api-keys, /dashboard/account, /dashboard/security, and /dashboard/security/permissions wrappers were mounting surfaces that call useSetPageHeader without a PageHeaderProvider. That would throw once those standalone pages reached their body on the apex console. Pushed fdcf57ab26 fix(ui): provide page headers for standalone account routes, matching the existing analytics/MCP standalone route pattern and adding regression tests with mocked surfaces that call useSetPageHeader.

Validation run:

  • bunx @biomejs/biome check packages/ui/src/cloud/api-keys/ApiKeysPage.tsx packages/ui/src/cloud/api-keys/ApiKeysPage.test.tsx packages/ui/src/cloud/account-security/AccountPage.tsx packages/ui/src/cloud/account-security/SecurityPage.tsx packages/ui/src/cloud/account-security/PermissionsPage.tsx packages/ui/src/cloud/account-security/StandalonePages.test.tsx packages/ui/src/cloud/shell/CloudRouterShell.tsx packages/ui/src/cloud/register-all.test.ts packages/ui/src/cloud/home/DashboardHomePage.test.tsx
  • bun run --cwd packages/ui typecheck
  • bun run --cwd packages/shared build:i18n
  • bun run --cwd packages/cloud/routing build
  • bun run --cwd packages/ui test -- src/cloud/shell/CloudRouterShell.test.tsx src/cloud/register-all.test.ts src/cloud/home/DashboardHomePage.test.tsx src/cloud/api-keys/ApiKeysPage.test.tsx src/cloud/account-security/StandalonePages.test.tsx --maxWorkers=2 (5 files / 20 tests passed)
  • git diff --check origin/develop...HEAD && git diff --check
  • git fetch origin develop && git rebase FETCH_HEAD (up to date)

I also attempted the required shared-UI app audit with ELIZA_NODE_PATH=/Users/shawwalters/.npm/_npx/387698761821791d/node_modules/.bin/node bun run --cwd packages/app audit:app. It did not reach screenshots: the live-stack renderer build failed on an existing unrelated export error, isAdminRole not exported by @elizaos/cloud-shared/lib/types/cloud-api while building packages/ui/src/cloud/admin/data/use-admin-gate.ts.

…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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@0xSolace
0xSolace merged commit 2e88d3b into develop Jul 4, 2026
26 of 84 checks passed
@0xSolace
0xSolace deleted the fix/apex-console-standalone-dashboard branch July 4, 2026 19:38
lalalune pushed a commit that referenced this pull request Jul 4, 2026
…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>
@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

Maintainer review note: this branch must be rebased before further review or merge.

After refreshing origin/develop to a068344d0c1, git diff origin/develop..origin/pr/13410 is heavily polluted with unrelated deletions from current develop, including many .github/issue-evidence/* artifacts, workflow/docs changes, benchmark/cloud/shared/script files, and other PR work. That means the branch is stale enough that merging it would remove unrelated current-tree content.

I attempted to disable auto-merge, but GitHub returned: Can not disable auto-merge for this pull request; branch protection should still prevent landing while mergeability is unknown/stale. Please rebase onto current origin/develop, resolve the unrelated deletions, and refresh the evidence if behavior changed. Once the diff is only the cloud apex/dashboard routing work plus its evidence, I can review the implementation and tests normally.

I did not run the claimed cloud/UI test matrix because the branch shape itself is unsafe.

@lalalune lalalune moved this from In progress to Done in Launch QA — Eliza app + Cloud (#13406) Jul 5, 2026
lalalune pushed a commit that referenced this pull request Jul 5, 2026
…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>
lalalune added a commit that referenced this pull request Jul 5, 2026
* 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…
lalalune pushed a commit to singlaamitesh/eliza that referenced this pull request Jul 7, 2026
…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>
standujar added a commit to standujar/eliza that referenced this pull request Aug 8, 2026
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.
lalalune pushed a commit that referenced this pull request Aug 8, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants