feat(boot): gate launch on core mode pick + version check#1316
feat(boot): gate launch on core mode pick + version check#1316senamakel merged 9 commits intotinyhumansai:mainfrom
Conversation
Add a pre-router BootCheckGate that runs on every Tauri launch: - First launch prompts Local (default) or Cloud core; choice persisted in a new `coreMode` redux slice (plain localStorage, pre-login scope). - Local mode now spawns the embedded core via a new `start_core_process` Tauri command instead of auto-spawning at setup; daemon_mode keeps its immediate spawn for the headless tray agent. - Compares core's `openhuman.update_version` against the app version and surfaces dedicated recovery screens for legacy daemon detection (service_stop + service_uninstall), outdated local core, outdated cloud core (update_run), missing version method, and unreachable core. - New `app_quit` Tauri command backs the Quit button in the unreachable state.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughDefers auto-spawning the embedded core in non-daemon runs and exposes Tauri commands ( ChangesBoot Check & Core Startup
Sequence Diagram(s)sequenceDiagram
participant Frontend as Frontend App
participant Gate as BootCheckGate
participant Redux as Redux Store
participant Tauri as Tauri Backend
participant Core as Core Process
participant RPC as RPC Endpoint
Frontend->>Gate: Mount BootCheckGate
Gate->>Redux: Read coreMode
alt coreMode is unset
Gate->>Gate: Show mode picker
Frontend->>Redux: User selects local or cloud
end
Gate->>Gate: Enter checking phase
alt local mode
Gate->>Tauri: invoke start_core_process()
Tauri->>Core: spawn/start core
Gate->>RPC: poll openhuman.ping
Core->>RPC: respond to ping
Gate->>RPC: openhuman.service_status / openhuman.update_version
else cloud mode
Gate->>Redux: persist cloud RPC URL
Gate->>RPC: openhuman.update_version (cloud endpoint)
end
alt version matches
Gate->>Frontend: pass-through children (app mounts)
else failure
Gate->>Frontend: show result screen (daemonDetected / outdated / noVersionMethod / unreachable)
Frontend->>Gate: user action (retry / switch mode / quit / update / remove daemon)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
app/src/components/BootCheckGate/BootCheckGate.tsx (1)
396-403: 💤 Low valueMinor simplification: identical branches.
Both branches set the same state — the condition doesn't affect the outcome. This can be simplified:
- if (checkResult.kind === 'match') { - // Gate resolves — render children. - setPhase('result'); - setResult(checkResult); - } else { - setPhase('result'); - setResult(checkResult); - } + setPhase('result'); + setResult(checkResult);🤖 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 `@app/src/components/BootCheckGate/BootCheckGate.tsx` around lines 396 - 403, The conditional in BootCheckGate that branches on checkResult.kind is redundant because both branches call setPhase('result') and setResult(checkResult); remove the if/else and replace with a single sequence invoking setPhase('result') followed by setResult(checkResult) where the current branching occurs (references: checkResult, setPhase, setResult in BootCheckGate).app/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx (1)
174-193: 💤 Low valueMinor: Redundant mock setup.
Line 180 duplicates the
mockResolvedValuealready set on line 176. The second assignment is unnecessary since the mock is already configured.it('shows outdated cloud screen', async () => { - mockRunBootCheck.mockResolvedValue({ kind: 'outdatedCloud' }); - const store = makeStore({ kind: 'cloud', url: 'https://core.example.com/rpc' }); - // Trigger the check by rendering with an already-set mode mockRunBootCheck.mockResolvedValue({ kind: 'outdatedCloud' }); render(🤖 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 `@app/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx` around lines 174 - 193, The test in the BootCheckGate — outdatedCloud suite redundantly calls mockRunBootCheck.mockResolvedValue twice; remove the duplicate call so only one mockResolvedValue({ kind: 'outdatedCloud' }) remains (keep the first one at the start of the test) to avoid unnecessary duplication and potential confusion when setting up the mock for the BootCheckGate test that uses mockRunBootCheck.
🤖 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 `@app/src/lib/bootCheck/index.ts`:
- Around line 93-106: The fixed delays array in waitForCore can finish before
maxMs is reached, causing premature timeout; replace the static delays with a
loop that generates backoff delays until elapsed >= maxMs (e.g., start at 200ms,
double each retry capped at 1000ms), compute nextDelay = Math.min(cappedBackoff,
maxMs - elapsed) so you never wait past the remaining time, and use that
nextDelay for the setTimeout and elapsed accumulation around the
callRpc('openhuman.ping', {}) retry logic.
- Around line 51-60: Replace the dynamic imports in defaultCallRpc and
defaultInvokeCmd with static module-level imports: import callCoreRpc from the
core RPC service and import invoke from `@tauri-apps/api/core` at the top of the
file, then call callCoreRpc<T>({ method, params }) inside defaultCallRpc and
invoke<T>(cmd, args) inside defaultInvokeCmd; ensure the imported symbols match
the names used in the functions (callCoreRpc and invoke) and remove the await
import(...) lines so the module uses static imports.
---
Nitpick comments:
In `@app/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx`:
- Around line 174-193: The test in the BootCheckGate — outdatedCloud suite
redundantly calls mockRunBootCheck.mockResolvedValue twice; remove the duplicate
call so only one mockResolvedValue({ kind: 'outdatedCloud' }) remains (keep the
first one at the start of the test) to avoid unnecessary duplication and
potential confusion when setting up the mock for the BootCheckGate test that
uses mockRunBootCheck.
In `@app/src/components/BootCheckGate/BootCheckGate.tsx`:
- Around line 396-403: The conditional in BootCheckGate that branches on
checkResult.kind is redundant because both branches call setPhase('result') and
setResult(checkResult); remove the if/else and replace with a single sequence
invoking setPhase('result') followed by setResult(checkResult) where the current
branching occurs (references: checkResult, setPhase, setResult in
BootCheckGate).
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a57f60b9-5933-4f06-a90f-d5e3c84d7bac
📒 Files selected for processing (9)
app/src-tauri/src/lib.rsapp/src/App.tsxapp/src/components/BootCheckGate/BootCheckGate.tsxapp/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsxapp/src/lib/bootCheck/index.test.tsapp/src/lib/bootCheck/index.tsapp/src/store/coreModeSlice.test.tsapp/src/store/coreModeSlice.tsapp/src/store/index.ts
- Replace dynamic import() of callCoreRpc and @tauri-apps/api/core with static imports per CLAUDE.md "no dynamic imports in app/src" rule. - Reshape waitForCore to use Date.now()-based elapsed tracking with exponential backoff capped at 1s, so the loop can use the full maxMs budget instead of giving up after a fixed 8400ms delay sum.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@app/src/lib/bootCheck/index.ts`:
- Line 253: The log call in bootCheck using log('[boot-check] cloud mode —
url=%s', mode.url) may print sensitive data (userinfo, tokens in query); update
the logging to redact or omit sensitive parts before passing to log: parse
mode.url (in the bootCheck/index.ts flow where mode.url is used) and
replace/remove URL.userinfo and query params (or log only scheme+host+path) so
credentials/tokens are never emitted, then call log with the sanitized string
instead of mode.url.
- Around line 14-15: The module currently imports and calls Tauri's invoke
directly; extract the defaultInvokeCmd wrapper into a new service module (e.g.,
bootCheckService) that encapsulates invoke, export defaultInvokeCmd from that
service, and update the BootCheckTransport type to accept a transport function
(the injected defaultInvokeCmd) instead of importing invoke in the bootCheck
index module; then remove the import of invoke from the bootCheck index, and
update all callers of the bootCheck initialization to pass the service-backed
defaultInvokeCmd implementation into BootCheckTransport so IPC is localized to
the service.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9be58047-3f58-485d-8787-988f15e7a035
📒 Files selected for processing (1)
app/src/lib/bootCheck/index.ts
… URL log - Move the production BootCheckTransport (invoke + callCoreRpc) into a new app/src/services/bootCheckService.ts so the orchestrator and gate no longer import @tauri-apps/api/core directly. Aligns with the IPC-localization guideline. - runBootCheck now requires an explicit transport (no defaultTransport fallback). BootCheckGate consumes bootCheckTransport from the service. - Redact mode.url before logging in cloud-mode flow; only the protocol/host/path are emitted, never userinfo or query (CodeRabbit).
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/src/services/bootCheckService.ts (1)
15-21: ⚡ Quick winAdd namespaced debug logs at transport boundaries.
callRpc/invokeCmdare the external-call edges, but they are currently silent. Add entry/success/failure debug logs (with redacted args) so boot-check failures are traceable in production.As per coding guidelines, "Add substantial, development-oriented logs at entry/exit points, branch decisions, external calls, retries/timeouts, state transitions, and error handling paths; use namespaced
debuglogs in production app code."🤖 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 `@app/src/services/bootCheckService.ts` around lines 15 - 21, Add namespaced debug logs to the transport edges: in callRpc and invokeCmd log an entry debug ("bootCheckService:callRpc" / "bootCheckService:invokeCmd") with method/cmd and redacted params/args, log a success debug on resolved result, and log an error debug on rejection including the error; callCoreRpc and invoke should remain the call targets (keep return values), ensure sensitive fields in params/args are redacted (e.g., replace values for keys like "password", "token", "secret") before logging, and use consistent namespacing so logs show entry/exit/failure for these functions.
🤖 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 `@app/src/lib/bootCheck/index.ts`:
- Around line 247-248: The code currently persists the raw user-provided URL by
calling storeRpcUrl(mode.url), which may contain embedded credentials or
sensitive query tokens; update the logic around storeRpcUrl (and keep
clearCoreRpcUrlCache) to either 1) sanitize/normalize mode.url before storing by
stripping userinfo (username:password@) and removing query parameters and
fragments so only scheme://host[:port]/path remains, or 2) validate and reject
URLs that contain userinfo or query strings and surface an error to the caller;
ensure the check/sanitization runs immediately before calling storeRpcUrl and
reference mode.url, storeRpcUrl, and clearCoreRpcUrlCache when implementing the
change.
---
Nitpick comments:
In `@app/src/services/bootCheckService.ts`:
- Around line 15-21: Add namespaced debug logs to the transport edges: in
callRpc and invokeCmd log an entry debug ("bootCheckService:callRpc" /
"bootCheckService:invokeCmd") with method/cmd and redacted params/args, log a
success debug on resolved result, and log an error debug on rejection including
the error; callCoreRpc and invoke should remain the call targets (keep return
values), ensure sensitive fields in params/args are redacted (e.g., replace
values for keys like "password", "token", "secret") before logging, and use
consistent namespacing so logs show entry/exit/failure for these functions.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1323190f-4ca2-4dbf-bf09-ce99d475097e
📒 Files selected for processing (3)
app/src/components/BootCheckGate/BootCheckGate.tsxapp/src/lib/bootCheck/index.tsapp/src/services/bootCheckService.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/components/BootCheckGate/BootCheckGate.tsx
Strip userinfo, query, and fragment from the user-provided cloud URL before passing it to storeRpcUrl, so credentials or tokens embedded in the URL never land on disk. Also short-circuit to an unreachable result on parse failure instead of persisting "<invalid-url>".
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@app/src/lib/bootCheck/index.ts`:
- Around line 239-250: The current code builds safeUrl including pathname and
then logs it (safeUrl), which may expose sensitive path segments; change the
construction to use only the origin (scheme + host + optional port) by deriving
it from the URL object (e.g., parsed.origin or
`${parsed.protocol}//${parsed.host}`) so safeUrl contains only origin, and
ensure the log call still uses safeUrl (now origin-only) so no path/segments are
ever written to logs; update the assignment of safeUrl and the subsequent
log('[boot-check] cloud mode — url=%s', safeUrl) accordingly.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e72aa90b-1d21-486c-ba71-078c69416303
📒 Files selected for processing (1)
app/src/lib/bootCheck/index.ts
…sport Add tests for the diff-coverage gate's missing lines: service_status RPC throw, empty version_info, start_core_process invoke failure, local version check transport failure, and invalid cloud URL guard. Add a small bootCheckService unit test asserting callRpc/invokeCmd delegate correctly to callCoreRpc and Tauri invoke.
- `dedup_key` now trims/dedupes/sorts source_refs and collapses runs of whitespace in the body before truncating, so LLM noise like duplicate ids or stray newlines no longer slip past the dedupe gate. - `resolve_entity` preserves the LLM's original kind prefix (`artifact:`, `person:`, `tool:`, …) instead of flattening every resolved entity to `kind: "entity"`. Routing also accepts any non-summary non-empty kind so we don't silently drop refs outside an arbitrary allowlist — the SQL miss decides whether the row exists. - `handle_reflections_act` logs a warning when the post-thread `mark_acted` stamp write fails. Without the log, a silent failure here leaves the reflection card visible and a re-Act would spawn a duplicate thread. - Vite `host: true` comment now calls out the 0.0.0.0 / LAN exposure side effect and points at `host: 'localhost'` as the override on shared / corporate networks. - `bootCheck::waitForCore` docstring corrected to reference `core.ping` (matched the implementation since the tinyhumansai#1316 fix; comment was stale). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
BootCheckGateruns on every launch before the router mounts; first launch prompts Local (default) or Cloud core, persisted in a newcoreModeredux slice (plain localStorage, pre-login scope).start_core_processTauri command. Onlydaemon_modekeeps the immediate spawn for the headless tray agent.openhuman.update_versionagainst the app version and renders dedicated recovery screens for: legacy daemon detected (service_stop+service_uninstall), outdated local core (restart_core_process), outdated cloud core (update_run), missing version method, and unreachable core. Adds anapp_quitTauri command for the unreachable Quit affordance.Problem
We had no way to detect a version drift between the React/Tauri app and the embedded
openhumancore binary. A stale local install or a legacy daemon left over from before the in-process model could silently bind to the RPC port, producing 401s, schema mismatches, and confusing failures downstream. The user also had no UI-level choice between running the core locally or pointing at a remote/cloud instance — the spawn happened unconditionally inside Tauri'ssetup.Solution
A pre-router gate handles three concerns in one place:
userScopedStoragesince this runs before login).app/src-tauri/src/lib.rssetup is gated behinddaemon_mode. Interactive launches now call the newstart_core_processTauri command from the gate after the user confirms Local mode. Cloud mode never spawns a local core.app/src/lib/bootCheck/probesservice_status(daemon detection) thenupdate_version, compares againstAPP_VERSIONfromapp/src/utils/config.ts, and returns one of:match | daemonDetected | outdatedLocal | outdatedCloud | noVersionMethod | unreachable. The gate component renders a tailored screen per state with a primary action that re-runs the orchestrator after success.Transport is dependency-injected so the orchestrator is testable without mocking globals. Existing daemon-recovery UI (
ServiceBlockingGate) is preserved unchanged — it serves a different purpose (post-boot health) and renders below the new gate.Submission Checklist
docs/TESTING-STRATEGY.md-32601and timeout via fake timers), gate component (15 tests covering picker/spinner/all states/switch-mode).docs/TESTING-STRATEGY.md)Impact
daemon_mode(headless tray) startup is unaffected — it still auto-spawns the embedded core.coreModeslice persists to plain localStorage (keypersist:coreMode) — not user-scoped on purpose since it must be readable pre-login.Related
BootCheckGate.tsx(552 lines) into per-state subcomponents to come back under the project's ≤500-line guideline.pnpm tauri devexercising each branch (legacy daemon detection in particular needs a machine with thecom.openhuman.daemonplist installed to verify end-to-end).AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
feat/core-updateValidation Run
pnpm --filter openhuman-app format:checkpnpm typecheckpnpm test:unit— 1678 pass / 4 skipped / 1 todocargo check --manifest-path app/src-tauri/Cargo.toml— 0 errorsValidation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
Parity Contract
daemon_modestill auto-spawns the embedded core; existingServiceBlockingGateandrestart_core_processflows are untouched. Existing JSON-RPC methods (update_version,update_run,service_*) are reused — no new core surface area.ensure_running()fallbacks insidecore_rpc_relayandrestart_core_processare intentionally kept as a safety net.Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Bug Fixes / Recovery
Tests