portable-agent-layer 0.95.0 → 0.95.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.95.0",
3
+ "version": "0.95.1",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { resolve } from "node:path";
12
12
  import { costOfUsage } from "../../hooks/lib/models";
13
- import { cacheWritesOf, type TranscriptUsage } from "./transcript-usage";
13
+ import { cacheWritesOf, finalPerMessage, type TranscriptUsage } from "./transcript-usage";
14
14
 
15
15
  export interface SessionUsage {
16
16
  input: number;
@@ -94,7 +94,7 @@ interface TranscriptLine {
94
94
  type?: string;
95
95
  timestamp?: string;
96
96
  sessionId?: string;
97
- message?: { model?: string; usage?: TranscriptUsage };
97
+ message?: { id?: string; model?: string; usage?: TranscriptUsage };
98
98
  }
99
99
 
100
100
  function emptyUsage(): SessionUsage {
@@ -113,6 +113,7 @@ function emptyUsage(): SessionUsage {
113
113
 
114
114
  export function accumulateUsage(transcript: string, sessionId: string): SessionUsage {
115
115
  const total = emptyUsage();
116
+ const assistantTurns: TranscriptLine[] = [];
116
117
  let firstTs = "";
117
118
  let lastTs = "";
118
119
 
@@ -133,7 +134,10 @@ export function accumulateUsage(transcript: string, sessionId: string): SessionU
133
134
  lastTs = entry.timestamp;
134
135
  }
135
136
 
136
- if (entry.type !== "assistant") continue;
137
+ if (entry.type === "assistant") assistantTurns.push(entry);
138
+ }
139
+
140
+ for (const entry of finalPerMessage(assistantTurns, (e) => e.message?.id)) {
137
141
  const usage = entry.message?.usage;
138
142
  const model = entry.message?.model;
139
143
  if (!usage || !model) continue;
@@ -18,6 +18,20 @@ export interface TranscriptUsage {
18
18
  };
19
19
  }
20
20
 
21
+ /**
22
+ * Claude Code writes one transcript line per content block of a message, each
23
+ * repeating the message's usage — and an early line can carry the usage while
24
+ * the message was still streaming. A message is billed once, at its last line.
25
+ */
26
+ export function finalPerMessage<T>(
27
+ calls: Iterable<T>,
28
+ messageIdOf: (call: T) => string | undefined
29
+ ): T[] {
30
+ const byMessage = new Map<unknown, T>();
31
+ for (const call of calls) byMessage.set(messageIdOf(call) ?? call, call);
32
+ return [...byMessage.values()];
33
+ }
34
+
21
35
  export interface CacheWrites {
22
36
  cacheWrite5m: number;
23
37
  cacheWrite1h: number;
@@ -7,7 +7,7 @@
7
7
  import { existsSync, readdirSync, readFileSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
9
  import { costOfUsage } from "../../hooks/lib/models";
10
- import { cacheWritesOf, type TranscriptUsage } from "./transcript-usage";
10
+ import { cacheWritesOf, finalPerMessage, type TranscriptUsage } from "./transcript-usage";
11
11
 
12
12
  export interface Bucket {
13
13
  input: number;
@@ -180,7 +180,7 @@ export function usageTally(projectFilter?: string, now: Date = new Date()): Usag
180
180
  interface TranscriptLine {
181
181
  type?: string;
182
182
  timestamp?: string;
183
- message?: { model?: string; usage?: TranscriptUsage };
183
+ message?: { id?: string; model?: string; usage?: TranscriptUsage };
184
184
  }
185
185
 
186
186
  /**
@@ -209,6 +209,7 @@ function transcriptsIn(projPath: string): string[] {
209
209
  }
210
210
 
211
211
  interface PricedCall {
212
+ messageId?: string;
212
213
  ts: string;
213
214
  model: string;
214
215
  tokens: Tokens;
@@ -230,6 +231,7 @@ function pricedCallOf(line: string): PricedCall | null {
230
231
  if (!usage || !model || !ts) return null;
231
232
 
232
233
  return {
234
+ messageId: entry.message?.id,
233
235
  ts,
234
236
  model,
235
237
  tokens: {
@@ -248,6 +250,7 @@ export function readClaudeCode(
248
250
  ): AgentUsage {
249
251
  const tally = usageTally(projectFilter, now);
250
252
  if (!existsSync(claudeDir)) return tally.usage;
253
+ const calls: (PricedCall & { project: string })[] = [];
251
254
 
252
255
  const projectDirs = readdirSync(claudeDir, { withFileTypes: true })
253
256
  .filter((entry) => entry.isDirectory())
@@ -269,16 +272,20 @@ export function readClaudeCode(
269
272
  // Cheap reject before the parse: most lines are not model turns.
270
273
  if (!line.includes('"usage"')) continue;
271
274
  const call = pricedCallOf(line);
272
- if (!call) continue;
273
- tally.record({
274
- ...call,
275
- project: projName,
276
- cost: costOfUsage(call.model, call.tokens),
277
- });
275
+ if (call) calls.push({ ...call, project: projName });
278
276
  }
279
277
  }
280
278
  }
281
279
 
280
+ for (const call of finalPerMessage(calls, (c) => c.messageId)) {
281
+ tally.record({
282
+ ts: call.ts,
283
+ model: call.model,
284
+ tokens: call.tokens,
285
+ project: call.project,
286
+ cost: costOfUsage(call.model, call.tokens),
287
+ });
288
+ }
282
289
  return tally.usage;
283
290
  }
284
291