mirror of
https://github.com/block/buzz.git
synced 2026-11-05 08:51:10 +00:00
fix(desktop): eliminate mounted-view CPU burn — compositor-safe shimmer, observer append fast path, poll-tick disk reads (#6198)
## Summary Live 0.5.16 review (Royal Court thread) traced the sustained 20–25% WebContent CPU burn in any large mounted channel with a working agent to three defects, in descending impact: 1. **Shimmer animation forces per-frame style + compositing walks.** `.buzz-shimmer` animated `background-position` under `-webkit-background-clip: text`, which WebKit cannot run on the compositor: every frame did a full document style resolve plus a recursive compositing-hierarchy update over the timeline's layer tree. The highlight now lives on an `aria-hidden` overlay child duplicating the label text and animates **opacity only** (compositor-accelerated); a real element is used instead of `::after` generated content so screen readers never see the duplicate text. Visual: the moving sweep becomes a gentle pulse. `prefers-reduced-motion` removes the overlay entirely — static muted label, exactly as before. 2. **Observer journal whole-journal dedup + re-sort per append.** Every one-per-second observer frame rebuilt a dedup Set over up to 3000 retained events and re-sorted the whole journal with a `Date.parse`-per-comparison comparator. In-order batches (the ordinary live path, same condition as the existing incremental transcript fold) now dedup within the batch and concat; out-of-order/replayed arrivals keep the full path. 3. **Redundant disk reads in the 5s agent-list poll.** `build_managed_agent_summary` re-read the global config from disk per call despite receiving it as a parameter, and re-read the teams store per tracked pair — 2N redundant reads per poll tick for N agents. Both are now caller-supplied; one-shot command paths use a `summarize_from_disk` helper. Same stores, same `unwrap_or_default` failure posture, read once. ## Evidence (mechanism attribution, live 0.5.16-block, mounted ~1500-event channel) - True-idle mounted view (working-state UI live): **25.08% mean / 24.15% median** WebContent CPU; post working-state decay: **6.33% / 3.50%**. - Reduce Motion A/B during a live agent turn (isolates the shimmer, working UI still mounted): **19.50% mean → 5.39% mean** (72% collapse). Native 10s samples: `Document::resolveStyle` 379 samples → 1; `updateCompositingLayersAfterStyleChange` 378 → 0; recursive `updateBackingAndHierarchy` 363 → 6. - The shimmer mechanism predates 0.5.16 (CSS unchanged since #3151); current multi-agent workloads exposed and amplified it. The mention regression itself was fixed separately in #6182. ## Testing - `desktop` observer store suites: 55/55 pass, including 5 new tests pinning the fast-path invariants (equal-timestamp seq ordering, duplicate-batch redelivery, intra-batch duplicates, overlapping late arrival takes the slow path, transcript-equals-replay on both paths). - `cargo test --lib managed_agents` (1016 passed) and `--lib commands` (727 passed); `cargo clippy` clean; `pnpm typecheck` + biome clean. - Release gate for the patched build (per Mongo): active-turn mounted CPU must collapse from the ~21–25% baseline with no per-frame style/compositing walk in a native sample — measured on Wes's workspace once a build with this branch is running. Findings and raw samples: `RESEARCH/LIVE_0_5_16_WEBKIT_ATTRIBUTION_2026_08_18.md`, `RESEARCH/RENDERER_STEADY_STATE_LOOP_AUDIT_2026_08_18.md`, `.scratch/live-app-review/` (Carl/Donut/Mongo/Brain, Royal Court thread 01a7fe75). --- *Opened by Brain (agent) via @wesbillman's account on his behalf — coordinated in Buzz channel agent-mention-policy-royal-court, thread 01a7fe75.* --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -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},
|
||||
|
||||
@@ -250,16 +250,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));
|
||||
(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -56,14 +55,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}"))?
|
||||
@@ -107,14 +99,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}"))?
|
||||
|
||||
@@ -26,6 +26,28 @@ pub(super) fn workspace_owner_hex(state: &AppState) -> Result<String, String> {
|
||||
Ok(keys.public_key().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
|
||||
@@ -333,18 +355,11 @@ pub(super) async fn start_local_agent_pairs_with_preflight(
|
||||
.managed_agent_processes
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let personas = load_personas(app).unwrap_or_default();
|
||||
let record = records
|
||||
.iter()
|
||||
.find(|record| record.pubkey == pubkey)
|
||||
.ok_or_else(|| format!("agent {pubkey} not found"))?;
|
||||
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)
|
||||
}
|
||||
|
||||
pub(super) async fn start_local_agent_with_preflight(
|
||||
@@ -436,6 +451,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(),
|
||||
)
|
||||
}
|
||||
@@ -474,14 +490,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()
|
||||
})
|
||||
@@ -854,15 +878,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,
|
||||
)
|
||||
};
|
||||
@@ -891,14 +908,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 {
|
||||
@@ -967,14 +977,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
|
||||
};
|
||||
@@ -1083,14 +1086,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:?}"
|
||||
@@ -1171,14 +1167,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}"))?
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user