Merge remote-tracking branch 'origin/main' into duncan/workspace-scoped-agent-store

* origin/main:
  fix(desktop): eliminate mounted-view CPU burn — compositor-safe shimmer, observer append fast path, poll-tick disk reads (#6198)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

# Conflicts:
#	desktop/src-tauri/src/commands/agents.rs
This commit is contained in:
Duncan
2026-08-18 09:16:10 -04:00
10 changed files with 237 additions and 134 deletions

View File

@@ -18,11 +18,11 @@ use super::agent_update_rollback::{rollback_failed_agent_update, AgentUpdateRoll
use crate::{
app_state::AppState,
managed_agents::{
build_managed_agent_summary, current_instance_id, discovery_env_with_baked_floor,
find_managed_agent_mut, known_acp_runtime, load_global_agent_config, load_managed_agents,
load_personas, managed_agent_avatar_url, missing_command_message, normalize_agent_args,
resolve_command, save_managed_agents, sync_managed_agent_processes, try_regenerate_nest,
AgentModelInfo, AgentModelsResponse, ManagedAgentRecord, UpdateManagedAgentRequest,
current_instance_id, discovery_env_with_baked_floor, find_managed_agent_mut,
known_acp_runtime, load_global_agent_config, load_managed_agents, load_personas,
managed_agent_avatar_url, missing_command_message, normalize_agent_args, resolve_command,
save_managed_agents, sync_managed_agent_processes, try_regenerate_nest, AgentModelInfo,
AgentModelsResponse, ManagedAgentRecord, UpdateManagedAgentRequest,
UpdateManagedAgentResponse, DEFAULT_ACP_COMMAND,
},
relay::{relay_ws_url_with_override, sync_managed_agent_profile},

View File

@@ -247,16 +247,7 @@ pub async fn update_managed_agent(
None
};
let summary = {
let personas = load_personas(&app).unwrap_or_default();
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
)?
};
let summary = { super::super::agents::summarize_from_disk(&app, record, &runtimes)? };
let rollback = name_changed
.then(|| AgentUpdateRollback::new(previous_record, record, access_policy_changed));
(

View File

@@ -4,9 +4,8 @@ use tauri::{AppHandle, Manager, State};
use crate::{
app_state::AppState,
managed_agents::{
build_managed_agent_summary, current_instance_id, find_managed_agent_mut,
load_managed_agents, load_personas, save_managed_agents, sync_managed_agent_processes,
ManagedAgentSummary,
current_instance_id, find_managed_agent_mut, load_managed_agents, save_managed_agents,
sync_managed_agent_processes, ManagedAgentSummary,
},
util::now_iso,
};
@@ -53,14 +52,7 @@ pub async fn set_managed_agent_start_on_app_launch(
.iter()
.find(|record| record.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?;
let personas = load_personas(&app).unwrap_or_default();
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
)
super::agents::summarize_from_disk(&app, record, &runtimes)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
@@ -101,14 +93,7 @@ pub async fn set_managed_agent_auto_restart(
.iter()
.find(|record| record.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?;
let personas = load_personas(&app).unwrap_or_default();
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
)
super::agents::summarize_from_disk(&app, record, &runtimes)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?

View File

@@ -25,6 +25,28 @@ pub(super) fn workspace_owner_hex(state: &AppState) -> Result<String, String> {
Ok(state.current_pubkey()?.to_hex())
}
/// Build a summary from fresh disk state (personas, teams, global config).
/// For one-shot command paths only — the 5s list poll calls
/// `build_managed_agent_summary` directly with stores loaded once per call,
/// not once per record.
pub(super) fn summarize_from_disk(
app: &AppHandle,
record: &ManagedAgentRecord,
runtimes: &std::collections::HashMap<
crate::managed_agents::ManagedAgentRuntimeKey,
crate::managed_agents::ManagedAgentPairRuntime,
>,
) -> Result<ManagedAgentSummary, String> {
build_managed_agent_summary(
app,
record,
runtimes,
&load_personas(app).unwrap_or_default(),
&load_teams(app).unwrap_or_default(),
&crate::managed_agents::load_global_agent_config(app).unwrap_or_default(),
)
}
/// Retain a freshly authored managed-agent event in the local store, flagged
/// for relay sync. MUST be called inside the `managed_agents_store_lock`-held
/// body after `save_managed_agents`, NEVER across an `.await`: it acquires
@@ -357,6 +379,7 @@ pub(super) async fn start_local_agent_with_preflight(
record,
&runtimes,
&personas,
&load_teams(app).unwrap_or_default(),
&crate::managed_agents::load_global_agent_config(app).unwrap_or_default(),
)
}
@@ -392,14 +415,22 @@ pub async fn list_managed_agents(app: AppHandle) -> Result<Vec<ManagedAgentSumma
let personas = load_personas(&app).unwrap_or_default();
// One disk read for the whole list — build_managed_agent_summary takes
// the config as a parameter precisely so this poll-every-5s call does
// not re-read it per record.
// teams and config as parameters precisely so this poll-every-5s call
// does not re-read them per record.
let teams = load_teams(&app).unwrap_or_default();
let global_config =
crate::managed_agents::load_global_agent_config(&app).unwrap_or_default();
records
.iter()
.map(|record| {
build_managed_agent_summary(&app, record, &runtimes, &personas, &global_config)
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&teams,
&global_config,
)
})
.collect()
})
@@ -768,15 +799,8 @@ pub async fn create_managed_agent(
// before any .await — owner-authored, every agent (Will's ruling: no
// is_builtin/persona-membership gate).
retain_managed_agent_pending(&app, &state, record);
let personas = load_personas(&app).unwrap_or_default();
(
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
)?,
summarize_from_disk(&app, record, &runtimes)?,
resolved_avatar_url,
)
};
@@ -805,14 +829,7 @@ pub async fn create_managed_agent(
.iter()
.find(|record| record.pubkey == pubkey)
.ok_or_else(|| "created agent disappeared unexpectedly".to_string())?;
let personas = load_personas(&app).unwrap_or_default();
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
)?
summarize_from_disk(&app, record, &runtimes)?
}
}
} else {
@@ -881,14 +898,7 @@ pub async fn create_managed_agent(
.iter()
.find(|r| r.pubkey == pubkey)
.ok_or_else(|| "agent disappeared".to_string())?;
let personas = load_personas(&app).unwrap_or_default();
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
)?
summarize_from_disk(&app, record, &runtimes)?
} else {
agent
};
@@ -994,14 +1004,7 @@ pub async fn start_managed_agent(
.iter()
.find(|r| r.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?;
let personas = load_personas(&app).unwrap_or_default();
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
)
summarize_from_disk(&app, record, &runtimes)
}
StartTarget::Provider { backend, .. } => Err(format!(
"agent {pubkey} has unsupported backend kind: {backend:?}"
@@ -1079,14 +1082,7 @@ pub async fn stop_managed_agent(
.iter()
.find(|record| record.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?;
let personas = load_personas(&app).unwrap_or_default();
build_managed_agent_summary(
&app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(&app).unwrap_or_default(),
)
summarize_from_disk(&app, record, &runtimes)
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?

View File

@@ -84,16 +84,9 @@ pub(crate) async fn start_local_agent_pairs_with_preflight(
.managed_agent_processes
.lock()
.map_err(|e| e.to_string())?;
let personas = super::load_personas(app).unwrap_or_default();
let record = records
.iter()
.find(|record| record.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found"))?;
super::build_managed_agent_summary(
app,
record,
&runtimes,
&personas,
&crate::managed_agents::load_global_agent_config(app).unwrap_or_default(),
)
super::summarize_from_disk(app, record, &runtimes)
}

View File

@@ -133,6 +133,7 @@ pub fn build_managed_agent_summary(
record: &ManagedAgentRecord,
runtimes: &HashMap<ManagedAgentRuntimeKey, ManagedAgentPairRuntime>,
personas: &[crate::managed_agents::types::AgentDefinition],
teams: &[crate::managed_agents::TeamRecord],
global_config: &crate::managed_agents::GlobalAgentConfig,
) -> Result<ManagedAgentSummary, String> {
use crate::managed_agents::BackendKind;
@@ -195,12 +196,10 @@ pub fn build_managed_agent_summary(
let (persona_out_of_date, persona_orphaned) = persona_drift_state(record, personas);
let global_for_summary =
crate::managed_agents::load_global_agent_config(app).unwrap_or_default();
let effective_cfg = crate::managed_agents::effective_config::resolve_effective_config(
record,
personas,
&global_for_summary,
global_config,
);
let (effective_model, effective_provider, effective_prompt, model_source) = match effective_cfg
{
@@ -242,14 +241,13 @@ pub fn build_managed_agent_summary(
// env layering below — the caller loads it once and passes it in, so
// list-style callers pay one disk read per call rather than one per record.
// The prospective side is computed only for a tracked pair: it costs a
// teams-store read, and an unstamped agent has nothing to compare against.
// The prospective side is computed only for a tracked pair: an unstamped
// agent has nothing to compare against.
let tracked_spawn = pair_key.as_ref().zip(pair_runtime).map(|(key, runtime)| {
let teams = crate::managed_agents::load_teams(app).unwrap_or_default();
let current = crate::managed_agents::spawn_snapshot::prospective_spawn_config_snapshot(
record,
personas,
&teams,
teams,
&key.relay_url,
global_config,
super::owner_only_access_build(),

View File

@@ -243,11 +243,26 @@ function appendAgentEvents(
: events;
if (admissible.length === 0) return null;
const seen = new Set(
current.map(
(event) => `${event.timestamp.length}:${event.timestamp}:${event.seq}`,
),
);
// Ordinary live path: the harness publishes frames in order once per
// second, so the whole batch lands strictly after the retained tail. In
// that case no admissible event can collide with a retained one (the
// journal is sorted), so dedup only needs to look inside the batch and the
// merged journal is a plain concat — no Set over the full journal and no
// whole-journal re-sort (whose comparator Date.parses per comparison).
// Out-of-order or replayed arrivals take the full dedup + re-sort path.
const currentLast = current.at(-1);
const allAtEnd =
!currentLast ||
admissible.every((event) => isObserverEventAfter(event, currentLast));
const seen = allAtEnd
? new Set<string>()
: new Set(
current.map(
(event) =>
`${event.timestamp.length}:${event.timestamp}:${event.seq}`,
),
);
const added: ObserverEvent[] = [];
for (const event of admissible) {
const eventKey = `${event.timestamp.length}:${event.timestamp}:${event.seq}`;
@@ -258,7 +273,9 @@ function appendAgentEvents(
if (added.length === 0) return null;
const sortedAdded = added.sort(compareObserverEvents);
const sorted = [...current, ...sortedAdded].sort(compareObserverEvents);
const sorted = allAtEnd
? [...current, ...sortedAdded]
: [...current, ...sortedAdded].sort(compareObserverEvents);
const trimmed = sorted.length > MAX_OBSERVER_EVENTS;
const final = trimmed
? sorted.slice(sorted.length - OBSERVER_EVENTS_LOW_WATER)
@@ -276,14 +293,11 @@ function appendAgentEvents(
});
}
// The common live path appends a sorted batch after the retained window. Fold
// The common live path appends a sorted batch after the retained window
// (the same `allAtEnd` that authorized the concat fast-path above). Fold
// that batch through the transcript state once without rebuilding history.
// Out-of-order arrivals and cap eviction rebuild from the final window so
// stateful tool/permission relationships remain correct.
const currentLast = current.at(-1);
const allAtEnd =
!currentLast ||
sortedAdded.every((event) => compareObserverEvents(event, currentLast) > 0);
if (allAtEnd && !trimmed) {
let transcriptState =
transcriptByAgent.get(key) ?? createEmptyTranscriptState();

View File

@@ -303,3 +303,116 @@ describe("live observer journal retention — eviction floor (reconnect replay)"
);
});
});
describe("live observer journal — in-order append fast path ordering/dedup", () => {
// The common live path (every batch strictly after the retained tail) skips
// the whole-journal dedup Set and re-sort. These pin the observable
// invariants that authorize that skip: identical ordering, dedup, and
// transcript against the general path.
beforeEach(() => {
resetAgentObserverStore();
});
/** An event with an explicit timestamp, for equal-timestamp tie-breaks. */
function makeEventAt(seq, timestampMs) {
return {
...makeEvent(seq),
timestamp: new Date(timestampMs).toISOString(),
};
}
it("test_equal_timestamp_batch_orders_by_seq", () => {
// A one-second harness frame batches several events sharing a timestamp;
// the tie-break is seq. Deliver them out of seq order in one batch.
const t = 1_760_000_100_000;
syncAgentObserverEvents(AGENT_PUBKEY, [makeEventAt(1, t - 1000)]);
syncAgentObserverEvents(AGENT_PUBKEY, [
makeEventAt(4, t),
makeEventAt(2, t),
makeEventAt(3, t),
]);
assert.deepEqual(
getAgentObserverSnapshot(AGENT_PUBKEY).events.map((event) => event.seq),
[1, 2, 3, 4],
"equal-timestamp events are retained in seq order",
);
});
it("test_duplicate_batch_redelivery_is_ignored", () => {
// Relay redelivery of the newest batch: every event duplicates the tail,
// so nothing is admitted, nothing is notified.
const batch = [makeEvent(1), makeEvent(2), makeEvent(3)];
syncAgentObserverEvents(AGENT_PUBKEY, batch);
let notifications = 0;
const unsubscribe = subscribeAgentObserverStore(() => {
notifications += 1;
});
try {
syncAgentObserverEvents(AGENT_PUBKEY, batch);
} finally {
unsubscribe();
}
assert.deepEqual(
getAgentObserverSnapshot(AGENT_PUBKEY).events.map((event) => event.seq),
[1, 2, 3],
"a redelivered batch adds nothing",
);
assert.equal(notifications, 0, "a pure-duplicate batch notifies no one");
});
it("test_batch_with_intra_batch_duplicate_admits_once", () => {
// A batch strictly after the tail still dedups within itself.
syncAgentObserverEvents(AGENT_PUBKEY, [makeEvent(1)]);
syncAgentObserverEvents(AGENT_PUBKEY, [
makeEvent(2),
makeEvent(3),
makeEvent(2),
]);
assert.deepEqual(
getAgentObserverSnapshot(AGENT_PUBKEY).events.map((event) => event.seq),
[1, 2, 3],
"an intra-batch duplicate is admitted exactly once",
);
});
it("test_late_arrival_overlapping_tail_takes_slow_path_and_dedups", () => {
// A replayed window straddling the tail: partly duplicate, partly new,
// partly older-than-tail. Not all-after, so the general path must dedup
// against the whole journal and re-sort.
syncAgentObserverEvents(AGENT_PUBKEY, [
makeEvent(1),
makeEvent(2),
makeEvent(4),
]);
syncAgentObserverEvents(AGENT_PUBKEY, [
makeEvent(2),
makeEvent(3),
makeEvent(4),
makeEvent(5),
]);
const events = getAgentObserverSnapshot(AGENT_PUBKEY).events;
assert.deepEqual(
events.map((event) => event.seq),
[1, 2, 3, 4, 5],
"overlapping late arrival dedups against the journal and sorts into place",
);
assert.deepEqual(
getAgentTranscript(AGENT_PUBKEY),
buildTranscript(events),
"the transcript equals a full replay after a mixed-path sequence",
);
});
it("test_fast_path_transcript_equals_full_replay", () => {
// Pure in-order streaming (fast path every time) must produce the same
// derived transcript as a replay of the retained window.
fillSequential(50);
const events = getAgentObserverSnapshot(AGENT_PUBKEY).events;
assert.equal(events.length, 50);
assert.deepEqual(
getAgentTranscript(AGENT_PUBKEY),
buildTranscript(events),
"in-order fast-path appends derive the same transcript as a replay",
);
});
});

View File

@@ -194,7 +194,6 @@
.buzz-shimmer {
--buzz-shimmer-duration: 2600ms;
--buzz-shimmer-band: 250%;
--buzz-shimmer-highlight: color-mix(
in srgb,
hsl(var(--background)) 60%,
@@ -202,49 +201,58 @@
);
--buzz-shimmer-spread: 2rem;
animation: buzz-shimmer var(--buzz-shimmer-duration) linear infinite;
background-clip: text;
background-image:
linear-gradient(
90deg,
transparent calc(50% - var(--buzz-shimmer-spread)),
var(--buzz-shimmer-highlight) 50%,
transparent calc(50% + var(--buzz-shimmer-spread))
),
linear-gradient(hsl(var(--muted-foreground)), hsl(var(--muted-foreground)));
background-position:
100% 0,
0 0;
background-repeat: no-repeat;
background-size:
var(--buzz-shimmer-band) 100%,
100% 100%;
color: transparent;
color: hsl(var(--muted-foreground));
display: inline-block;
position: relative;
}
/*
* The highlight lives on an aria-hidden overlay child that duplicates the
* label text and animates ONLY opacity. The previous implementation animated
* background-position under -webkit-background-clip: text, which WebKit
* cannot run on the compositor: every animation frame forced a full document
* style resolve + compositing-hierarchy walk, burning ~20% CPU at rest
* whenever a large timeline was mounted with a working agent. Opacity is
* compositor-accelerated: the layer paints once and the pulse runs outside
* the web process's main thread. A real element (not ::after generated
* content) is used so the duplicate text can be aria-hidden — pseudo-element
* text is inconsistently exposed to screen readers and cannot be hidden.
*/
.buzz-shimmer > .buzz-shimmer-overlay {
animation: buzz-shimmer var(--buzz-shimmer-duration) ease-in-out infinite;
background-clip: text;
background-image: linear-gradient(
90deg,
transparent calc(50% - var(--buzz-shimmer-spread)),
var(--buzz-shimmer-highlight) 50%,
transparent calc(50% + var(--buzz-shimmer-spread))
);
color: transparent;
inset: 0;
overflow: inherit;
padding: inherit;
position: absolute;
text-overflow: inherit;
white-space: inherit;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
@keyframes buzz-shimmer {
0% {
background-position:
100% 0,
0 0;
0%,
100% {
opacity: 0;
}
100% {
background-position:
0 0,
0 0;
50% {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.buzz-shimmer {
.buzz-shimmer > .buzz-shimmer-overlay {
animation: none;
background: none;
color: hsl(var(--muted-foreground));
-webkit-text-fill-color: currentcolor;
display: none;
}
}

View File

@@ -16,6 +16,11 @@ export function Shimmer({ children, className }: ShimmerProps) {
}
>
{children}
{/* Visual-only highlight copy; the sibling text node above is the sole
accessible content. */}
<span aria-hidden="true" className="buzz-shimmer-overlay">
{children}
</span>
</span>
);
}