Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
40539a6
fix(daemon): full ACP tool transcript and richer usage telemetry
mrcfps Jul 24, 2026
980bfd4
fix(daemon): prefer ACP kind over noncanonical tool names
mrcfps Jul 24, 2026
eb4911d
fix(daemon): fail-closed redaction for unknown ACP tool payloads
mrcfps Jul 24, 2026
859f1bd
fix(daemon): flush open ACP tools as errors on session fail
mrcfps Jul 24, 2026
d592a37
fix(daemon): redact ACP Bash stdout in tool transcripts
mrcfps Jul 24, 2026
8437d4b
fix(daemon): redact Linux home paths in Langfuse Bash tool spans
mrcfps Jul 24, 2026
367a193
fix(daemon): redact custom ACP tool names before Langfuse telemetry
mrcfps Jul 24, 2026
3f5416b
test(e2e): force-click unstable comment targets in preview iframe
mrcfps Jul 24, 2026
5eba609
fix(daemon): merge partial usage frames in reverse analytics scan
mrcfps Jul 24, 2026
67dfa84
fix(daemon): flush open ACP tools before AMR no-output fail
mrcfps Jul 24, 2026
527eb10
fix(daemon): merge partial primary usage and flush tools on ACP abort
mrcfps Jul 24, 2026
49272eb
fix(daemon): hash path-like ACP toolCallIds before telemetry
mrcfps Jul 24, 2026
1180633
fix(daemon): always hash ACP adapter toolCallIds for telemetry
mrcfps Jul 24, 2026
137e1c2
fix(daemon): keep reverse usage scan open for missing cache fields
mrcfps Jul 24, 2026
7c35252
fix(daemon): retain flushed ACP tool IDs against late terminals
mrcfps Jul 24, 2026
953fd8d
fix(daemon): block kind:other Bash-like tool names
mrcfps Jul 24, 2026
d0958e5
fix(daemon): fail closed on Bash names without execute kind
mrcfps Jul 24, 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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,10 @@ deploy/agent-bins/

# Looper harness attachments (bug screenshots etc.) — agent scratch, not repo content
.looper-attachments/

# Deepwork local progress (git-local; OpenCode-readable via .ignore)
.slim/deepwork/

# Local agent worktrees (must stay git-ignored so pnpm guard does not scan them)
.slim/worktrees/
.slim/worktrees.json
3 changes: 3 additions & 0 deletions .ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Keep deepwork progress readable by OpenCode while git-ignored
!.slim/deepwork/
!.slim/deepwork/**
75 changes: 65 additions & 10 deletions apps/daemon/src/agent-protocol/acp/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,40 @@ export function promotedOpenCodeSessionErrorPayload(data: unknown, fallbackMessa
export interface FormattedUsage {
input_tokens?: number;
output_tokens?: number;
/** OpenAI-like inclusive cache-read subset (input already includes cache). */
cached_read_tokens?: number;
/** Anthropic-like additive cache-read (input is uncached remainder). */
cache_read_input_tokens?: number;
/** Generic / OpenAI-like cache write alias. */
cache_creation_tokens?: number;
/** Anthropic-like cache creation field name. */
cache_creation_input_tokens?: number;
/** OpenAI-like cache write alias used by some ACP adapters. */
cached_write_tokens?: number;
thought_tokens?: number;
total_tokens?: number;
}

function firstFiniteNumber(src: Record<string, unknown>, keys: string[]): number | undefined {
for (const key of keys) {
const value = src[key];
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
return value;
}
}
return undefined;
}

/**
* Normalises an ACP agent's raw `usage` object (camelCase keys) into the
* snake_case `FormattedUsage` shape used by the daemon event stream. Returns
* `null` when the input is not a recognisable usage object or has no known
* fields.
* Normalises an ACP agent's raw `usage` object (camelCase and/or snake_case
* keys) into the snake_case `FormattedUsage` shape used by the daemon event
* stream. Returns `null` when the input is not a recognisable usage object or
* has no known fields.
*
* Cache field names are preserved by family so
* `scanRunEventsForUsageAnalytics` can classify anthropic (additive) vs
* openai (inclusive) correctly. Do not collapse Anthropic
* `cache_read_input_tokens` into `cached_read_tokens`.
*
* @param usage - The raw `result.usage` value from a `session/prompt` response.
* @returns A `FormattedUsage` object with at least one field, or `null`.
Expand All @@ -145,13 +170,43 @@ export function formatUsage(usage: unknown): FormattedUsage | null {
const src = asObject(usage);
if (!src) return null;
const out: FormattedUsage = {};
if (typeof src.inputTokens === 'number') out.input_tokens = src.inputTokens;
if (typeof src.outputTokens === 'number') out.output_tokens = src.outputTokens;
if (typeof src.cachedReadTokens === 'number') {
out.cached_read_tokens = src.cachedReadTokens;
const inputTokens = firstFiniteNumber(src, ['inputTokens', 'input_tokens']);
const outputTokens = firstFiniteNumber(src, ['outputTokens', 'output_tokens']);
// Prefer source-family keys independently so mixed payloads keep both
// semantics when present; otherwise map only within the matching family.
const anthropicCacheRead = firstFiniteNumber(src, [
'cache_read_input_tokens',
'cacheReadInputTokens',
]);
const openAiCacheRead = firstFiniteNumber(src, [
'cachedReadTokens',
'cached_read_tokens',
]);
const anthropicCacheCreation = firstFiniteNumber(src, [
'cache_creation_input_tokens',
'cacheCreationInputTokens',
]);
const openAiCacheCreation = firstFiniteNumber(src, [
'cacheCreationTokens',
'cache_creation_tokens',
]);
const openAiCachedWrite = firstFiniteNumber(src, [
'cached_write_tokens',
'cachedWriteTokens',
]);
const thoughtTokens = firstFiniteNumber(src, ['thoughtTokens', 'thought_tokens']);
const totalTokens = firstFiniteNumber(src, ['totalTokens', 'total_tokens']);
if (inputTokens !== undefined) out.input_tokens = inputTokens;
if (outputTokens !== undefined) out.output_tokens = outputTokens;
if (anthropicCacheRead !== undefined) out.cache_read_input_tokens = anthropicCacheRead;
if (openAiCacheRead !== undefined) out.cached_read_tokens = openAiCacheRead;
if (anthropicCacheCreation !== undefined) {
out.cache_creation_input_tokens = anthropicCacheCreation;
}
if (typeof src.thoughtTokens === 'number') out.thought_tokens = src.thoughtTokens;
if (typeof src.totalTokens === 'number') out.total_tokens = src.totalTokens;
if (openAiCacheCreation !== undefined) out.cache_creation_tokens = openAiCacheCreation;
if (openAiCachedWrite !== undefined) out.cached_write_tokens = openAiCachedWrite;
if (thoughtTokens !== undefined) out.thought_tokens = thoughtTokens;
if (totalTokens !== undefined) out.total_tokens = totalTokens;
return Object.keys(out).length > 0 ? out : null;
}
/**
Expand Down
Loading
Loading