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
67 changes: 57 additions & 10 deletions crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,56 @@ fn emit_runtime_lifecycle(
}
}

fn startup_runtime_lifecycle(lazy_pool: bool) -> &'static str {
if lazy_pool {
"listening"
} else {
"ready"
}
}

fn emit_startup_runtime_lifecycle(
observer: Option<&observer::ObserverHandle>,
start_nonce: &str,
pubkey: &str,
relay_url: &str,
lazy_pool: bool,
) {
emit_runtime_lifecycle(
observer,
start_nonce,
pubkey,
relay_url,
startup_runtime_lifecycle(lazy_pool),
None,
);
}

#[cfg(test)]
mod runtime_lifecycle_tests {
use crate::observer::ObserverHandle;

#[test]
fn startup_lifecycle_emits_once_for_each_pool_mode() {
for (lazy_pool, expected) in [(false, "ready"), (true, "listening")] {
let observer = ObserverHandle::in_process();

super::emit_startup_runtime_lifecycle(
Some(&observer),
"nonce",
"pubkey",
"ws://localhost:3000",
lazy_pool,
);

let events = observer.snapshot();
assert_eq!(events.len(), 1);
assert_eq!(events[0].kind, "managed_agent_runtime_lifecycle");
assert_eq!(events[0].payload["lifecycle"], expected);
}
}
}

/// Resolve the agent's owner pubkey at startup.
///
/// Priority:
Expand Down Expand Up @@ -2174,16 +2224,13 @@ async fn tokio_main() -> Result<()> {
}
}

if config.lazy_pool {
emit_runtime_lifecycle(
observer.as_ref(),
&runtime_start_nonce,
&pubkey_hex,
&config.relay_url,
"listening",
None,
);
}
emit_startup_runtime_lifecycle(
observer.as_ref(),
&runtime_start_nonce,
&pubkey_hex,
&config.relay_url,
config.lazy_pool,
);

let base_prompt_content = config.base_prompt_content.take();
let ctx = Arc::new(PromptContext {
Expand Down
37 changes: 37 additions & 0 deletions desktop/src/features/agents/observerEventOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ObserverEvent } from "./ui/agentSessionTypes";

export function compareObserverEvents(
left: ObserverEvent,
right: ObserverEvent,
) {
const leftTime = Date.parse(left.timestamp);
const rightTime = Date.parse(right.timestamp);
if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) {
const timeDiff = leftTime - rightTime;
if (timeDiff !== 0) {
return timeDiff;
}
}

return left.seq - right.seq;
}

/**
* Returns true if `candidate` sorts strictly after `stored` using the same
* two-key ordering as `compareObserverEvents`: later timestamp wins; equal
* timestamp falls back to higher seq. Extracted so latest-live advancement
* cannot drift from transcript ordering.
*/
export function isObserverEventAfter(
candidate: { timestamp: string; seq: number },
stored: { timestamp: string; seq: number },
): boolean {
const candidateTime = Date.parse(candidate.timestamp);
const storedTime = Date.parse(stored.timestamp);
if (Number.isFinite(candidateTime) && Number.isFinite(storedTime)) {
if (candidateTime !== storedTime) {
return candidateTime > storedTime;
}
}
return candidate.seq > stored.seq;
}
42 changes: 42 additions & 0 deletions desktop/src/features/agents/observerLifecycleSeq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { normalizePubkey } from "@/shared/lib/pubkey";
import type { ObserverEvent } from "./ui/agentSessionTypes";

const appliedLifecycleSeqByPairNonce = new Map<string, number>();

function lifecycleSequenceKey(
agentPubkey: string,
payload: unknown,
): string | null {
if (payload === null || typeof payload !== "object") return null;
const record = payload as { relayUrl?: unknown; startNonce?: unknown };
if (
typeof record.relayUrl !== "string" ||
typeof record.startNonce !== "string"
) {
return null;
}
return `${normalizePubkey(agentPubkey)}\0${record.relayUrl}\0${record.startNonce}`;
}

export function shouldApplyLifecycleFrame(
agentPubkey: string,
event: ObserverEvent,
): boolean {
const key = lifecycleSequenceKey(agentPubkey, event.payload);
if (key === null) return false;
const applied = appliedLifecycleSeqByPairNonce.get(key);
return applied === undefined || event.seq > applied;
}

export function recordAppliedLifecycleFrame(
agentPubkey: string,
event: ObserverEvent,
): void {
const key = lifecycleSequenceKey(agentPubkey, event.payload);
if (key === null) return;
appliedLifecycleSeqByPairNonce.set(key, event.seq);
}

