Skip to content

Onboarding reliability: brain-unreadable account session, false "Incorrect PIN.", and Repair - #1019

Merged
arul28 merged 5 commits into
mainfrom
ade/ok-need-your-diagnosing-issue
Aug 4, 2026
Merged

Onboarding reliability: brain-unreadable account session, false "Incorrect PIN.", and Repair#1019
arul28 merged 5 commits into
mainfrom
ade/ok-need-your-diagnosing-issue

Conversation

@arul28

@arul28 arul28 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Fixes two real onboarding failures a new user hit on a fresh install, plus the follow-on cleanups the review found. Both bugs were diagnosed from a real report; the cloud side (Clerk sessions, account-directory worker) was verified healthy, so both causes are local.

Bug 1 — the ADE brain could not read the stored account session

The Connections pane showed "Signed in, but this Mac is not published · The ADE brain could not read the stored account session", the account's machine list stayed empty, and "route publish failing" alarmed — all one defect surfaced three times. Only the brain publishes the machine, and it hard-requires the account token, so an undecryptable credential store silently disables cloud publication (and with it, phone quick-connect).

Three flaws could put a machine there; all are fixed:

  • Race-safe key material. The macOS keychain fallback overwrote the shared secret with add-generic-password -U on any read failure (timeout, locked keychain, item-not-yet-created), so on a first run the app and the brain could each mint a different secret and the loser could never decrypt the file again. Creation now happens only on a genuine not-found, uses a non-clobbering add, and adopts the winner's secret on a lost race; every other outcome fails closed.
  • Self-heal. A decrypt failure now invalidates the cached key material, re-reads it once, and retries before declaring the store unreadable — so this state recovers on its own instead of persisting until a brain restart.
  • Migration safety. The safeStorage migration no longer moves the account session and sync bootstrap token into an Electron-only file the brain cannot read, prunes migrated duplicates out of the retained file, and — the blocker this review caught — aborts entirely when the legacy store reads as unreadable, which previously wrote an empty store and deleted credentials.json.enc and .machine-key.

ade_account_session_unreadable (internal-only, edge-triggered per episode, coarse code) makes field incidence measurable.

Bug 2 — "Incorrect PIN." on a pairing that actually succeeded

First-time QR + PIN pairing showed a hard failure and cleared the digits, twice, on a pairing the host had already accepted; dismissing the sheet revealed the phone was connected. #867 moved .connected behind deferred post-hello work, so the sheet's connectionState == .connected check was false 100% of the time at the moment it ran, and the successful hello had already cleared lastError — so it fell through to a hardcoded "Incorrect PIN.". Broken for every first-time PIN pairing since ~v1.2.36.

The sheet now tests isAttached (the predicate every other surface already used), .connected is published at hello-apply, and .syncing — unobservable and dead once that landed — is retired.

Also

  • Repair button on the unreadable-session banner (both surfaces), routed through the verified restart seam: force install → wait for the endpoint → ping, mutually exclusive with an in-flight project repair, honest failure copy. ade doctor prints the matching ade brain restart remedy.
  • Tailscale-not-installed is now info-severity muted text instead of a warning glyph — every fresh Mac without Tailscale was seeing it.
  • Windows parity. Merged with main's Windows support: key-material binding dispatches macOS keychain vs Windows DPAPI through one seam, self-heal invalidation is wired for DPAPI (including an epoch guard so an in-flight read cannot repopulate a cache the self-heal just cleared — the Windows half of the exact bug class this PR fixes), and forced restart is pinned by a regression test.

Verification

/quality (3 passes, all findings fixed, gate empty) → /test (prune/consolidate/add, logging + PostHog gate, docs/mobile/CLI/TUI parity) → merge of main → commit-bound quality revalidation of the merge resolution.

Desktop 1182 · ade-cli 409 · iOS 82/82 (SyncRecoveryPolicyTests, SyncAccountConnectRecoveryTests) · both typechecks clean · PostHog spec validators green.

