@diegopetrucci/pi-extensions 0.1.78 → 0.1.80

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 (46) 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/prompt.ts +2 -8
  9. package/extensions/annotate-last-message/web/index.html +0 -27
  10. package/extensions/code-reviewer/index.ts +2 -46
  11. package/extensions/code-reviewer/package.json +1 -1
  12. package/extensions/context-cap/README.md +9 -9
  13. package/extensions/context-cap/index.ts +3 -3
  14. package/extensions/context-cap/package.json +2 -2
  15. package/extensions/context-inspector/index.ts +2 -46
  16. package/extensions/context-inspector/package.json +1 -1
  17. package/extensions/contrarian/index.ts +5 -55
  18. package/extensions/contrarian/package.json +1 -1
  19. package/extensions/dynamic-context-pruning/README.md +24 -7
  20. package/extensions/dynamic-context-pruning/index.ts +21 -130
  21. package/extensions/dynamic-context-pruning/package.json +1 -1
  22. package/extensions/fast/README.md +16 -9
  23. package/extensions/fast/index.ts +9 -5
  24. package/extensions/fast/package.json +1 -1
  25. package/extensions/git-footer/index.ts +2 -16
  26. package/extensions/git-footer/package.json +1 -1
  27. package/extensions/librarian/index.ts +8 -46
  28. package/extensions/librarian/package.json +1 -1
  29. package/extensions/minimal-footer/README.md +5 -5
  30. package/extensions/minimal-footer/index.ts +3 -17
  31. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  32. package/extensions/minimal-footer/openai-usage.ts +2 -11
  33. package/extensions/minimal-footer/package.json +1 -1
  34. package/extensions/oracle/index.ts +5 -55
  35. package/extensions/oracle/package.json +1 -1
  36. package/extensions/permission-gate/index.ts +31 -63
  37. package/extensions/permission-gate/package.json +1 -1
  38. package/extensions/project-mcp-json/config.ts +2 -1
  39. package/extensions/project-mcp-json/package.json +1 -1
  40. package/extensions/quiet-tools/index.ts +15 -6
  41. package/extensions/quiet-tools/package.json +1 -1
  42. package/extensions/review/README.md +2 -0
  43. package/extensions/review/package.json +1 -1
  44. package/extensions/triage-comments/index.ts +1 -22
  45. package/extensions/triage-comments/package.json +1 -1
  46. package/package.json +4 -4
@@ -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
 
@@ -287,13 +243,15 @@ export interface PruneGateConfig {
287
243
  /** Fraction of full price still paid for cached ( r ). Default 0.1. */
288
244
  cachedPriceRatio: number;
289
245
  /**
290
- * Default break-even threshold (max future calls to amortize a cache
291
- * bust) used when no state-specific override applies. Recalibrated from
292
- * the representative-corpus benchmark; see DEFAULT_BREAK_EVEN_THRESHOLD
293
- * for the full rationale and ratio-sensitivity caveats.
246
+ * @deprecated Legacy global fallback retained for direct callers that pass
247
+ * an unnormalized gate object to resolveBreakEvenThreshold. normalizeConfig
248
+ * always supplies per-state values, so loaded config does not use this field
249
+ * to fill missing or invalid state values. Recalibrated from the
250
+ * representative-corpus benchmark; see DEFAULT_BREAK_EVEN_THRESHOLD for the
251
+ * full rationale and ratio-sensitivity caveats.
294
252
  */
295
253
  breakEvenThreshold: number;
296
- /** Per-state overrides. Defaults: idle 22, mid_loop 1. */
254
+ /** Per-state overrides. normalizeConfig defaults to idle 22 and mid_loop 1. */
297
255
  breakEvenThresholdByState: { idle: number; mid_loop: number };
298
256
  }
299
257
 
