diff --git a/desktop/src-tauri/src/managed_agents/persona_events.rs b/desktop/src-tauri/src/managed_agents/persona_events.rs
index 21b5794995..2033003bc8 100644
--- a/desktop/src-tauri/src/managed_agents/persona_events.rs
+++ b/desktop/src-tauri/src/managed_agents/persona_events.rs
@@ -478,6 +478,25 @@ pub fn apply_persona_snapshot(record: &mut ManagedAgentRecord, persona: &AgentDe
record.model = snapshot.model;
record.provider = snapshot.provider;
record.runtime = snapshot.runtime;
+ // Drop a stale create-time harness pin when the definition names a
+ // different known runtime; custom commands stay pinned.
+ if let Some(def_runtime) = persona
+ .runtime
+ .as_deref()
+ .map(str::trim)
+ .filter(|r| !r.is_empty())
+ .and_then(crate::managed_agents::known_acp_runtime_exact)
+ {
+ if let Some(pin_runtime) = record
+ .agent_command_override
+ .as_deref()
+ .and_then(crate::managed_agents::known_acp_runtime)
+ {
+ if !std::ptr::eq(pin_runtime, def_runtime) {
+ record.agent_command_override = None;
+ }
+ }
+ }
// env_vars stay overrides-only. Self-heal records written before the env
// refresh: persona env used to be baked into `record.env_vars`, turning
// inherited values into pseudo-overrides that shadow later persona edits.
diff --git a/desktop/src-tauri/src/managed_agents/spawn_hash/tests.rs b/desktop/src-tauri/src/managed_agents/spawn_hash/tests.rs
index 1f53a365f3..203863b0ed 100644
--- a/desktop/src-tauri/src/managed_agents/spawn_hash/tests.rs
+++ b/desktop/src-tauri/src/managed_agents/spawn_hash/tests.rs
@@ -366,21 +366,39 @@ fn definition_runtime_edit_changes_hash_for_materialized_record() {
);
}
-/// (c) An explicit agent_command_override (ladder step 1) must beat a
-/// changed definition runtime — the badge must NOT fire for a pinned instance.
+/// (c) A pin naming a KNOWN runtime no longer beats a changed definition
+/// runtime — apply_persona_snapshot clears the stale pin, so the badge fires.
#[test]
-fn agent_command_override_beats_definition_runtime_change() {
+fn known_runtime_pin_yields_to_definition_runtime_change() {
let mut rec = record();
rec.persona_id = Some("pers".into());
rec.runtime = Some("goose".into()); // materialized runtime
- rec.agent_command_override = Some("goose".into()); // explicit per-instance pin
+ rec.agent_command_override = Some("goose".into()); // create-time pin
+
+ let before = [persona("pers", Some("goose"), "prompt")];
+ let after = [persona("pers", Some("claude"), "prompt")];
+ assert_ne!(
+ spawn_config_hash(&rec, &before, &[], "wss://ws.example", &Default::default()),
+ spawn_config_hash(&rec, &after, &[], "wss://ws.example", &Default::default()),
+ "stale known-runtime pin must not shadow a definition runtime edit"
+ );
+}
+
+/// (c2) A custom-command override (no matching known runtime) still beats a
+/// changed definition runtime — the badge must NOT fire for such a pin.
+#[test]
+fn custom_command_override_beats_definition_runtime_change() {
+ let mut rec = record();
+ rec.persona_id = Some("pers".into());
+ rec.runtime = Some("goose".into()); // materialized runtime
+ rec.agent_command_override = Some("/opt/custom/my-agent".into());
let before = [persona("pers", Some("goose"), "prompt")];
let after = [persona("pers", Some("claude"), "prompt")];
assert_eq!(
spawn_config_hash(&rec, &before, &[], "wss://ws.example", &Default::default()),
spawn_config_hash(&rec, &after, &[], "wss://ws.example", &Default::default()),
- "explicit override must win regardless of definition runtime change"
+ "custom command override must win regardless of definition runtime change"
);
}
diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx
index 09e8d51487..c1f713d230 100644
--- a/desktop/src/features/profile/ui/UserProfilePanel.tsx
+++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx
@@ -32,13 +32,9 @@ import {
buildInstanceInputForDefinition,
resolveStartRuntimeForDefinition,
} from "@/features/agents/lib/instanceInputForDefinition";
-import {
- isManagedAgentActive,
- startManagedAgentWithRules,
- stopManagedAgentWithRules,
-} from "@/features/agents/lib/managedAgentControlActions";
import { describeLogFile } from "@/features/agents/ui/agentUi";
import { AgentDialog } from "@/features/agents/ui/AgentDialog";
+import { useAgentLifecycleActions } from "@/features/profile/ui/useAgentLifecycleActions";
import {
consumePendingOpenEditAgent,
type EditAgentFocusTarget,
@@ -451,42 +447,14 @@ export function UserProfilePanel({
],
);
- const handleAgentPrimaryAction = React.useCallback(async () => {
- if (!managedAgent) return;
-
- try {
- if (isManagedAgentActive(managedAgent)) {
- const result = await stopManagedAgentWithRules({
- agent: managedAgent,
- channels: channelsQuery.data ?? [],
- relayAgents: relayAgentsQuery.data ?? [],
- stopManagedAgent: stopAgentMutation.mutateAsync,
- });
- toast.success(result.noticeMessage ?? `Stopped ${managedAgent.name}.`);
- return;
- }
-
- await startManagedAgentWithRules({
- agent: managedAgent,
- startManagedAgent: startAgentMutation.mutateAsync,
- });
- toast.success(
- managedAgent.backend.type === "provider"
- ? `Deploying ${managedAgent.name}.`
- : `Started ${managedAgent.name}.`,
- );
- } catch (error) {
- toast.error(
- error instanceof Error ? error.message : "Agent action failed.",
- );
- }
- }, [
- channelsQuery.data,
- managedAgent,
- relayAgentsQuery.data,
- startAgentMutation.mutateAsync,
- stopAgentMutation.mutateAsync,
- ]);
+ const { handleAgentPrimaryAction, handleAgentRestart } =
+ useAgentLifecycleActions({
+ channels: channelsQuery.data,
+ managedAgent,
+ relayAgents: relayAgentsQuery.data,
+ startManagedAgent: startAgentMutation.mutateAsync,
+ stopManagedAgent: stopAgentMutation.mutateAsync,
+ });
const handleInstantiateAgent = React.useCallback(async () => {
if (!resolvedPersona) return;
@@ -828,6 +796,7 @@ export function UserProfilePanel({
followMutation={followMutation}
agentInstruction={agentInstruction}
handleAgentPrimaryAction={handleAgentPrimaryAction}
+ handleAgentRestart={handleAgentRestart}
handleEditAgent={handleEditAgent}
handleEditPersona={canEditPersona ? handleEditPersona : undefined}
handleInstantiateAgent={handleInstantiateAgent}
diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
index 2a46dcc7a5..4514b62d95 100644
--- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
+++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx
@@ -8,6 +8,7 @@ import {
MessageSquare,
Pencil,
Play,
+ RefreshCw,
Square,
UserMinus,
UserPlus,
@@ -77,6 +78,7 @@ export type ProfileSummaryViewProps = {
canInstantiateAgent: boolean;
agentInstruction: string | null;
handleAgentPrimaryAction: () => void;
+ handleAgentRestart: () => void;
handleEditAgent: () => void;
handleEditPersona?: () => void;
handleInstantiateAgent: () => void;
@@ -189,6 +191,7 @@ export function ProfileSummaryView({
canInstantiateAgent,
agentInstruction,
handleAgentPrimaryAction,
+ handleAgentRestart,
handleEditAgent,
handleEditPersona,
handleInstantiateAgent,
@@ -369,6 +372,14 @@ export function ProfileSummaryView({
? handleAgentPrimaryAction
: undefined
}
+ onAgentRestart={
+ isOwner === true &&
+ managedAgent?.backend.type === "local" &&
+ (managedAgent.status === "running" ||
+ managedAgent.status === "deployed")
+ ? handleAgentRestart
+ : undefined
+ }
isFollowing={isFollowing}
messagePending={isMessagePending}
onMessage={onOpenDm ? handleMessage : undefined}
@@ -629,6 +640,7 @@ function ProfilePrimaryActions({
isFollowing,
messagePending,
onAgentPrimaryAction,
+ onAgentRestart,
onEditAgent,
onMessage,
pubkey,
@@ -642,6 +654,7 @@ function ProfilePrimaryActions({
isFollowing: boolean;
messagePending?: boolean;
onAgentPrimaryAction?: () => void;
+ onAgentRestart?: () => void;
onEditAgent: () => void;
onMessage?: () => void;
pubkey: string;
@@ -698,6 +711,15 @@ function ProfilePrimaryActions({
testId="user-profile-agent-primary-action"
/>
) : null}
+ {onAgentRestart ? (
+
+ ) : null}
);
}
diff --git a/desktop/src/features/profile/ui/useAgentLifecycleActions.ts b/desktop/src/features/profile/ui/useAgentLifecycleActions.ts
new file mode 100644
index 0000000000..f84e149c11
--- /dev/null
+++ b/desktop/src/features/profile/ui/useAgentLifecycleActions.ts
@@ -0,0 +1,80 @@
+import * as React from "react";
+import { toast } from "sonner";
+
+import {
+ isManagedAgentActive,
+ respawnManagedAgentWithRules,
+ startManagedAgentWithRules,
+ stopManagedAgentWithRules,
+} from "@/features/agents/lib/managedAgentControlActions";
+import type { Channel, ManagedAgent, RelayAgent } from "@/shared/api/types";
+
+export function useAgentLifecycleActions({
+ channels,
+ managedAgent,
+ relayAgents,
+ startManagedAgent,
+ stopManagedAgent,
+}: {
+ channels: readonly Channel[] | undefined;
+ managedAgent: ManagedAgent | undefined;
+ relayAgents: readonly RelayAgent[] | undefined;
+ startManagedAgent: (pubkey: string) => Promise;
+ stopManagedAgent: (pubkey: string) => Promise;
+}) {
+ const handleAgentPrimaryAction = React.useCallback(async () => {
+ if (!managedAgent) return;
+
+ try {
+ if (isManagedAgentActive(managedAgent)) {
+ const result = await stopManagedAgentWithRules({
+ agent: managedAgent,
+ channels: channels ?? [],
+ relayAgents: relayAgents ?? [],
+ stopManagedAgent,
+ });
+ toast.success(result.noticeMessage ?? `Stopped ${managedAgent.name}.`);
+ return;
+ }
+
+ await startManagedAgentWithRules({
+ agent: managedAgent,
+ startManagedAgent,
+ });
+ toast.success(
+ managedAgent.backend.type === "provider"
+ ? `Deploying ${managedAgent.name}.`
+ : `Started ${managedAgent.name}.`,
+ );
+ } catch (error) {
+ toast.error(
+ error instanceof Error ? error.message : "Agent action failed.",
+ );
+ }
+ }, [
+ channels,
+ managedAgent,
+ relayAgents,
+ startManagedAgent,
+ stopManagedAgent,
+ ]);
+
+ const handleAgentRestart = React.useCallback(async () => {
+ if (!managedAgent) return;
+
+ try {
+ await respawnManagedAgentWithRules({
+ agent: managedAgent,
+ startManagedAgent,
+ stopManagedAgent,
+ });
+ toast.success(`Restarted ${managedAgent.name}.`);
+ } catch (error) {
+ toast.error(
+ error instanceof Error ? error.message : "Agent restart failed.",
+ );
+ }
+ }, [managedAgent, startManagedAgent, stopManagedAgent]);
+
+ return { handleAgentPrimaryAction, handleAgentRestart };
+}