Skip to content
Closed
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
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ starts the Runtime and its daemon authenticates and initiates the Core connectio
Core verifies principal ownership and the exact Environment binding. These are
management responsibilities, not separate execution architectures.

Runtime telemetry uses a separate read-only service boundary documented in
[`contracts/agents-api/runtime-observability.md`](contracts/agents-api/runtime-observability.md).
Resolve durable Session, Environment and Runtime-instance identity before selecting
a provider source. Observation never extends a lease or changes compute lifecycle.
Keep observed zero, unavailable data and unsupported Runtime modes distinct. Metrics
may inform operators, but automatic suspension requires durable Core-owned activity
state and must not use a monitoring backend as lifecycle authority.

In V1, our daemon fills the user-side executor role. Users deploy daemon, the
selected harness, local tools and workspace together. Do not require Codex
`exec-server`, a service-side harness, registry/Noise transport or remote tool
Expand Down
17 changes: 11 additions & 6 deletions apps/web/e2e/agents-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2329,10 +2329,11 @@ test("presents Dashboard page-chain results and System boundaries without extra
await expect.poll(async () => {
const entries = await fixtureRequests(request);
return [count(entries, "/v1/agents"), count(entries, "/v1/agents/sessions")];
}).toEqual([count(before, "/v1/agents") + 1, count(before, "/v1/agents/sessions") + 1]);
}).toEqual([count(before, "/v1/agents") + 1, count(before, "/v1/agents/sessions") + 2]);
const after = await fixtureRequests(request);
expect(count(after, "/v1/agents")).toBe(count(before, "/v1/agents") + 1);
expect(count(after, "/v1/agents/sessions")).toBe(count(before, "/v1/agents/sessions") + 1);
expect(count(after, "/v1/agents/sessions")).toBe(count(before, "/v1/agents/sessions") + 2);
expect(count(after, "/v1/agents/runtime-observations")).toBe(count(before, "/v1/agents/runtime-observations") + 1);
for (const path of detailPaths) expect(count(after, path)).toBe(count(before, path));
await attachScreenshot(page, testInfo, "desktop-dashboard-loaded-snapshot");

Expand Down Expand Up @@ -2387,11 +2388,12 @@ test("presents Dashboard page-chain results and System boundaries without extra
return [count(entries, "/v1/agents"), count(entries, "/v1/agents/sessions")];
}).toEqual([
count(beforeSystemRefresh, "/v1/agents") + 1,
count(beforeSystemRefresh, "/v1/agents/sessions") + 1,
count(beforeSystemRefresh, "/v1/agents/sessions") + 2,
]);
const afterSystemRefresh = await fixtureRequests(request);
expect(count(afterSystemRefresh, "/v1/agents")).toBe(count(beforeSystemRefresh, "/v1/agents") + 1);
expect(count(afterSystemRefresh, "/v1/agents/sessions")).toBe(count(beforeSystemRefresh, "/v1/agents/sessions") + 1);
expect(count(afterSystemRefresh, "/v1/agents/sessions")).toBe(count(beforeSystemRefresh, "/v1/agents/sessions") + 2);
expect(count(afterSystemRefresh, "/v1/agents/runtime-observations")).toBe(count(beforeSystemRefresh, "/v1/agents/runtime-observations") + 1);
for (const path of detailPaths) expect(count(afterSystemRefresh, path)).toBe(count(beforeSystemRefresh, path));
await attachScreenshot(page, testInfo, "desktop-system-contract-boundary");

Expand Down Expand Up @@ -2480,7 +2482,9 @@ test("publishes Dashboard counts only after every top-level Agent and Session pa
await expect(dashboard.locator(".dashboard-summary > div").filter({ hasText: "Agents" })).toContainText("3");
await expect(dashboard.locator(".dashboard-summary > div").filter({ hasText: "Sessions" })).toContainText("2");
expect(agentAfters).toEqual([null, "agent_b"]);
expect(sessionAfters).toEqual([null, "session_snapshot"]);
expect(sessionAfters).toHaveLength(4);
expect(sessionAfters.filter((after) => after === null)).toHaveLength(2);
expect(sessionAfters.filter((after) => after === "session_snapshot")).toHaveLength(2);
});

