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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,13 @@ ADMIN_USER_ID=
# Examples: http://myserver.com:8088, https://dashboard.example.com
# WEB_URL=http://localhost:8088


# Hackathon race monitor (scripts/hackathon_monitor.py)
# HACKATHON_API_URL=https://example.org
# HACKATHON_API_TOKEN=
# HACKATHON_SLUG=agent-builders-cup-1
# Comma-separated server names; empty means every server in config.yml
# HACKATHON_SERVERS=
# YAML of race agent id -> Condor run keys, for strategies whose slug differs
# (template: scripts/hackathon_agents.example.yml)
# HACKATHON_AGENT_MAP=
133 changes: 127 additions & 6 deletions frontend/src/components/chat/DockExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ import {
pnlColor,
shortBotName,
} from "@/lib/formatters";
import { quoteConverter, runningLeaves } from "@/lib/perf-population";
import {
botsByController,
quoteConverter,
runningLeaves,
terminatedLeaves,
} from "@/lib/perf-population";
import { UNATTACHED_BOT, controllerNodeId, type PerfLeaf } from "@/lib/perf-tree";
import { agentsQuery } from "@/lib/queryClient";

Expand Down Expand Up @@ -177,7 +182,10 @@ export function DockExecution({
// and the caches are shared rather than doubled, and a reader with the
// browser open pays nothing for this panel. No history walk: this folds, it
// does not chart, and the walk costs a paged request per controller.
const fleet = useFleetData(server, { population: "running", history: false });
// `terminated: true` adds the runs and the controllers they left behind, under
// the keys `/bots`' Terminated side already holds: an agent row is credited
// with what its stopped bots made, not only with what is trading now.
const fleet = useFleetData(server, { population: "running", history: false, terminated: true });

// The shared roster (key and cadence: `agentsQuery`), so the agent rows'
// liveness arrives with a request nobody made for them.
Expand Down Expand Up @@ -248,12 +256,50 @@ export function DockExecution({
return [...controllers, ...all.filter((leaf) => leaf.kind !== "controller")];
}, [fleet.controllers, fleet.executors, fleet.owners, fleet.deeds]);

/**
* Everything that has finished on this server — `terminatedLeaves`, the
* construction `/bots?population=terminated` folds, with no period window.
*
* `executionRows` keeps only what is credited to an agent and folds it into
* that agent's row, so an agent that stopped a bot or closed an executor keeps
* the money it made. The live half still moves on every socket frame; this
* half changes only when something stops.
*/
const history = useMemo(
() =>
terminatedLeaves({
executors: fleet.executors,
terminatedControllers: fleet.terminatedControllers,
runs: fleet.runs,
owners: fleet.owners,
deeds: fleet.deeds,
botByController: botsByController(fleet.controllers),
}),
Comment on lines +270 to +277

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stopped bot totals misconverted. If a stopped bot trades in a non-USD quote currency that no live controller or loaded executor uses, the dock includes that bot in the agent’s history but does not request its exchange rate. Conversion leaves its PnL and volume unchanged while displaying them in the selected currency, so the agent’s lifetime totals are wrong.

Knowledge Base Used: Frontend application

[
fleet.executors,
fleet.terminatedControllers,
fleet.runs,
fleet.owners,
fleet.deeds,
fleet.controllers,
],
);

const convert = useMemo(() => quoteConverter(fleet.convert), [fleet.convert]);
const currencySymbol = fleet.currencySymbol ?? "$";

const rows = useMemo(
() => executionRows({ leaves, deeds: fleet.deeds, agents, owners: fleet.owners, convert, now }),
[leaves, fleet.deeds, agents, fleet.owners, convert, now],
() =>
executionRows({
leaves,
history,
deeds: fleet.deeds,
agents,
owners: fleet.owners,
convert,
now,
}),
[leaves, history, fleet.deeds, agents, fleet.owners, convert, now],
);

// Only what the reader has actually clicked; the default for everything else
Expand Down Expand Up @@ -334,7 +380,9 @@ export function DockExecution({
);
}

if (!deployedCount && !unattached) {
// An agent that has stopped everything still has a row — its history is on
// this server — so the empty sentence is for a server with no rows at all.
if (!deployedCount && !unattached && rows.length === 0) {
return (
<div className="flex flex-col">
<p className="px-3 py-2 text-[11px] text-[var(--color-text-muted)]">
Expand Down Expand Up @@ -447,6 +495,17 @@ export function DockExecution({
onToggle={() => toggle(row.id)}
onOpenAgent={onOpenAgent}
/>
) : row.kind === "history" ? (
<HistoryRow
row={row}
symbol={currencySymbol}
capped={fleet.paging.capped}
onOpen={() =>
navigate(
`/bots?population=terminated&scope=${encodeURIComponent(row.parentId ?? "")}`,
)
Comment on lines +503 to +506

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 History link omits older activity. The history row shows an agent’s lifetime total, but clicking it opens the terminated browser with its default three-month window. For agents with older stopped bots or closed executors, the detail view omits figures included in the row, making the total difficult to verify.

Knowledge Base Used: Frontend application

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

}
/>
) : (
<tr>
<td colSpan={COLUMNS.length} className="p-0">
Expand Down Expand Up @@ -574,7 +633,7 @@ function AgentRow({
*/
const total = (value: number, what: string, pnl: boolean) => (
<td
title={`${row.label} — ${what}, summed over its controllers on this server`}
title={`${row.label} — ${what} on this server: its live controllers plus every bot it stopped and executor it closed`}
className={`whitespace-nowrap px-1.5 pt-1.5 text-right align-top font-mono font-semibold tabular-nums ${
pnl ? "" : "text-[var(--color-text-muted)]"
}`}
Expand Down Expand Up @@ -681,6 +740,68 @@ function AgentRow({
);
}

/**
* What an agent has already finished on this server: its stopped bots and its
* closed executors, as one line under the agent.
*
* One row and not a tree, because nothing in it can be paused or started — the
* terminated browser is where it is read in full, and the row opens it there.
* Its figures are the ones its agent's totals include, so the rows beneath an
* agent still add up to it.
*/
function HistoryRow({
row,
symbol,
capped,
onOpen,
}: {
row: ExecutionRow;
symbol: string;
/** The bounded executor walk stopped early, so older closed executors are missing. */
capped: boolean;
onOpen: () => void;
}) {
const cell = "whitespace-nowrap px-1.5 py-0.5 text-right font-mono tabular-nums";
const pnl = (value: number) => (
<td className={cell} style={{ color: pnlColor(value) }}>
{formatCurrencyPnl(value, symbol)}
</td>
);
const described = `${row.label} — realized PnL only: a stopped bot's last open position is not counted${
capped ? ". Older closed executors are past the loaded page and are missing" : ""
}`;
return (
<tr
data-history-row={row.id}
role="button"
tabIndex={0}
aria-label={described}
onClick={onOpen}
onKeyDown={(e) => {
if (e.key !== "Enter" && e.key !== " ") return;
e.preventDefault();
onOpen();
}}
className="cursor-pointer text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-hover)]"
>
<td
colSpan={4}
title={described}
className="truncate py-0.5 pr-1.5 italic"
style={{ paddingLeft: 12 + row.depth * 10 }}
>
{row.label}
{capped ? " +" : ""}
</td>
<td className={cell}>{formatCompactVolume(row.totals.volume, symbol)}</td>
{pnl(row.totals.realized)}
{pnl(row.totals.unrealized)}
{pnl(row.totals.net)}
<td />
</tr>
);
}

/**
* One bot, once per group: its whole branch in the browser, and the level the
* executors under it are counted at.
Expand Down
98 changes: 98 additions & 0 deletions frontend/src/components/chat/executionTree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { describe, expect, it } from "vitest";

import {
AUTO_OPEN_AGENTS,
HISTORY_SUFFIX,
executionCounts,
executionRows,
openRows,
Expand Down Expand Up @@ -296,3 +297,100 @@ describe("the header's counts", () => {
expect(executionCounts(rows)).toEqual({ controllers: 3, paused: 1 });
});
});

describe("an agent's history", () => {
const live = [
leaf({ id: "c1", bot: "race-live", agent: "race.mm", how: "namespace", realized: 10, unrealized: 5, net: 15, volume: 1000 }),
];
// A stopped bot: its final snapshot still carries a frozen 30 unrealized.
const stoppedCtrl = leaf({
id: "c0",
bot: "race-old",
agent: "race.mm",
how: "namespace",
realized: 40,
unrealized: 30,
net: 70,
volume: 5000,
running: false,
status: "stopped",
endedAt: NOW - 600_000,
});
// A standalone executor the agent's session opened and closed.
const closedExec = leaf({
id: "e9",
kind: "executor",
controllerId: "",
bot: "race.mm_s3",
agent: "race.mm",
how: "session",
realized: -4,
net: -4,
volume: 200,
running: false,
status: "terminated",
endedAt: NOW - 60_000,
});
const rowsWith = (history: PerfLeaf[], leaves: PerfLeaf[] = live) =>
executionRows({ leaves, history, deeds: null, agents: [], owners: [CONDOR_UI], convert, now: NOW });

it("credits the agent with its stopped bots and closed executors, under one history row", () => {
const rows = rowsWith([stoppedCtrl, closedExec]);
const agentRow = rows.find((r) => r.kind === "agent")!;
const history = rows.find((r) => r.kind === "history")!;

expect(history.id).toBe(`agent:race.mm${HISTORY_SUFFIX}`);
expect(history.parentId).toBe("agent:race.mm");
expect(history.label).toBe("History · 1 stopped bot, 1 closed executor");
// Realized only for the stopped bot: its last mark is not money.
expect(history.totals.net).toBe(36);
expect(history.totals.unrealized).toBe(0);
// The agent is live + history, and its rows still add up to it.
expect(agentRow.totals.net).toBe(15 + 36);
expect(agentRow.totals.volume).toBe(6200);
const children = rows.filter((r) => r.parentId === agentRow.id);
expect(children.reduce((n, r) => n + r.totals.net, 0)).toBe(agentRow.totals.net);
});

it("draws no row for a finished controller or a bot with nothing live under it", () => {
const rows = rowsWith([stoppedCtrl]);
expect(rows.map((r) => r.kind)).toEqual(["agent", "controller", "history"]);
expect(executionCounts(rows)).toEqual({ controllers: 1, paused: 0 });
});

it("keeps an agent that stopped everything on the panel", () => {
const rows = rowsWith([stoppedCtrl], []);
expect(rows.map((r) => [r.kind, r.totals.net])).toEqual([
["agent", 40],
["history", 40],
]);
});

it("does not count a live controller's closed executor twice", () => {
// The controller record already covers every executor it ever ran.
const closedUnderLive = leaf({
id: "e1",
kind: "executor",
controllerId: "c1",
bot: "race-live",
agent: "race.mm",
how: "namespace",
realized: 3,
net: 3,
running: false,
status: "terminated",
});
const rows = rowsWith([closedUnderLive]);
expect(rows.find((r) => r.kind === "agent")!.totals.net).toBe(15);
expect(rows.some((r) => r.kind === "history")).toBe(false);
});

it("leaves unowned and pseudo-run history out", () => {
const rows = rowsWith([
leaf({ ...closedExec, id: "e2", agent: "", how: "none", bot: "main" }),
leaf({ ...closedExec, id: "e3", agent: "condor.ui", how: "deed", bot: "main" }),
]);
expect(rows.map((r) => r.kind)).toEqual(["agent", "controller"]);
expect(rows[0].totals.net).toBe(15);
});
});
Loading
Loading