Skip to content
Merged
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
5 changes: 5 additions & 0 deletions apps/server/src/agents/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ export class AgentManager {

/** Harvest token usage for an agent, scoped to its CLI session if known. */
async harvestAgentTokens(agent: AgentRecord): Promise<void> {
// Inert runtimes never launch CLI sessions, so there cannot be new token
// usage to collect. Skipping also keeps inert dev/test servers from
// scanning session history that belongs to the host environment.
if (!this.runtime.tracksSessions()) return;

await harvestTokenUsage(
this.pool,
{
Expand Down
44 changes: 44 additions & 0 deletions apps/server/test/db/agent-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1789,6 +1789,50 @@ describe("AgentManager", () => {
await rm(tmpDir, { recursive: true, force: true });
});

it("should skip harvesting only when the runtime is inert", async () => {
const { cwdToClaudeProjectDir } =
await import("../../src/agents/token-harvester.js");
const projectDir = cwdToClaudeProjectDir(tmpDir);
await mkdir(projectDir, { recursive: true });

const inertManager = new AgentManager(pool, noopLogger, inertTestConfig);
const agent = await inertManager.createAgent({
name: "inert-agent",
type: "claude",
cwd: tmpDir,
useWorktree: false,
});
await writeFile(
path.join(projectDir, `${agent.cliSessionId}.jsonl`),
`${JSON.stringify({
type: "assistant",
message: {
model: "claude-opus-4-6",
usage: { input_tokens: 500, output_tokens: 10 },
},
timestamp: "2026-04-01T10:00:00.000Z",
})}\n`
);

await inertManager.harvestAgentTokens(agent);

const usage = await pool.query(
`SELECT COUNT(*)::int AS count FROM agent_token_usage WHERE agent_id = $1`,
[agent.id]
);
expect(usage.rows[0].count).toBe(0);

await manager.harvestAgentTokens(agent);

const trackedRuntimeUsage = await pool.query(
`SELECT SUM(input_tokens)::int AS total FROM agent_token_usage WHERE agent_id = $1`,
[agent.id]
);
expect(trackedRuntimeUsage.rows[0].total).toBe(500);

await rm(projectDir, { recursive: true, force: true });
});

it("should harvest only the persona's session for a persona agent", async () => {
const { cwdToClaudeProjectDir } =
await import("../../src/agents/token-harvester.js");
Expand Down
20 changes: 17 additions & 3 deletions e2e/overflow-layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
type Locator,
type Page,
} from "@playwright/test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import {
cleanupE2EAgents,
Expand Down Expand Up @@ -84,13 +87,17 @@ async function getWindowScrollY(page: Page): Promise<number> {

async function seedOverflowAgents(
request: APIRequestContext,
count: number
count: number,
cwd: string
): Promise<Array<{ id: string; name: string }>> {
const created = await Promise.all(
Array.from({ length: count }, async (_, index) => {
const agent = await createAgentViaAPI(request, {
name: `e2e-agent-overflow-${Date.now()}-${index}`,
cwd: process.cwd(),
// These agents only provide enough rows to exercise sidebar overflow.
// Keep them in an owned directory outside the repo so bulk cleanup does
// not discover repository or shared temporary-directory lifecycle hooks.
cwd,
});
await setAgentLatestEventViaAPI(request, agent.id, {
type: "working",
Expand All @@ -104,15 +111,22 @@ async function seedOverflowAgents(
}

test.describe("Overflow layout", () => {
let overflowCwd: string;

test.beforeAll(async () => {
overflowCwd = await mkdtemp(join(tmpdir(), "dispatch-e2e-overflow-"));
});

test.afterAll(async ({ request }) => {
await cleanupE2EAgents(request);
await rm(overflowCwd, { recursive: true, force: true });
});

test("agents workspace keeps sidebar, media, and terminal overflow isolated", async ({
page,
request,
}) => {
const agents = await seedOverflowAgents(request, 24);
const agents = await seedOverflowAgents(request, 24, overflowCwd);
const focusAgent = agents[0]!;

await setAgentPinsViaDB(
Expand Down
Loading