Operator could not log in to Anthropic or Codex on a fresh container: every
attempt ended "Login did not complete. Please try again.", while the same
providers worked flawlessly on their long-lived native install.
FusionAuthStorage.modify() is the seam pi persists a COMPLETED LOGIN through
(Models.login -> credentials.modify(provider.id, ...) in pi-ai models.js:198).
It resolved its write target with `creating: false` and returned before invoking
the callback whenever the provider had no credential row yet:
const target = this.resolveWriteTarget(provider, current, false);
if (!target || !this.credential(target, current)) return { changed: false };
So a first login completed its browser flow, exchanged the code, took and
released the lock file, wrote NOTHING, and resolved as success — leaving the
dashboard poll to see authenticated:false and report the generic failure.
It reproduces only on a store with no existing row, which is why it looked
environment-specific: an install that has logged in before takes the same path
as a refresh over an existing row and is fine, while every new container, new
machine, or wiped ~/.fusion can never complete a first login for ANY provider.
Evidence from the operator's container: flow ended with err=None (pi resolved,
no error), nothing logged, auth.json still {}, the agent directory's mtime
bumped when the lock was taken and released while auth.json itself never
changed, and an API-key write — which goes through set(), not modify() — landed
immediately.
modify() now creates when absent and updates when present; a callback returning
undefined still writes nothing, so pi's refresh-bails-out behaviour is unchanged.
auth-storage-instances.test.ts asserted the old behaviour, grouping modify() with
remove/logout/removeInstance as "non-creating". The removal guarantees are kept;
the modify() assertion is inverted, because it encoded the defect.
Also surfaces the server's own loginError through a new describeLoginFailure()
helper instead of the generic sentence, so an OAuth state mismatch reads as the
stale-tab instruction it is. Writing its test caught a bad regex of mine:
`code.*expired` matched "OpenAI Codex ... token_expired", a different failure.
Verified: the new first-login test fails against the old `creating: false` and
passes with the fix; 86 engine auth tests, 238 dashboard auth/dialog tests, and
pnpm test:gate all pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings had its own copy of the flow onboarding just replaced: instructions and
the paste field rendered inline in the provider row of a scrolling list, with no
single place showing what the login was waiting on. Same dialog now serves both,
so an operator who learns the flow at first run sees it again when adding a
provider later.
Settings differs in one way that matters: every flow is keyed by `stateKey`
(`providerId`, or `providerId[instance]` for a named credential instance),
because one provider can hold several accounts. `loginDialog` therefore carries
{ stateKey, providerId, instanceId, providerName } and threads instanceId back
to handleSubmitManualCode / handleCancelLogin, and the row suppresses its own
instructions + paste field ONLY for the key the dialog owns — a sibling account
keeps its inline field. (An early draft keyed on `provider:default`, which is
not the real format and broke exactly that case; caught by the new tests.)
The dialog renders outside renderModalShell: the modal presentation is a
FloatingWindow, and a portaled dialog inside a window's React subtree lifts that
window above itself on first click. The embedded presentation is unaffected.
Verified in a container build against the real Settings UI: dialog opens on
Continue to login, the row's inline paste field disappears (0 present), exactly
one paste field exists, and the dialog is not a descendant of the window.
756 dashboard tests pass, including 3 new handoff tests; typecheck and eslint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator report: during a container login there was nowhere obvious to paste the
redirect URL and no sign of what the app was waiting on. The flow was split
across a pre-flight confirm that warned about paste-back and vanished, a card
that shrank to a disabled "Waiting for login…" chip, and the paste field
rendered inline in that card below the fold of a scrolling modal.
ProviderLoginDialog now opens with the flow and stays until it ends: a two-step
progress list, a button to re-open a lost sign-in tab, the paste field, and the
terminal outcome inline instead of a toast that disappears while the operator is
in another browser tab.
Three defects found and fixed while verifying it in a real container:
- It sank behind the onboarding modal and clicks landed on the modal instead.
createPortal relocates the DOM node but NOT the React tree, so pointer events
bubbled to the host FloatingWindow, which raises itself to a fresh
nextFloatingZ() on every pointerdown — each click in the dialog lifted the
window above it. Fixed by rendering the dialog as a sibling of the window,
claiming z once on open (it was calling nextFloatingZ() inline on every render
of a modal that re-renders on a 2s poll), and stopping propagation on the
overlay. Ratcheted for every portaled .modal-overlay.
- Spacing did not match any other dialog: it hand-rolled header/action padding
instead of using .modal-header/.modal-actions, and padded each child
separately. Every row now shares var(--modal-padding) — verified at a uniform
17px inset across header, steps, paste prompt, field, Submit, and actions.
- The paste field was invisible (.form-input fills with var(--surface), and so
does .modal — measured #0c0c0e on #0c0c0e), Submit was a 25px row-density
btn-sm, and both could scroll out of reach. The field now sinks to var(--bg)
with var(--border-strong), Submit takes standard control padding, and the
paste region is pinned outside the scroll area.
Dialog anatomy rules (spacing primitives, portal/stacking) documented in
docs/dashboard-guide.md.
Verified: 441 dashboard tests including 4 new dialog tests, eslint, dashboard
typecheck, and the rendered dialog measured in a container build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator report from a containerized dashboard: OpenAI Codex login never opened
a browser window at all, and floating windows still needed the FN-8015 follow-up.
- pi's `AuthPrompt` is a discriminated union — text, secret, select, manual_code —
and FusionAuthStorage.login's interaction shim flattened every variant into
`onPrompt({message, placeholder})`, discarding `type` and a select's `options`.
pi's Codex `login()` OPENS with `prompt({type:"select"})` (Browser vs Device
code) before emitting any auth URL, so the dashboard answered the method picker
with the promise that waits for a pasted code — input the UI never solicits,
because nothing had been surfaced yet. The flow hung until the route's 30s
kickoff timeout: "Login initiation timed out", no window. The route's
onSelect/selectOauthOption has had the right answer since FN-5917, but the
callback was dead code from the moment login moved to pi's ModelRuntime.
Verified against a real container: the login endpoint now returns Codex's
auth.openai.com URL in 0.03s instead of timing out after 30s.
- Promote FN-8766's outboard east/NE/SE resize targets from Task Detail to every
desktop window. With FN-8015's body gutter deleted, a hosted scrollbar sits
flush against the painted edge where those hit zones used to cover it (issue
#2140); moving the targets outside the shell keeps it grabbable without
insetting anything. That needs the host to stop clipping, so the body and its
direct child inherit the corner radius — only 8 of ~30 callers set that
themselves — and phones re-assert clipping since they hide every handle.
- Document the fixed OAuth callback ports (Anthropic 53692, Codex 1455) and
PI_OAUTH_CALLBACK_HOST for Docker: without them the browser callback cannot
reach the container's loopback listener, which is why subscription logins
appeared to fail there.
Verified: 14989 dashboard tests, 58 engine auth-storage tests (4 new, covering
each prompt type), pnpm test:gate, eslint, and both typechecks all pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator report: the first-run "Set Up AI" modal had extra space along its right
edge, and it asked a browser visitor to connect a remote server and mentioned a
native shell they do not have.
Three defects, verified in a real container build at desktop and mobile widths:
- FN-8015 reserved `margin-inline-end: var(--space-lg)` on the shared
`.floating-window__body` so a hosted scrollbar cleared the east resize hot
zones. One shared reservation every caller had to know about produced a
recurring class of asymmetric-right-inset bugs instead: it was zeroed
piecemeal five times (FN-8766, mobile task detail, FN-8722, FN-8702, every
tablet window) while two callers leaned on it for their right inset and had to
restore it whenever one of those predicates fired. Delete the gutter, its five
overrides, and GitHub Import's borrowed-inset compensation. Where a scrollbar
and a resize target actually collide, use FN-8766's outboard east targets.
Trade-off accepted by the operator; the ratchet test now forbids the gutter in
any stylesheet at any breakpoint.
- The hosted Set Up AI modal did not fill its window: its standalone
`height: min(85vh, ...)` rule ties on specificity with FloatingWindow's
`height: 100%` and won on source order, leaving ~60px of dead window surface
under the footer alongside the 16px gutter strip. Right gap 17px -> 1px,
bottom gap 62px -> 1px.
- The "Connect remote Fusion server" card keyed only on
`desktopMode !== "local"`, and `desktopMode` is undefined on web, so every
browser first-run led with a native-shell hand-off form. Now also requires
`host !== "web"`.
Verified: 165 dashboard test files / 5829 tests pass, eslint and
`tsc -p tsconfig.app.json` clean, and a container built from this tree shows the
panel flush at 1280x800 and as a symmetric full-screen sheet at 390x844 with no
remote-server card.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes the dashboard's stuck-task tagging per operator request: the Stuck
card/status badges, stuck row styling, the footer Stuck segment and
stuckTaskCount stat, utils/taskStuck.ts, the isStuck agent-activity gate,
and the taskStuckTimeoutMs prop plumbing (App -> Board/Lane/Column/
WorktreeGroup/MainContent -> TaskCard/ListView/ExecutorStatusBar). Stuck-task
tests are deleted or reconciled. The taskStuckTimeoutMs setting and the
engine's recovery sweeps (including the stuck-killed status) are unchanged —
the setting is engine-side only now.
Also repoints the FN-6756 liveness-gate ratchet's facade scans at
executor/task-executor-session-facades.ts, where the wave20 extraction moved
hasLiveSessionSurface/clearPhantomExecutorBinding (the two pre-existing red
tests on main).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary
The dashboard settings quality lane is one of the slowest CI-run packs.
Most Scheduling cases remounted Authentication and clicked the sidebar
just to reach fields they already know by `initialSection`. That extra
render is also the flake surface that previously needed `findByRole`
after settings fetch.
This change opens the target section on first render, polls readiness at
5ms instead of 50ms, and names the 500ms auto-save debounce so
fake-timer flushes stay locked to product behavior. Persist assertions
are unchanged.
## Test plan
- [x] `pnpm --filter @fusion/dashboard exec vitest run --project
dashboard-app-quality-settings --silent=passed-only --reporter=dot
--exclude '**/build-output.test.ts'` — 330 passed
- [ ] Confirm the settings quality lane still runs in `full-suite.yml`
shard packing (`pnpm --filter @fusion/dashboard run
test:quality:app:settings`)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Improvements**
* Improved consistency and reliability of settings autosave behavior.
* Streamlined scheduling settings validation to reduce timing-related
test flakiness.
* Improved settings readiness and persistence checks for more dependable
results.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
createFnAgent now delegates to createResolvedAgentSession — deriving the CLI
runtime hint (cursor/claude/grok/omp/hermes), mock/test-mode forcing, and
session:runtime-resolved visibility — using a host-registered default
PluginRunner keyed by project root (published by InProcessRuntime at plugin
init). DefaultPiRuntime re-enters via a __rawPiSession marker so the seam's
own pi bridge cannot recurse, and the raw constructor survives as
createPiAgentSessionRaw for that bridge and pi-internal tests.
Mission and milestone/slice interviews additionally pass their request-scoped
pluginRunner through the seam and prompt via the engine promptWithFallback
dispatcher (plugin CLI runtime sessions have no session.prompt()). This fixes
"Configured model cursor-cli/auto ... was not found in the pi model registry"
in mission planning while chat on the same model worked, and closes the same
gap for every remaining bare createFnAgent lane.
Also updates the pi skill-filtering test that still asserted pre-FN-9114
allow-list narrowing; requested skill names are ensure-present since FN-9114.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep visible Planning Mode controls bound to the current turn during asynchronous refreshes.
- preserve active question and plan-review workspaces during same-session hydration
- submit from live session state and retain dirty answers across response identity changes
- add desktop and mobile regression coverage for deferred hydration and stale Stop polling
- document the ownership race and add a patch changeset
Files changed:
.changeset/fn-9117-planning-turn-ownership.md | 7 +
.../suite-only-flakes-observed-register.md | 24 ++++
.../dashboard/app/components/PlanningModeModal.tsx | 141 ++++++++++++---------
.../PlanningModeModal.planning-flow.test.tsx | 132 +++++++++++++++++++
.../PlanningModeModal.ui-interactions.test.tsx | 20 +++
5 files changed, 265 insertions(+), 59 deletions(-)
Fusion-Task-Id: FN-9117
Fusion-Task-Lineage: 18d63f63-b532-49eb-a6d1-241e31c5b2c7
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Second root cause of the browser-back flake: the board is a lazy chunk
behind Suspense fallback={null} and waitForAppShell only proves the
header, so a synchronous getByText raced chunk resolution under
sharded-lane load (failure DOM showed a header-only page). The first
board lookup in both back-nav tests is now the async finder — the
correct wait mechanism, no widened timeouts. Complements the
once-queue purge fix; 3/3 solo runs green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Local e2e runs regenerated the fn-8602/fn-8607 baselines with small rendering
deltas and produced the FN-8806 task-title-stable-modal baselines that were
never checked in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An SSE subscription with a nonexistent projectId (stale client tab, e2e fixture
page using projectId "fixture") surfaced the PG startup-factory construction
chain as a 500 on every poll, filling operator logs with alarming
"failed to construct TaskStore" errors. Map project-not-found to a clean 404,
matching the project routes' existing handling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gate blocked approved clean-room squashes on per-file shrinkage with no
override path ("AI merge diff-volume gate blocked the approved squash").
Removed by operator decision: delete checkDiffVolume/DiffVolumeRegressionError,
the merge:diff-volume-blocked audit event, the runDiffVolumeGate call sites in
every legacy squash finalizer, the AI-merge pre-land check, and the
mergeDiffVolume* settings. File scope remains the pre-land guard; the
post-squash audit policy remains the shrinkage backstop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mobile-styles guard forbids raw px anywhere in the stylesheet,
comments included; the FN-8866 token-fix comment mentioned the named
steps' pixel values and tripped it. Reworded — CSS rules unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root-cause fix for the order-sensitive 'closes board-opened main-panel
task detail on one browser back' flake: vi.clearAllMocks() clears calls
but never drops unconsumed mockResolvedValueOnce entries, and a plain
mockResolvedValue default does not purge them either — the once-queue
wins first. A prior test's unconsumed auth/settings/health/plugin-views
Once value poisoned the next test's first fetch, rendering an
auto-opened modal surface instead of the board. The global beforeEach
now mockReset()s every once-queue-prone API mock before re-applying its
default (fetchPluginDashboardViews gains an explicit empty default
since its factory impl dies with the reset). 4/4 consecutive full-file
runs green (was ~1-in-3 failing); no assertions changed, no quarantine
needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dashboard bare-run repair, mock-drift cluster. Six route suites'
inline @fusion/engine mocks predated FN-8902's
DEFAULT_MODEL_REGISTRY_REFRESH_TIMEOUT_MS import and now wrap the
canonical createEngineMock fallback proxy; App.test's useTasks mock
becomes an importOriginal spread so the real mergeTaskSnapshot reaches
components, its Todo/graph tests move to FN-8762's plugin-view
enablement invariants (with a mockReset fix for a stale
mockResolvedValueOnce queue the clearAllMocks reset never drops); the
Mailbox lucide mock learns FN-9014's Archive icon; and two source-scan
tests repoint files moved by the #2398 domain-folder refactor
(app/api/agents/agents.ts, engine healing/self-healing-constants.ts),
both verified via git log --follow. Verified 10 files / 252 tests green
under their standard lane projects; both dashboard typechecks clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dashboard bare-run repair, route/store cluster. Real product regression
fixed: the FNXC:IntakeOwnership boundary replaced the 'Workflow ... not
found' message with a typed TaskIntakeOwnerResolutionError, so the
mission triage routes' message-pattern mapping stopped firing and an
unknown workflowId leaked as a 500 — both feature and slice triage
handlers now match the typed error structurally and return 404 (patch
changeset included). Everything else was stale fixtures behind the PG
cutover and recent seams: sse's mock stores learn getAsyncLayer, the
retry fixtures learn FN-8908's resetTerminalFailureAutoRecoveryBudget,
approve-plan tests materialize a real on-disk PROMPT.md per the
SpecLockApproval 409 contract (fingerprint assertion strengthened to
the always-hash contract), and the MCP settings route body pins the new
host-dependent fusionMemoryMcpAvailable field. Verified 7 files / 213
tests green under their assigned lane projects, src typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dashboard bare-run repair, component-drift cluster. All nine suites
were stale expectations behind cited intentional commits — no UI
regressions found: FN-8826's always-on WIP lifecycle badge
(TaskCard.oversight now asserts no overseer element instead of an empty
header), FN-8762's Todo Lists plugin extraction (MobileNavBar,
MainContent.graph-popout, useAppSettings repurposed to the
pluginDashboardViews contract, with a regression pin that the removed
todosEnabled field never returns), FN-8796's clock-arbitrated
mergeTaskSnapshot, FN-8797's prompt-only planning refresh, FN-8702's
767.98px phone-sheet boundary, FN-8764's primary-role split, and
FN-8947's added touch-target selector. Every update carries an FNXC
comment citing the causing commit. Verified 9 files / 293 tests green,
tsconfig.app.json typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The browser-lane suite regenerates these PNGs when it runs locally; six
regenerated baselines were swept into 9673f15c11 from the shared index.
Restore the committed baselines — no intentional visual re-baseline
happened.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dashboard bare-run repair, census/token cluster. Real product bugs: the
Command Center activity panel (FN-8866) and structural-mail badge
(FN-8872) referenced undefined --space-* tokens, zeroing their
gaps/padding — mapped to the defined named scale; the settings search
index lagged FN-8829/FN-9021 additions and FN-8855's requiredChecks
entry had no scroll anchor (now a SettingsTextRow). Test-side: the
theme census learns FN-8730's intentional midnight theme, and the
Chromium touch-resize suite self-gates with describe.runIf per the
sibling browser-lane convention (CI/FUSION_BROWSER_SMOKE_REQUIRE still
fail loudly; all 62 tests still run where Chromium exists).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gateway 502/503/504 non-JSON bodies (for example Traefik "no available server")
were dumped as content-type diagnostics into the Planning error banner.
Slow-test trim, DELETE + dashboard OPTIMIZE tiers. Deletions follow the
docs/testing.md taxonomy exactly — 40 tests removed across
TaskDetailModal.rendering (134→129), QuickEntryBox (300→280),
TerminalModal (247→240), ModelOnboardingModal (205→197), and
AgentDetailView.settings (27→25): byte-identical field-presence twins,
CSS-class permutation residue, and strict-subset interaction copies,
with every FN-tagged describe and user-noticeable behavior kept and
condensed matrices converted to it.each with all cases retained. The
real payoff is the FN-2707 latency pass: SettingsModal's real 500ms
auto-save debounce (and AgentDetailView's 700ms config debounce) now
flush under fake timers via a shared flushSettingsAutoSave harness
export — SettingsModal splits 102.3s→63.6s. Also fixed (not appeased) a
pre-existing race the speedup exposed: nav-button lookups now
findByRole after the settings promise resolves, file-wide. Verified:
1235 tests green across the touched set, tsconfig.app.json typecheck
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-suite repair, engine source-scan cluster. The package code
organization waves moved ~30 engine modules into subdirectories
(plugins/, execution/, scheduling/, healing/, worktree/, executor/
peels); the log-severity manifest, prompt carve-out, emit-surface,
failure-lane, and worktree-invariant scanners now read the moved
locations, verified per file via git log --follow. Two scans caught
real drift rather than moves: the lifecycle census had 12 unexamined
column guards (resolved with DELIBERATE-LITERAL markers for the mailbox
archived tab, the FN-9059 lease-owner terminality check, and the FN-9056
legacy done fallback — baseline re-recorded with zero absorbed debt),
and planning-claim gained a genuine second writer in self-healing's
FN-8998 transport-failure recovery, admitted to the allowlist with its
CAS-guarded justification. 9 files / 119 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The task-created hook (GitHub tracking issue creation) was deferred behind
the fire-and-forget auto-title-summarize chain whenever autoSummarizeTitles
is on and the task has a long description and no title — i.e. for every
'fn task create'. The short-lived CLI process closed the store and exited
before the deferred chain ran, so tasks ended up with
githubTracking.enabled=true but no issue (observed on FN-9045..FN-9061).
fn task create now suppresses the deferred hook and calls
createTrackingIssueForTask directly after create, printing the linked
issue; exports that helper from @fusion/dashboard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>