@tekmidian/pai 0.47.0 → 0.48.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-BxpeczCx.mjs";
10
+ import { n as drainStdio, t as buildProgram } from "../program-Bgv1lFFA.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-BxpeczCx.mjs";
9
+ import { t as buildProgram } from "../program-Bgv1lFFA.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) report.firstTurnContext = context;
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;
@@ -10239,6 +10278,44 @@ async function auditSession(path, threshold) {
10239
10278
  percentages
10240
10279
  };
10241
10280
  }
10281
+ /**
10282
+ * First-turn context of every session transcript in this project's
10283
+ * `~/.claude/projects/<encoded-cwd>` directory, newest first — the readings
10284
+ * needed to see when a step change (model switch, hook growth) happened
10285
+ * across sessions without an ad-hoc script.
10286
+ */
10287
+ async function sessionHistory(cwd, limit = 20) {
10288
+ const dir = join(homedir(), ".claude", "projects", encodeDir(cwd));
10289
+ let entries;
10290
+ try {
10291
+ entries = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
10292
+ } catch {
10293
+ return [];
10294
+ }
10295
+ const rows = [];
10296
+ for (const entry of entries) {
10297
+ const path = join(dir, entry);
10298
+ const report = await parseSessionUsage(path);
10299
+ if (report.turns === 0) continue;
10300
+ rows.push({
10301
+ path,
10302
+ sessionId: entry.replace(/\.jsonl$/, ""),
10303
+ firstTurnAt: report.firstTurnAt,
10304
+ firstTurnContext: report.firstTurnContext,
10305
+ turns: report.turns,
10306
+ models: report.models,
10307
+ switches: report.modelSwitches.length,
10308
+ fallbacks: report.fallbacks.length
10309
+ });
10310
+ }
10311
+ rows.sort((a, b) => {
10312
+ if (a.firstTurnAt === null && b.firstTurnAt === null) return 0;
10313
+ if (a.firstTurnAt === null) return 1;
10314
+ if (b.firstTurnAt === null) return -1;
10315
+ return b.firstTurnAt.localeCompare(a.firstTurnAt);
10316
+ });
10317
+ return rows.slice(0, limit);
10318
+ }
10242
10319
 
10243
10320
  //#endregion
10244
10321
  //#region src/audit/spawn.ts
@@ -11026,6 +11103,7 @@ const FIRST_TURN_CONTEXT_LIMIT = 3e4;
11026
11103
  const DAEMON_FAILURE_RATE_LIMIT = .05;
11027
11104
  const CONTEXT_GROWTH_AVG_RED = 1e5;
11028
11105
  const CONTEXT_GROWTH_MAX_AMBER = 15e4;
11106
+ const MODEL_SWITCH_REBUILD_LIMIT = 2e4;
11029
11107
  const SEVERITY_RANK = {
11030
11108
  RED: 0,
11031
11109
  AMBER: 1,
@@ -11152,6 +11230,22 @@ function buildFindings(input) {
11152
11230
  severity: overshoots.length > 0 ? "RED" : "GREEN",
11153
11231
  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
11232
  });
11233
+ const switches = input.session.modelSwitches;
11234
+ const fallbacks = input.session.fallbacks;
11235
+ const rebuilds = switches.filter((s) => s.cacheCreation > MODEL_SWITCH_REBUILD_LIMIT);
11236
+ const short = (model) => model.replace(/^claude-/, "");
11237
+ let switchEvidence;
11238
+ if (switches.length === 0) switchEvidence = `0 switches in ${input.session.turns} turns, models ${JSON.stringify(input.session.models)}`;
11239
+ else {
11240
+ const list = switches.map((s) => `${short(s.from)}->${short(s.to)} at turn ${s.turnIndex} (cache_read ${s.cacheRead}, cache_creation ${s.cacheCreation})`).join("; ");
11241
+ switchEvidence = `${switches.length} switch(es): ${list}`;
11242
+ if (fallbacks.length > 0) switchEvidence += `; ${fallbacks.length} safeguard fallback(s): ${fallbacks.map((f) => f.category).join(",")}`;
11243
+ }
11244
+ findings.push({
11245
+ finding: "mid-session model switches",
11246
+ severity: switches.length === 0 ? "GREEN" : rebuilds.length > 0 ? "RED" : "AMBER",
11247
+ evidence: switchEvidence
11248
+ });
11155
11249
  }
11156
11250
  return sortFindings(findings);
11157
11251
  }
@@ -11701,8 +11795,40 @@ function printSession(report) {
11701
11795
  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
11796
  console.log(`compactions: ${report.compactions.length}`);
11703
11797
  for (const c of report.compactions) console.log(dim$1(` ${c.trigger} at turn ${c.turnIndex}: preTokens=${c.preTokens}`));
11798
+ console.log(`model switches: ${report.modelSwitches.length}`);
11799
+ 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}`));
11800
+ if (report.fallbacks.length > 0) {
11801
+ console.log(`safeguard fallbacks: ${report.fallbacks.length}`);
11802
+ for (const f of report.fallbacks) console.log(dim$1(` ${f.from} -> ${f.to} before turn ${f.turnIndex}: ${f.category} (${f.scope})`));
11803
+ }
11804
+ }
11805
+ function printSessionHistory(rows) {
11806
+ console.log(header("Session history (newest first)"));
11807
+ const tableRows = rows.map((r) => [
11808
+ r.firstTurnAt ?? "n/a",
11809
+ r.sessionId,
11810
+ r.firstTurnContext === null ? "n/a" : String(r.firstTurnContext),
11811
+ String(r.turns),
11812
+ String(r.switches),
11813
+ Object.entries(r.models).map(([name, count]) => `${name}:${count}`).join(",")
11814
+ ]);
11815
+ console.log(renderTable([
11816
+ "first turn (UTC)",
11817
+ "session",
11818
+ "first-turn ctx",
11819
+ "turns",
11820
+ "switches",
11821
+ "models"
11822
+ ], tableRows));
11704
11823
  }
11705
11824
  async function cmdSession(path, opts) {
11825
+ if (opts.history !== void 0) {
11826
+ const limit = typeof opts.history === "string" ? parseInt(opts.history, 10) : 20;
11827
+ const rows = await sessionHistory(process.cwd(), limit);
11828
+ if (opts.json) return printJson(rows);
11829
+ printSessionHistory(rows);
11830
+ return;
11831
+ }
11706
11832
  const report = await auditSession(path, opts.ctxThreshold ? parseInt(opts.ctxThreshold, 10) : void 0);
11707
11833
  if (!report) {
11708
11834
  console.error(err(" No session transcript found."));
@@ -12186,7 +12312,7 @@ function registerAuditCommands(auditCmd) {
12186
12312
  await cmdFiles(paths, jsonOpt());
12187
12313
  });
12188
12314
  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) => {
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) => {
12190
12316
  await cmdSession(path, {
12191
12317
  ...jsonOpt(),
12192
12318
  ...opts
@@ -20064,4 +20190,4 @@ claude() {
20064
20190
 
20065
20191
  //#endregion
20066
20192
  export { drainStdio as n, buildProgram as t };
20067
- //# sourceMappingURL=program-BxpeczCx.mjs.map
20193
+ //# sourceMappingURL=program-Bgv1lFFA.mjs.map