@@ -493,29 +451,8 @@ export function estimateTokensForContent(
493
451
  export type TokenEstimator = (text: string) => number;
494
452
 
495
453
  /**
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.)
454
+ * Shared runtime/benchmark state classifier; see README.md Configuration and
455
+ * docs/v2-design.md §4 for the turn-start definition and retired turn-end analysis.
519
456
  */
520
457
  export function classifyAgentStateFromMessages(messages: MinimalMessage[]): AgentState {
521
458
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -677,7 +614,7 @@ export function isWithinRecencyWindow(index: number, boundaryIndex: number): boo
677
614
  * Used by the error-input purge strategy's "older than N turns" rule; a
678
615
  * message at the very end of the conversation has 0 turns elapsed.
679
616
  */
680
- export function computeTurnsElapsedSince(messages: MinimalMessage[], index: number): number {
617
+ function computeTurnsElapsedSince(messages: MinimalMessage[], index: number): number {
681
618
  let count = 0;
682
619
  for (let i = index + 1; i < messages.length; i++) {
683
620
  if (messages[i].role === "user") count++;
@@ -974,7 +911,7 @@ export function resolvePruneTombstoneState(entries: MinimalSessionEntry[]): Prun
974
911
  return { activeDecisions, activeIdempotencyKeys, restoredKeys, lastDecisionByKey };
975
912
  }
976
913
 
977
- export function buildPlaceholderText(reason: string, charsRemoved: number): string {
914
+ function buildPlaceholderText(reason: string, charsRemoved: number): string {
978
915
  return `[pruned by ${EXTENSION_ID}: ${reason} (${charsRemoved} chars removed)]`;
979
916
  }
980
917
 
@@ -1581,7 +1518,7 @@ export function collectCompletedToolCallOccurrences(
1581
1518
  }
1582
1519
 
1583
1520
  /** Placeholder text for a duplicate tool result whose content was replaced. */
1584
- export function buildDedupePlaceholder(toolName: string, newestToolCallId: string): string {
1521
+ function buildDedupePlaceholder(toolName: string, newestToolCallId: string): string {
1585
1522
  return `[pruned by ${EXTENSION_ID}: duplicate ${toolName} call result; see newest occurrence (call ${newestToolCallId})]`;
1586
1523
  }
1587
1524
 
@@ -1642,46 +1579,6 @@ export const errorPurgeStrategy: PruneStrategy = {
1642
1579
  },
1643
1580
  };
1644
1581
 
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
1582
  const SUPERSEDED_FILE_OPS_STRATEGY_ID = "superseded-file-ops";
1686
1583
 
1687
1584
  type FileOpKind = "read" | "write" | "edit";
@@ -1736,11 +1633,6 @@ export function computeFileReadRange(args: Record<string, unknown> | undefined):
1736
1633
  return { start, end };
1737
1634
  }
1738
1635
 
1739
- /** True when a range represents a full-file read (starts at line 1, no limit). */
1740
- export function isFullFileReadRange(range: FileReadRange): boolean {
1741
- return range.start === 1 && range.end === undefined;
1742
- }
1743
-
1744
1636
  /**
1745
1637
  * Conservative range-coverage check: does `later` fully cover `earlier`?
1746
1638
  * Implements rules 1+2 above via one unified representation (a full-file
@@ -2234,7 +2126,7 @@ export function sessionEntriesToMessages(
2234
2126
  entries: MinimalSessionEntry[],
2235
2127
  projection?: MinimalSessionProjection,
2236
2128
  ): MinimalMessage[] {
2237
- if (projection) return sessionProjectionToMessages(projection);
2129
+ if (projection) return projection.entries.flatMap((entry) => entry.messages);
2238
2130
  const messages: MinimalMessage[] = [];
2239
2131
  for (const entry of entries) {
2240
2132
  if (entry.type === "message" && entry.message) messages.push(entry.message);
@@ -2373,11 +2265,9 @@ const STRATEGY_ALIASES: Record<string, StrategyKey> = {
2373
2265
  "error-purge": "errorPurge",
2374
2266
  errorpurge: "errorPurge",
2375
2267
  error_purge: "errorPurge",
2376
- errorPurge: "errorPurge",
2377
2268
  "superseded-file-ops": "supersededFileOps",
2378
2269
  supersededfileops: "supersededFileOps",
2379
2270
  superseded_file_ops: "supersededFileOps",
2380
- supersededFileOps: "supersededFileOps",
2381
2271
  };
2382
2272
 
2383
2273
  const STRATEGY_DISPLAY_NAMES: Record<StrategyKey, string> = {
@@ -2420,9 +2310,11 @@ export function parseContextPruningArgs(rawArgs: string): ContextPruningSubcomma
2420
2310
  return { kind: "toggle" };
2421
2311
  case "strategy": {
2422
2312
  const [nameRaw, valueRaw] = rest;
2423
- const key = nameRaw
2424
- ? (STRATEGY_ALIASES[nameRaw.toLowerCase()] ?? STRATEGY_ALIASES[nameRaw])
2425
- : undefined;
2313
+ const normalizedName = nameRaw?.toLowerCase();
2314
+ const key =
2315
+ normalizedName && Object.hasOwn(STRATEGY_ALIASES, normalizedName)
2316
+ ? STRATEGY_ALIASES[normalizedName]
2317
+ : undefined;
2426
2318
  const value = valueRaw?.toLowerCase();
2427
2319
  if (!key || (value !== "on" && value !== "off")) return { kind: "unknown", raw: rawArgs };
2428
2320
  return { kind: "strategy", strategy: key, value: value === "on" };
@@ -2568,7 +2460,6 @@ function notify(
2568
2460
  }
2569
2461
 
2570
2462
  export const __testing = {
2571
- globToRegExp,
2572
2463
  getConfigPath,
2573
2464
  readConfig,
2574
2465
  writeConfig,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-dynamic-context-pruning",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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",
@@ -13,10 +13,6 @@ type GitStatusSnapshot = {
13
13
 
14
14
  type PullRequestSnapshot = {
15
15
  number?: number | string;
16
- state?: string;
17
- isDraft?: boolean;
18
- url?: string;
19
- title?: string;
20
16
  };
21
17
 
22
18
  type CommandResult = {
@@ -74,7 +70,7 @@ const GIT_STATUS_ARGS = [
74
70
  "--porcelain=v2",
75
71
  "--branch",
76
72
  ] as const;
77
- const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number,state,isDraft,url,title"] as const;
73
+ const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
78
74
 
79
75
  function createEmptyGitStatus(): GitStatusSnapshot {
80
76
  return {
@@ -202,10 +198,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
202
198
  if (typeof record.number === "number" || typeof record.number === "string") {
203
199
  snapshot.number = record.number;
204
200
  }
205
- if (typeof record.state === "string") snapshot.state = record.state;
206
- if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
207
- if (typeof record.url === "string") snapshot.url = record.url;
208
- if (typeof record.title === "string") snapshot.title = record.title;
209
201
  return snapshot;
210
202
  }
211
203
 
@@ -228,13 +220,7 @@ function pullRequestSnapshotsEqual(
228
220
  left: PullRequestSnapshot | undefined,
229
221
  right: PullRequestSnapshot | undefined,
230
222
  ): boolean {
231
- return (
232
- left?.number === right?.number &&
233
- left?.state === right?.state &&
234
- left?.isDraft === right?.isDraft &&
235
- left?.url === right?.url &&
236
- left?.title === right?.title
237
- );
223
+ return left?.number === right?.number;
238
224
  }
239
225
 
240
226
  function defaultRunner(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-git-footer",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "A TLH-style git status add-on for pi's built-in footer.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -766,11 +766,6 @@ async function writeLibrarianPreferences(preferences: LibrarianPreferences): Pro
766
766
  await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
767
767
  }
768
768
 
769
- async function writeCachePreference(preference: CacheMode): Promise<void> {
770
- const preferences = await readLibrarianPreferences();
771
- await writeLibrarianPreferences({ ...preferences, cacheMode: preference });
772
- }
773
-
774
769
  function resolveCacheDecision(preference: CacheMode): { enabled: boolean; reason: string } {
775
770
  if (preference === "enabled") {
776
771
  return { enabled: true, reason: "cache preference enabled; using cached local checkouts" };
@@ -1045,7 +1040,7 @@ function tryCurlyQuotePathVariant(filePath: string): string {
1045
1040
  }
1046
1041
  /**
1047
1042
  * Returns all read path candidates including Pi's resolveReadPathAsync fallback
1048
- * variants. Used only for the read tool guard; grep/find/ls use resolveToCwd only.
1043
+ * variants.
1049
1044
  */
1050
1045
  function getReadPathCandidates(resolved: string): string[] {
1051
1046
  const nfd = tryNFDPathVariant(resolved);
@@ -1058,11 +1053,6 @@ function getReadPathCandidates(resolved: string): string[] {
1058
1053
  ];
1059
1054
  }
1060
1055
 
1061
- function resolveToolPath(cwd: string, rawPath: string): string {
1062
- const normalized = rawPath.startsWith("@") ? rawPath.slice(1) : rawPath;
1063
- return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
1064
- }
1065
-
1066
1056
  function getBlockedBashReason(
1067
1057
  command: string,
1068
1058
  options: { workspace: string; cacheRoot: string; cacheEnabled: boolean },
@@ -1165,17 +1155,9 @@ function createLibrarianRuntimeGuardExtension(options: {
1165
1155
  (options.cacheEnabled && (isInside(options.cacheRoot, p) || isInside(realCacheRoot, p)))
1166
1156
  );
1167
1157
  }
1168
- const resolved = resolveToolPath(options.workspace, input.path);
1169
- const realPath = await fs.realpath(resolved).catch(() => resolved);
1170
- if (!isAllowedPath(realPath))
1171
- return {
1172
- block: true,
1173
- reason: `Librarian read is limited to its workspace/cache: ${realPath}`,
1174
- };
1175
- // Pi-effective path check (monotonic: block if either resolution is
1176
- // outside). For the read tool, also check Pi's resolveReadPathAsync
1177
- // fallback variants so a missing file whose variant resolves outside is
1178
- // not silently allowed.
1158
+ // For the read tool, also check Pi's resolveReadPathAsync fallback
1159
+ // variants so a missing file whose variant resolves outside is not
1160
+ // silently allowed.
1179
1161
  try {
1180
1162
  const piResolved = resolveToCwd(input.path, options.workspace);
1181
1163
  const candidates = getReadPathCandidates(piResolved);
@@ -1293,12 +1275,6 @@ function addAssistantMessageUsage(total: Usage, message: unknown): void {
1293
1275
  addUsage(total, assistant.usage as Usage);
1294
1276
  }
1295
1277
 
1296
- function aggregateAssistantUsage(messages: unknown[]): Usage {
1297
- const total = createEmptyUsage();
1298
- for (const message of messages) addAssistantMessageUsage(total, message);
1299
- return total;
1300
- }
1301
-
1302
1278
  function addSessionEventUsage(total: Usage, event: unknown): void {
1303
1279
  const sessionEvent = event as { type?: string; message?: unknown; result?: { usage?: Usage } };
1304
1280
  if (sessionEvent?.type === "message_end")
@@ -1481,8 +1457,7 @@ export const __test__ = {
1481
1457
  isModelAvailabilityError,
1482
1458
  parseModelPreference,
1483
1459
  resolveThinkingLevel,
1484
- aggregateAssistantUsage,
1485
- addAssistantMessageUsage,
1460
+ createEmptyUsage,
1486
1461
  addSessionEventUsage,
1487
1462
  inspectFinalAssistant,
1488
1463
  classifyRunFailure,
@@ -1490,14 +1465,12 @@ export const __test__ = {
1490
1465
  createLibrarianAbortController,
1491
1466
  cleanupExpiredCache,
1492
1467
  cleanupExpiredRepos,
1493
- cleanupLockOwnershipError,
1494
1468
  tryAcquireCleanupLock,
1495
1469
  heartbeatCleanupLock,
1496
1470
  releaseCleanupLock,
1497
1471
  CLEANUP_LOCK_FILE,
1498
1472
  CLEANUP_LOCK_STALE_MS,
1499
1473
  CLEANUP_LOCK_MAX_LEASE_MS,
1500
- isCleanupLockLeaseExpired,
1501
1474
  };
1502
1475
 
1503
1476
  export default function librarianExtension(pi: ExtensionAPI) {
@@ -1668,17 +1641,6 @@ export default function librarianExtension(pi: ExtensionAPI) {
1668
1641
  const cacheRoot = getUserCacheReposRoot();
1669
1642
  const configPath = getCacheConfigPath();
1670
1643
 
1671
- const setPreference = async (mode: CacheMode): Promise<string | undefined> => {
1672
- cachePreference = mode;
1673
- try {
1674
- await writeCachePreference(mode);
1675
- return undefined;
1676
- } catch (error) {
1677
- const message = error instanceof Error ? error.message : String(error);
1678
- return `Preference changed for this process, but could not save ${configPath}: ${message}`;
1679
- }
1680
- };
1681
-
1682
1644
  const formatSetMessage = (mode: CacheMode, warning?: string): string => {
1683
1645
  const main =
1684
1646
  mode === "enabled"
@@ -1688,20 +1650,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
1688
1650
  };
1689
1651
 
1690
1652
  if (action === "on" || action === "enable") {
1691
- const warning = await setPreference("enabled");
1653
+ const warning = await savePreferences({ ...currentPreferences(), cacheMode: "enabled" });
1692
1654
  notifyCommand(ctx, formatSetMessage("enabled", warning), warning ? "warning" : "info");
1693
1655
  return;
1694
1656
  }
1695
1657
 
1696
1658
  if (action === "off" || action === "disable") {
1697
- const warning = await setPreference("disabled");
1659
+ const warning = await savePreferences({ ...currentPreferences(), cacheMode: "disabled" });
1698
1660
  notifyCommand(ctx, formatSetMessage("disabled", warning), warning ? "warning" : "info");
1699
1661
  return;
1700
1662
  }
1701
1663
 
1702
1664
  if (action === "toggle") {
1703
1665
  const next = cachePreference === "enabled" ? "disabled" : "enabled";
1704
- const warning = await setPreference(next);
1666
+ const warning = await savePreferences({ ...currentPreferences(), cacheMode: next });
1705
1667
  notifyCommand(ctx, formatSetMessage(next, warning), warning ? "warning" : "info");
1706
1668
  return;
1707
1669
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-librarian",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "A pi GitHub research scout with a toggleable local repo checkout cache under the user's OS cache directory.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -10,7 +10,7 @@ It replaces pi's built-in footer with a cleaner two-line layout that focuses on
10
10
  - TLH-style git dirty counts, ahead/behind counts, and optional PR number
11
11
  - current repo name
12
12
  - current context percentage
13
- - red `DUMB ZONE` indicator when context usage is above 300k tokens
13
+ - red `DUMB ZONE` indicator when context usage is above 350k tokens
14
14
  - current model and thinking level
15
15
  - OpenAI Codex usage windows, labeled from the durations reported by OpenAI
16
16
  - `xp` marker when Pi experimental features are enabled
@@ -39,7 +39,7 @@ main · +1 ~2 ?3 ↑1 • PR #42
39
39
 
40
40
  The markers are `!` conflicts, `+` staged, `~` unstaged, `?` untracked, `↑` ahead, and `↓` behind. PR numbers are fetched best-effort with `gh pr view`. Git polling is skipped until Pi reports that the project is trusted, and `git status` is run with fsmonitor disabled.
41
41
 
42
- When context usage is above 300k tokens, the bottom-left line includes a red warning:
42
+ When context usage is above 350k tokens, the bottom-left line includes a red warning:
43
43
 
44
44
  ```text
45
45
  44.1% · DUMB ZONE
@@ -115,7 +115,7 @@ Example:
115
115
  "showPercent": true,
116
116
  "dumbZone": {
117
117
  "enabled": true,
118
- "thresholdTokens": 300000,
118
+ "thresholdTokens": 350000,
119
119
  "label": "DUMB ZONE",
120
120
  "color": "error"
121
121
  }
@@ -231,7 +231,7 @@ Disable git dirty/ahead/PR status:
231
231
 
232
232
  - **Top left:** current git branch plus dirty/ahead/PR status when available
233
233
  - **Top right:** current repo directory name
234
- - **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above 300k context tokens
234
+ - **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above 350k context tokens
235
235
  - **Bottom left on `openai-codex`:** current context usage percentage plus the Codex usage windows reported for the account
236
236
  - **Bottom left on `openai` ChatGPT OAuth:** current context usage percentage plus `usage unsupported`; no legacy WHAM request is attempted
237
237
  - **Bottom left with `PI_EXPERIMENTAL=1`:** current context usage percentage plus `xp`
@@ -246,7 +246,7 @@ This extension also lives inside the broader [`pi-extensions`](../../README.md)
246
246
  - Replaces pi's built-in footer entirely.
247
247
  - Uses pi footer data for git branch updates, plus background cached git/gh checks for dirty counts and PR number after project trust is granted.
248
248
  - Shows only context percentage, not context window size.
249
- - Shows `DUMB ZONE` only while context usage is above 300k tokens.
249
+ - Shows `DUMB ZONE` only while context usage is above 350k tokens.
250
250
  - Shows the model id rather than a provider-specific display label.
251
251
  - Shows `xp` when `PI_EXPERIMENTAL=1`.
252
252
  - For `openai-codex`, reads pi's stored OAuth login and fetches usage from ChatGPT's backend usage endpoint. Window labels are inferred from each window's reported duration; missing or unrecognized durations use neutral `usage` labels rather than guessing from window position.
@@ -24,7 +24,7 @@ const DEFAULT_CONFIG: MinimalFooterConfig = {
24
24
  showPercent: true,
25
25
  dumbZone: {
26
26
  enabled: true,
27
- thresholdTokens: 300_000,
27
+ thresholdTokens: 350_000,
28
28
  label: "DUMB ZONE",
29
29
  color: "error",
30
30
  },
@@ -148,10 +148,6 @@ type GitStatusSnapshot = {
148
148
 
149
149
  type PullRequestSnapshot = {
150
150
  number?: number | string;
151
- state?: string;
152
- isDraft?: boolean;
153
- url?: string;
154
- title?: string;
155
151
  };
156
152
 
157
153
  type CommandResult = {
@@ -200,7 +196,7 @@ const GIT_STATUS_ARGS = [
200
196
  "--porcelain=v2",
201
197
  "--branch",
202
198
  ] as const;
203
- const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number,state,isDraft,url,title"] as const;
199
+ const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
204
200
 
205
201
  function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
206
202
  if (!existsSync(path)) return {};
@@ -468,10 +464,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
468
464
  if (typeof record.number === "number" || typeof record.number === "string") {
469
465
  snapshot.number = record.number;
470
466
  }
471
- if (typeof record.state === "string") snapshot.state = record.state;
472
- if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
473
- if (typeof record.url === "string") snapshot.url = record.url;
474
- if (typeof record.title === "string") snapshot.title = record.title;
475
467
  return snapshot;
476
468
  }
477
469
 
@@ -494,13 +486,7 @@ function pullRequestSnapshotsEqual(
494
486
  left: PullRequestSnapshot | undefined,
495
487
  right: PullRequestSnapshot | undefined,
496
488
  ): boolean {
497
- return (
498
- left?.number === right?.number &&
499
- left?.state === right?.state &&
500
- left?.isDraft === right?.isDraft &&
501
- left?.url === right?.url &&
502
- left?.title === right?.title
503
- );
489
+ return left?.number === right?.number;
504
490
  }
505
491
 
506
492
  function defaultRunner(