export function resetAppliedLifecycleSeq(): void {
appliedLifecycleSeqByPairNonce.clear();
}
152 changes: 152 additions & 0 deletions desktop/src/features/agents/observerRelayStore.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import assert from "node:assert/strict";
import { afterEach, beforeEach, test } from "node:test";

import {
_testProcessLiveObserverEvents,
getAgentObserverSnapshot,
resetAgentObserverStore,
} from "./observerRelayStore.ts";

const AGENT = "a".repeat(64);

function lifecycleEvent(seq, lifecycle, startNonce = "gen-1") {
return {
seq,
timestamp: `2026-08-15T00:00:0${seq}Z`,
kind: "managed_agent_runtime_lifecycle",
agentIndex: 0,
channelId: null,
sessionId: null,
turnId: null,
payload: {
relayUrl: "ws://localhost:3000",
startNonce,
lifecycle,
pid: 123,
},
};
}

beforeEach(() => {
resetAgentObserverStore();
});

afterEach(() => {
delete globalThis.__TAURI_INTERNALS__;
delete globalThis.window;
});

test("applies lifecycle frames in observer order", async () => {
const started = [];
const completed = [];
let releaseWaking;

const tauriInternals = {
invoke: (command, args) => {
assert.equal(command, "put_managed_agent_runtime_lifecycle");
const lifecycle = args.payload.lifecycle;
started.push(lifecycle);
if (lifecycle === "waking") {
return new Promise((resolve) => {
releaseWaking = () => {
completed.push(lifecycle);
resolve({});
};
});
}
completed.push(lifecycle);
return Promise.resolve({});
},
};
globalThis.__TAURI_INTERNALS__ = tauriInternals;
globalThis.window = { __TAURI_INTERNALS__: tauriInternals };

const processing = _testProcessLiveObserverEvents(AGENT, [
lifecycleEvent(1, "waking"),
lifecycleEvent(2, "ready"),
]);
await Promise.resolve();

assert.deepEqual(
started,
["waking"],
"ready must wait for the preceding waking write",
);
releaseWaking();
await processing;

assert.deepEqual(started, ["waking", "ready"]);
assert.deepEqual(completed, ["waking", "ready"]);
});

test("drops the remainder of a lifecycle batch after a store reset", async () => {
const started = [];
let releaseWaking;

const tauriInternals = {
invoke: (_command, args) => {
const lifecycle = args.payload.lifecycle;
started.push(lifecycle);
if (lifecycle === "waking") {
return new Promise((resolve) => {
releaseWaking = resolve;
});
}
return Promise.resolve({});
},
};
globalThis.__TAURI_INTERNALS__ = tauriInternals;
globalThis.window = { __TAURI_INTERNALS__: tauriInternals };

const processing = _testProcessLiveObserverEvents(AGENT, [
lifecycleEvent(1, "waking"),
lifecycleEvent(2, "ready"),
]);
await Promise.resolve();

resetAgentObserverStore();
releaseWaking();
await processing;

assert.deepEqual(started, ["waking"]);
assert.deepEqual(getAgentObserverSnapshot(AGENT).events, []);
});

test("newest-first replay does not regress ready to stale waking", async () => {
const started = [];
const tauriInternals = {
invoke: (command, args) => {
assert.equal(command, "put_managed_agent_runtime_lifecycle");
started.push(args.payload.lifecycle);
return Promise.resolve({});
},
};
globalThis.__TAURI_INTERNALS__ = tauriInternals;
globalThis.window = { __TAURI_INTERNALS__: tauriInternals };

await _testProcessLiveObserverEvents(AGENT, [lifecycleEvent(2, "ready")]);
await _testProcessLiveObserverEvents(AGENT, [lifecycleEvent(1, "waking")]);

assert.deepEqual(started, ["ready"]);
});

test("a new startNonce restarts the lifecycle sequence domain", async () => {
const started = [];
const tauriInternals = {
invoke: (_command, args) => {
started.push(`${args.payload.startNonce}:${args.payload.lifecycle}`);
return Promise.resolve({});
},
};
globalThis.__TAURI_INTERNALS__ = tauriInternals;
globalThis.window = { __TAURI_INTERNALS__: tauriInternals };

await _testProcessLiveObserverEvents(AGENT, [
lifecycleEvent(2, "ready", "gen-1"),
]);
await _testProcessLiveObserverEvents(AGENT, [
lifecycleEvent(1, "waking", "gen-2"),
]);

assert.deepEqual(started, ["gen-1:ready", "gen-2:waking"]);
});
Loading