Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ jobs:
- '.github/workflows/ci.yml'
- name: Release workflow source contract
run: scripts/test-release-ref-contract.sh
- name: Migration immutability
if: github.event_name == 'pull_request'
run: |
git fetch --depth=1 origin "${{ github.base_ref }}"
scripts/check-migration-immutability.sh

rust-lint:
name: Rust Lint
Expand Down
6 changes: 5 additions & 1 deletion desktop/src-tauri/src/commands/agent_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, HashSet};

use nostr::Keys;
use serde::Deserialize;
use tauri::{AppHandle, State};
use tauri::{AppHandle, Emitter, State};

use super::agent_model_process::run_agent_models_command;
use super::agent_update_rollback::{rollback_failed_agent_update, AgentUpdateRollback};
Expand Down Expand Up @@ -970,6 +970,10 @@ pub async fn update_managed_agent(
}
}

// Persona/team mutate paths emit this so the Agents UI refreshes; do the
// same after instance edits (rename, respond-to, prompt, …).
let _ = app.emit("agents-data-changed", ());

Ok(UpdateManagedAgentResponse {
agent: summary,
profile_sync_error: None,
Expand Down
4 changes: 4 additions & 0 deletions desktop/src-tauri/src/managed_agents/spawn_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ pub(crate) fn spawn_config_hash(
record.model.hash(&mut hasher);
record.provider.hash(&mut hasher);
record.auth_tag.hash(&mut hasher);
// Display name is written into AGENTS.md / nest identity and used for
// @mentions mid-session; a rename must trip needsRestart so running
// agents pick up the new identity (issue #1823).
record.name.hash(&mut hasher);
record.respond_to.as_str().hash(&mut hasher);
// The allowlist is hashed as the env receives it: spawn sets
// BUZZ_ACP_RESPOND_TO_ALLOWLIST only in allowlist mode, and normalized
Expand Down
13 changes: 13 additions & 0 deletions desktop/src-tauri/src/managed_agents/spawn_hash/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ fn record_prompt_edit_changes_hash() {
);
}

#[test]
fn record_name_edit_changes_hash() {
// Renames regenerate AGENTS.md; running agents must restart to see the
// new display name in core memory / mention routing.
let rec = record();
let mut edited = record();
edited.name = "Renamed Scout".into();
assert_ne!(
spawn_config_hash(&rec, &[], &[], "wss://ws.example", &Default::default()),
spawn_config_hash(&edited, &[], &[], "wss://ws.example", &Default::default())
);
}

#[test]
fn persona_runtime_edit_changes_hash() {
// The harness command resolves live personas at spawn, so a persona
Expand Down
8 changes: 6 additions & 2 deletions desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ function StandaloneAgentCard({
}) {
const title = agent.name;
const profileQuery = useUserProfileQuery(agent.pubkey);
const avatarUrl = firstAvatarUrl(
agent.avatarUrl,
profileQuery.data?.avatarUrl,
);
const friendlyError = friendlyAgentLastError(
agent.lastError,
agent.lastErrorCode,
Expand All @@ -364,7 +368,7 @@ function StandaloneAgentCard({
avatar={
<AgentRuntimeAvatarControl
activeTestId={`agent-runtime-active-${agent.pubkey}`}
avatarUrl={profileQuery.data?.avatarUrl}
avatarUrl={avatarUrl}
errorLabel={friendlyError}
errorTestId={`agent-runtime-error-${agent.pubkey}`}
isActive={isActive}
Expand All @@ -377,7 +381,7 @@ function StandaloneAgentCard({
onStart={() => onStartAgent(agent.pubkey)}
/>
}
avatarUrl={profileQuery.data?.avatarUrl}
avatarUrl={avatarUrl}
dataTestId={`managed-agent-${agent.pubkey}`}
label={title}
modelLabel={
Expand Down
40 changes: 40 additions & 0 deletions scripts/check-migration-immutability.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Fail if a PR edits a migration that already exists on the base branch.
#
# sqlx stores a checksum per applied migration. Editing a shipped migration
# in place makes every long-lived database refuse to start on upgrade
# ("migration N was previously applied but has been modified") — see #2472.
# New migration files are fine; only content changes to pre-existing paths fail.

set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

if [[ "${GITHUB_EVENT_NAME:-}" == "pull_request" ]]; then
BASE_REF="${GITHUB_BASE_REF:-main}"
git fetch --depth=1 origin "$BASE_REF" 2>/dev/null || true
BASE="origin/${BASE_REF}"
else
# Local / push: compare against the previous commit when available.
BASE="${MIGRATION_IMMUTABILITY_BASE:-HEAD^}"
fi

if ! git rev-parse --verify "$BASE" >/dev/null 2>&1; then
echo "check-migration-immutability: base '$BASE' unavailable; skipping"
exit 0
fi

# Paths under migrations/ that exist on BASE and differ at HEAD.
# Two-dot diff stays reliable on shallow CI checkouts (three-dot needs a merge base).
changed="$(git diff --name-only --diff-filter=M "$BASE" HEAD -- migrations/ || true)"

if [[ -z "${changed}" ]]; then
echo "check-migration-immutability: no pre-existing migration files modified"
exit 0
fi

echo "check-migration-immutability: refusing in-place edits to shipped migrations:" >&2
echo "${changed}" | sed 's/^/ /' >&2
echo "Add a new migration instead (append-only). See #2472." >&2
exit 1