Known and deliberate: on Windows the self-heal can hold the credential lock across two PowerShell spawns while the peer lock timeout is 15s, so a peer can see a lock timeout during recovery. Accepted — the alternative is no self-heal at all; raising the timeout or moving key reads outside the lock changes main's concurrency contract and belongs in its own change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Brain Repair actions for unreadable account sessions, including automatic service restart and status refresh.
    • Improved credential recovery across supported platforms, including safer migration and stale-key recovery.
    • Added clearer analytics tracking for account-session failures and repair outcomes.
  • Bug Fixes

    • Prevented slow network failures from incorrectly recommending a Brain restart.
    • Improved service installation timeout and retry handling.
    • iOS now distinguishes fully connected from still-syncing states, preventing premature live actions.
  • UI Improvements

    • Tailscale discovery messages now distinguish informational notices from warnings.

arul28 and others added 3 commits August 4, 2026 02:35
…ir button, iOS PIN fix, .syncing retirement

- credentialStore: race-safe keychain material (no -U clobber), decrypt self-heal, migration abort-on-unreadable + retain/prune of file-backed keys, safeStorage write guard
- new osBoundKeyMaterial module + episodeAnalytics helper + ade_account_session_unreadable event
- desktop: verified brain Repair (restartBrain + IPC), Tailscale info severity, brain_repair analytics
- iOS: PIN sheet isAttached fix, .connected at hello-apply, .syncing retired
- ade doctor: brain-restart remedy on session failures; docs + dashboard spec updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nosing-issue

# Conflicts:
#	apps/ade-cli/src/services/credentials/credentialStore.test.ts
#	apps/ade-cli/src/services/credentials/credentialStore.ts
#	apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts
#	apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.test.tsx
#	apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx
Windows DPAPI cache invalidation now carries an epoch so a read that started
before an invalidation cannot repopulate the cache the self-heal just cleared —
the macOS resolver already guarded this, the Windows half did not.

