@tekmidian/pai 0.48.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.
@@ -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-Bgv1lFFA.mjs";
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";
@@ -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-Bgv1lFFA.mjs";
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";
@@ -10222,6 +10222,272 @@ function totalUsageTokens(totals) {
10222
10222
  return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;
10223
10223
  }
10224
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
+
10225
10491
  //#endregion
10226
10492
  //#region src/audit/session.ts
10227
10493
  /**
@@ -10275,7 +10541,8 @@ async function auditSession(path, threshold) {
10275
10541
  return {
10276
10542
  ...report,
10277
10543
  totalTokens: total,
10278
- percentages
10544
+ percentages,
10545
+ firstTurn: firstTurnBreakdown(target)
10279
10546
  };
10280
10547
  }
10281
10548
  /**
@@ -11143,10 +11410,11 @@ function buildFindings(input) {
11143
11410
  });
11144
11411
  if (input.session) {
11145
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})`;
11146
11414
  findings.push({
11147
11415
  finding: "first-turn context size",
11148
11416
  severity: first > FIRST_TURN_CONTEXT_LIMIT ? "AMBER" : "GREEN",
11149
- evidence: `${first} tokens (${input.session.path})`
11417
+ evidence
11150
11418
  });
11151
11419
  }
11152
11420
  const rate = input.daemon.kgParseFailureRate;
@@ -11789,6 +12057,23 @@ function printSession(report) {
11789
12057
  "share"
11790
12058
  ], rows));
11791
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
+ }
11792
12077
  console.log(`context on last turn: ${report.lastTurnContext ?? "n/a"}`);
11793
12078
  console.log(`cache_creation split: ephemeral_5m=${report.cacheCreationSplit.ephemeral5m}, ephemeral_1h=${report.cacheCreationSplit.ephemeral1h}`);
11794
12079
  console.log(dim$1(`models: ${JSON.stringify(report.models)}`));
@@ -11802,6 +12087,24 @@ function printSession(report) {
11802
12087
  for (const f of report.fallbacks) console.log(dim$1(` ${f.from} -> ${f.to} before turn ${f.turnIndex}: ${f.category} (${f.scope})`));
11803
12088
  }
11804
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
+ }
11805
12108
  function printSessionHistory(rows) {
11806
12109
  console.log(header("Session history (newest first)"));
11807
12110
  const tableRows = rows.map((r) => [
@@ -11829,6 +12132,23 @@ async function cmdSession(path, opts) {
11829
12132
  printSessionHistory(rows);
11830
12133
  return;
11831
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
+ }
11832
12152
  const report = await auditSession(path, opts.ctxThreshold ? parseInt(opts.ctxThreshold, 10) : void 0);
11833
12153
  if (!report) {
11834
12154
  console.error(err(" No session transcript found."));
@@ -12312,7 +12632,7 @@ function registerAuditCommands(auditCmd) {
12312
12632
  await cmdFiles(paths, jsonOpt());
12313
12633
  });
12314
12634
  tokensCmd.command("hooks").description("Token cost of every SessionStart / UserPromptSubmit hook").action(() => cmdHooks(jsonOpt()));
12315
- 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)").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) => {
12316
12636
  await cmdSession(path, {
12317
12637
  ...jsonOpt(),
12318
12638
  ...opts
@@ -20190,4 +20510,4 @@ claude() {
20190
20510
 
20191
20511
  //#endregion
20192
20512
  export { drainStdio as n, buildProgram as t };
20193
- //# sourceMappingURL=program-Bgv1lFFA.mjs.map
20513
+ //# sourceMappingURL=program-D9-wfmkZ.mjs.map