@diegopetrucci/pi-extensions 0.1.78 → 0.1.79

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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/extensions/agent-workflow-audit/index.ts +3 -20
  3. package/extensions/agent-workflow-audit/package.json +1 -1
  4. package/extensions/annotate-git-diff/glimpseui.d.ts +5 -65
  5. package/extensions/annotate-git-diff/package.json +1 -1
  6. package/extensions/annotate-last-message/glimpseui.d.ts +5 -65
  7. package/extensions/annotate-last-message/package.json +1 -1
  8. package/extensions/annotate-last-message/web/index.html +0 -27
  9. package/extensions/code-reviewer/index.ts +0 -34
  10. package/extensions/code-reviewer/package.json +1 -1
  11. package/extensions/context-cap/README.md +9 -9
  12. package/extensions/context-cap/index.ts +3 -3
  13. package/extensions/context-cap/package.json +2 -2
  14. package/extensions/context-inspector/index.ts +2 -46
  15. package/extensions/context-inspector/package.json +1 -1
  16. package/extensions/contrarian/index.ts +1 -27
  17. package/extensions/contrarian/package.json +1 -1
  18. package/extensions/dynamic-context-pruning/README.md +16 -7
  19. package/extensions/dynamic-context-pruning/index.ts +9 -114
  20. package/extensions/dynamic-context-pruning/package.json +1 -1
  21. package/extensions/fast/README.md +16 -9
  22. package/extensions/fast/index.ts +9 -5
  23. package/extensions/fast/package.json +1 -1
  24. package/extensions/git-footer/index.ts +2 -16
  25. package/extensions/git-footer/package.json +1 -1
  26. package/extensions/librarian/index.ts +7 -39
  27. package/extensions/librarian/package.json +1 -1
  28. package/extensions/minimal-footer/README.md +5 -5
  29. package/extensions/minimal-footer/index.ts +3 -17
  30. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  31. package/extensions/minimal-footer/openai-usage.ts +2 -11
  32. package/extensions/minimal-footer/package.json +1 -1
  33. package/extensions/oracle/index.ts +1 -27
  34. package/extensions/oracle/package.json +1 -1
  35. package/extensions/permission-gate/index.ts +31 -63
  36. package/extensions/permission-gate/package.json +1 -1
  37. package/extensions/review/README.md +2 -0
  38. package/extensions/review/package.json +1 -1
  39. package/extensions/triage-comments/index.ts +0 -15
  40. package/extensions/triage-comments/package.json +1 -1
  41. package/package.json +1 -1