Also: require the key-binding directory on the Windows invalidate, name that
parameter for what it is (keyBindingDir, the machine key's directory), name the
macOS-only resolver accordingly, restore the Windows no-negative-cache and
in-flight-dedup rationale lost in the merge, rename no_keychain_material to
no_os_key_material (unreachable on win32 under the old meaning; never shipped),
drop a dead export, and document the Windows self-heal lock-hold cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Aug 4, 2026 8:31am

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@arul28, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3698cd9c-46d2-450a-b294-638a48c0c270

📥 Commits

Reviewing files that changed from the base of the PR and between 14063b7 and e661ca5.

⛔ Files ignored due to path filters (1)
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
📒 Files selected for processing (12)
  • apps/ade-cli/src/services/account/accountMachinePublisherService.test.ts
  • apps/ade-cli/src/services/account/episodeAnalytics.ts
  • apps/desktop/src/main/services/ipc/ipcTimeouts.test.ts
  • apps/desktop/src/main/services/ipc/ipcTimeouts.ts
  • apps/desktop/src/main/services/runtime/projectRecoveryService.test.ts
  • apps/desktop/src/main/services/runtime/projectRecoveryService.ts
  • apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.tsx
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/ios/ADETests/SyncRecoveryPolicyTests.swift
  • scripts/posthog/dashboard-spec.mjs
📝 Walkthrough

Walkthrough

The pull request adds credential key-material recovery and failure classification, brain-restart repair flows across the CLI and desktop app, analytics coverage, and iOS connection-state changes that separate transport attachment from hydration.

Changes

Credential and brain repair flow

Layer / File(s) Summary
Credential storage and key-material recovery
apps/ade-cli/src/services/credentials/*, apps/ade-cli/src/services/sync/syncHostService.test.ts
Credential stores use injected OS-bound key material, classify read failures, retry refreshed keys, preserve file-backed credentials, and harden legacy migration.
Account-session health reporting
apps/ade-cli/src/services/account/*, apps/ade-cli/src/commands/doctor.*
Account authentication exposes session-read failure reasons. Publisher analytics deduplicates failure episodes. Doctor diagnostics recommend ade brain restart only for brain-local session failures.
Desktop brain repair
apps/desktop/src/main/services/runtime/*, apps/desktop/src/main/services/localRuntime/*, apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/preload/*
Service installation supports forced restarts and timeouts. Project recovery exposes restartBrain() through IPC and preload APIs.
Desktop repair UI and analytics
apps/desktop/src/renderer/components/{remoteTargets,settings}/*, apps/desktop/src/renderer/hooks/useBrainRepair.ts, apps/desktop/src/main/services/analytics/*, apps/desktop/src/shared/*
The desktop app shows repair controls for unreadable account sessions, refreshes health after repair, separates discovery warning and informational diagnostics, and records repair analytics.

iOS connected-state model

Layer / File(s) Summary
Transport attachment and restoration
apps/ios/ADE/Services/SyncService.swift
The .syncing transport state is removed. Hello handling publishes .connected before hydration, and deferred restoration checks socket and generation validity.
Connected-gated UI behavior
apps/ios/ADE/Views/**/*.swift
Live content, uploads, actions, reachability, project visibility, and connection controls now require .connected. Connection-health accessibility labels no longer describe syncing changes.
iOS validation
apps/ios/ADETests/*
Tests now cover connected attachment, connecting transport behavior, and stale restoration completion after socket teardown.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • arul28/ADE#835: Both changes update account-session health and readability handling.
  • arul28/ADE#808: Both changes extend product analytics policy and dashboard handling.
  • arul28/ADE#995: Both changes modify iOS sync connection-state and account-pairing behavior.

Suggested labels: desktop, ios

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's main onboarding reliability fixes, including unreadable account sessions, incorrect PIN errors, and repair handling.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/ok-need-your-diagnosing-issue

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.

@arul28 arul28 changed the title Ok Need Your Diagnosing Issue -> Primary Onboarding reliability: brain-unreadable account session, false "Incorrect PIN.", and Repair Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ios/ADE/Views/Hub/HubQuickConnect.swift (1)

221-229: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the reconnect result to report success.

reconnect(toSavedHost:) returns whether the requested host connected. Line [224] checks only the shared connectionState. If the attempt fails and the previous machine is restored, connectionState can still be .connected, so this branch reports success for the wrong machine. Capture the returned Boolean and branch on it, as apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift does at Lines [1191]-[1194].

Proposed fix
-      await syncService.reconnect(toSavedHost: host)
+      let reconnected = await syncService.reconnect(toSavedHost: host)
       connectingId = nil
-      if syncService.connectionState == .connected {
+      if reconnected {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/Hub/HubQuickConnect.swift` around lines 221 - 229, Update
the reconnect flow in the Task around reconnect(toSavedHost:) to capture its
returned Boolean and use that result for the success/error branch instead of
syncService.connectionState. Preserve the existing haptics, callback, and
error-text behavior for each outcome.
🧹 Nitpick comments (2)
apps/ade-cli/src/services/credentials/credentialStore.test.ts (2)

449-460: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the no_os_key_material reason.

The suite covers decrypt_failure (line 446) and store_format (line 459). It does not cover no_os_key_material.

That branch cannot be reached from a test process today. decodeCredentialStore selects it through expectsOsBoundKeyMaterial(), which reads process.env directly, and osBindingDisabledByEnv returns true whenever VITEST === "true". Every test therefore resolves the binding to "disabled" and falls through to decrypt_failure.

This reason is the one that gates the desktop brain-repair control for brain-local unreadable sessions, so a silent regression in the classification would not fail any test. Add an injection seam so the branch is testable, for example an optional binding argument on expectsOsBoundKeyMaterial() or an explicit expectsOsMaterial input to decodeCredentialStore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/services/credentials/credentialStore.test.ts` around lines
449 - 460, Add an injectable binding or explicit OS-material expectation to
expectsOsBoundKeyMaterial or decodeCredentialStore, instead of relying
exclusively on process.env and the Vitest override, so tests can reach the
no_os_key_material classification. Update the credential-store test suite with
coverage that exercises this path and asserts getLastReadFailureReason() returns
no_os_key_material while preserving existing decrypt_failure and store_format
behavior.

962-974: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the epoch guard for an invalidation during an in-flight asynchronous read.

This test covers inFlight coalescing. It does not cover the record epoch check in createMacKeychainMaterialResolver, which is what stops material rejected by an invalidation from being cached again.

invalidate() sets inFlight = null but the already-started promise still calls record. Only the readEpoch !== epoch comparison prevents that result from repopulating cached. The equivalent Windows behavior is tested in apps/ade-cli/src/services/credentials/windowsDpapiMaterial.test.ts; the macOS resolver has no such test.

💚 Suggested test
it("does not let a read that started before an invalidation repopulate the cache", async () => {
  let resolveRead: ((value: { material: Buffer }) => void) | null = null;
  const readAsync = vi.fn(() => new Promise<{ material: Buffer }>((resolve) => {
    resolveRead = resolve;
  }));
  const read = vi.fn(() => ({ material }));
  const resolver = createMacKeychainMaterialResolver({ read, readAsync });

  const inFlight = resolver.readAsync();
  resolver.invalidate();
  resolveRead!({ material: Buffer.alloc(32, 9) });

  // The read still answers its own caller; only the cache write is dropped.
  expect(await inFlight).toEqual(Buffer.alloc(32, 9));
  expect(resolver.read()).toBe(material);
  expect(read).toHaveBeenCalledTimes(1);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/services/credentials/credentialStore.test.ts` around lines
962 - 974, Add a test alongside the existing coalescing case that starts a
deferred resolver.readAsync() call, invokes invalidate() before resolving it,
then verifies the in-flight caller receives the resolved material while a
subsequent resolver.read() still returns the pre-existing material and performs
no additional OS read. Use createMacKeychainMaterialResolver and assert the
invalidation epoch prevents the stale result from repopulating cached.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/services/account/episodeAnalytics.ts`:
- Around line 39-45: In
apps/ade-cli/src/services/account/episodeAnalytics.ts:39-45, update the
analytics capture flow after setting emitted so synchronous exceptions from
args.capture()?. do not escape; swallow the capture failure and preserve
unreadable-session reporting. In
apps/ade-cli/src/services/account/accountMachinePublisherService.test.ts:1421-1496,
add coverage with a throwing capture callback and assert report() does not throw
while the episode remains consumed.

In `@apps/ade-cli/src/services/credentials/credentialStore.ts`:
- Around line 972-976: Update the readAllAsync flow around
bindLegacyCiphertextUnderLock so legacy ciphertext binding does not
synchronously block the brain read path while waiting for
withCredentialFileLock. Move the retry/write work off the asynchronous read path
or apply a bounded wait specifically for this call, while preserving the
returned attempt.values and rewrite behavior.

In `@apps/desktop/src/main/services/ipc/registerIpc.ts`:
- Around line 3947-3970: Add the IPC.appRestartBackgroundService channel to the
ipcInvokeTimeoutMs table with a timeout covering the 60-second service install
timeout plus the 20-second brain restart timeout, ensuring the renderer does not
time out before captureRepairOutcome records completion or failure.

In `@apps/desktop/src/main/services/runtime/projectRecoveryService.ts`:
- Around line 411-475: Serialize repair and restart operations through a shared
in-flight operation rather than relying only on repairInFlight. Update
restartBrain() and runRepair() so each registers its promise before starting
work, and repair awaits any already-running restart before stopping the service
or accessing the database; preserve rejection of new restarts while repair is
active and ensure the shared state is cleared when the operation settles.

In `@apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.tsx`:
- Around line 465-478: Update refreshPublishHealth to maintain a monotonically
increasing request ID for each getInfo request, and capture the ID when starting
the request. In the promise resolution, update setLocalPublishHealth only when
the component is mounted and the response ID matches the latest request ID,
preventing stale responses from overwriting newer state.

In `@apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx`:
- Around line 233-237: The generic token_unreadable state must not independently
enable repair. Add a producer-authoritative repairability field to the shared
account-directory health contract, set it only for session-read failures, and
update SyncDevicesSection to require that typed marker when computing
showRepair; in
apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx lines
233-237, gate Repair on the marker, and in
apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx lines
252-262, add a token_unreadable case with the marker false and assert Repair is
absent.

In `@apps/ios/ADETests/SyncRecoveryPolicyTests.swift`:
- Line 1281: Replace the unbounded readiness loop around
performPostHelloRestorationForTesting with a timeout-aware XCTest expectation or
wait that observes restoration.isWaiting; fail the test with a clear message if
the DeferredRecoveryWork continuation is not installed before the timeout, then
continue only after readiness is confirmed.

In `@scripts/posthog/dashboard-spec.mjs`:
- Line 540: Update the reliability dashboard description near the existing brain
wedge, route-publish, and update-flow incident text to also mention
account-session unreadable incidents, matching the
EVENTS.ACCOUNT_SESSION_UNREADABLE series added in the dashboard event nodes.

---

Outside diff comments:
In `@apps/ios/ADE/Views/Hub/HubQuickConnect.swift`:
- Around line 221-229: Update the reconnect flow in the Task around
reconnect(toSavedHost:) to capture its returned Boolean and use that result for
the success/error branch instead of syncService.connectionState. Preserve the
existing haptics, callback, and error-text behavior for each outcome.

---

Nitpick comments:
In `@apps/ade-cli/src/services/credentials/credentialStore.test.ts`:
- Around line 449-460: Add an injectable binding or explicit OS-material
expectation to expectsOsBoundKeyMaterial or decodeCredentialStore, instead of
relying exclusively on process.env and the Vitest override, so tests can reach
the no_os_key_material classification. Update the credential-store test suite
with coverage that exercises this path and asserts getLastReadFailureReason()
returns no_os_key_material while preserving existing decrypt_failure and
store_format behavior.
- Around line 962-974: Add a test alongside the existing coalescing case that
starts a deferred resolver.readAsync() call, invokes invalidate() before
resolving it, then verifies the in-flight caller receives the resolved material
while a subsequent resolver.read() still returns the pre-existing material and
performs no additional OS read. Use createMacKeychainMaterialResolver and assert
the invalidation epoch prevents the stale result from repopulating cached.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 78467702-b53b-4d2b-826c-83824a8a8077

📥 Commits

Reviewing files that changed from the base of the PR and between 872807f and 14063b7.

⛔ Files ignored due to path filters (8)
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/onboarding-and-settings/README.md is excluded by !docs/**
  • docs/features/remote-runtime/README.md is excluded by !docs/**
  • docs/features/remote-runtime/internal-architecture.md is excluded by !docs/**
  • docs/features/storage-and-recovery/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
  • docs/logging.md is excluded by !docs/**
📒 Files selected for processing (64)
  • apps/ade-cli/src/commands/doctor.test.ts
  • apps/ade-cli/src/commands/doctor.ts
  • apps/ade-cli/src/multiProjectRpcServer.test.ts
  • apps/ade-cli/src/serviceManager/installWindows.test.ts
  • apps/ade-cli/src/services/account/accountAuthService.ts
  • apps/ade-cli/src/services/account/accountMachinePublisherService.test.ts
  • apps/ade-cli/src/services/account/accountMachinePublisherService.ts
  • apps/ade-cli/src/services/account/episodeAnalytics.ts
  • apps/ade-cli/src/services/credentials/credentialStore.test.ts
  • apps/ade-cli/src/services/credentials/credentialStore.ts
  • apps/ade-cli/src/services/credentials/osBoundKeyMaterial.ts
  • apps/ade-cli/src/services/credentials/windowsDpapiMaterial.test.ts
  • apps/ade-cli/src/services/credentials/windowsDpapiMaterial.ts
  • apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts
  • apps/ade-cli/src/services/sync/syncHostService.test.ts
  • apps/desktop/src/main/services/analytics/productAnalyticsPolicy.ts
  • apps/desktop/src/main/services/analytics/productAnalyticsService.test.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.test.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts
  • apps/desktop/src/main/services/remoteRuntime/runtimeDiscovery.ts
  • apps/desktop/src/main/services/runtime/projectRecoveryService.test.ts
  • apps/desktop/src/main/services/runtime/projectRecoveryService.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.test.tsx
  • apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.tsx
  • apps/desktop/src/renderer/components/settings/BrainRepairButton.tsx
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
  • apps/desktop/src/renderer/components/settings/accountDirectorySummary.test.ts
  • apps/desktop/src/renderer/components/settings/accountDirectorySummary.ts
  • apps/desktop/src/renderer/components/settings/useSyncConnections.ts
  • apps/desktop/src/renderer/hooks/useBrainRepair.ts
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/types/productAnalytics.ts
  • apps/desktop/src/shared/types/remoteRuntime.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Components/ADEDesignSystem.swift
  • apps/ios/ADE/Views/Cto/CtoRootScreen.swift
  • apps/ios/ADE/Views/Cto/CtoSessionDestinationView.swift
  • apps/ios/ADE/Views/Deeplinks/SendToMacCard.swift
  • apps/ios/ADE/Views/Files/FilesDetailComponents.swift
  • apps/ios/ADE/Views/Files/FilesRootScreen+Actions.swift
  • apps/ios/ADE/Views/Hub/HubComposerDrawer.swift
  • apps/ios/ADE/Views/Hub/HubQuickConnect.swift
  • apps/ios/ADE/Views/Hub/HubScreen.swift
  • apps/ios/ADE/Views/Lanes/LaneDetailScreen.swift
  • apps/ios/ADE/Views/Lanes/LaneRootStateViews.swift
  • apps/ios/ADE/Views/LanesTabView.swift
  • apps/ios/ADE/Views/PRs/PrDetailScreen.swift
  • apps/ios/ADE/Views/PRs/PrsRootScreen.swift
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
  • apps/ios/ADE/Views/Settings/SettingsConnectionHeader.swift
  • apps/ios/ADE/Views/Settings/SettingsPinSheet.swift
  • apps/ios/ADE/Views/Work/TerminalSessionScreen.swift
  • apps/ios/ADE/Views/Work/WorkNewChatScreen.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/ios/ADE/Views/Work/WorkSessionDestinationView.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/ios/ADETests/SyncAccountConnectRecoveryTests.swift
  • apps/ios/ADETests/SyncRecoveryPolicyTests.swift
  • scripts/posthog/dashboard-spec.mjs
💤 Files with no reviewable changes (3)
  • apps/desktop/src/renderer/components/settings/accountDirectorySummary.test.ts
  • apps/ios/ADE/Views/Lanes/LaneRootStateViews.swift
  • apps/ios/ADE/Views/Components/ADEDesignSystem.swift

Comment thread apps/ade-cli/src/services/account/episodeAnalytics.ts Outdated
Comment on lines +972 to +976
// The asynchronous path binds legacy ciphertext too. It is the brain's read
// path, and on a machine whose only reader is the brain the store would
// otherwise stay machine-key-sealed forever.
if (attempt.rewriteWithCurrentKey) this.bindLegacyCiphertextUnderLock(attempt.key);
return attempt.values;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the credential file lock implementation for blocking waits.
set -euo pipefail

fd -t f 'credentialStore.ts|.*[Ll]ock.*\.ts' apps/ade-cli/src | while IFS= read -r f; do
  rg -n -C 12 'withCredentialFileLock|withOptionalCredentialFileLock' "$f" || true
done

# Look for synchronous sleep/spin primitives used by the lock helper.
rg -n -C 5 'Atomics\.wait|sleepSync|hrtime|LOCK_RETRY_MS|LOCK_TIMEOUT_MS' apps/ade-cli/src

Repository: arul28/ADE

Length of output: 21145


Avoid blocking the brain read path while binding legacy ciphertext.

readAllAsync synchronously calls bindLegacyCiphertextUnderLock, which enters withCredentialFileLock and performs synchronous file writes during the lock retry. This blocks the event loop until the lock is acquired, so move or delegate the retry wait away from the brain read path, or bound the blocking wait to this call site.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/services/credentials/credentialStore.ts` around lines 972 -
976, Update the readAllAsync flow around bindLegacyCiphertextUnderLock so legacy
ciphertext binding does not synchronously block the brain read path while
waiting for withCredentialFileLock. Move the retry/write work off the
asynchronous read path or apply a bounded wait specifically for this call, while
preserving the returned attempt.values and rewrite behavior.

Comment thread apps/desktop/src/main/services/ipc/registerIpc.ts
Comment thread apps/desktop/src/main/services/runtime/projectRecoveryService.ts
Comment on lines +233 to +237
// A brain-side unreadable account session is the one directory failure a
// restart clears — same test RemoteTargetList runs on its publish health.
const showRepair = accountSignedIn
&& isBrainAccountSessionFailure(status.routeHealth?.accountDirectory?.state)
&& repair.available;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not treat every token_unreadable result as repairable.

The publisher uses token_unreadable when getAccessToken() cannot read or refresh a token. The renderer matches that generic state, so it can offer a background-service restart for a token-refresh or transport fault.

Add a producer-authoritative repairability marker to the shared account-directory health contract. Set it only for session-read failures. Do not infer it from the generic state string.

  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx#L233-L237: gate Repair on the new typed marker.
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx#L252-L262: add a token_unreadable result with a false repairability marker and assert that Repair is absent.

As per coding guidelines, “Fix underlying services or shared types instead of adding renderer-only workarounds.”

📍 Affects 2 files
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx#L233-L237 (this comment)
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx#L252-L262
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx` around
lines 233 - 237, The generic token_unreadable state must not independently
enable repair. Add a producer-authoritative repairability field to the shared
account-directory health contract, set it only for session-read failures, and
update SyncDevicesSection to require that typed marker when computing
showRepair; in
apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx lines
233-237, gate Repair on the marker, and in
apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx lines
252-262, add a token_unreadable case with the marker false and assert Repair is
absent.

Source: Coding guidelines

Comment thread apps/ios/ADETests/SyncRecoveryPolicyTests.swift Outdated
Comment thread scripts/posthog/dashboard-spec.mjs
- serialize repair() behind an in-flight restartBrain() instead of racing it
  into exclusive database work, with a bounded wait so the recovery screen
  never parks; repair still runs whatever the restart did
- bound the readiness ping (RuntimeRpcClient's default is 10 minutes) and give
  the restart IPC a budget that covers a forced install queued behind an
  in-flight one
- keep analytics capture best-effort so a throwing sink cannot rewrite a
  token_unreadable outcome as transport_error
- drop stale publish-health responses with a request generation
- bound two unbounded waits in SyncRecoveryPolicyTests
- name relay suppression in the reliability dashboard description

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28

arul28 commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@arul28
arul28 merged commit 6b1d4f1 into main Aug 4, 2026
36 checks passed
@arul28
arul28 deleted the ade/ok-need-your-diagnosing-issue branch August 4, 2026 08:46
arul28 added a commit that referenced this pull request Aug 4, 2026
Three conflicts, two of them semantic rather than textual.

credentialStore.ts — taken entirely from main. #1019 fixed the same
destroy-on-migration class from a different angle and did it better: an
intrinsic `isFileBackedCredentialKey` predicate instead of the per-caller
`sharedLegacyKeys` option I added (so a call site cannot forget it), the
sync bootstrap token covered as well as the account session, a throw on
writing a file-backed key into safeStorage, and — the part mine lacked —
pruning migrated duplicates out of the retained file store. Without that
prune both files hold a copy and the brain keeps serving a stale one
after the app rotates a token. My option and both of my tests are gone;
main's tests cover the same ground and assert more.

runtimeDiscovery.ts — genuinely complementary, so both sides kept. Mine
rewrote the Tailscale diagnostics to name what is missing from the list
and what to do; main added a `severity` field so "not installed" reads as
info rather than a warning. The merged form is my copy carrying their
severity.

installWindows.test.ts — the auto-merge silently produced broken code by
combining my 41 renamed lazy-accessor uses with main's 3 remaining
old-constant uses. That is the failure mode textual merging has with a
rename, and it typechecks as an undefined name rather than a conflict.

main's new Repair test asserted `taskkill /PID <supervisor> /T /F`. This
branch deliberately drops `/T`: the brain is the supervisor's child, so a
tree kill took it down mid-write with no SQLite/CRDT flush and no lock
release, and the next start had to recover from a lock whose owner never
got to release it. The brain is now talked down separately, after the
supervisor can no longer relaunch it. The test's intent — Repair must not
degrade into a no-op — still holds and is still asserted; only the args
changed. Its positional spawn queue also needed the stale-serve scan this
branch added to the install path.

ade-cli: 2809 passed. Both typechecks clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 10, 2026
`hasPairedHost` answered two different questions with one boolean: "has this
phone ever been paired" and "can this phone read the credential for the
pairing it has". A Keychain entry that is missing, cleared out from under the
app, or unreadable made a paired phone report as never-paired.

What that looks like to a user: they pair, the sheet confirms the machine,
and the accountless gate then re-presents the same connect sheet they just
finished — with nothing said about why. They have already done the step it is
asking for, so the app reads as ignoring them. It is the same user-facing
class as the token_unreadable work in #1019; the accountless gate simply
never got that treatment.

`syncPairedHostCredentialState` now separates `notPaired` from
`credentialUnreadable`, and `hasPairedHost` is defined as its `isUsable` case
so every existing gate keeps its exact behaviour — an unusable credential
still blocks entry, because without the secret the phone genuinely cannot use
the pairing. Only the words change: the gate names the fault and offers "Pair
again", which is the existing repair vocabulary on this platform
(`laneOfflineAction`), rather than the unexplained loop.

The classifier is pure and takes the credential lookup as a parameter,
because the branch that matters is only reachable with a real Keychain fault.
The test drives it directly with a persisted paired profile and a lookup that
returns nil — standing in for the `errSecMissingEntitlement` (-34018) read
failure — and asserts the state is `credentialUnreadable`, is not
`notPaired`, and still gates entry.

Found while diagnosing why a simulator build could pair over LAN and forget
on relaunch. That turned out to be an unentitled build with no Keychain
access at all, not a product defect — but it surfaced this gate, which is
real and does affect a device whose Keychain entry goes bad.

1306 tests, same 13 pre-existing failing cases as clean main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 10, 2026
`hasPairedHost` answered two different questions with one boolean: "has this
phone ever been paired" and "can this phone read the credential for the
pairing it has". A Keychain entry that is missing, cleared out from under the
app, or unreadable made a paired phone report as never-paired.

What that looks like to a user: they pair, the sheet confirms the machine,
and the accountless gate then re-presents the same connect sheet they just
finished — with nothing said about why. They have already done the step it is
asking for, so the app reads as ignoring them. It is the same user-facing
class as the token_unreadable work in #1019; the accountless gate simply
never got that treatment.

`syncPairedHostCredentialState` now separates `notPaired` from
`credentialUnreadable`, and `hasPairedHost` is defined as its `isUsable` case
so every existing gate keeps its exact behaviour — an unusable credential
still blocks entry, because without the secret the phone genuinely cannot use
the pairing. Only the words change: the gate names the fault and offers "Pair
again", which is the existing repair vocabulary on this platform
(`laneOfflineAction`), rather than the unexplained loop.

The classifier is pure and takes the credential lookup as a parameter,
because the branch that matters is only reachable with a real Keychain fault.
The test drives it directly with a persisted paired profile and a lookup that
returns nil — standing in for the `errSecMissingEntitlement` (-34018) read
failure — and asserts the state is `credentialUnreadable`, is not
`notPaired`, and still gates entry.

Found while diagnosing why a simulator build could pair over LAN and forget
on relaunch. That turned out to be an unentitled build with no Keychain
access at all, not a product defect — but it surfaced this gate, which is
real and does affect a device whose Keychain entry goes bad.

1306 tests, same 13 pre-existing failing cases as clean main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Aug 10, 2026
`hasPairedHost` answered two different questions with one boolean: "has this
phone ever been paired" and "can this phone read the credential for the
pairing it has". A Keychain entry that is missing, cleared out from under the
app, or unreadable made a paired phone report as never-paired.

What that looks like to a user: they pair, the sheet confirms the machine,
and the accountless gate then re-presents the same connect sheet they just
finished — with nothing said about why. They have already done the step it is
asking for, so the app reads as ignoring them. It is the same user-facing
class as the token_unreadable work in #1019; the accountless gate simply
never got that treatment.

`syncPairedHostCredentialState` now separates `notPaired` from
`credentialUnreadable`, and `hasPairedHost` is defined as its `isUsable` case
so every existing gate keeps its exact behaviour — an unusable credential
still blocks entry, because without the secret the phone genuinely cannot use
the pairing. Only the words change: the gate names the fault and offers "Pair
again", which is the existing repair vocabulary on this platform
(`laneOfflineAction`), rather than the unexplained loop.

The classifier is pure and takes the credential lookup as a parameter,
because the branch that matters is only reachable with a real Keychain fault.
The test drives it directly with a persisted paired profile and a lookup that
returns nil — standing in for the `errSecMissingEntitlement` (-34018) read
failure — and asserts the state is `credentialUnreadable`, is not
`notPaired`, and still gates entry.

Found while diagnosing why a simulator build could pair over LAN and forget
on relaunch. That turned out to be an unentitled build with no Keychain
access at all, not a product defect — but it surfaced this gate, which is
real and does affect a device whose Keychain entry goes bad.

1306 tests, same 13 pre-existing failing cases as clean main.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant