@tekmidian/pai 0.47.0 → 0.49.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/dist/cli/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import "../helpers-crDEr6S2.mjs";
|
|
|
7
7
|
import "../sync-Btcb6xA4.mjs";
|
|
8
8
|
import "../embeddings-DvRKcQyt.mjs";
|
|
9
9
|
import "../search-B69cQwDB.mjs";
|
|
10
|
-
import { n as drainStdio, t as buildProgram } from "../program-
|
|
10
|
+
import { n as drainStdio, t as buildProgram } from "../program-D9-wfmkZ.mjs";
|
|
11
11
|
import "../context-handover-cache-B9NKSjwR.mjs";
|
|
12
12
|
import "../config-DCu8THKi.mjs";
|
|
13
13
|
import "../config-DxMZjpvY.mjs";
|
package/dist/cli/program.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import "../helpers-crDEr6S2.mjs";
|
|
|
6
6
|
import "../sync-Btcb6xA4.mjs";
|
|
7
7
|
import "../embeddings-DvRKcQyt.mjs";
|
|
8
8
|
import "../search-B69cQwDB.mjs";
|
|
9
|
-
import { t as buildProgram } from "../program-
|
|
9
|
+
import { t as buildProgram } from "../program-D9-wfmkZ.mjs";
|
|
10
10
|
import "../context-handover-cache-B9NKSjwR.mjs";
|
|
11
11
|
import "../config-DCu8THKi.mjs";
|
|
12
12
|
import "../config-DxMZjpvY.mjs";
|
|
@@ -10076,6 +10076,24 @@ function parseCompactionEvent(line, turnIndex) {
|
|
|
10076
10076
|
};
|
|
10077
10077
|
}
|
|
10078
10078
|
/**
|
|
10079
|
+
* `type:"system"` `subtype:"model_refusal_fallback"` lines mark an
|
|
10080
|
+
* automatic model switch fired by an API safety refusal (e.g. "cyber"
|
|
10081
|
+
* category), not a user or config choice — worth flagging separately since
|
|
10082
|
+
* it can rebuild the whole prompt cache mid-session.
|
|
10083
|
+
*/
|
|
10084
|
+
function isModelFallback(line) {
|
|
10085
|
+
return line.type === "system" && line.subtype === "model_refusal_fallback";
|
|
10086
|
+
}
|
|
10087
|
+
function parseFallbackEvent(line, turnIndex) {
|
|
10088
|
+
return {
|
|
10089
|
+
turnIndex,
|
|
10090
|
+
from: line.originalModel ?? "unknown",
|
|
10091
|
+
to: line.fallbackModel ?? "unknown",
|
|
10092
|
+
category: line.apiRefusalCategory ?? "unknown",
|
|
10093
|
+
scope: line.scope ?? "unknown"
|
|
10094
|
+
};
|
|
10095
|
+
}
|
|
10096
|
+
/**
|
|
10079
10097
|
* Fold one already-parsed JSONL line into a report being accumulated.
|
|
10080
10098
|
* Exported separately so both the streaming file reader below and tests
|
|
10081
10099
|
* (which build fixtures as arrays of objects, not files) share one path.
|
|
@@ -10098,13 +10116,24 @@ function foldAssistantLine(report, seenIds, line, threshold) {
|
|
|
10098
10116
|
if (typeof v === "number") report.totals[key] += v;
|
|
10099
10117
|
}
|
|
10100
10118
|
const context = (Number(usage.cache_read_input_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.input_tokens) || 0);
|
|
10101
|
-
if (report.firstTurnContext === null)
|
|
10119
|
+
if (report.firstTurnContext === null) {
|
|
10120
|
+
report.firstTurnContext = context;
|
|
10121
|
+
report.firstTurnAt = line.timestamp ?? null;
|
|
10122
|
+
}
|
|
10102
10123
|
report.lastTurnContext = context;
|
|
10103
10124
|
report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);
|
|
10104
10125
|
if (context > threshold) report.turnsAboveThreshold++;
|
|
10105
10126
|
if ((Number(usage.cache_creation_input_tokens) || 0) > 2e4) report.cacheRebuildTurns++;
|
|
10106
10127
|
const model = message?.model ?? "unknown";
|
|
10107
10128
|
report.models[model] = (report.models[model] ?? 0) + 1;
|
|
10129
|
+
if (report.lastModel !== null && report.lastModel !== model) report.modelSwitches.push({
|
|
10130
|
+
turnIndex: report.turns,
|
|
10131
|
+
from: report.lastModel,
|
|
10132
|
+
to: model,
|
|
10133
|
+
cacheRead: Number(usage.cache_read_input_tokens) || 0,
|
|
10134
|
+
cacheCreation: Number(usage.cache_creation_input_tokens) || 0
|
|
10135
|
+
});
|
|
10136
|
+
report.lastModel = model;
|
|
10108
10137
|
const split = usage.cache_creation;
|
|
10109
10138
|
if (split) {
|
|
10110
10139
|
report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;
|
|
@@ -10120,6 +10149,8 @@ function foldAssistantLine(report, seenIds, line, threshold) {
|
|
|
10120
10149
|
*/
|
|
10121
10150
|
function isRealUserPrompt(line) {
|
|
10122
10151
|
if (line.type !== "user") return false;
|
|
10152
|
+
if (line.isMeta) return false;
|
|
10153
|
+
if (line.origin?.kind && line.origin.kind !== "human") return false;
|
|
10123
10154
|
const content = line.message?.content;
|
|
10124
10155
|
if (typeof content === "string") return true;
|
|
10125
10156
|
if (Array.isArray(content)) return !content.some((block) => block?.type === "tool_result");
|
|
@@ -10145,7 +10176,11 @@ async function parseSessionUsage(path, threshold = 2e5) {
|
|
|
10145
10176
|
cacheRebuildTurns: 0,
|
|
10146
10177
|
userPrompts: 0,
|
|
10147
10178
|
promptExposure: 0,
|
|
10148
|
-
compactions: []
|
|
10179
|
+
compactions: [],
|
|
10180
|
+
firstTurnAt: null,
|
|
10181
|
+
lastModel: null,
|
|
10182
|
+
modelSwitches: [],
|
|
10183
|
+
fallbacks: []
|
|
10149
10184
|
};
|
|
10150
10185
|
const seenIds = /* @__PURE__ */ new Set();
|
|
10151
10186
|
let contextSum = 0;
|
|
@@ -10170,6 +10205,10 @@ async function parseSessionUsage(path, threshold = 2e5) {
|
|
|
10170
10205
|
if (event) report.compactions.push(event);
|
|
10171
10206
|
continue;
|
|
10172
10207
|
}
|
|
10208
|
+
if (isModelFallback(obj)) {
|
|
10209
|
+
report.fallbacks.push(parseFallbackEvent(obj, report.turns));
|
|
10210
|
+
continue;
|
|
10211
|
+
}
|
|
10173
10212
|
const context = foldAssistantLine(report, seenIds, obj, threshold);
|
|
10174
10213
|
if (context !== null) {
|
|
10175
10214
|
contextSum += context;
|
|
@@ -10183,6 +10222,272 @@ function totalUsageTokens(totals) {
|
|
|
10183
10222
|
return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;
|
|
10184
10223
|
}
|
|
10185
10224
|
|
|
10225
|
+
//#endregion
|
|
10226
|
+
//#region src/audit/first-turn.ts
|
|
10227
|
+
/**
|
|
10228
|
+
* first-turn.ts — where a turn's context actually comes from, attributed
|
|
10229
|
+
* line by line from the transcript.
|
|
10230
|
+
*
|
|
10231
|
+
* The API reports one number per assistant turn (cache_read + cache_creation
|
|
10232
|
+
* + input). The transcript holds the pieces Claude Code injected since the
|
|
10233
|
+
* previous turn: SessionStart / UserPromptSubmit hook output, the skill
|
|
10234
|
+
* listing, the deferred-tool name list, MCP server instructions, the human
|
|
10235
|
+
* prompt, any isMeta expansion (a skill body, a pasted slash-command result),
|
|
10236
|
+
* tool results, and the previous turn's own visible output (re-sent as
|
|
10237
|
+
* context on every later turn). Each of those is counted in cl100k here;
|
|
10238
|
+
* what is left over is the system prompt, the built-in tool schemas, agents
|
|
10239
|
+
* and memory files, which never appear in the transcript.
|
|
10240
|
+
*
|
|
10241
|
+
* Only the live lineage counts. Lines chain by parentUuid, and a prompt the
|
|
10242
|
+
* user abandoned before a reply (Escape, re-submit) leaves a dead branch
|
|
10243
|
+
* whose lines were never sent — run 9 measured a session whose first
|
|
10244
|
+
* /Name was abandoned and read 24k where the true baseline is 30k, because
|
|
10245
|
+
* the 3.5k-token skill listing sat on the dead branch.
|
|
10246
|
+
*
|
|
10247
|
+
* `type:"system"` lines with subtype local_command hold a slash command's
|
|
10248
|
+
* raw stdout, and the model receives it verbatim in addition to the isMeta
|
|
10249
|
+
* markdown copy — run 11 measured both present on the same turn, so both are
|
|
10250
|
+
* counted.
|
|
10251
|
+
*/
|
|
10252
|
+
function parseLines(path) {
|
|
10253
|
+
const lines = [];
|
|
10254
|
+
for (const raw of readFileSync(path, "utf8").split("\n")) {
|
|
10255
|
+
if (!raw.trim()) continue;
|
|
10256
|
+
try {
|
|
10257
|
+
lines.push(JSON.parse(raw));
|
|
10258
|
+
} catch {}
|
|
10259
|
+
}
|
|
10260
|
+
return lines;
|
|
10261
|
+
}
|
|
10262
|
+
function classify(line) {
|
|
10263
|
+
if (line.type === "attachment") {
|
|
10264
|
+
const a = line.attachment ?? {};
|
|
10265
|
+
switch (a.type) {
|
|
10266
|
+
case "hook_success": {
|
|
10267
|
+
const name = a.hookName ?? "";
|
|
10268
|
+
return {
|
|
10269
|
+
kind: name.startsWith("SessionStart") ? "hook:SessionStart" : name.startsWith("UserPromptSubmit") ? "hook:UserPromptSubmit" : "hook:other",
|
|
10270
|
+
tokens: countTokens(a.stdout ?? a.content ?? ""),
|
|
10271
|
+
label: name
|
|
10272
|
+
};
|
|
10273
|
+
}
|
|
10274
|
+
case "skill_listing": return {
|
|
10275
|
+
kind: "skill_listing",
|
|
10276
|
+
tokens: countTokens(a.content ?? ""),
|
|
10277
|
+
label: "skill_listing"
|
|
10278
|
+
};
|
|
10279
|
+
case "deferred_tools_delta": return {
|
|
10280
|
+
kind: "deferred_tools",
|
|
10281
|
+
tokens: countTokens((a.addedNames ?? []).join("\n")),
|
|
10282
|
+
label: "deferred_tools_delta"
|
|
10283
|
+
};
|
|
10284
|
+
case "mcp_instructions_delta": return {
|
|
10285
|
+
kind: "mcp_instructions",
|
|
10286
|
+
tokens: countTokens(JSON.stringify(a.instructions ?? a.addedInstructions ?? a)),
|
|
10287
|
+
label: "mcp_instructions_delta"
|
|
10288
|
+
};
|
|
10289
|
+
default: return {
|
|
10290
|
+
kind: "attachment:other",
|
|
10291
|
+
tokens: countTokens(JSON.stringify(a)),
|
|
10292
|
+
label: a.type ?? "attachment"
|
|
10293
|
+
};
|
|
10294
|
+
}
|
|
10295
|
+
}
|
|
10296
|
+
if (line.type === "user") {
|
|
10297
|
+
const c = line.message?.content;
|
|
10298
|
+
const isToolResult = Array.isArray(c) && c.some((b) => b?.type === "tool_result");
|
|
10299
|
+
const text = typeof c === "string" ? c : JSON.stringify(c ?? "");
|
|
10300
|
+
return {
|
|
10301
|
+
kind: isToolResult ? "tool_result" : line.isMeta ? "prompt:meta" : "prompt",
|
|
10302
|
+
tokens: countTokens(text),
|
|
10303
|
+
label: text.slice(0, 40).replace(/\s+/g, " ")
|
|
10304
|
+
};
|
|
10305
|
+
}
|
|
10306
|
+
if (line.type === "system") return {
|
|
10307
|
+
kind: line.subtype === "local_command" ? "prompt:local_command" : "system:display-only",
|
|
10308
|
+
tokens: countTokens(line.content ?? ""),
|
|
10309
|
+
label: line.subtype ?? "system"
|
|
10310
|
+
};
|
|
10311
|
+
return null;
|
|
10312
|
+
}
|
|
10313
|
+
function apiContextOf(usage) {
|
|
10314
|
+
if (!usage) return null;
|
|
10315
|
+
return (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.input_tokens ?? 0);
|
|
10316
|
+
}
|
|
10317
|
+
/**
|
|
10318
|
+
* Attribute API call `turn`'s context growth to the transcript lines
|
|
10319
|
+
* injected since the previous call. Turn numbering matches
|
|
10320
|
+
* session-usage.ts's `turnIndex`: several assistant lines sharing one
|
|
10321
|
+
* message.id (one line per streamed content block) are one call. Returns
|
|
10322
|
+
* null if the transcript has fewer than `turn` calls.
|
|
10323
|
+
*/
|
|
10324
|
+
function turnBreakdown(path, turn) {
|
|
10325
|
+
if (!existsSync(path)) return null;
|
|
10326
|
+
const lines = parseLines(path);
|
|
10327
|
+
const byUuid = /* @__PURE__ */ new Map();
|
|
10328
|
+
for (const l of lines) if (l.uuid) byUuid.set(l.uuid, l);
|
|
10329
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10330
|
+
let n = 0;
|
|
10331
|
+
let target = null;
|
|
10332
|
+
for (const l of lines) if (l.type === "assistant" && l.message?.usage) {
|
|
10333
|
+
const id = l.message.id ?? l.uuid ?? "";
|
|
10334
|
+
if (!seen.has(id)) {
|
|
10335
|
+
seen.add(id);
|
|
10336
|
+
n++;
|
|
10337
|
+
if (n === turn) {
|
|
10338
|
+
target = l;
|
|
10339
|
+
break;
|
|
10340
|
+
}
|
|
10341
|
+
}
|
|
10342
|
+
}
|
|
10343
|
+
if (!target) return null;
|
|
10344
|
+
const rawChain = [];
|
|
10345
|
+
let prevLine = null;
|
|
10346
|
+
let cur = target.parentUuid ? byUuid.get(target.parentUuid) : void 0;
|
|
10347
|
+
while (cur) {
|
|
10348
|
+
if (cur.type === "assistant" && cur.message?.usage) {
|
|
10349
|
+
prevLine = cur;
|
|
10350
|
+
break;
|
|
10351
|
+
}
|
|
10352
|
+
rawChain.unshift(cur);
|
|
10353
|
+
cur = cur.parentUuid ? byUuid.get(cur.parentUuid) : void 0;
|
|
10354
|
+
}
|
|
10355
|
+
const items = [];
|
|
10356
|
+
for (const l of rawChain) {
|
|
10357
|
+
const item = classify(l);
|
|
10358
|
+
if (item) items.push(item);
|
|
10359
|
+
}
|
|
10360
|
+
let prevApiContext = 0;
|
|
10361
|
+
let prevOutputTokens = 0;
|
|
10362
|
+
if (prevLine) {
|
|
10363
|
+
prevApiContext = apiContextOf(prevLine.message?.usage) ?? 0;
|
|
10364
|
+
prevOutputTokens = prevLine.message?.usage?.output_tokens ?? 0;
|
|
10365
|
+
const prevId = prevLine.message?.id ?? prevLine.uuid ?? "";
|
|
10366
|
+
const prevBlocks = [prevLine];
|
|
10367
|
+
let cur2 = prevLine.parentUuid ? byUuid.get(prevLine.parentUuid) : void 0;
|
|
10368
|
+
while (cur2 && cur2.type === "assistant" && (cur2.message?.id ?? cur2.uuid ?? "") === prevId) {
|
|
10369
|
+
prevBlocks.unshift(cur2);
|
|
10370
|
+
cur2 = cur2.parentUuid ? byUuid.get(cur2.parentUuid) : void 0;
|
|
10371
|
+
}
|
|
10372
|
+
let prevText = "";
|
|
10373
|
+
let thinkingCount = 0;
|
|
10374
|
+
for (const b of prevBlocks) {
|
|
10375
|
+
const content = b.message?.content;
|
|
10376
|
+
if (!Array.isArray(content)) continue;
|
|
10377
|
+
for (const block of content) if (block?.type === "text") prevText += block.text ?? "";
|
|
10378
|
+
else if (block?.type === "tool_use") prevText += JSON.stringify(block.input ?? {});
|
|
10379
|
+
else if (block?.type === "thinking") thinkingCount++;
|
|
10380
|
+
}
|
|
10381
|
+
const label = thinkingCount > 0 ? `previous call output (${thinkingCount} thinking block(s) redacted)` : "previous call output";
|
|
10382
|
+
items.push({
|
|
10383
|
+
kind: "assistant:prev",
|
|
10384
|
+
tokens: countTokens(prevText),
|
|
10385
|
+
label
|
|
10386
|
+
});
|
|
10387
|
+
}
|
|
10388
|
+
const byKind = {};
|
|
10389
|
+
let transcriptTokens = 0;
|
|
10390
|
+
let prevVisibleTokens = 0;
|
|
10391
|
+
for (const item of items) {
|
|
10392
|
+
if (item.kind === "system:display-only") continue;
|
|
10393
|
+
if (item.kind === "assistant:prev") {
|
|
10394
|
+
prevVisibleTokens = item.tokens;
|
|
10395
|
+
continue;
|
|
10396
|
+
}
|
|
10397
|
+
transcriptTokens += item.tokens;
|
|
10398
|
+
byKind[item.kind] = (byKind[item.kind] ?? 0) + item.tokens;
|
|
10399
|
+
}
|
|
10400
|
+
const apiContext = apiContextOf(target.message?.usage);
|
|
10401
|
+
const billedDelta = apiContext === null ? null : apiContext - prevApiContext;
|
|
10402
|
+
const residual = billedDelta === null ? null : billedDelta - transcriptTokens - prevOutputTokens;
|
|
10403
|
+
return {
|
|
10404
|
+
path,
|
|
10405
|
+
turn,
|
|
10406
|
+
apiContext,
|
|
10407
|
+
prevApiContext,
|
|
10408
|
+
billedDelta,
|
|
10409
|
+
prevOutputTokens,
|
|
10410
|
+
items,
|
|
10411
|
+
transcriptTokens,
|
|
10412
|
+
byKind,
|
|
10413
|
+
prevVisibleTokens,
|
|
10414
|
+
residual
|
|
10415
|
+
};
|
|
10416
|
+
}
|
|
10417
|
+
/** Attribute the first assistant turn's context to the transcript lines on its live lineage. */
|
|
10418
|
+
function firstTurnBreakdown(path) {
|
|
10419
|
+
const empty = {
|
|
10420
|
+
path,
|
|
10421
|
+
apiContext: null,
|
|
10422
|
+
items: [],
|
|
10423
|
+
transcriptTokens: 0,
|
|
10424
|
+
byKind: {},
|
|
10425
|
+
remainder: null,
|
|
10426
|
+
deadBranchTokens: 0,
|
|
10427
|
+
deadBranchLines: 0
|
|
10428
|
+
};
|
|
10429
|
+
if (!existsSync(path)) return empty;
|
|
10430
|
+
const tb = turnBreakdown(path, 1);
|
|
10431
|
+
if (!tb) return empty;
|
|
10432
|
+
const lines = parseLines(path);
|
|
10433
|
+
const byUuid = /* @__PURE__ */ new Map();
|
|
10434
|
+
let firstIndex = -1;
|
|
10435
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10436
|
+
const l = lines[i];
|
|
10437
|
+
if (l.uuid) byUuid.set(l.uuid, l);
|
|
10438
|
+
if (l.type === "assistant" && l.message?.usage && firstIndex < 0) firstIndex = i;
|
|
10439
|
+
}
|
|
10440
|
+
if (firstIndex < 0) return empty;
|
|
10441
|
+
const first = lines[firstIndex];
|
|
10442
|
+
const live = /* @__PURE__ */ new Set();
|
|
10443
|
+
let cur = first.parentUuid ? byUuid.get(first.parentUuid) : void 0;
|
|
10444
|
+
while (cur && !live.has(cur)) {
|
|
10445
|
+
live.add(cur);
|
|
10446
|
+
cur = cur.parentUuid ? byUuid.get(cur.parentUuid) : void 0;
|
|
10447
|
+
}
|
|
10448
|
+
let deadBranchTokens = 0;
|
|
10449
|
+
let deadBranchLines = 0;
|
|
10450
|
+
for (let i = 0; i < firstIndex; i++) {
|
|
10451
|
+
const l = lines[i];
|
|
10452
|
+
if (!l.uuid || live.has(l)) continue;
|
|
10453
|
+
const item = classify(l);
|
|
10454
|
+
if (!item || item.kind === "system:display-only") continue;
|
|
10455
|
+
deadBranchLines++;
|
|
10456
|
+
deadBranchTokens += item.tokens;
|
|
10457
|
+
}
|
|
10458
|
+
return {
|
|
10459
|
+
path,
|
|
10460
|
+
apiContext: tb.apiContext,
|
|
10461
|
+
items: tb.items,
|
|
10462
|
+
transcriptTokens: tb.transcriptTokens,
|
|
10463
|
+
byKind: tb.byKind,
|
|
10464
|
+
remainder: tb.residual,
|
|
10465
|
+
deadBranchTokens,
|
|
10466
|
+
deadBranchLines
|
|
10467
|
+
};
|
|
10468
|
+
}
|
|
10469
|
+
/** One-line evidence string for the combined report's first-turn finding. */
|
|
10470
|
+
function firstTurnEvidence(b) {
|
|
10471
|
+
const parts = [];
|
|
10472
|
+
for (const k of [
|
|
10473
|
+
"skill_listing",
|
|
10474
|
+
"mcp_instructions",
|
|
10475
|
+
"deferred_tools",
|
|
10476
|
+
"hook:SessionStart",
|
|
10477
|
+
"hook:UserPromptSubmit",
|
|
10478
|
+
"hook:other",
|
|
10479
|
+
"prompt:meta",
|
|
10480
|
+
"prompt:local_command",
|
|
10481
|
+
"prompt",
|
|
10482
|
+
"attachment:other"
|
|
10483
|
+
]) {
|
|
10484
|
+
const v = b.byKind[k];
|
|
10485
|
+
if (v) parts.push(`${k} ${v}`);
|
|
10486
|
+
}
|
|
10487
|
+
const dead = b.deadBranchTokens > 0 ? `; dead-branch ${b.deadBranchTokens} not sent` : "";
|
|
10488
|
+
return `transcript-side ${b.transcriptTokens} (${parts.join(", ")}), remainder ${b.remainder ?? "n/a"}${dead}`;
|
|
10489
|
+
}
|
|
10490
|
+
|
|
10186
10491
|
//#endregion
|
|
10187
10492
|
//#region src/audit/session.ts
|
|
10188
10493
|
/**
|
|
@@ -10236,9 +10541,48 @@ async function auditSession(path, threshold) {
|
|
|
10236
10541
|
return {
|
|
10237
10542
|
...report,
|
|
10238
10543
|
totalTokens: total,
|
|
10239
|
-
percentages
|
|
10544
|
+
percentages,
|
|
10545
|
+
firstTurn: firstTurnBreakdown(target)
|
|
10240
10546
|
};
|
|
10241
10547
|
}
|
|
10548
|
+
/**
|
|
10549
|
+
* First-turn context of every session transcript in this project's
|
|
10550
|
+
* `~/.claude/projects/<encoded-cwd>` directory, newest first — the readings
|
|
10551
|
+
* needed to see when a step change (model switch, hook growth) happened
|
|
10552
|
+
* across sessions without an ad-hoc script.
|
|
10553
|
+
*/
|
|
10554
|
+
async function sessionHistory(cwd, limit = 20) {
|
|
10555
|
+
const dir = join(homedir(), ".claude", "projects", encodeDir(cwd));
|
|
10556
|
+
let entries;
|
|
10557
|
+
try {
|
|
10558
|
+
entries = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
10559
|
+
} catch {
|
|
10560
|
+
return [];
|
|
10561
|
+
}
|
|
10562
|
+
const rows = [];
|
|
10563
|
+
for (const entry of entries) {
|
|
10564
|
+
const path = join(dir, entry);
|
|
10565
|
+
const report = await parseSessionUsage(path);
|
|
10566
|
+
if (report.turns === 0) continue;
|
|
10567
|
+
rows.push({
|
|
10568
|
+
path,
|
|
10569
|
+
sessionId: entry.replace(/\.jsonl$/, ""),
|
|
10570
|
+
firstTurnAt: report.firstTurnAt,
|
|
10571
|
+
firstTurnContext: report.firstTurnContext,
|
|
10572
|
+
turns: report.turns,
|
|
10573
|
+
models: report.models,
|
|
10574
|
+
switches: report.modelSwitches.length,
|
|
10575
|
+
fallbacks: report.fallbacks.length
|
|
10576
|
+
});
|
|
10577
|
+
}
|
|
10578
|
+
rows.sort((a, b) => {
|
|
10579
|
+
if (a.firstTurnAt === null && b.firstTurnAt === null) return 0;
|
|
10580
|
+
if (a.firstTurnAt === null) return 1;
|
|
10581
|
+
if (b.firstTurnAt === null) return -1;
|
|
10582
|
+
return b.firstTurnAt.localeCompare(a.firstTurnAt);
|
|
10583
|
+
});
|
|
10584
|
+
return rows.slice(0, limit);
|
|
10585
|
+
}
|
|
10242
10586
|
|
|
10243
10587
|
//#endregion
|
|
10244
10588
|
//#region src/audit/spawn.ts
|
|
@@ -11026,6 +11370,7 @@ const FIRST_TURN_CONTEXT_LIMIT = 3e4;
|
|
|
11026
11370
|
const DAEMON_FAILURE_RATE_LIMIT = .05;
|
|
11027
11371
|
const CONTEXT_GROWTH_AVG_RED = 1e5;
|
|
11028
11372
|
const CONTEXT_GROWTH_MAX_AMBER = 15e4;
|
|
11373
|
+
const MODEL_SWITCH_REBUILD_LIMIT = 2e4;
|
|
11029
11374
|
const SEVERITY_RANK = {
|
|
11030
11375
|
RED: 0,
|
|
11031
11376
|
AMBER: 1,
|
|
@@ -11065,10 +11410,11 @@ function buildFindings(input) {
|
|
|
11065
11410
|
});
|
|
11066
11411
|
if (input.session) {
|
|
11067
11412
|
const first = input.session.firstTurnContext ?? 0;
|
|
11413
|
+
const evidence = typeof input.session.firstTurn?.apiContext === "number" ? `${first} tokens: ${firstTurnEvidence(input.session.firstTurn)} (${input.session.path})` : `${first} tokens (${input.session.path})`;
|
|
11068
11414
|
findings.push({
|
|
11069
11415
|
finding: "first-turn context size",
|
|
11070
11416
|
severity: first > FIRST_TURN_CONTEXT_LIMIT ? "AMBER" : "GREEN",
|
|
11071
|
-
evidence
|
|
11417
|
+
evidence
|
|
11072
11418
|
});
|
|
11073
11419
|
}
|
|
11074
11420
|
const rate = input.daemon.kgParseFailureRate;
|
|
@@ -11152,6 +11498,22 @@ function buildFindings(input) {
|
|
|
11152
11498
|
severity: overshoots.length > 0 ? "RED" : "GREEN",
|
|
11153
11499
|
evidence: overshoots.length > 0 ? `override not honoured: ${overshoots.map((c) => `preTokens=${c.preTokens}`).join(", ")} vs configured ${input.ctxThreshold}` : compactions.length === 0 ? `0 compactions in ${input.session.turns} turns` : `${compactions.length} compaction(s), all within 25% of configured ${input.ctxThreshold}`
|
|
11154
11500
|
});
|
|
11501
|
+
const switches = input.session.modelSwitches;
|
|
11502
|
+
const fallbacks = input.session.fallbacks;
|
|
11503
|
+
const rebuilds = switches.filter((s) => s.cacheCreation > MODEL_SWITCH_REBUILD_LIMIT);
|
|
11504
|
+
const short = (model) => model.replace(/^claude-/, "");
|
|
11505
|
+
let switchEvidence;
|
|
11506
|
+
if (switches.length === 0) switchEvidence = `0 switches in ${input.session.turns} turns, models ${JSON.stringify(input.session.models)}`;
|
|
11507
|
+
else {
|
|
11508
|
+
const list = switches.map((s) => `${short(s.from)}->${short(s.to)} at turn ${s.turnIndex} (cache_read ${s.cacheRead}, cache_creation ${s.cacheCreation})`).join("; ");
|
|
11509
|
+
switchEvidence = `${switches.length} switch(es): ${list}`;
|
|
11510
|
+
if (fallbacks.length > 0) switchEvidence += `; ${fallbacks.length} safeguard fallback(s): ${fallbacks.map((f) => f.category).join(",")}`;
|
|
11511
|
+
}
|
|
11512
|
+
findings.push({
|
|
11513
|
+
finding: "mid-session model switches",
|
|
11514
|
+
severity: switches.length === 0 ? "GREEN" : rebuilds.length > 0 ? "RED" : "AMBER",
|
|
11515
|
+
evidence: switchEvidence
|
|
11516
|
+
});
|
|
11155
11517
|
}
|
|
11156
11518
|
return sortFindings(findings);
|
|
11157
11519
|
}
|
|
@@ -11695,14 +12057,98 @@ function printSession(report) {
|
|
|
11695
12057
|
"share"
|
|
11696
12058
|
], rows));
|
|
11697
12059
|
console.log(`context on first turn: ${report.firstTurnContext ?? "n/a"}`);
|
|
12060
|
+
if (report.firstTurn && report.firstTurn.apiContext !== null) {
|
|
12061
|
+
const ft = report.firstTurn;
|
|
12062
|
+
console.log(header("first-turn breakdown (live lineage, cl100k)"));
|
|
12063
|
+
const ftRows = ft.items.filter((i) => i.tokens > 0).map((i) => [
|
|
12064
|
+
i.kind,
|
|
12065
|
+
String(i.tokens),
|
|
12066
|
+
i.label
|
|
12067
|
+
]);
|
|
12068
|
+
console.log(renderTable([
|
|
12069
|
+
"kind",
|
|
12070
|
+
"tokens",
|
|
12071
|
+
"label"
|
|
12072
|
+
], ftRows));
|
|
12073
|
+
console.log(`transcript-side: ${ft.transcriptTokens}`);
|
|
12074
|
+
console.log(`remainder (system prompt + tool schemas + agents + memory): ${ft.remainder}`);
|
|
12075
|
+
console.log(`dead-branch: ${ft.deadBranchTokens} tokens on ${ft.deadBranchLines} lines, persisted but never sent`);
|
|
12076
|
+
}
|
|
11698
12077
|
console.log(`context on last turn: ${report.lastTurnContext ?? "n/a"}`);
|
|
11699
12078
|
console.log(`cache_creation split: ephemeral_5m=${report.cacheCreationSplit.ephemeral5m}, ephemeral_1h=${report.cacheCreationSplit.ephemeral1h}`);
|
|
11700
12079
|
console.log(dim$1(`models: ${JSON.stringify(report.models)}`));
|
|
11701
12080
|
console.log(`avg context: ${report.avgContext ?? "n/a"}, max context: ${report.maxContext ?? "n/a"}, turns above threshold: ${report.turnsAboveThreshold}, cache-rebuild turns: ${report.cacheRebuildTurns}, user prompts: ${report.userPrompts}, prompt exposure: ${report.promptExposure}`);
|
|
11702
12081
|
console.log(`compactions: ${report.compactions.length}`);
|
|
11703
12082
|
for (const c of report.compactions) console.log(dim$1(` ${c.trigger} at turn ${c.turnIndex}: preTokens=${c.preTokens}`));
|
|
12083
|
+
console.log(`model switches: ${report.modelSwitches.length}`);
|
|
12084
|
+
for (const s of report.modelSwitches) console.log(dim$1(` ${s.from} -> ${s.to} at turn ${s.turnIndex}: cache_read=${s.cacheRead}, cache_creation=${s.cacheCreation}`));
|
|
12085
|
+
if (report.fallbacks.length > 0) {
|
|
12086
|
+
console.log(`safeguard fallbacks: ${report.fallbacks.length}`);
|
|
12087
|
+
for (const f of report.fallbacks) console.log(dim$1(` ${f.from} -> ${f.to} before turn ${f.turnIndex}: ${f.category} (${f.scope})`));
|
|
12088
|
+
}
|
|
12089
|
+
}
|
|
12090
|
+
function printTurnBreakdown(tb) {
|
|
12091
|
+
console.log(header(`turn ${tb.turn} attribution (live lineage, cl100k)`));
|
|
12092
|
+
const rows = tb.items.filter((i) => i.tokens > 0).map((i) => [
|
|
12093
|
+
String(i.tokens),
|
|
12094
|
+
i.kind,
|
|
12095
|
+
i.label.replace(/\s+/g, " ").slice(0, 60)
|
|
12096
|
+
]);
|
|
12097
|
+
console.log(renderTable([
|
|
12098
|
+
"tokens",
|
|
12099
|
+
"kind",
|
|
12100
|
+
"label"
|
|
12101
|
+
], rows));
|
|
12102
|
+
console.log(`transcript-side ${tb.transcriptTokens}`);
|
|
12103
|
+
console.log(`billed delta ${tb.billedDelta ?? "n/a"} (context ${tb.apiContext ?? "n/a"} after ${tb.prevApiContext})`);
|
|
12104
|
+
console.log(`previous call output ${tb.prevOutputTokens} (includes thinking, re-sent on tool turns)`);
|
|
12105
|
+
console.log(`previous call visible output ${tb.prevVisibleTokens} (cl100k, listed as assistant:prev, not in transcript-side)`);
|
|
12106
|
+
console.log(`residual ${tb.residual ?? "n/a"}: positive = cl100k undercount vs the billing tokenizer (1.2-1.45x measured on tool output and markdown tables); negative = the previous call's thinking was not re-sent (only re-sent inside a tool loop)`);
|
|
12107
|
+
}
|
|
12108
|
+
function printSessionHistory(rows) {
|
|
12109
|
+
console.log(header("Session history (newest first)"));
|
|
12110
|
+
const tableRows = rows.map((r) => [
|
|
12111
|
+
r.firstTurnAt ?? "n/a",
|
|
12112
|
+
r.sessionId,
|
|
12113
|
+
r.firstTurnContext === null ? "n/a" : String(r.firstTurnContext),
|
|
12114
|
+
String(r.turns),
|
|
12115
|
+
String(r.switches),
|
|
12116
|
+
Object.entries(r.models).map(([name, count]) => `${name}:${count}`).join(",")
|
|
12117
|
+
]);
|
|
12118
|
+
console.log(renderTable([
|
|
12119
|
+
"first turn (UTC)",
|
|
12120
|
+
"session",
|
|
12121
|
+
"first-turn ctx",
|
|
12122
|
+
"turns",
|
|
12123
|
+
"switches",
|
|
12124
|
+
"models"
|
|
12125
|
+
], tableRows));
|
|
11704
12126
|
}
|
|
11705
12127
|
async function cmdSession(path, opts) {
|
|
12128
|
+
if (opts.history !== void 0) {
|
|
12129
|
+
const limit = typeof opts.history === "string" ? parseInt(opts.history, 10) : 20;
|
|
12130
|
+
const rows = await sessionHistory(process.cwd(), limit);
|
|
12131
|
+
if (opts.json) return printJson(rows);
|
|
12132
|
+
printSessionHistory(rows);
|
|
12133
|
+
return;
|
|
12134
|
+
}
|
|
12135
|
+
if (opts.turn !== void 0) {
|
|
12136
|
+
const target = path ?? newestSessionLog();
|
|
12137
|
+
if (!target) {
|
|
12138
|
+
console.error(err(" No session transcript found."));
|
|
12139
|
+
process.exitCode = 1;
|
|
12140
|
+
return;
|
|
12141
|
+
}
|
|
12142
|
+
const tb = turnBreakdown(target, parseInt(opts.turn, 10));
|
|
12143
|
+
if (!tb) {
|
|
12144
|
+
console.error(err(` Turn ${opts.turn} not found in ${target}.`));
|
|
12145
|
+
process.exitCode = 1;
|
|
12146
|
+
return;
|
|
12147
|
+
}
|
|
12148
|
+
if (opts.json) return printJson(tb);
|
|
12149
|
+
printTurnBreakdown(tb);
|
|
12150
|
+
return;
|
|
12151
|
+
}
|
|
11706
12152
|
const report = await auditSession(path, opts.ctxThreshold ? parseInt(opts.ctxThreshold, 10) : void 0);
|
|
11707
12153
|
if (!report) {
|
|
11708
12154
|
console.error(err(" No session transcript found."));
|
|
@@ -12186,7 +12632,7 @@ function registerAuditCommands(auditCmd) {
|
|
|
12186
12632
|
await cmdFiles(paths, jsonOpt());
|
|
12187
12633
|
});
|
|
12188
12634
|
tokensCmd.command("hooks").description("Token cost of every SessionStart / UserPromptSubmit hook").action(() => cmdHooks(jsonOpt()));
|
|
12189
|
-
tokensCmd.command("session").description("Cache/input/output token split for a session transcript (default: newest)").argument("[path]", "Session JSONL path (default: newest under ~/.claude/projects)").option("--ctx-threshold <n>", "Per-turn context size above which a turn counts as 'above threshold' (default 200000)").action(async (path, opts) => {
|
|
12635
|
+
tokensCmd.command("session").description("Cache/input/output token split for a session transcript (default: newest)").argument("[path]", "Session JSONL path (default: newest under ~/.claude/projects)").option("--ctx-threshold <n>", "Per-turn context size above which a turn counts as 'above threshold' (default 200000)").option("--history [n]", "List first-turn context of the newest n sessions in this project's transcript directory (default 20)").option("--turn <n>", "Attribute API call n's context growth to the transcript lines injected since the previous call").action(async (path, opts) => {
|
|
12190
12636
|
await cmdSession(path, {
|
|
12191
12637
|
...jsonOpt(),
|
|
12192
12638
|
...opts
|
|
@@ -20064,4 +20510,4 @@ claude() {
|
|
|
20064
20510
|
|
|
20065
20511
|
//#endregion
|
|
20066
20512
|
export { drainStdio as n, buildProgram as t };
|
|
20067
|
-
//# sourceMappingURL=program-
|
|
20513
|
+
//# sourceMappingURL=program-D9-wfmkZ.mjs.map
|