@@ -143,7 +143,6 @@ type MinimalMessage = {
143
143
  customType?: string;
144
144
  display?: boolean;
145
145
  summary?: string;
146
- fromId?: string;
147
146
  tokensBefore?: number;
148
147
  command?: string;
149
148
  output?: string;
@@ -165,7 +164,6 @@ type MinimalEntry = {
165
164
  display?: boolean;
166
165
  details?: unknown;
167
166
  summary?: string;
168
- fromId?: string;
169
167
  tokensBefore?: number;
170
168
  firstKeptEntryId?: string;
171
169
  };
@@ -177,7 +175,6 @@ type MinimalProjectionEntry = {
177
175
 
178
176
  type MinimalSessionProjection = {
179
177
  entries: MinimalProjectionEntry[];
180
- messages?: MinimalMessage[];
181
178
  };
182
179
 
183
180
  type AnalysisResult = {
@@ -192,7 +189,6 @@ type Segment = {
192
189
  source: string;
193
190
  role: string;
194
191
  tokens: number;
195
- chars: number;
196
192
  turn: number;
197
193
  sequence: number;
198
194
  entryId?: string;
@@ -211,7 +207,6 @@ type Segment = {
211
207
  type CategoryStat = {
212
208
  id: CategoryId;
213
209
  label: string;
214
- shortLabel: string;
215
210
  color: string;
216
211
  description: string;
217
212
  tokens: number;
@@ -230,13 +225,9 @@ type AggregateStat = {
230
225
 
231
226
  type DatasetStats = {
232
227
  tokens: number;
233
- rawTokens: number;
234
- visibleRawTokens: number;
235
228
  excludedTokens: number;
236
229
  providerDeltaTokens: number;
237
230
  estimatorOverageTokens: number;
238
- reconciliationScale: number;
239
- segmentCount: number;
240
231
  messageCount: number;
241
232
  categories: CategoryStat[];
242
233
  topSegments: Segment[];
@@ -247,8 +238,6 @@ type DatasetStats = {
247
238
 
248
239
  type Dataset = {
249
240
  id: DatasetId;
250
- label: string;
251
- description: string;
252
241
  segments: Segment[];
253
242
  stats: DatasetStats;
254
243
  };
@@ -259,7 +248,6 @@ type RoutedModel = {
259
248
  };
260
249
 
261
250
  type ReportData = {
262
- generatedAt: string;
263
251
  cwd: string;
264
252
  session: {
265
253
  id?: string;
@@ -280,7 +268,6 @@ type ReportData = {
280
268
  percent: number | null;
281
269
  };
282
270
  options: {
283
- redacted: boolean;
284
271
  defaultDataset: DatasetId;
285
272
  };
286
273
  categoryMeta: typeof CATEGORY_META;
@@ -507,7 +494,6 @@ function addTextSegment(
507
494
  source: base.source,
508
495
  role: base.role,
509
496
  tokens,
510
- chars,
511
497
  turn: base.turn ?? state.turn,
512
498
  entryId: base.entryId,
513
499
  timestamp: base.timestamp,
@@ -543,7 +529,6 @@ function addImageSegment(
543
529
  source: base.source,
544
530
  role: base.role,
545
531
  tokens: IMAGE_TOKEN_ESTIMATE,
546
- chars: IMAGE_TOKEN_ESTIMATE * 4,
547
532
  turn: base.turn ?? state.turn,
548
533
  entryId: base.entryId,
549
534
  timestamp: base.timestamp,
@@ -820,7 +805,6 @@ function contextEntryToMessage(entry: MinimalEntry): MinimalMessage | undefined
820
805
  return {
821
806
  role: "branchSummary",
822
807
  summary: entry.summary,
823
- fromId: entry.fromId,
824
808
  timestamp: entry.timestamp ? new Date(entry.timestamp).getTime() : undefined,
825
809
  };
826
810
  }
@@ -916,10 +900,7 @@ function resolveSessionProjection(
916
900
  (!Array.isArray(projection.messages) || !projection.messages.every(isProjectionMessage))
917
901
  )
918
902
  return undefined;
919
- return {
920
- entries,
921
- messages: entries.flatMap((entry) => entry.messages),
922
- };
903
+ return { entries };
923
904
  }
924
905
 
925
906
  function resolveCurrentContextEntries(
@@ -1045,8 +1026,6 @@ function aggregateBy(
1045
1026
 
1046
1027
  function finalizeDataset(
1047
1028
  id: DatasetId,
1048
- label: string,
1049
- description: string,
1050
1029
  segments: Segment[],
1051
1030
  messageCount: number,
1052
1031
  authoritativeTokens: number | null,
@@ -1091,7 +1070,6 @@ function finalizeDataset(
1091
1070
  return {
1092
1071
  id: categoryId,
1093
1072
  label: meta.label,
1094
- shortLabel: meta.shortLabel,
1095
1073
  color: meta.color,
1096
1074
  description: meta.description,
1097
1075
  tokens: Math.round(rawTokens),
@@ -1105,18 +1083,12 @@ function finalizeDataset(
1105
1083
 
1106
1084
  return {
1107
1085
  id,
1108
- label,
1109
- description,
1110
1086
  segments,
1111
1087
  stats: {
1112
1088
  tokens: Math.round(displayTotal),
1113
- rawTokens: Math.round(segments.reduce((sum, segment) => sum + segment.tokens, 0)),
1114
- visibleRawTokens: Math.round(visibleRawTokens),
1115
1089
  excludedTokens: Math.round(excludedTokens),
1116
1090
  providerDeltaTokens,
1117
1091
  estimatorOverageTokens,
1118
- reconciliationScale,
1119
- segmentCount: segments.length,
1120
1092
  messageCount,
1121
1093
  categories,
1122
1094
  topSegments: [...visibleSegments].sort((a, b) => b.tokens - a.tokens).slice(0, 24),
@@ -1192,20 +1164,11 @@ function buildReportData(
1192
1164
  const fullSegments = [...overhead, ...fullAnalysis.segments];
1193
1165
  const currentDataset = finalizeDataset(
1194
1166
  "current",
1195
- "Current model context",
1196
- "What pi is expected to send to the model on the next turn: latest compaction summary plus unsummarized messages, system prompt, and active tool schemas.",
1197
1167
  currentSegments,
1198
1168
  currentAnalysis.messageCount,
1199
1169
  contextTokens,
1200
1170
  );
1201
- const fullDataset = finalizeDataset(
1202
- "full",
1203
- "Full active branch history",
1204
- "Every context-bearing entry on the active branch, including pre-compaction history that may no longer be sent verbatim.",
1205
- fullSegments,
1206
- fullAnalysis.messageCount,
1207
- null,
1208
- );
1171
+ const fullDataset = finalizeDataset("full", fullSegments, fullAnalysis.messageCount, null);
1209
1172
 
1210
1173
  const notes: string[] = [
1211
1174
  "Per-component token counts are local estimates based mostly on characters/4. Providers expose aggregate usage, not exact per-message attribution.",
@@ -1250,7 +1213,6 @@ function buildReportData(
1250
1213
  }
1251
1214
 
1252
1215
  return {
1253
- generatedAt: new Date().toISOString(),
1254
1216
  cwd: options.redact ? "[redacted cwd]" : ctx.cwd,
1255
1217
  session: {
1256
1218
  id: options.redact ? "[redacted session]" : safeCall(() => ctx.sessionManager.getSessionId()),
@@ -1273,7 +1235,6 @@ function buildReportData(
1273
1235
  percent: contextPercent,
1274
1236
  },
1275
1237
  options: {
1276
- redacted: options.redact,
1277
1238
  defaultDataset: options.defaultDataset,
1278
1239
  },
1279
1240
  categoryMeta: CATEGORY_META,
@@ -1368,16 +1329,12 @@ function buildHtml(data: ReportData): string {
1368
1329
  color-scheme: dark;
1369
1330
  --bg: #070a12;
1370
1331
  --panel: rgba(15, 23, 42, 0.72);
1371
- --panel-strong: rgba(15, 23, 42, 0.94);
1372
1332
  --line: rgba(148, 163, 184, 0.18);
1373
1333
  --text: #e5edf7;
1374
1334
  --muted: #94a3b8;
1375
1335
  --dim: #64748b;
1376
1336
  --accent: #8b5cf6;
1377
1337
  --accent-2: #06b6d4;
1378
- --good: #22c55e;
1379
- --warn: #f59e0b;
1380
- --bad: #ef4444;
1381
1338
  --shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
1382
1339
  }
1383
1340
  * { box-sizing: border-box; }
@@ -1588,7 +1545,6 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #cbd5e1;
1588
1545
  for (const [key, value] of Object.entries(attrs)) {
1589
1546
  if (key === 'class') node.className = value;
1590
1547
  else if (key === 'text') node.textContent = value;
1591
- else if (key === 'html') node.innerHTML = value;
1592
1548
  else if (key.startsWith('on') && typeof value === 'function') node.addEventListener(key.slice(2), value);
1593
1549
  else if (value !== undefined && value !== null) node.setAttribute(key, value);
1594
1550
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-context-inspector",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "A pi extension that opens a local HTML dashboard explaining where the current session context is going.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1944,8 +1944,7 @@ function createContrarianExtension(pi: ExtensionAPI, deps: ContrarianExtensionDe
1944
1944
  onUpdate?.(partial);
1945
1945
  };
1946
1946
 
1947
- let lastErrorResult: { ok: false; error: string; details: ContrarianDetails } | undefined;
1948
- for (let index = 0; index < attempts.length; index++) {
1947
+ for (let index = 0; ; index++) {
1949
1948
  const previous = index > 0 ? attempts[index - 1] : undefined;
1950
1949
  const attempt = previous
1951
1950
  ? withFallbackReason(attempts[index], previous)
@@ -1969,7 +1968,6 @@ function createContrarianExtension(pi: ExtensionAPI, deps: ContrarianExtensionDe
1969
1968
  });
1970
1969
  }
1971
1970
 
1972
- lastErrorResult = result;
1973
1971
  const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1974
1972
  if (!canFallBack) {
1975
1973
  return finalizeResult({
@@ -1978,30 +1976,6 @@ function createContrarianExtension(pi: ExtensionAPI, deps: ContrarianExtensionDe
1978
1976
  });
1979
1977
  }
1980
1978
  }
1981
-
1982
- return finalizeResult({
1983
- content: [
1984
- {
1985
- type: "text",
1986
- text: lastErrorResult?.error ?? "Contrarian could not select an available model.",
1987
- },
1988
- ],
1989
- details: lastErrorResult?.details ?? {
1990
- modelRef: "",
1991
- provider: ctx.model?.provider ?? "unknown",
1992
- modelId: "",
1993
- modelName: undefined,
1994
- thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1995
- autoSelected: true,
1996
- selectionReason: "No authenticated models were available to run the contrarian.",
1997
- includeBash: params.includeBash ?? false,
1998
- usage: createEmptyUsage(),
1999
- stderr: "",
2000
- exitCode: 1,
2001
- durationMs: 0,
2002
- cwd: params.cwd ?? ctx.cwd,
2003
- },
2004
- });
2005
1979
  } finally {
2006
1980
  activeRuns.delete(toolCallId);
2007
1981
  updateContrarianUi(ctx, activeRuns);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-contrarian",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "A read-only contrarian subagent extension for pi that stress-tests plans, designs, assumptions, and conclusions.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -278,8 +278,10 @@ Notes:
278
278
  - `gate.breakEvenThreshold`'s default of `22` is calibrated from the
279
279
  representative-corpus benchmark at `cachedPriceRatio` r=0.1 (aggressive
280
280
  prompt caching, the common case). It is **provider/ratio-
281
- dependent**: the optimal threshold rises as caching gets weaker (T=29 at
282
- r=0.25, T=54 at r=0.5, T=58 at r=0.9 on the same corpus).
281
+ dependent**. In the **historical, retired turn-end run**, the optimal
282
+ threshold rose as caching got weaker (T=29 at r=0.25, T=54 at r=0.5, T=58
283
+ at r=0.9); these are not current turn-start results. See
284
+ [`docs/v2-design.md` §4](./docs/v2-design.md#4-benchmark-decision-gate).
283
285
 
284
286
  ## Prompt-cache trade-off
285
287
 
@@ -326,6 +328,8 @@ Options:
326
328
  - `--limit N`: only process the first N session files found.
327
329
  - `--ratio R`: cached-price ratio(s) to model; repeatable and/or
328
330
  comma-separated (e.g. `--ratio 0.1,0.2`). Default: `0.1`.
331
+ - `--sweep-max N`: explicit ceiling for the auto-expanding threshold sweep
332
+ (overrides the corpus-derived ceiling). Default: auto-expand from 30 as needed.
329
333
  - `--json`: emit a full machine-readable JSON dump instead of aligned text.
330
334
  - `--simulate-compression`: additionally SIMULATE v2-style range compression
331
335
  (docs/v2-design.md §1/§4 go/no-go evidence) alongside — never mixed into —
@@ -390,9 +394,11 @@ reports two kinds of numbers:
390
394
  prune point, and the *actual* net benefit that prune would have produced —
391
395
  `realizedNetBenefit = actualRemainingCalls * recurringSaving - penalty`.
392
396
 
393
- It also sweeps the break-even threshold `T` from 1 to 30 and reports the `T`
394
- that maximizes total realized net benefit across all gate-eligible
395
- candidates, both overall and split by idle/mid-loop agent state.
397
+ It also sweeps the break-even threshold `T` from 1 to 30, then keeps doubling
398
+ the range while the best `T` sits at the top edge, up to a corpus-derived
399
+ ceiling (hard cap 500). `--sweep-max N` sets that ceiling explicitly. It
400
+ reports the `T` that maximizes total realized net benefit across all
401
+ gate-eligible candidates, both overall and split by idle/mid-loop agent state.
396
402
 
397
403
  ## Comparison
398
404
 
@@ -428,9 +434,11 @@ net benefit, at sufficient candidate volume, with enough remaining-calls
428
434
  runway per session to plausibly amortize a compression summary's own token
429
435
  cost — see `docs/v2-design.md` §4 for the exact go/no-go bar.
430
436
 
431
- The representative-corpus run of the benchmark harness (over
437
+ The **historical, retired turn-end run** of the benchmark harness (over
432
438
  `~/.the-last-harness/agent/sessions`: 1,390+ session files, 556 gate-eligible
433
- candidates — see the corpus-choice note above) produced:
439
+ candidates — see the corpus-choice note above) produced the following
440
+ figures, preserved as historical context rather than current turn-start
441
+ results (see [`docs/v2-design.md` §4](./docs/v2-design.md#4-benchmark-decision-gate)):
434
442
 
435
443
  - **Candidates and tokens removed by strategy**: `dedupe` 111 candidates /
436
444
  93,330 tokens removed; `error-purge` 187 / 28,191; `superseded-file-ops`
@@ -440,6 +448,7 @@ candidates — see the corpus-choice note above) produced:
440
448
  this corpus is dominated by long, tool-heavy agent sessions rather than
441
449
  short orchestrator sessions.
442
450
 
451
+ The current turn-start re-derivation has 460 gate-eligible candidates.
443
452
  Shipped gate defaults remain `idle=22` / `mid_loop=1` (see
444
453
  [Configuration](#configuration)).
445
454
 
@@ -140,12 +140,6 @@ export function normalizeSessionProjection(value: unknown): MinimalSessionProjec
140
140
  return { entries, messages: entries.flatMap((entry) => entry.messages) };
141
141
  }
142
142
 
143
- export function sessionProjectionToMessages(
144
- projection: MinimalSessionProjection,
145
- ): MinimalMessage[] {
146
- return projection.entries.flatMap((entry) => entry.messages);
147
- }
148
-
149
143
  const EXTENSION_ID = "dynamic-context-pruning";
150
144
  const CONFIG_FILE_NAME = "dynamic-context-pruning.json";
151
145
  const DECISION_ENTRY_TYPE = "dynamic-context-pruning:decision";
@@ -164,37 +158,10 @@ const DEFAULT_RECENT_TURNS = 4;
164
158
  /** Cache cost model default: fraction of full (non-cached) price still paid for cached tokens. */
165
159
  const DEFAULT_CACHED_PRICE_RATIO = 0.1;
166
160
 
167
- /**
168
- * Default net-benefit gate threshold (max amortization calls). Recalibrated
169
- * from the representative-corpus benchmark (~/.the-last-harness/
170
- * agent/sessions, 1,390+ session files, 556 candidates): this is the
171
- * hindsight-optimal break-even T at cachedPriceRatio r=0.1 (aggressive
172
- * prompt caching, the common Anthropic case). IMPORTANT caveats:
173
- * - Provider/ratio-dependent: this optimum shifts higher as caching gets
174
- * weaker (T=29 at r=0.25, T=54 at r=0.5, T=58 at r=0.9 on the same
175
- * corpus) — see docs/v2-design.md section 4 and the extension README.
176
- * - At r=0.1 the realized deterministic savings this threshold buys are
177
- * economically marginal (~20.6k token-units total across the whole
178
- * corpus, i.e. pennies); savings only become material at r>=0.25.
179
- * - Re-derive via `scripts/benchmark.mjs <sessions-dir> --ratio 0.1` if
180
- * the representative corpus changes meaningfully.
181
- */
161
+ /** Default idle net-benefit gate threshold; see README.md and docs/v2-design.md §4. */
182
162
  const DEFAULT_BREAK_EVEN_THRESHOLD = 22;
183
163
 
184
- /**
185
- * Default mid_loop break-even threshold. The package's own
186
- * representative-corpus benchmark (turn-START state definition — the same one
187
- * the runtime `context` handler uses) favored rejecting mid_loop candidates
188
- * at cachedPriceRatio r=0.1 (mid_loop-optimal T=1, total realized
189
- * net benefit ~0.0). This supports a conservative default for the measured
190
- * corpus, not a universal claim that mid_loop pruning cannot pay off.
191
- * The reported 2026-09-08 byte-level capture (a mid_loop prune at T=22
192
- * invalidating 81% of a warm request prefix) illustrates cache invalidation,
193
- * not an inability to amortize it: enough subsequent calls can recoup a
194
- * one-time bust. Idle keeps T=22; turn-start classification does not imply
195
- * the provider cache is cold. Users who want the old behavior can override
196
- * via gate.breakEvenThresholdByState.mid_loop.
197
- */
164
+ /** Default mid_loop threshold; see README.md for the runtime-aligned benchmark rationale. */
198
165
  const DEFAULT_MID_LOOP_BREAK_EVEN_THRESHOLD = 1;
199
166
 
200
167
  /** Tool names never pruned by default: orchestration/subagent-style tools and todo-like state. */
@@ -243,18 +210,7 @@ export interface PruneProtections {
243
210
  }
244
211
 
245
212
  export interface PruneThresholds {
246
- /**
247
- * Minimum raw text characters a fresh AUTOMATIC prune proposal must remove
248
- * (original text chars minus placeholder chars) to be considered at all
249
- * Enforced by `runDynamicContextPruningPipeline` BEFORE the
250
- * net-benefit gate (see `PruneGateConfig`/`gate`, which uses break-even token
251
- * math and is a separate, later check): text-only candidates below this floor
252
- * are dropped up front and never reach gate/batch consideration. Image-bearing
253
- * tool-result candidates bypass this raw-text floor (images receive no
254
- * synthetic character count) and proceed to the token-based gate. Manual
255
- * prunes and already-persisted/replayed decisions always bypass this floor,
256
- * same as the gate. A value of 0 disables the floor (no filtering).
257
- */
213
+ /** Minimum automatic-prune text saving; see README.md Configuration for bypass rules. */
258
214
  minCharsSaved: number;
259
215
  }
260
216
 
@@ -493,29 +449,8 @@ export function estimateTokensForContent(
493
449
  export type TokenEstimator = (text: string) => number;
494
450
 
495
451
  /**
496
- * Classify the agent's current state from the message payload alone:
497
- * this is the runtime-observable definition used both by the live `context`
498
- * event handler and (via re-export) by the offline benchmark, so the two can
499
- * never drift apart.
500
- *
501
- * Walk backwards from the end of `messages`, skipping any message that is
502
- * neither `user`, `assistant`, nor `toolResult` (e.g. system-role bookkeeping
503
- * entries). The first such message found determines the state:
504
- * - `user` (a plain user turn message) => "idle": this call is the FIRST LLM
505
- * call of a turn, i.e. the decision is being made from a turn-start state.
506
- * - `assistant` or `toolResult` (mid-chain) => "mid_loop": the agent is
507
- * iterating (already made at least one call this turn, or is reacting to a
508
- * tool result).
509
- * - No such message found (empty history, or system-only) => "idle" (safe
510
- * default: nothing suggests an in-progress loop).
511
- *
512
- * The benchmark (scripts/benchmark.mjs) reclassifies its candidate labels to
513
- * this SAME definition, reusing this function directly, so the
514
- * defaults below rest on evidence measured with the exact definition the
515
- * runtime observes. (An earlier turn-end-based benchmark definition -- "idle"
516
- * meant "this IS the turn's last assistant message", only knowable in
517
- * hindsight -- has been retired: the runtime cannot know in advance whether a
518
- * call is a turn's last one, so that definition was never runtime-observable.)
452
+ * Shared runtime/benchmark state classifier; see README.md Configuration and
453
+ * docs/v2-design.md §4 for the turn-start definition and retired turn-end analysis.
519
454
  */
520
455
  export function classifyAgentStateFromMessages(messages: MinimalMessage[]): AgentState {
521
456
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -677,7 +612,7 @@ export function isWithinRecencyWindow(index: number, boundaryIndex: number): boo
677
612
  * Used by the error-input purge strategy's "older than N turns" rule; a
678
613
  * message at the very end of the conversation has 0 turns elapsed.
679
614
  */
680
- export function computeTurnsElapsedSince(messages: MinimalMessage[], index: number): number {
615
+ function computeTurnsElapsedSince(messages: MinimalMessage[], index: number): number {
681
616
  let count = 0;
682
617
  for (let i = index + 1; i < messages.length; i++) {
683
618
  if (messages[i].role === "user") count++;
@@ -974,7 +909,7 @@ export function resolvePruneTombstoneState(entries: MinimalSessionEntry[]): Prun
974
909
  return { activeDecisions, activeIdempotencyKeys, restoredKeys, lastDecisionByKey };
975
910
  }
976
911
 
977
- export function buildPlaceholderText(reason: string, charsRemoved: number): string {
912
+ function buildPlaceholderText(reason: string, charsRemoved: number): string {
978
913
  return `[pruned by ${EXTENSION_ID}: ${reason} (${charsRemoved} chars removed)]`;
979
914
  }
980
915
 
@@ -1581,7 +1516,7 @@ export function collectCompletedToolCallOccurrences(
1581
1516
  }
1582
1517
 
1583
1518
  /** Placeholder text for a duplicate tool result whose content was replaced. */
1584
- export function buildDedupePlaceholder(toolName: string, newestToolCallId: string): string {
1519
+ function buildDedupePlaceholder(toolName: string, newestToolCallId: string): string {
1585
1520
  return `[pruned by ${EXTENSION_ID}: duplicate ${toolName} call result; see newest occurrence (call ${newestToolCallId})]`;
1586
1521
  }
1587
1522
 
@@ -1642,46 +1577,6 @@ export const errorPurgeStrategy: PruneStrategy = {
1642
1577
  },
1643
1578
  };
1644
1579
 
1645
- // When the same file path is read and/or written multiple times in a
1646
- // session, older read/write/edit tool *outputs* for that path can become
1647
- // stale relative to a later operation. This strategy proposes replacing
1648
- // those older outputs with a placeholder, per the following CONSERVATIVE,
1649
- // binding rules (do not loosen without an architect decision):
1650
- //
1651
- // 1. A full-file read (no offset/limit args) is superseded by a LATER
1652
- // full-file read of the same normalized path.
1653
- // 2. A partial read (offset/limit args present) is NOT superseded by a
1654
- // later partial read UNLESS the later read's range fully covers the
1655
- // earlier one's range. If range coverage can't be reliably determined
1656
- // from the args (e.g. earlier read has no limit — reads to EOF — but
1657
- // the later read is bounded), skip: do not propose supersession.
1658
- // Full-file reads are represented as the range [1, EOF); comparing
1659
- // that representation via the same coverage check naturally satisfies
1660
- // rule 1 as a special case of rule 2 (see `readRangeCovers`).
1661
- // 3. ANY read of a path (full or partial) is superseded once a LATER
1662
- // *successful* (non-error) write/edit to that same path occurs — the
1663
- // old read output is now known-stale regardless of read-vs-read range
1664
- // comparisons. The placeholder explicitly notes the file has since
1665
- // changed. An ERRORED later write/edit does NOT count (the file may be
1666
- // unchanged; conservative).
1667
- // 4. Older write/edit outputs for a path are superseded by a LATER
1668
- // *successful* write/edit to that same path (again, a later errored
1669
- // write/edit never supersedes). Write/edit tool outputs are usually
1670
- // small, so proposals are only ever emitted when there is a non-empty
1671
- // result to prune; the shared net-benefit gate additionally decides
1672
- // whether the estimated savings are worth the cache-bust cost.
1673
- //
1674
- // The NEWEST operation for a given path (by message order) is never
1675
- // superseded/pruned, regardless of kind — there is nothing later to point
1676
- // the placeholder at.
1677
- //
1678
- // Path extraction is defensive: pi's built-in `read`/`write`/`edit` tools
1679
- // all accept a `path` argument (see packages/coding-agent/src/core/tools/
1680
- // {read,write,edit}.ts); some tool-call renderers also fall back to a
1681
- // legacy `file_path` name, and `filePath` is accepted as a further
1682
- // defensive fallback. `bash` is explicitly OUT of scope: this strategy does
1683
- // not attempt to parse shell commands for file paths.
1684
-
1685
1580
  const SUPERSEDED_FILE_OPS_STRATEGY_ID = "superseded-file-ops";
1686
1581
 
1687
1582
  type FileOpKind = "read" | "write" | "edit";
@@ -2234,7 +2129,7 @@ export function sessionEntriesToMessages(
2234
2129
  entries: MinimalSessionEntry[],
2235
2130
  projection?: MinimalSessionProjection,
2236
2131
  ): MinimalMessage[] {
2237
- if (projection) return sessionProjectionToMessages(projection);
2132
+ if (projection) return projection.entries.flatMap((entry) => entry.messages);
2238
2133
  const messages: MinimalMessage[] = [];
2239
2134
  for (const entry of entries) {
2240
2135
  if (entry.type === "message" && entry.message) messages.push(entry.message);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-dynamic-context-pruning",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "description": "A pi extension that dynamically prunes stale context to keep sessions lean.",
6
6
  "keywords": [
@@ -2,7 +2,7 @@
2
2
 
3
3
  A pi extension that provides `/fast` and `/ultrafast` toggles for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models. It selects the provider-specific Fast-mode request shape from the currently selected model.
4
4
 
5
- For eligible direct OpenAI API models it injects:
5
+ For eligible direct OpenAI API-key models it injects:
6
6
 
7
7
  ```json
8
8
  {
@@ -10,6 +10,14 @@ For eligible direct OpenAI API models it injects:
10
10
  }
11
11
  ```
12
12
 
13
+ For eligible direct OpenAI ChatGPT OAuth models it injects:
14
+
15
+ ```json
16
+ {
17
+ "service_tier": "priority"
18
+ }
19
+ ```
20
+
13
21
  For eligible OpenAI Codex OAuth models it injects:
14
22
 
15
23
  ```json
@@ -39,16 +47,15 @@ Direct OpenAI API Fast mode requires all of the following:
39
47
  - Provider `openai`.
40
48
  - API `openai-responses` or `openai-completions`.
41
49
  - Model `gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-sol`, or `gpt-6-luna`.
42
- - API-key auth or Pi's `openai` ChatGPT OAuth (`Sign in with ChatGPT`).
50
+ - API-key auth sets `service_tier: "fast"`; Pi's `openai` ChatGPT OAuth sets `priority`.
43
51
  - No existing `service_tier` field in the request payload.
44
52
 
45
- The direct OpenAI route stays on OpenAI's direct API contract for both API-key
46
- auth and Pi's `openai` ChatGPT OAuth. Acceptance of `service_tier: "fast"` by
47
- Pi's `openai` ChatGPT OAuth sign-in endpoint has not been live-verified.
48
- This direct route is never forwarded to legacy `openai-codex` or its
49
- subscription-usage endpoint. The documented request shape is support evidence,
50
- not live certification; this change was validated only with mocked eligibility
51
- and request payloads, not live or paid provider calls.
53
+ The direct OpenAI route stays on OpenAI's direct API contract but selects the tier by auth
54
+ type. OAuth uses `priority` because its endpoint rejected `fast`; acceptance or application
55
+ of `priority` on Pi's OAuth endpoint has not been live-verified. This direct route is never
56
+ forwarded to legacy `openai-codex` or its subscription-usage endpoint. The request shape is
57
+ support evidence, not certification; tests use mocked eligibility and payloads only, with
58
+ no live or paid provider calls.
52
59
 
53
60
  ### Ultrafast support (initial, fail-closed)
54
61
 
@@ -155,7 +155,7 @@ function describeMode(state: SessionState): string {
155
155
  return state.config.enabled ? "on (config default)" : "off (config default)";
156
156
  }
157
157
 
158
- function getEligibility(ctx: ExtensionContext): Eligibility {
158
+ function getEligibility(ctx: ExtensionContext, includeServiceTier = true): Eligibility {
159
159
  const model = ctx.model;
160
160
  if (!model) {
161
161
  return { eligible: false, modelKey: "no-model", reason: "no model is selected" };
@@ -237,7 +237,11 @@ function getEligibility(ctx: ExtensionContext): Eligibility {
237
237
  eligible: true,
238
238
  modelKey: key,
239
239
  provider: "openai",
240
- serviceTier: OPENAI_API_FAST_SERVICE_TIER,
240
+ serviceTier: includeServiceTier
241
+ ? ctx.modelRegistry.isUsingOAuth(model)
242
+ ? OPENAI_FAST_SERVICE_TIER
243
+ : OPENAI_API_FAST_SERVICE_TIER
244
+ : undefined,
241
245
  };
242
246
  }
243
247
 
@@ -328,7 +332,7 @@ function updateStatus(ctx: ExtensionContext, state: SessionState): void {
328
332
  return;
329
333
  }
330
334
 
331
- const eligibility = getEligibility(ctx);
335
+ const eligibility = getEligibility(ctx, false);
332
336
  ctx.ui.setStatus(EXTENSION_ID, isFastEnabled(state) && eligibility.eligible ? "fast" : undefined);
333
337
  }
334
338
 
@@ -346,7 +350,7 @@ function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
346
350
  }
347
351
 
348
352
  const enabled = isFastEnabled(state);
349
- const eligibility = getEligibility(ctx);
353
+ const eligibility = getEligibility(ctx, enabled);
350
354
  const active = enabled && eligibility.eligible;
351
355
 
352
356
  if (active) {
@@ -405,7 +409,7 @@ async function injectAnthropicFastHeader(
405
409
  hasActiveTools: boolean,
406
410
  ): Promise<void> {
407
411
  if (state.ultrafast || !isFastEnabled(state)) return;
408
- const eligibility = getEligibility(ctx);
412
+ const eligibility = getEligibility(ctx, false);
409
413
  if (!eligibility.eligible || eligibility.provider !== "anthropic") return;
410
414
 
411
415
  const matchingKeys = Object.keys(headers).filter(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-fast",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "A pi extension that enables Fast mode for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models based on the currently selected model.",
5
5
  "keywords": [
6
6
  "pi-package",