savemytokens 0.2.0
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/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/adapters/claude-code/index.js +130 -0
- package/dist/adapters/claude-code/merge.js +90 -0
- package/dist/adapters/claude-code/parse.js +642 -0
- package/dist/adapters/claude-code/provider.js +105 -0
- package/dist/adapters/codex/index.js +74 -0
- package/dist/adapters/codex/parse.js +389 -0
- package/dist/adapters/codex/provider.js +171 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/pending.js +29 -0
- package/dist/adapters/types.js +1 -0
- package/dist/analyze/aggregate.js +180 -0
- package/dist/analyze/combine.js +11 -0
- package/dist/analyze/detectors.js +244 -0
- package/dist/analyze/index.js +29 -0
- package/dist/analyze/score.js +20 -0
- package/dist/cli-options.js +149 -0
- package/dist/cli.js +141 -0
- package/dist/collect.js +62 -0
- package/dist/commands/audit.js +74 -0
- package/dist/commands/control.js +654 -0
- package/dist/commands/hud.js +71 -0
- package/dist/commands/install.js +369 -0
- package/dist/commands/policy.js +93 -0
- package/dist/commands/privacy.js +28 -0
- package/dist/commands/set.js +83 -0
- package/dist/commands/theme.js +136 -0
- package/dist/commands/watch.js +135 -0
- package/dist/core/cost.js +24 -0
- package/dist/core/hash.js +0 -0
- package/dist/core/pricing.js +63 -0
- package/dist/core/resource.js +1 -0
- package/dist/core/tokens.js +32 -0
- package/dist/core/types.js +1 -0
- package/dist/hooks/nudge.js +111 -0
- package/dist/hooks/rules.js +14 -0
- package/dist/privacy/payload.js +22 -0
- package/dist/report/graph.js +162 -0
- package/dist/report/graphs.js +61 -0
- package/dist/report/render.js +183 -0
- package/dist/report/schedule.js +143 -0
- package/dist/report/settings.js +237 -0
- package/dist/report/views.js +418 -0
- package/dist/runtime/hook.mjs +234 -0
- package/dist/runtime/kernel.mjs +1472 -0
- package/dist/runtime/statusline.mjs +243 -0
- package/dist/scheduler/keys.js +112 -0
- package/dist/scheduler/plan.js +287 -0
- package/dist/storage/cache.js +38 -0
- package/dist/storage/paths.js +29 -0
- package/dist/storage/store.js +48 -0
- package/dist/util/ansi.js +35 -0
- package/dist/util/fmt.js +76 -0
- package/package.json +51 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FIVE_HOUR_MS, WINDOW_MS, loadClaimants, loadMeter, loadQuota, liveWindow, sampleFiles, upsertClaimant, } from "../../runtime/kernel.mjs";
|
|
3
|
+
import { claudeCodeAdapter } from "./index.js";
|
|
4
|
+
export const ADAPTER_ID = "claude-code";
|
|
5
|
+
const WINDOW_LABELS = {
|
|
6
|
+
five_hour: "5-hour window",
|
|
7
|
+
seven_day: "7-day window",
|
|
8
|
+
spend_limit: "gateway spend limit",
|
|
9
|
+
};
|
|
10
|
+
export function resourcesFor(now = Date.now()) {
|
|
11
|
+
const quota = loadQuota(ADAPTER_ID);
|
|
12
|
+
const keys = ["five_hour", "seven_day"];
|
|
13
|
+
if (quota?.windows?.spend_limit)
|
|
14
|
+
keys.push("spend_limit");
|
|
15
|
+
return keys.map((key) => {
|
|
16
|
+
const window = liveWindow(quota, key, now);
|
|
17
|
+
return {
|
|
18
|
+
id: `${ADAPTER_ID}:${key}`,
|
|
19
|
+
adapter: ADAPTER_ID,
|
|
20
|
+
label: WINDOW_LABELS[key],
|
|
21
|
+
unit: key === "spend_limit" ? "usd" : "observed_usage",
|
|
22
|
+
window: { kind: "rolling", ms: WINDOW_MS[key] ?? FIVE_HOUR_MS, ...(window ? { resetsAt: window.resetsAt } : {}) },
|
|
23
|
+
capacity: window
|
|
24
|
+
? { amount: 100, confidence: "published", asOf: quota?.at ?? now }
|
|
25
|
+
: { amount: 0, confidence: "unknown" },
|
|
26
|
+
usedPercent: window ? window.usedPercent : null,
|
|
27
|
+
rolledOver: !window && typeof quota?.windows?.[key]?.usedPercent === "number",
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function sweep(since, now = Date.now()) {
|
|
32
|
+
const known = new Map(loadClaimants(ADAPTER_ID).map((claimant) => [claimant.id, claimant]));
|
|
33
|
+
const refs = claudeCodeAdapter.discover({ since, project: null });
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const ref of refs) {
|
|
36
|
+
const id = path.basename(ref.file, ".jsonl");
|
|
37
|
+
seen.add(id);
|
|
38
|
+
const record = sampleFiles(ADAPTER_ID, id, [ref.file, ...(ref.extraFiles ?? [])], now);
|
|
39
|
+
const project = record.project || "";
|
|
40
|
+
const existing = known.get(id);
|
|
41
|
+
const lastSeen = record.lastAt > 0 ? record.lastAt : Math.min(existing?.lastSeen ?? ref.mtimeMs, ref.mtimeMs);
|
|
42
|
+
upsertClaimant(ADAPTER_ID, id, {
|
|
43
|
+
project: existing?.project || project,
|
|
44
|
+
label: existing?.label || (project ? path.basename(project) : id.slice(0, 8)),
|
|
45
|
+
prompt: record.prompt || existing?.prompt || "",
|
|
46
|
+
...(lastSeen > 0 ? { lastSeen } : {}),
|
|
47
|
+
...(existing ? {} : { startedAt: record.buckets[0]?.[0] ?? now, state: "active" }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
for (const claimant of known.values()) {
|
|
51
|
+
if (seen.has(claimant.id))
|
|
52
|
+
continue;
|
|
53
|
+
const files = Object.keys(loadMeter(ADAPTER_ID, claimant.id).files);
|
|
54
|
+
if (files.length === 0)
|
|
55
|
+
continue;
|
|
56
|
+
const record = sampleFiles(ADAPTER_ID, claimant.id, files, now);
|
|
57
|
+
if (record.lastAt > 0 && record.lastAt !== claimant.lastSeen) {
|
|
58
|
+
upsertClaimant(ADAPTER_ID, claimant.id, { lastSeen: record.lastAt, prompt: record.prompt || claimant.prompt });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const claudeCodeMeter = {
|
|
63
|
+
async sample(since, until = Date.now()) {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const claimant of loadClaimants(ADAPTER_ID)) {
|
|
66
|
+
const record = loadMeter(ADAPTER_ID, claimant.id);
|
|
67
|
+
for (const row of record.buckets) {
|
|
68
|
+
const at = row[0] ?? 0;
|
|
69
|
+
if (at < since || at > until)
|
|
70
|
+
continue;
|
|
71
|
+
const input = row[1] ?? 0;
|
|
72
|
+
const output = row[2] ?? 0;
|
|
73
|
+
const cacheWrite = row[3] ?? 0;
|
|
74
|
+
const cacheRead = row[4] ?? 0;
|
|
75
|
+
const weighted = input + output * 5 + cacheWrite * 1.25 + cacheRead * 0.1;
|
|
76
|
+
out.push({
|
|
77
|
+
claimantId: claimant.id,
|
|
78
|
+
amount: weighted,
|
|
79
|
+
at,
|
|
80
|
+
metrics: { tokens: input + output + cacheWrite + cacheRead, weighted, requests: row[5] ?? 0 },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out.sort((a, b) => a.at - b.at);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
export const claudeCodeEnforcer = {
|
|
88
|
+
supports: ["advise"],
|
|
89
|
+
async apply(_claimant, level, reason) {
|
|
90
|
+
if (level !== "advise") {
|
|
91
|
+
return { applied: false, message: `Claude Code supports advice only; ${level} is not available in V0.` };
|
|
92
|
+
}
|
|
93
|
+
return { applied: true, message: reason };
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
export const claudeCodeProvider = {
|
|
97
|
+
id: ADAPTER_ID,
|
|
98
|
+
label: "Claude Code",
|
|
99
|
+
detect: () => claudeCodeAdapter.detect(),
|
|
100
|
+
resources: (now) => resourcesFor(now),
|
|
101
|
+
sweep,
|
|
102
|
+
meter: claudeCodeMeter,
|
|
103
|
+
dataDir: claudeCodeAdapter.dataDir,
|
|
104
|
+
enforcer: claudeCodeEnforcer,
|
|
105
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseCodexSession } from "./parse.js";
|
|
5
|
+
const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
6
|
+
const DATA_DIR = path.join(CODEX_HOME, "sessions");
|
|
7
|
+
const MAX_DEPTH = 5;
|
|
8
|
+
function walk(dir, depth, since, out) {
|
|
9
|
+
if (depth > MAX_DEPTH)
|
|
10
|
+
return;
|
|
11
|
+
let entries;
|
|
12
|
+
try {
|
|
13
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const full = path.join(dir, entry.name);
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
walk(full, depth + 1, since, out);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl"))
|
|
25
|
+
continue;
|
|
26
|
+
let stat;
|
|
27
|
+
try {
|
|
28
|
+
stat = fs.statSync(full);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (stat.size === 0 || stat.mtimeMs < since)
|
|
34
|
+
continue;
|
|
35
|
+
out.push({ adapter: "codex", file: full, size: stat.size, mtimeMs: stat.mtimeMs, projectKey: "" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export const codexAdapter = {
|
|
39
|
+
id: "codex",
|
|
40
|
+
label: "Codex",
|
|
41
|
+
supported: true,
|
|
42
|
+
dataDir: DATA_DIR,
|
|
43
|
+
detect() {
|
|
44
|
+
try {
|
|
45
|
+
return fs.statSync(DATA_DIR).isDirectory();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
discover(options) {
|
|
52
|
+
const refs = [];
|
|
53
|
+
walk(DATA_DIR, 0, options.since, refs);
|
|
54
|
+
if (!options.project)
|
|
55
|
+
return refs;
|
|
56
|
+
return refs.filter((ref) => {
|
|
57
|
+
try {
|
|
58
|
+
const head = fs.readFileSync(ref.file, "utf8").slice(0, 4_000);
|
|
59
|
+
return head.includes(`"cwd":"${options.project}"`);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
async parse(ref) {
|
|
67
|
+
try {
|
|
68
|
+
return await parseCodexSession(ref.file, fs.statSync(ref.file));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { LifetimeCost } from "../../core/cost.js";
|
|
6
|
+
import { usd } from "../../core/pricing.js";
|
|
7
|
+
import { addUsage, emptyUsage, estimateTokens, weigh } from "../../core/tokens.js";
|
|
8
|
+
import { EVIDENCE_SCHEMA, } from "../../core/types.js";
|
|
9
|
+
import { DEAD_CARRY_MIN_TURNS, DEAD_CARRY_TOKENS, HIGH_CONTEXT_TOKENS, LARGE_OUTPUT_CHARS, USEFUL_OUTPUT_CHARS, commandLabel, isSelfContained, } from "../claude-code/parse.js";
|
|
10
|
+
const MAX_BUCKETS = 24;
|
|
11
|
+
const MECHANICAL_TOOLS = new Set(["exec_command", "shell", "read_file", "web_search"]);
|
|
12
|
+
function bucket(map, key, tool) {
|
|
13
|
+
let b = map.get(key);
|
|
14
|
+
if (!b) {
|
|
15
|
+
b = { key, tool, count: 0, chars: 0, maxChars: 0, edits: 0, wastedChars: 0, wastedCount: 0, cost: new LifetimeCost() };
|
|
16
|
+
map.set(key, b);
|
|
17
|
+
}
|
|
18
|
+
return b;
|
|
19
|
+
}
|
|
20
|
+
function topBuckets(map, ends) {
|
|
21
|
+
return [...map.values()]
|
|
22
|
+
.map((b) => ({ ...b, weighted: b.cost.resolve(ends) }))
|
|
23
|
+
.sort((a, b) => b.weighted - a.weighted || b.chars - a.chars)
|
|
24
|
+
.slice(0, MAX_BUCKETS);
|
|
25
|
+
}
|
|
26
|
+
function displayPath(filePath, cwd) {
|
|
27
|
+
if (cwd && filePath.startsWith(cwd + path.sep))
|
|
28
|
+
return filePath.slice(cwd.length + 1);
|
|
29
|
+
const home = os.homedir();
|
|
30
|
+
if (home && filePath.startsWith(home + path.sep))
|
|
31
|
+
return "~/" + filePath.slice(home.length + 1);
|
|
32
|
+
return filePath;
|
|
33
|
+
}
|
|
34
|
+
export function patchedFiles(patch) {
|
|
35
|
+
const files = [];
|
|
36
|
+
for (const line of patch.split("\n")) {
|
|
37
|
+
const match = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/.exec(line.trim());
|
|
38
|
+
if (match?.[1])
|
|
39
|
+
files.push(match[1].trim());
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
function commandOf(args) {
|
|
44
|
+
if (typeof args !== "string")
|
|
45
|
+
return "";
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(args);
|
|
48
|
+
if (typeof parsed?.cmd === "string")
|
|
49
|
+
return parsed.cmd;
|
|
50
|
+
if (Array.isArray(parsed?.command))
|
|
51
|
+
return parsed.command.join(" ");
|
|
52
|
+
if (typeof parsed?.command === "string")
|
|
53
|
+
return parsed.command;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
export async function parseCodexSession(file, stat) {
|
|
61
|
+
const rl = readline.createInterface({ input: fs.createReadStream(file, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
62
|
+
const usage = emptyUsage();
|
|
63
|
+
const models = new Map();
|
|
64
|
+
const outputs = new Map();
|
|
65
|
+
const writes = new Map();
|
|
66
|
+
const failures = new Map();
|
|
67
|
+
const pending = new Map();
|
|
68
|
+
const tasks = [];
|
|
69
|
+
const contexts = [];
|
|
70
|
+
let sessionId = path.basename(file, ".jsonl");
|
|
71
|
+
let cwd = "";
|
|
72
|
+
let agentVersion = "";
|
|
73
|
+
let model = "gpt-5";
|
|
74
|
+
let startedAt = 0;
|
|
75
|
+
let endedAt = 0;
|
|
76
|
+
let turn = 0;
|
|
77
|
+
let humanPrompts = 0;
|
|
78
|
+
let toolCalls = 0;
|
|
79
|
+
let toolErrors = 0;
|
|
80
|
+
let interruptions = 0;
|
|
81
|
+
let peakContext = 0;
|
|
82
|
+
let bloatTurns = 0;
|
|
83
|
+
let bloatTokens = 0;
|
|
84
|
+
let bloatWeighted = 0;
|
|
85
|
+
let quotaPeakPercent = 0;
|
|
86
|
+
let lastUsageSignature = "";
|
|
87
|
+
let current = null;
|
|
88
|
+
let pendingMechanical = false;
|
|
89
|
+
const openTask = (ts, prompt) => {
|
|
90
|
+
const task = {
|
|
91
|
+
id: `${sessionId}-${tasks.length}`,
|
|
92
|
+
sessionId,
|
|
93
|
+
project: cwd,
|
|
94
|
+
prompt: prompt.replace(/\s+/g, " ").trim().slice(0, 120),
|
|
95
|
+
startedAt: ts,
|
|
96
|
+
endedAt: ts,
|
|
97
|
+
promptChars: prompt.length,
|
|
98
|
+
turns: 0,
|
|
99
|
+
toolCalls: 0,
|
|
100
|
+
models: [],
|
|
101
|
+
modelSet: new Set(),
|
|
102
|
+
fileSet: new Set(),
|
|
103
|
+
usage: emptyUsage(),
|
|
104
|
+
weighted: 0,
|
|
105
|
+
usd: 0,
|
|
106
|
+
peakContext: 0,
|
|
107
|
+
carriedContext: 0,
|
|
108
|
+
carriedUsd: 0,
|
|
109
|
+
carriedIsDead: false,
|
|
110
|
+
touchedPriorFiles: false,
|
|
111
|
+
selfContained: isSelfContained(prompt),
|
|
112
|
+
outcome: "completed",
|
|
113
|
+
toolErrors: 0,
|
|
114
|
+
};
|
|
115
|
+
tasks.push(task);
|
|
116
|
+
return task;
|
|
117
|
+
};
|
|
118
|
+
for await (const line of rl) {
|
|
119
|
+
if (!line || line.charCodeAt(0) !== 123)
|
|
120
|
+
continue;
|
|
121
|
+
let record;
|
|
122
|
+
try {
|
|
123
|
+
record = JSON.parse(line);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const ts = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : NaN;
|
|
129
|
+
if (Number.isFinite(ts)) {
|
|
130
|
+
if (!startedAt || ts < startedAt)
|
|
131
|
+
startedAt = ts;
|
|
132
|
+
if (ts > endedAt)
|
|
133
|
+
endedAt = ts;
|
|
134
|
+
if (current)
|
|
135
|
+
current.endedAt = ts;
|
|
136
|
+
}
|
|
137
|
+
const payload = record.payload ?? {};
|
|
138
|
+
if (record.type === "session_meta") {
|
|
139
|
+
if (typeof payload.id === "string")
|
|
140
|
+
sessionId = payload.id;
|
|
141
|
+
if (typeof payload.cwd === "string")
|
|
142
|
+
cwd = payload.cwd;
|
|
143
|
+
if (typeof payload.cli_version === "string")
|
|
144
|
+
agentVersion = payload.cli_version;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (record.type === "turn_context") {
|
|
148
|
+
if (typeof payload.cwd === "string" && !cwd)
|
|
149
|
+
cwd = payload.cwd;
|
|
150
|
+
if (typeof payload.model === "string")
|
|
151
|
+
model = payload.model;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (record.type !== "event_msg" && record.type !== "response_item")
|
|
155
|
+
continue;
|
|
156
|
+
switch (payload.type) {
|
|
157
|
+
case "user_message": {
|
|
158
|
+
const text = typeof payload.message === "string" ? payload.message : "";
|
|
159
|
+
if (!text || text.startsWith("<"))
|
|
160
|
+
break;
|
|
161
|
+
const trimmed = text.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
162
|
+
if (current && current.turns === 0 && current.prompt === trimmed)
|
|
163
|
+
break;
|
|
164
|
+
humanPrompts++;
|
|
165
|
+
current = openTask(Number.isFinite(ts) ? ts : 0, text);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case "token_count": {
|
|
169
|
+
const info = payload.info;
|
|
170
|
+
const quota = payload.rate_limits?.primary?.used_percent;
|
|
171
|
+
if (typeof quota === "number" && quota > quotaPeakPercent)
|
|
172
|
+
quotaPeakPercent = quota;
|
|
173
|
+
if (!info?.last_token_usage)
|
|
174
|
+
break;
|
|
175
|
+
const last = info.last_token_usage;
|
|
176
|
+
const signature = JSON.stringify(last);
|
|
177
|
+
if (signature === lastUsageSignature)
|
|
178
|
+
break;
|
|
179
|
+
lastUsageSignature = signature;
|
|
180
|
+
const cached = last.cached_input_tokens ?? 0;
|
|
181
|
+
const turnUsage = {
|
|
182
|
+
input: Math.max(0, (last.input_tokens ?? 0) - cached),
|
|
183
|
+
output: last.output_tokens ?? 0,
|
|
184
|
+
cacheWrite: 0,
|
|
185
|
+
cacheRead: cached,
|
|
186
|
+
};
|
|
187
|
+
const context = last.input_tokens ?? 0;
|
|
188
|
+
if (context <= 0 && turnUsage.output <= 0)
|
|
189
|
+
break;
|
|
190
|
+
turn++;
|
|
191
|
+
contexts.push(context);
|
|
192
|
+
if (context > peakContext)
|
|
193
|
+
peakContext = context;
|
|
194
|
+
if (context > HIGH_CONTEXT_TOKENS) {
|
|
195
|
+
bloatTurns++;
|
|
196
|
+
const excess = context - HIGH_CONTEXT_TOKENS;
|
|
197
|
+
bloatTokens += excess;
|
|
198
|
+
bloatWeighted += excess * 0.1;
|
|
199
|
+
}
|
|
200
|
+
addUsage(usage, turnUsage);
|
|
201
|
+
let entry = models.get(model);
|
|
202
|
+
if (!entry) {
|
|
203
|
+
entry = { model, turns: 0, usage: emptyUsage(), weighted: 0, trivialTurns: 0, trivialWeighted: 0 };
|
|
204
|
+
models.set(model, entry);
|
|
205
|
+
}
|
|
206
|
+
const turnWeighted = weigh(turnUsage);
|
|
207
|
+
entry.turns++;
|
|
208
|
+
addUsage(entry.usage, turnUsage);
|
|
209
|
+
entry.weighted += turnWeighted;
|
|
210
|
+
if (pendingMechanical && turnUsage.output < 600) {
|
|
211
|
+
entry.trivialTurns++;
|
|
212
|
+
entry.trivialWeighted += turnWeighted;
|
|
213
|
+
}
|
|
214
|
+
pendingMechanical = false;
|
|
215
|
+
if (current) {
|
|
216
|
+
if (current.turns === 0)
|
|
217
|
+
current.carriedContext = context;
|
|
218
|
+
current.turns++;
|
|
219
|
+
addUsage(current.usage, turnUsage);
|
|
220
|
+
current.weighted += turnWeighted;
|
|
221
|
+
current.usd += usd(model, turnUsage);
|
|
222
|
+
current.modelSet.add(model);
|
|
223
|
+
if (context > current.peakContext)
|
|
224
|
+
current.peakContext = context;
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case "function_call": {
|
|
229
|
+
toolCalls++;
|
|
230
|
+
const name = typeof payload.name === "string" ? payload.name : "tool";
|
|
231
|
+
const command = commandOf(payload.arguments);
|
|
232
|
+
if (MECHANICAL_TOOLS.has(name))
|
|
233
|
+
pendingMechanical = true;
|
|
234
|
+
if (typeof payload.call_id === "string")
|
|
235
|
+
pending.set(payload.call_id, { name, command, turn });
|
|
236
|
+
if (current)
|
|
237
|
+
current.toolCalls++;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "custom_tool_call": {
|
|
241
|
+
toolCalls++;
|
|
242
|
+
const patch = typeof payload.input === "string" ? payload.input : "";
|
|
243
|
+
const files = patchedFiles(patch);
|
|
244
|
+
for (const raw of files) {
|
|
245
|
+
const key = displayPath(raw, cwd);
|
|
246
|
+
if (current)
|
|
247
|
+
current.fileSet.add(raw);
|
|
248
|
+
const b = bucket(writes, key, "apply_patch");
|
|
249
|
+
b.count++;
|
|
250
|
+
const chars = Math.round(patch.length / Math.max(1, files.length));
|
|
251
|
+
b.chars += chars;
|
|
252
|
+
if (b.count > 1) {
|
|
253
|
+
b.wastedCount++;
|
|
254
|
+
b.wastedChars += chars;
|
|
255
|
+
b.cost.add(0, turn, estimateTokens(chars));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (current)
|
|
259
|
+
current.toolCalls++;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "function_call_output":
|
|
263
|
+
case "custom_tool_call_output": {
|
|
264
|
+
const meta = typeof payload.call_id === "string" ? pending.get(payload.call_id) : undefined;
|
|
265
|
+
if (typeof payload.call_id === "string")
|
|
266
|
+
pending.delete(payload.call_id);
|
|
267
|
+
const output = typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output ?? "");
|
|
268
|
+
const chars = output.length;
|
|
269
|
+
const failed = /Process exited with code [1-9]/.test(output);
|
|
270
|
+
const label = meta?.command ? commandLabel(meta.command) : (meta?.name ?? "tool");
|
|
271
|
+
const at = meta?.turn ?? turn;
|
|
272
|
+
if (failed) {
|
|
273
|
+
toolErrors++;
|
|
274
|
+
if (current)
|
|
275
|
+
current.toolErrors++;
|
|
276
|
+
const b = bucket(failures, label, meta?.name ?? "tool");
|
|
277
|
+
b.count++;
|
|
278
|
+
b.chars += chars;
|
|
279
|
+
b.wastedChars += chars;
|
|
280
|
+
b.wastedCount++;
|
|
281
|
+
b.cost.add(0, at, estimateTokens(chars));
|
|
282
|
+
}
|
|
283
|
+
if (chars > LARGE_OUTPUT_CHARS) {
|
|
284
|
+
const b = bucket(outputs, label, meta?.name === "apply_patch" ? "apply_patch" : "Bash");
|
|
285
|
+
b.count++;
|
|
286
|
+
b.chars += chars;
|
|
287
|
+
if (chars > b.maxChars)
|
|
288
|
+
b.maxChars = chars;
|
|
289
|
+
const excess = chars - USEFUL_OUTPUT_CHARS;
|
|
290
|
+
b.wastedChars += excess;
|
|
291
|
+
b.wastedCount++;
|
|
292
|
+
b.cost.add(0, at, estimateTokens(excess));
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "turn_aborted": {
|
|
297
|
+
interruptions++;
|
|
298
|
+
if (current && current.outcome === "completed")
|
|
299
|
+
current.outcome = "interrupted";
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
default:
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (turn === 0)
|
|
307
|
+
return null;
|
|
308
|
+
const ends = [turn];
|
|
309
|
+
const seenFiles = new Set();
|
|
310
|
+
const finishedTasks = tasks.map((t) => {
|
|
311
|
+
const { modelSet, fileSet, ...rest } = t;
|
|
312
|
+
const taskModels = [...modelSet];
|
|
313
|
+
const touchedPriorFiles = [...fileSet].some((f) => seenFiles.has(f));
|
|
314
|
+
for (const f of fileSet)
|
|
315
|
+
seenFiles.add(f);
|
|
316
|
+
const carriedUsd = usd(taskModels[0] ?? model, {
|
|
317
|
+
input: 0,
|
|
318
|
+
output: 0,
|
|
319
|
+
cacheWrite: 0,
|
|
320
|
+
cacheRead: rest.carriedContext * rest.turns,
|
|
321
|
+
});
|
|
322
|
+
const carriedIsDead = rest.carriedContext >= DEAD_CARRY_TOKENS &&
|
|
323
|
+
rest.turns >= DEAD_CARRY_MIN_TURNS &&
|
|
324
|
+
rest.selfContained &&
|
|
325
|
+
!touchedPriorFiles;
|
|
326
|
+
return { ...rest, models: taskModels, touchedPriorFiles, carriedUsd, carriedIsDead, project: cwd };
|
|
327
|
+
});
|
|
328
|
+
const sorted = [...contexts].sort((a, b) => a - b);
|
|
329
|
+
const mid = Math.floor(sorted.length / 2);
|
|
330
|
+
return {
|
|
331
|
+
schema: EVIDENCE_SCHEMA,
|
|
332
|
+
adapter: "codex",
|
|
333
|
+
sessionId,
|
|
334
|
+
project: cwd || "unknown",
|
|
335
|
+
sourceFile: file,
|
|
336
|
+
sourceSize: stat.size,
|
|
337
|
+
sourceMtimeMs: stat.mtimeMs,
|
|
338
|
+
agentVersion,
|
|
339
|
+
startedAt,
|
|
340
|
+
endedAt,
|
|
341
|
+
turns: turn,
|
|
342
|
+
humanPrompts,
|
|
343
|
+
usage,
|
|
344
|
+
weighted: weigh(usage),
|
|
345
|
+
peakContext,
|
|
346
|
+
contextP50: sorted.length ? (sorted[mid] ?? 0) : 0,
|
|
347
|
+
compactions: 0,
|
|
348
|
+
coldStart: turn <= 3 && usage.cacheRead === 0,
|
|
349
|
+
bloatTurns,
|
|
350
|
+
bloatTokens,
|
|
351
|
+
bloatWeighted,
|
|
352
|
+
apiErrors: 0,
|
|
353
|
+
rateLimitHits: quotaPeakPercent >= 100 ? 1 : 0,
|
|
354
|
+
interruptions,
|
|
355
|
+
toolCalls,
|
|
356
|
+
toolErrors,
|
|
357
|
+
sidechainTurns: 0,
|
|
358
|
+
sidechainWeighted: 0,
|
|
359
|
+
searchChars: 0,
|
|
360
|
+
models: [...models.values()],
|
|
361
|
+
tasks: finishedTasks,
|
|
362
|
+
reads: [],
|
|
363
|
+
outputs: topBuckets(outputs, ends).map((b) => ({
|
|
364
|
+
label: b.key,
|
|
365
|
+
tool: b.tool,
|
|
366
|
+
calls: b.count,
|
|
367
|
+
chars: b.chars,
|
|
368
|
+
maxChars: b.maxChars,
|
|
369
|
+
excessChars: b.wastedChars,
|
|
370
|
+
excessWeighted: b.weighted,
|
|
371
|
+
})),
|
|
372
|
+
hooks: [],
|
|
373
|
+
attachments: [],
|
|
374
|
+
writes: topBuckets(writes, ends).map((b) => ({
|
|
375
|
+
path: b.key,
|
|
376
|
+
writes: b.count,
|
|
377
|
+
edits: b.edits,
|
|
378
|
+
rewrittenChars: b.wastedChars,
|
|
379
|
+
rewrittenWeighted: b.weighted,
|
|
380
|
+
})),
|
|
381
|
+
failures: topBuckets(failures, ends).map((b) => ({
|
|
382
|
+
label: b.key,
|
|
383
|
+
tool: b.tool,
|
|
384
|
+
failures: b.count,
|
|
385
|
+
chars: b.chars,
|
|
386
|
+
weighted: b.weighted,
|
|
387
|
+
})),
|
|
388
|
+
};
|
|
389
|
+
}
|