mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 21:44:38 +00:00
feat(config): split chat_onboarding_completed from React UI onboarding flag (#525)
End-to-end testing on the Tauri desktop app revealed that the welcome
agent could never run from the in-app chat pane — even with all of
Commits 4b/8 in place — because the React layer's
`OnboardingOverlay.tsx` renders a full-screen wizard whenever
`onboarding_completed = false` and gates the chat pane behind it. By
the time a user can type a chat message the React wizard has already
flipped `onboarding_completed = true` (via `OnboardingOverlay::handleDone`
or the `Onboarding.tsx` wizard's completion handler), so the welcome
agent's routing condition is never satisfied on the Tauri surface.
This commit fixes the architectural conflict by splitting the single
`onboarding_completed` flag into two orthogonal flags:
* **`onboarding_completed`** — unchanged semantics. Tracks whether the
React UI wizard has been completed/dismissed. Continues to be set
by `OnboardingOverlay.tsx::handleDone` and `Onboarding.tsx` via the
existing `config.set_onboarding_completed` JSON-RPC method. Used
exclusively to gate whether the React layer renders the wizard.
* **`chat_onboarding_completed`** — NEW. Tracks whether the welcome
agent's chat-based greeting flow has run. Set exclusively by the
welcome agent itself via `complete_onboarding(action="complete")`.
Used by both the Tauri web channel
(`channels::providers::web::build_session_agent`) and the external
channel dispatch path (`channels::runtime::dispatch::resolve_target_agent`)
to decide whether to route to welcome or orchestrator.
The two flags are intentionally orthogonal:
* A Tauri desktop user completes the React wizard → only
`onboarding_completed` flips → wizard disappears → user types `hi`
→ welcome agent runs (because `chat_onboarding_completed` is still
false) → welcome calls `complete_onboarding(complete)` →
`chat_onboarding_completed` flips → next chat turn routes to
orchestrator.
* A Telegram/Discord user (no React wizard exists) sends a message
→ external channel dispatch checks `chat_onboarding_completed` →
routes to welcome → welcome runs → flips the flag → next inbound
message routes to orchestrator.
Both paths give every user the chat welcome experience, regardless of
which surface they came in through, and without requiring the React
wizard to be removed or restructured.
## Files changed
`src/openhuman/config/schema/types.rs`
* Add `chat_onboarding_completed: bool` field with `#[serde(default)]`
for backward compat — existing `config.toml` files that don't have
the field default to `false`, which means existing users will see
the welcome agent on their next chat turn (correct behaviour, the
welcome agent is idempotent).
* Default impl initializes the new field to `false`.
* Extensive rustdoc on both flags explaining the orthogonal split,
why two flags exist, and which code paths gate on which.
`src/openhuman/tools/impl/agent/complete_onboarding.rs`
* `check_status` now reports BOTH flags side-by-side so the welcome
agent's LLM can see whether the React wizard has run AND whether
the chat welcome itself has run. Old single "Onboarding completed"
line replaced with two lines: "UI onboarding wizard completed" and
"Chat welcome flow completed".
* `complete()` now flips `chat_onboarding_completed`, NOT
`onboarding_completed`. The React UI's flag is left untouched —
that's owned by the React layer. Idempotency guard updated to
check the chat flag.
* Rustdoc on `complete()` explains the orthogonal-flags rationale
for future readers.
`src/openhuman/channels/providers/web.rs::build_session_agent`
* Reads `effective.chat_onboarding_completed` instead of
`effective.onboarding_completed` for the welcome-vs-orchestrator
decision.
* Log line now includes BOTH flags so observability captures the
full picture (e.g. `chat_onboarding_completed=false,
ui_onboarding_completed=true` is the expected steady state for a
Tauri user who completed the React wizard but hasn't typed yet).
`src/openhuman/channels/runtime/dispatch.rs::resolve_target_agent`
* Same flag swap for the external-channel path.
* `[dispatch::routing] selected target agent` info trace also
reports both flags.
* Function-level rustdoc updated with the new semantics.
## Backward compatibility
* Existing `config.toml` files: `chat_onboarding_completed` defaults
to `false` via `#[serde(default)]`. Means existing users get a
welcome message on their next chat turn — this is the desired
behaviour, not a regression.
* React layer: untouched. The `config.set_onboarding_completed`
JSON-RPC method continues to write the same field; the new flag is
not exposed to React at all.
* External callers of `complete_onboarding(complete)`: they now flip
a different flag. This may matter for callers who were depending
on the flag flip side effect; a quick grep shows
`tools/impl/agent/complete_onboarding.rs` is the only caller and
the rest of the codebase reads `onboarding_completed` for UI
purposes (snapshots, app state) and `chat_onboarding_completed`
for routing — the split is clean.
## Tests
399/400 tools tests pass. The single failure
(`tools::impl::browser::screenshot::screenshot_command_contains_output_path`)
is a pre-existing Windows-environment bug that fails identically on
`upstream/main` baseline and is unrelated to this PR. No test
expectations were modified.
## Next step
Restage sidecar, relaunch Tauri, click through the React wizard once
to dismiss it, then type `hi` in the chat pane. This time
`build_session_agent` should read `chat_onboarding_completed=false`
and route to welcome, even though `onboarding_completed=true` was set
by the React layer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c737ebd2fc
commit
56d95e2c38
@@ -508,27 +508,40 @@ fn build_session_agent(
|
|||||||
effective.default_temperature = temp;
|
effective.default_temperature = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route to welcome vs orchestrator based on the per-user onboarding
|
// Route to welcome vs orchestrator based on the per-user
|
||||||
// flag. #525 fix: pre-onboarding users see the welcome agent's
|
// **chat-onboarding** flag. #525 fix: pre-onboarding users see the
|
||||||
// persona with its 2-tool TOML scope (complete_onboarding +
|
// welcome agent's persona with its 2-tool TOML scope
|
||||||
// memory_recall) instead of the orchestrator's default delegation
|
// (complete_onboarding + memory_recall) instead of the
|
||||||
// surface. Post-onboarding they transition automatically on the
|
// orchestrator's default delegation surface. Post-onboarding they
|
||||||
// next chat turn because `Config::load_or_init` reads fresh from
|
// transition automatically on the next chat turn because
|
||||||
// disk every call.
|
// `Config::load_or_init` reads fresh from disk every call.
|
||||||
|
//
|
||||||
|
// We deliberately read `chat_onboarding_completed`, NOT
|
||||||
|
// `onboarding_completed`. The latter is the React UI wizard's
|
||||||
|
// gate (`OnboardingOverlay.tsx`) which flips to `true` the moment
|
||||||
|
// the user dismisses the wizard — which happens BEFORE they ever
|
||||||
|
// type in the chat pane. If we routed on that flag the welcome
|
||||||
|
// agent could never run from the Tauri desktop app. The chat
|
||||||
|
// flag is set only by the welcome agent itself via
|
||||||
|
// `complete_onboarding(action="complete")`, so it stays `false`
|
||||||
|
// for the user's actual first chat message regardless of what
|
||||||
|
// the React layer did, then flips on the welcome turn so the
|
||||||
|
// very next message routes to orchestrator.
|
||||||
//
|
//
|
||||||
// The config reached here has already been loaded by
|
// The config reached here has already been loaded by
|
||||||
// `run_chat_task` via `config_rpc::load_config_with_timeout`, so
|
// `run_chat_task` via `config_rpc::load_config_with_timeout`, so
|
||||||
// the `onboarding_completed` field reflects the current persisted
|
// both flags reflect the current persisted state — no cache to
|
||||||
// state — no cache to invalidate.
|
// invalidate.
|
||||||
let target_agent_id = if effective.onboarding_completed {
|
let target_agent_id = if effective.chat_onboarding_completed {
|
||||||
"orchestrator"
|
"orchestrator"
|
||||||
} else {
|
} else {
|
||||||
"welcome"
|
"welcome"
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"[web-channel] routing chat turn to '{}' (onboarding_completed={}, client_id={}, thread_id={})",
|
"[web-channel] routing chat turn to '{}' (chat_onboarding_completed={}, ui_onboarding_completed={}, client_id={}, thread_id={})",
|
||||||
target_agent_id,
|
target_agent_id,
|
||||||
|
effective.chat_onboarding_completed,
|
||||||
effective.onboarding_completed,
|
effective.onboarding_completed,
|
||||||
client_id,
|
client_id,
|
||||||
thread_id
|
thread_id
|
||||||
|
|||||||
@@ -214,7 +214,8 @@ impl AgentScoping {
|
|||||||
/// Decide which agent should run for this channel turn and build the
|
/// Decide which agent should run for this channel turn and build the
|
||||||
/// matching tool-scoping payload.
|
/// matching tool-scoping payload.
|
||||||
///
|
///
|
||||||
/// The selection is purely a function of `config.onboarding_completed`:
|
/// The selection is purely a function of
|
||||||
|
/// `config.chat_onboarding_completed`:
|
||||||
///
|
///
|
||||||
/// * **`false`** → route to the `welcome` agent. Welcome's TOML
|
/// * **`false`** → route to the `welcome` agent. Welcome's TOML
|
||||||
/// restricts it to two tools (`complete_onboarding`, `memory_recall`)
|
/// restricts it to two tools (`complete_onboarding`, `memory_recall`)
|
||||||
@@ -228,8 +229,20 @@ impl AgentScoping {
|
|||||||
/// field in its TOML; this function expands that field into a list
|
/// field in its TOML; this function expands that field into a list
|
||||||
/// of `delegate_*` tools spliced alongside the global registry.
|
/// of `delegate_*` tools spliced alongside the global registry.
|
||||||
///
|
///
|
||||||
/// The next channel message after `complete_onboarding` flips the flag
|
/// We deliberately read `chat_onboarding_completed` and NOT the
|
||||||
/// is automatically routed to the orchestrator because
|
/// React-UI-managed `onboarding_completed` flag. The latter is the
|
||||||
|
/// gate `OnboardingOverlay.tsx` uses to render its full-screen wizard
|
||||||
|
/// in the Tauri desktop app — by the time a desktop user can type a
|
||||||
|
/// chat message it's already `true`, so routing on it would mean
|
||||||
|
/// welcome could never run from the Tauri app. The chat flag is set
|
||||||
|
/// exclusively by the welcome agent itself when it calls
|
||||||
|
/// `complete_onboarding(complete)`, so it stays `false` for the
|
||||||
|
/// user's actual first message regardless of what the React layer
|
||||||
|
/// did. See `Config::chat_onboarding_completed` rustdoc for the full
|
||||||
|
/// rationale.
|
||||||
|
///
|
||||||
|
/// The next channel message after `complete_onboarding` flips the
|
||||||
|
/// flag is automatically routed to the orchestrator because
|
||||||
/// `Config::load_or_init()` reads from disk every call (no in-process
|
/// `Config::load_or_init()` reads from disk every call (no in-process
|
||||||
/// cache, verified at `config/schema/load.rs:409`), so the new value
|
/// cache, verified at `config/schema/load.rs:409`), so the new value
|
||||||
/// is observed on the next turn without any explicit handoff event.
|
/// is observed on the next turn without any explicit handoff event.
|
||||||
@@ -251,7 +264,7 @@ async fn resolve_target_agent(channel: &str) -> AgentScoping {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let target_id = if config.onboarding_completed {
|
let target_id = if config.chat_onboarding_completed {
|
||||||
"orchestrator"
|
"orchestrator"
|
||||||
} else {
|
} else {
|
||||||
"welcome"
|
"welcome"
|
||||||
@@ -260,7 +273,8 @@ async fn resolve_target_agent(channel: &str) -> AgentScoping {
|
|||||||
tracing::info!(
|
tracing::info!(
|
||||||
channel = %channel,
|
channel = %channel,
|
||||||
target_agent = target_id,
|
target_agent = target_id,
|
||||||
onboarding_completed = config.onboarding_completed,
|
chat_onboarding_completed = config.chat_onboarding_completed,
|
||||||
|
ui_onboarding_completed = config.onboarding_completed,
|
||||||
"[dispatch::routing] selected target agent"
|
"[dispatch::routing] selected target agent"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -142,9 +142,58 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dictation: DictationConfig,
|
pub dictation: DictationConfig,
|
||||||
|
|
||||||
/// Whether the user has completed the onboarding flow.
|
/// Whether the user has completed the **React UI** onboarding flow.
|
||||||
|
///
|
||||||
|
/// Set by `OnboardingOverlay.tsx::handleDone` and the multi-step
|
||||||
|
/// `Onboarding.tsx` wizard via the `config.set_onboarding_completed`
|
||||||
|
/// JSON-RPC method. Gates whether the React layer renders the
|
||||||
|
/// full-screen onboarding overlay on top of the chat pane: when
|
||||||
|
/// `false`, the overlay is shown and the user cannot interact with
|
||||||
|
/// the chat until they complete or defer the wizard.
|
||||||
|
///
|
||||||
|
/// Distinct from [`Config::chat_onboarding_completed`] — this flag
|
||||||
|
/// only tracks the UI wizard, NOT the welcome agent's chat-based
|
||||||
|
/// greeting flow. See that field for the agent routing semantics.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub onboarding_completed: bool,
|
pub onboarding_completed: bool,
|
||||||
|
|
||||||
|
/// Whether the **chat-based welcome agent** flow has run for this
|
||||||
|
/// user. Distinct from [`Config::onboarding_completed`] (the
|
||||||
|
/// React UI wizard flag) so the welcome agent can run on the very
|
||||||
|
/// first chat turn even after the React wizard has already
|
||||||
|
/// completed.
|
||||||
|
///
|
||||||
|
/// Routing semantics:
|
||||||
|
/// * **`false`** — incoming channel messages and Tauri in-app
|
||||||
|
/// chat turns route to the `welcome` agent definition (see
|
||||||
|
/// `channels::providers::web::build_session_agent` and
|
||||||
|
/// `channels::runtime::dispatch::resolve_target_agent`). The
|
||||||
|
/// welcome agent inspects the user's setup, delivers a
|
||||||
|
/// personalized greeting, and (when the essentials are in
|
||||||
|
/// place) calls `complete_onboarding(action="complete")` which
|
||||||
|
/// flips this flag to `true`.
|
||||||
|
/// * **`true`** — the welcome agent has already run; future chat
|
||||||
|
/// turns route to the orchestrator.
|
||||||
|
///
|
||||||
|
/// Why two separate flags:
|
||||||
|
///
|
||||||
|
/// In the Tauri desktop app, `OnboardingOverlay` blocks the chat
|
||||||
|
/// pane until `onboarding_completed=true`. If the welcome agent
|
||||||
|
/// also gated on `onboarding_completed`, by the time the user
|
||||||
|
/// could type in chat the flag would already be `true` and the
|
||||||
|
/// welcome agent would never run on the desktop. Using a separate
|
||||||
|
/// flag lets the React wizard manage UI gating while the chat
|
||||||
|
/// welcome runs orthogonally — every user gets greeted by the
|
||||||
|
/// welcome agent on their first chat turn regardless of which
|
||||||
|
/// surface they came from (web, Telegram, Discord, etc.).
|
||||||
|
///
|
||||||
|
/// Defaults to `false` for backward compatibility — existing
|
||||||
|
/// `config.toml` files without this field will get the welcome
|
||||||
|
/// agent on their next chat turn, which is the correct behaviour
|
||||||
|
/// (the welcome agent is idempotent and re-running it for an
|
||||||
|
/// already-onboarded user just produces a recognition message).
|
||||||
|
#[serde(default)]
|
||||||
|
pub chat_onboarding_completed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -196,6 +245,7 @@ impl Default for Config {
|
|||||||
update: UpdateConfig::default(),
|
update: UpdateConfig::default(),
|
||||||
dictation: DictationConfig::default(),
|
dictation: DictationConfig::default(),
|
||||||
onboarding_completed: false,
|
onboarding_completed: false,
|
||||||
|
chat_onboarding_completed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,10 +100,20 @@ async fn check_status() -> anyhow::Result<ToolResult> {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or(crate::openhuman::config::DEFAULT_MODEL)
|
.unwrap_or(crate::openhuman::config::DEFAULT_MODEL)
|
||||||
));
|
));
|
||||||
|
// Two distinct flags after the chat / UI split:
|
||||||
|
// * `onboarding_completed` — React wizard (Tauri desktop UI) gate
|
||||||
|
// * `chat_onboarding_completed` — welcome agent's own gate, which
|
||||||
|
// determines whether YOU (the welcome agent reading this report)
|
||||||
|
// are routed to handle the next chat turn. Use the chat flag,
|
||||||
|
// not the UI flag, when deciding whether your work here is done.
|
||||||
report.push_str(&format!(
|
report.push_str(&format!(
|
||||||
"- Onboarding completed: {}\n",
|
"- UI onboarding wizard completed: {}\n",
|
||||||
config.onboarding_completed
|
config.onboarding_completed
|
||||||
));
|
));
|
||||||
|
report.push_str(&format!(
|
||||||
|
"- Chat welcome flow completed: {}\n",
|
||||||
|
config.chat_onboarding_completed
|
||||||
|
));
|
||||||
|
|
||||||
// ── Channels ────────────────────────────────────────────────────
|
// ── Channels ────────────────────────────────────────────────────
|
||||||
report.push_str("\n### Channels\n");
|
report.push_str("\n### Channels\n");
|
||||||
@@ -240,20 +250,37 @@ async fn check_status() -> anyhow::Result<ToolResult> {
|
|||||||
Ok(ToolResult::success(report))
|
Ok(ToolResult::success(report))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks onboarding as complete and seeds proactive cron jobs.
|
/// Marks the **chat-based welcome agent flow** as complete and seeds
|
||||||
|
/// proactive cron jobs.
|
||||||
|
///
|
||||||
|
/// After the #525 chat/UI onboarding split this tool flips
|
||||||
|
/// [`Config::chat_onboarding_completed`] — NOT the React UI's
|
||||||
|
/// [`Config::onboarding_completed`] flag. The welcome agent gates on
|
||||||
|
/// the chat flag, so flipping it here is what tells dispatch to route
|
||||||
|
/// the next chat turn to the orchestrator instead of welcome.
|
||||||
|
///
|
||||||
|
/// The React UI manages its own `onboarding_completed` flag via the
|
||||||
|
/// `config.set_onboarding_completed` JSON-RPC method (called by
|
||||||
|
/// `OnboardingOverlay.tsx::handleDone` and `Onboarding.tsx`). The two
|
||||||
|
/// flags are intentionally orthogonal so that:
|
||||||
|
/// * a Tauri user who completes the React wizard still sees the
|
||||||
|
/// welcome agent on their first chat turn (because the chat flag
|
||||||
|
/// is still `false` until the agent runs);
|
||||||
|
/// * a Telegram/Discord user (no React wizard) sees the welcome
|
||||||
|
/// agent on their first inbound message (same reason).
|
||||||
async fn complete() -> anyhow::Result<ToolResult> {
|
async fn complete() -> anyhow::Result<ToolResult> {
|
||||||
let mut config = Config::load_or_init()
|
let mut config = Config::load_or_init()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to load config: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Failed to load config: {e}"))?;
|
||||||
|
|
||||||
if config.onboarding_completed {
|
if config.chat_onboarding_completed {
|
||||||
tracing::debug!("[complete_onboarding] already completed — no-op");
|
tracing::debug!("[complete_onboarding] chat welcome flow already completed — no-op");
|
||||||
return Ok(ToolResult::success(
|
return Ok(ToolResult::success(
|
||||||
"Onboarding was already marked as complete.",
|
"Chat welcome flow was already marked as complete.",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
config.onboarding_completed = true;
|
config.chat_onboarding_completed = true;
|
||||||
config
|
config
|
||||||
.save()
|
.save()
|
||||||
.await
|
.await
|
||||||
@@ -267,11 +294,13 @@ async fn complete() -> anyhow::Result<ToolResult> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tracing::info!("[complete_onboarding] onboarding marked complete, proactive agents seeded");
|
tracing::info!(
|
||||||
|
"[complete_onboarding] chat welcome flow marked complete, proactive agents seeded"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(ToolResult::success(
|
Ok(ToolResult::success(
|
||||||
"Onboarding marked as complete. Morning briefing and proactive agent jobs have been \
|
"Chat welcome flow marked as complete. Morning briefing and proactive agent jobs have \
|
||||||
set up. The user is all set!",
|
been set up. The user is all set!",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user