test("keeps the previous Dashboard result when pagination exceeds the safety limit", async ({ page, request }) => {
Expand Down Expand Up @@ -2523,8 +2527,9 @@ test("keeps the previous Dashboard result when pagination exceeds the safety lim
await refresh.click();
await expect.poll(() => reads).toBe(100);
await expect(refresh).toBeEnabled();
await expect(dashboard).toContainText("Using the last successful snapshot");
await expect(dashboard).toContainText("Snapshot incomplete");
await expect(dashboard).toContainText("collection pagination exceeded the Web safety limit");
await expect(dashboard).toContainText("Sessions changed while Runtime observations were loading");
await expect(loadedAgents).toContainText("3");
await dashboard.getByRole("button", { name: "Connection settings" }).click();
const connectionDialog = page.getByRole("dialog", { name: "Connect an Agent Core" });
Expand Down
9 changes: 9 additions & 0 deletions apps/web/e2e/fixture-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,15 @@ const server = http.createServer(async (request, response) => {
return sendJson(response, created, 201);
}

// The legacy Web fixture uses human-readable Session IDs for interaction
// assertions. Runtime observation resources require canonical UUIDs, so this
// fixture advertises an empty, valid collection instead of inventing a false
// identity join. Positive Runtime rendering is covered by the typed component
// and coordinator tests with canonical identities.
if (request.method === "GET" && url.pathname === "/v1/agents/runtime-observations") {
return sendJson(response, page([]));
}

if (request.method === "GET" && url.pathname === "/v1/agents/sessions") {
trackAbort(response, "sessionListReads");
const control = consumeControl("sessionList");
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@agents-core-web/agents-client": "workspace:*",
"@tanstack/react-table": "^8.21.3",
"lucide-react": "^1.22.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
Expand Down
103 changes: 101 additions & 2 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import {
requestAgentUpdate,
} from "./features/agents/agent-actions";
import { DashboardView } from "./features/dashboard/DashboardView";
import {
loadRuntimeDashboardSnapshot,
RUNTIME_SNAPSHOT_REFRESH_MS,
RUNTIME_SNAPSHOT_TIMEOUT_MS,
type RuntimeDashboardSnapshot,
} from "./features/dashboard/runtime-snapshot";
import {
SessionsView,
type SessionDetailState,
Expand Down Expand Up @@ -270,6 +276,10 @@ export function App() {
const [sessionCollectionState, setSessionCollectionState] = useState<CoreConnectionState>("connecting");
const [sessionCollectionError, setSessionCollectionError] = useState<string | null>(null);
const [sessionCollectionHasSnapshot, setSessionCollectionHasSnapshot] = useState(false);
const [runtimeSnapshot, setRuntimeSnapshot] = useState<RuntimeDashboardSnapshot | null>(null);
const [runtimeCollectionState, setRuntimeCollectionState] = useState<CoreConnectionState>("connecting");
const [runtimeCollectionError, setRuntimeCollectionError] = useState<string | null>(null);
const [runtimeCollectionHasSnapshot, setRuntimeCollectionHasSnapshot] = useState(false);
const [sessionAgentFilter, setSessionAgentFilter] = useState<string | null>(null);
const [filteredSessions, setFilteredSessions] = useState<AgentSession[]>([]);
const [filteredSessionCollectionState, setFilteredSessionCollectionState] = useState<CoreConnectionState>("connecting");
Expand Down Expand Up @@ -305,6 +315,8 @@ export function App() {
const sessionCollectionRequestRef = useRef(0);
const agentCollectionAbortRef = useRef<AbortController | null>(null);
const sessionCollectionAbortRef = useRef<AbortController | null>(null);
const runtimeCollectionAbortRef = useRef<AbortController | null>(null);
const runtimeCollectionRequestRef = useRef(0);
const filteredSessionCollectionAbortRef = useRef<AbortController | null>(null);
const filteredSessionCollectionRequestRef = useRef(0);
const sessionAgentFilterRef = useRef<string | null>(sessionAgentFilter);
Expand Down Expand Up @@ -516,6 +528,54 @@ export function App() {
}
}, [core, coreGeneration, notify]);

const refreshRuntimeSnapshot = useCallback(async () => {
if (coreGeneration !== connectionGenerationRef.current) return false;
runtimeCollectionAbortRef.current?.abort();
const controller = new AbortController();
runtimeCollectionAbortRef.current = controller;
const request = runtimeCollectionRequestRef.current + 1;
runtimeCollectionRequestRef.current = request;
let timedOut = false;
const timeout = window.setTimeout(() => {
timedOut = true;
controller.abort();
}, RUNTIME_SNAPSHOT_TIMEOUT_MS);
setRuntimeCollectionState("connecting");
setRuntimeCollectionError(null);
try {
const result = await settleCollection(() => loadRuntimeDashboardSnapshot(
core,
() => sessionCollectionRevisionRef.current,
controller.signal,
));
if (
coreGeneration !== connectionGenerationRef.current ||
request !== runtimeCollectionRequestRef.current
) return false;
if (result.status === "rejected") {
if (isAbort(result.reason) && !timedOut) return false;
const message = timedOut
? "Runtime snapshot exceeded the 15 second Web refresh budget."
: errorMessage(result.reason);
setRuntimeCollectionState("failed");
setRuntimeCollectionError(message);
return false;
}
if (result.value === null) {
setRuntimeCollectionState("failed");
setRuntimeCollectionError("Session data changed while Runtime observations were loading. The previous complete snapshot was retained.");
return false;
}
setRuntimeSnapshot(result.value);
setRuntimeCollectionHasSnapshot(true);
setRuntimeCollectionState("ready");
return true;
} finally {
window.clearTimeout(timeout);
if (runtimeCollectionAbortRef.current === controller) runtimeCollectionAbortRef.current = null;
}
}, [core, coreGeneration]);

const refreshFilteredSessions = useCallback(async (agentId: string) => {
if (
coreGeneration !== connectionGenerationRef.current ||
Expand Down Expand Up @@ -855,9 +915,10 @@ export function App() {
void refreshSessions();
void refreshVaults();
void refreshEnvironmentTemplates();
void refreshRuntimeSnapshot();
const filter = sessionAgentFilterRef.current;
if (filter) void refreshFilteredSessions(filter);
}, [refreshAgents, refreshEnvironmentTemplates, refreshFilteredSessions, refreshSessions, refreshVaults]);
}, [refreshAgents, refreshEnvironmentTemplates, refreshFilteredSessions, refreshRuntimeSnapshot, refreshSessions, refreshVaults]);

const changeSessionAgentFilter = useCallback((agentId: string | null) => {
if (sessionAgentFilterRef.current === agentId) return;
Expand Down Expand Up @@ -894,6 +955,10 @@ export function App() {
setSessions([]);
setAgentCollectionHasSnapshot(false);
setSessionCollectionHasSnapshot(false);
setRuntimeSnapshot(null);
setRuntimeCollectionState("connecting");
setRuntimeCollectionError(null);
setRuntimeCollectionHasSnapshot(false);
setItems([]);
setTurns([]);
setEnvironmentObservations(new Map());
Expand All @@ -908,7 +973,29 @@ export function App() {
void refreshSessions();
void refreshVaults();
void refreshEnvironmentTemplates();
}, [refreshAgents, refreshEnvironmentTemplates, refreshSessions, refreshVaults]);
void refreshRuntimeSnapshot();
}, [refreshAgents, refreshEnvironmentTemplates, refreshRuntimeSnapshot, refreshSessions, refreshVaults]);

useEffect(() => {
if (view !== "dashboard") return;
let timer: number | null = null;
const schedule = () => {
const jitter = Math.floor(Math.random() * 5_000);
timer = window.setTimeout(() => {
if (!document.hidden) void refreshRuntimeSnapshot();
schedule();
}, RUNTIME_SNAPSHOT_REFRESH_MS + jitter);
};
const onVisibilityChange = () => {
if (!document.hidden) void refreshRuntimeSnapshot();
};
document.addEventListener("visibilitychange", onVisibilityChange);
schedule();
return () => {
if (timer !== null) window.clearTimeout(timer);
document.removeEventListener("visibilitychange", onVisibilityChange);
};
}, [refreshRuntimeSnapshot, view]);

useEffect(() => {
filteredSessionCollectionAbortRef.current?.abort();
Expand Down Expand Up @@ -1943,6 +2030,7 @@ export function App() {
connectionGenerationRef.current += 1;
agentCollectionRequestRef.current += 1;
sessionCollectionRequestRef.current += 1;
runtimeCollectionRequestRef.current += 1;
filteredSessionCollectionRequestRef.current += 1;
vaultCollectionRequestRef.current += 1;
agentCollectionRevisionRef.current = 0;
Expand All @@ -1966,6 +2054,8 @@ export function App() {
agentCollectionAbortRef.current = null;
sessionCollectionAbortRef.current?.abort();
sessionCollectionAbortRef.current = null;
runtimeCollectionAbortRef.current?.abort();
runtimeCollectionAbortRef.current = null;
filteredSessionCollectionAbortRef.current?.abort();
filteredSessionCollectionAbortRef.current = null;
vaultCollectionAbortRef.current?.abort();
Expand All @@ -1981,6 +2071,10 @@ export function App() {
setSessionCollectionState("connecting");
setSessionCollectionError(null);
setSessionCollectionHasSnapshot(false);
setRuntimeSnapshot(null);
setRuntimeCollectionState("connecting");
setRuntimeCollectionError(null);
setRuntimeCollectionHasSnapshot(false);
sessionAgentFilterRef.current = null;
filteredSessionsRef.current = [];
setSessionAgentFilter(null);
Expand Down Expand Up @@ -2108,6 +2202,10 @@ export function App() {
sessionCollectionState={sessionCollectionState}
sessionCollectionError={sessionCollectionError}
sessionCollectionHasSnapshot={sessionCollectionHasSnapshot}
runtimeSnapshot={runtimeSnapshot}
runtimeCollectionState={runtimeCollectionState}
runtimeCollectionError={runtimeCollectionError}
runtimeCollectionHasSnapshot={runtimeCollectionHasSnapshot}
onRefresh={refreshDashboard}
onCreateAgent={openAgentSetup}
onStartSession={() => openSessionSetup()}
Expand Down Expand Up @@ -2208,6 +2306,7 @@ export function App() {
refreshing={
agentCollectionState === "connecting" ||
sessionCollectionState === "connecting" ||
runtimeCollectionState === "connecting" ||
vaultCollectionState === "connecting"
}
onRefresh={refreshDashboard}
Expand Down
Loading
Loading