Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6f1f006
fix: count busy descendants in the agents-view subagents indicator
snimu Sep 2, 2026
c4d65b8
fix: tally busy descendants before heartbeat promotion inflates sections
snimu Sep 2, 2026
608cd02
fix: make the descendant tally iterative so deep chains cannot overflow
snimu Sep 2, 2026
d7839fc
fix: classify running from the session's own work, not delegated chil…
snimu Sep 2, 2026
e4fc5e7
chore: note the running-means-the-session policy in the changelog fra…
snimu Sep 2, 2026
2389e38
fix: split display activity from live-work safety in the busy predicates
snimu Sep 2, 2026
b5e3afe
feat: show token and cost totals on agents view rows
snimu Sep 2, 2026
c1ed142
feat: final row usage format with arrows and conditional recursive cost
snimu Sep 2, 2026
5df8a38
fix: one own-spend truth across scan, loader, and filters
snimu Sep 2, 2026
65d5d6e
Merge origin/main into fix/agents-view-busy-descendant-indicator
snimu Sep 3, 2026
d5a9d80
Merge fix/agents-view-busy-descendant-indicator (post-main-merge) int…
snimu Sep 3, 2026
0bd4d19
fix: gate destructive agent actions on live work, not display section
snimu Sep 3, 2026
5efd0e5
Merge fix/agents-view-busy-descendant-indicator (live-work delete gat…
snimu Sep 3, 2026
0511890
fix: stop live descendants when cancelling a settled child run
snimu Sep 3, 2026
3e3a5c2
Merge fix/agents-view-busy-descendant-indicator (descendant-aware can…
snimu Sep 3, 2026
12fc3f8
feat: render the usage cell unconditionally, zeros included
snimu Sep 3, 2026
5b536e7
chore: trim comments and fold overlapping pins on the indicator branch
snimu Sep 3, 2026
c501efb
chore: cut the usage feature's comments and collapse its pins
snimu Sep 3, 2026
6174b5e
Merge cleaned fix/agents-view-busy-descendant-indicator into feat/age…
snimu Sep 3, 2026
e071ab8
fix: cancel and descend at every node of the rlm cancel walk
snimu Sep 3, 2026
af95347
Merge fix/agents-view-busy-descendant-indicator into feat/agents-view…
snimu Sep 3, 2026
03e09d4
fix: passivated RLM descendants survive restarts as saved-catalog rows
snimu Sep 3, 2026
1bc0f21
feat: compaction and branch-summary calls bill the session they serve
snimu Sep 3, 2026
98d14b1
fix: harden the passive-descendant catalog merge (round 2)
snimu Sep 3, 2026
c27b10c
chore: consolidate the accumulated review-round pins
snimu Sep 3, 2026
d93ea21
Merge origin/main into feat/agents-view-costs
sethkarten Sep 3, 2026
af309b8
test(coding-agent): align unified view regression with usage rows
sethkarten Sep 3, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added token and cost details to agents view rows: input/output tokens plus the session's own cost and its recursive total including all subagents; the message-count detail is gone.
27 changes: 24 additions & 3 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ import { type BashOperations, createLocalBashOperations } from "./tools/bash.js"
import { createAllToolDefinitions } from "./tools/index.js";
import { IpythonKernelProvisioner } from "./tools/ipython.js";
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
import { addAssistantUsage, emptyUsage } from "./usage.js";
import { addAssistantUsage, emptyUsage, type SessionUsageSummary, sessionUsageSummaryFrom } from "./usage.js";
import { SERPER_CREDENTIAL_ID, SERPER_ENV_VAR, WEBSEARCH_SKILL_NAME } from "./websearch-credential.js";

export type { GoalState, GoalStatus } from "./goals.js";
Expand Down Expand Up @@ -7565,6 +7565,7 @@ export class AgentSession {
let firstKeptEntryId: string;
let tokensBefore: number;
let details: CompactionResult["details"];
let usage: CompactionResult["usage"];
try {
if (this._extensionRunner.hasHandlers("session_before_compact")) {
const result = (await this._extensionRunner.emit({
Expand All @@ -7586,7 +7587,7 @@ export class AgentSession {
}

if (extensionCompaction) {
({ summary, firstKeptEntryId, tokensBefore, details } = extensionCompaction);
({ summary, firstKeptEntryId, tokensBefore, details, usage } = extensionCompaction);
} else {
// Each summary wire call gets its own request ID: split turns send two
// different bodies, and one Idempotency-Key must never cover both. A slice
Expand Down Expand Up @@ -7615,7 +7616,7 @@ export class AgentSession {
throw error;
}
};
({ summary, firstKeptEntryId, tokensBefore, details } = await compact(
({ summary, firstKeptEntryId, tokensBefore, details, usage } = await compact(
preparation,
model,
apiKey,
Expand Down Expand Up @@ -7647,6 +7648,7 @@ export class AgentSession {
details,
fromExtension,
customInstructions,
usage,
);
} catch (error) {
compactionSettled = true;
Expand Down Expand Up @@ -11732,6 +11734,7 @@ export class AgentSession {

let summaryText: string | undefined;
let summaryDetails: unknown;
let summaryUsage: Usage | undefined;
if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
const model = this.model!;
const { apiKey, headers } = await this._getRequiredRequestAuth(model);
Expand All @@ -11752,6 +11755,7 @@ export class AgentSession {
throw new Error(result.error);
}
summaryText = result.summary;
summaryUsage = result.usage;
summaryDetails = {
readFiles: result.readFiles || [],
modifiedFiles: result.modifiedFiles || [],
Expand Down Expand Up @@ -11787,6 +11791,7 @@ export class AgentSession {
summaryText,
summaryDetails,
fromExtension,
summaryUsage,
);
summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;

Expand Down Expand Up @@ -11957,6 +11962,22 @@ export class AgentSession {
return (provider, modelId) => this._modelRegistry.find(provider, modelId)?.contextWindow;
}

private _ownUsageMemo?: { count: number; tailId: string | undefined; usage: SessionUsageSummary | undefined };

// Whole-file own spend, identical to the catalog scan so rows never shift at passivation.
getOwnUsageSummary(): SessionUsageSummary | undefined {
const entries = this.sessionManager.getEntries();
const tailId = entries.at(-1)?.id;
const memo = this._ownUsageMemo;
if (memo && memo.count === entries.length && memo.tailId === tailId) {
return memo.usage;
}
const { ownUsage } = computeOwnAndTotalUsage(entries, entries);
Comment thread
snimu marked this conversation as resolved.
const usage = sessionUsageSummaryFrom(ownUsage);
this._ownUsageMemo = { count: entries.length, tailId, usage };
return usage;
}

/**
* Build the agent context overview for /context: this session as the root
* plus one node per RLM sub-agent, recursively. Running children are read
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import type { AgentMessage } from "@earendil-works/pi-agent-core";
import type { Model } from "@earendil-works/pi-ai";
import type { Model, Usage } from "@earendil-works/pi-ai";
import { completeSimple } from "@earendil-works/pi-ai";
import {
convertToLlm,
Expand All @@ -31,6 +31,7 @@ export interface BranchSummaryResult {
modifiedFiles?: string[];
aborted?: boolean;
error?: string;
usage?: Usage;
}

/** Details stored in BranchSummaryEntry.details for file tracking */
Expand Down Expand Up @@ -303,5 +304,6 @@ export async function generateBranchSummary(
summary: summary || "No summary generated",
readFiles,
modifiedFiles,
usage: response.usage,
};
}
42 changes: 32 additions & 10 deletions packages/coding-agent/src/core/compaction/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
createCustomMessage,
} from "../messages.js";
import { buildSessionContext, type CompactionEntry, type SessionEntry } from "../session-manager.js";
import { addAssistantUsage, emptyUsage } from "../usage.js";
import {
computeFileLists,
createFileOps,
Expand All @@ -30,6 +31,11 @@ export interface CompactionDetails {
modifiedFiles: string[];
}

export interface SummarySlice {
summary: string;
usage?: Usage;
}

/**
* Extract file operations from messages and previous compaction entries.
*/
Expand Down Expand Up @@ -98,6 +104,8 @@ export interface CompactionResult<T = unknown> {
tokensBefore: number;
/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
details?: T;
/** What the summarization call(s) billed; persisted on the compaction entry. */
usage?: Usage;
}
export const COMPACT_SKILL_NAME = "compact";

Expand Down Expand Up @@ -515,7 +523,7 @@ export async function generateSummary(
customInstructions?: string,
previousSummary?: string,
thinkingLevel?: ThinkingLevel,
): Promise<string> {
): Promise<SummarySlice> {
const maxTokens = Math.floor(0.8 * reserveTokens);

const basePrompt = buildSummarizationPrompt(customInstructions, previousSummary);
Expand Down Expand Up @@ -556,7 +564,7 @@ export async function generateSummary(
.map((c) => c.text)
.join("\n");

return textContent;
return { summary: textContent, usage: response.usage };
}
export interface CompactionPreparation {
/** UUID of first entry to keep */
Expand Down Expand Up @@ -696,6 +704,7 @@ export async function compact(
settings,
} = preparation;
let summary: string;
const slices: SummarySlice[] = [];

if (isSplitTurn && turnPrefixMessages.length > 0) {
// Split turns make two wire calls with different bodies; each needs its own identity.
Expand All @@ -714,7 +723,7 @@ export async function compact(
thinkingLevel,
),
)
: Promise.resolve("No prior history."),
: Promise.resolve<SummarySlice>({ summary: "No prior history." }),
summaryCall((callHeaders) =>
generateTurnPrefixSummary(
turnPrefixMessages,
Expand All @@ -727,9 +736,10 @@ export async function compact(
),
),
]);
summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
slices.push(historyResult, turnPrefixResult);
summary = `${historyResult.summary}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.summary}`;
} else {
summary = await summaryCall((callHeaders) =>
const result = await summaryCall((callHeaders) =>
generateSummary(
messagesToSummarize,
model,
Expand All @@ -742,6 +752,8 @@ export async function compact(
thinkingLevel,
),
);
slices.push(result);
summary = result.summary;
}
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
summary += formatFileOperations(readFiles, modifiedFiles);
Expand All @@ -750,11 +762,18 @@ export async function compact(
throw new Error("First kept entry has no UUID - session may need migration");
}

let usage: Usage | undefined;
for (const slice of slices) {
if (!slice.usage) continue;
usage ??= emptyUsage();
addAssistantUsage(usage, slice.usage);
}
return {
summary,
firstKeptEntryId,
tokensBefore,
details: { readFiles, modifiedFiles } as CompactionDetails,
usage,
};
}

Expand All @@ -769,7 +788,7 @@ async function generateTurnPrefixSummary(
headers?: Record<string, string>,
signal?: AbortSignal,
thinkingLevel?: ThinkingLevel,
): Promise<string> {
): Promise<SummarySlice> {
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
const llmMessages = convertToLlm(messages);
const conversationText = serializeConversation(llmMessages);
Expand All @@ -794,8 +813,11 @@ async function generateTurnPrefixSummary(
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
}

return response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n");
return {
summary: response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n"),
usage: response.usage,
};
}
2 changes: 2 additions & 0 deletions packages/coding-agent/src/core/context-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export function computeOwnAndTotalUsage(
if (isAssistantEntry(entry)) {
branchAssistantIds.add(entry.id);
addAssistantUsage(totalUsage, entry.message.usage);
} else if ((entry.type === "compaction" || entry.type === "branch_summary") && entry.usage) {
addAssistantUsage(totalUsage, entry.usage);
}
}
const ownUsage = cloneUsage(totalUsage);
Expand Down
53 changes: 50 additions & 3 deletions packages/coding-agent/src/core/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ import {
createCompactionSummaryMessage,
createCustomMessage,
} from "./messages.js";
import { cloneUsage } from "./usage.js";
import {
addAssistantUsage,
cloneUsage,
emptyUsage,
type SessionUsageSummary,
sessionUsageSummaryFrom,
subtractAssistantUsage,
} from "./usage.js";

export const CURRENT_SESSION_VERSION = 3;
const SESSION_LIST_SEARCH_TEXT_MAX_CHARS = 64 * 1024;
Expand Down Expand Up @@ -129,6 +136,7 @@ export interface CompactionEntry<T = unknown> extends SessionEntryBase {
details?: T;
fromHook?: boolean;
customInstructions?: string;
usage?: Usage;
}

export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
Expand All @@ -137,6 +145,7 @@ export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
summary: string;
details?: T;
fromHook?: boolean;
usage?: Usage;
}

export interface CustomEntry<T = unknown> extends SessionEntryBase {
Expand Down Expand Up @@ -255,6 +264,7 @@ export interface SessionInfo {
firstMessage: string;
allMessagesText: string;
agentStatus?: AgentStatus;
usage?: SessionUsageSummary;
}

export type ReadonlySessionManager = Pick<
Expand Down Expand Up @@ -953,6 +963,10 @@ async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeo
let state: SessionState | undefined;
let agentStatus: AgentStatus | undefined;
let lastActivityTime: number | undefined;
// Fold attribution aggregates like the loader: either disk representation cancels to the same own spend.
const assistantUsageById = new Map<string, Usage>();
const attributedChildUsages: Usage[] = [];
const summarizationUsages: Usage[] = [];

for await (const lineBuffer of readLinesAsBuffers(filePath)) {
const line = lineBuffer.toString("utf8");
Expand Down Expand Up @@ -999,7 +1013,17 @@ async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeo
if (entry.type === "agent_status") {
agentStatus = (entry as AgentStatusEntry).status;
}

if (entry.type === "child_usage_attributed") {
const attribution = entry as ChildUsageAttributionEntry;
if (assistantUsageById.has(attribution.targetId)) {
assistantUsageById.set(attribution.targetId, attribution.aggregateUsage);
attributedChildUsages.push(attribution.childUsage);
}
}
if (entry.type === "compaction" || entry.type === "branch_summary") {
const summarizationUsage = (entry as CompactionEntry | BranchSummaryEntry).usage;
if (summarizationUsage) summarizationUsages.push(summarizationUsage);
}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
if (!header) {
if (entry.type !== "session") {
return null;
Expand All @@ -1013,6 +1037,9 @@ async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeo
messageCount++;

const message = (entry as SessionMessageEntry).message;
if (message.role === "assistant" && (message as { usage?: Usage }).usage) {
assistantUsageById.set(entry.id, (message as { usage: Usage }).usage);
}
if (!isMessageWithContent(message)) continue;
if (message.role !== "user" && message.role !== "assistant") continue;

Expand All @@ -1026,6 +1053,16 @@ async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeo
}

if (!header) return null;
const usageTotal = emptyUsage();
for (const usage of assistantUsageById.values()) {
addAssistantUsage(usageTotal, usage);
}
for (const usage of summarizationUsages) {
addAssistantUsage(usageTotal, usage);
}
for (const childUsage of attributedChildUsages) {
subtractAssistantUsage(usageTotal, childUsage);
}
const cwd = typeof header.cwd === "string" ? header.cwd : "";
const parentSessionPath = header.parentSession;
const rlmDepth = resolveSessionRlmDepth(header, filePath);
Expand All @@ -1045,6 +1082,7 @@ async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeo
firstMessage: firstMessage || "(no messages)",
allMessagesText,
agentStatus,
usage: sessionUsageSummaryFrom(usageTotal),
};
} catch {
return null;
Expand Down Expand Up @@ -1447,6 +1485,7 @@ export class SessionManager {
details?: T,
fromHook?: boolean,
customInstructions?: string,
usage?: Usage,
): string {
const entry: CompactionEntry<T> = {
type: "compaction",
Expand All @@ -1459,6 +1498,7 @@ export class SessionManager {
details,
fromHook,
customInstructions,
usage,
};
this._appendEntry(entry);
return entry.id;
Expand Down Expand Up @@ -1836,7 +1876,13 @@ export class SessionManager {
this.leafId = null;
}

branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromHook?: boolean): string {
branchWithSummary(
branchFromId: string | null,
summary: string,
details?: unknown,
fromHook?: boolean,
usage?: Usage,
): string {
if (branchFromId !== null && !this.byId.has(branchFromId)) {
throw new Error(`Entry ${branchFromId} not found`);
}
Expand All @@ -1850,6 +1896,7 @@ export class SessionManager {
summary,
details,
fromHook,
usage,
};
this._appendEntry(entry);
return entry.id;
Expand Down
Loading
Loading