@oh-my-pi/pi-coding-agent 15.11.4 → 15.11.7

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 (98) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/dist/cli.js +520 -451
  3. package/dist/types/cli/bench-cli.d.ts +78 -0
  4. package/dist/types/cli/usage-cli.d.ts +10 -1
  5. package/dist/types/commands/bench.d.ts +29 -0
  6. package/dist/types/commands/usage.d.ts +9 -0
  7. package/dist/types/config/model-resolver.d.ts +3 -2
  8. package/dist/types/config/settings-schema.d.ts +125 -3
  9. package/dist/types/edit/renderer.d.ts +1 -0
  10. package/dist/types/modes/components/oauth-selector.d.ts +10 -1
  11. package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
  12. package/dist/types/modes/components/session-selector.d.ts +1 -1
  13. package/dist/types/modes/components/settings-selector.d.ts +8 -1
  14. package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
  15. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/interactive-mode.d.ts +10 -0
  18. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  19. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
  20. package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
  21. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
  22. package/dist/types/modes/types.d.ts +2 -0
  23. package/dist/types/modes/utils/context-usage.d.ts +6 -1
  24. package/dist/types/session/agent-session.d.ts +14 -1
  25. package/dist/types/session/auth-storage.d.ts +1 -1
  26. package/dist/types/session/codex-auto-reset.d.ts +107 -0
  27. package/dist/types/session/snapcompact-inline.d.ts +107 -4
  28. package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
  29. package/dist/types/task/render.d.ts +1 -0
  30. package/dist/types/tools/bash.d.ts +2 -0
  31. package/dist/types/tools/eval-render.d.ts +1 -0
  32. package/dist/types/tools/renderers.d.ts +13 -0
  33. package/dist/types/tools/ssh.d.ts +1 -0
  34. package/dist/types/tools/todo.d.ts +0 -11
  35. package/package.json +11 -11
  36. package/src/cli/bench-cli.ts +437 -0
  37. package/src/cli/usage-cli.ts +187 -16
  38. package/src/cli-commands.ts +1 -0
  39. package/src/commands/bench.ts +42 -0
  40. package/src/commands/usage.ts +8 -0
  41. package/src/config/model-registry.ts +52 -5
  42. package/src/config/model-resolver.ts +36 -5
  43. package/src/config/settings-schema.ts +148 -3
  44. package/src/config/settings.ts +9 -0
  45. package/src/edit/renderer.ts +5 -0
  46. package/src/hindsight/client.ts +26 -1
  47. package/src/hindsight/state.ts +6 -2
  48. package/src/internal-urls/docs-index.generated.ts +2 -2
  49. package/src/mcp/transports/stdio.ts +81 -7
  50. package/src/modes/components/oauth-selector.ts +67 -7
  51. package/src/modes/components/reset-usage-selector.ts +161 -0
  52. package/src/modes/components/session-selector.ts +8 -2
  53. package/src/modes/components/settings-selector.ts +89 -47
  54. package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
  55. package/src/modes/components/snapcompact-shape-preview.ts +192 -0
  56. package/src/modes/components/tool-execution.ts +26 -0
  57. package/src/modes/components/transcript-container.ts +23 -1
  58. package/src/modes/controllers/command-controller.ts +24 -1
  59. package/src/modes/controllers/input-controller.ts +8 -6
  60. package/src/modes/controllers/selector-controller.ts +72 -2
  61. package/src/modes/interactive-mode.ts +83 -0
  62. package/src/modes/session-observer-registry.ts +61 -3
  63. package/src/modes/setup-wizard/index.ts +1 -0
  64. package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
  65. package/src/modes/setup-wizard/scenes/providers.ts +36 -2
  66. package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
  67. package/src/modes/setup-wizard/scenes/theme.ts +28 -1
  68. package/src/modes/setup-wizard/scenes/types.ts +10 -1
  69. package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
  70. package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
  71. package/src/modes/theme/theme.ts +2 -2
  72. package/src/modes/types.ts +2 -0
  73. package/src/modes/utils/context-usage.ts +75 -1
  74. package/src/prompts/bench.md +7 -0
  75. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  76. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  77. package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
  78. package/src/prompts/tools/browser.md +33 -43
  79. package/src/prompts/tools/eval.md +27 -50
  80. package/src/prompts/tools/irc.md +29 -31
  81. package/src/prompts/tools/read.md +31 -37
  82. package/src/prompts/tools/todo.md +1 -2
  83. package/src/sdk.ts +4 -2
  84. package/src/session/agent-session.ts +136 -6
  85. package/src/session/auth-storage.ts +3 -0
  86. package/src/session/codex-auto-reset.ts +190 -0
  87. package/src/session/snapcompact-inline.ts +404 -75
  88. package/src/slash-commands/builtin-registry.ts +145 -8
  89. package/src/slash-commands/helpers/context-report.ts +28 -1
  90. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  91. package/src/slash-commands/helpers/usage-report.ts +12 -0
  92. package/src/task/index.ts +30 -7
  93. package/src/task/render.ts +34 -19
  94. package/src/tools/bash.ts +3 -0
  95. package/src/tools/eval-render.ts +4 -0
  96. package/src/tools/renderers.ts +13 -0
  97. package/src/tools/ssh.ts +3 -0
  98. package/src/tools/todo.ts +8 -128
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Pure decision predicate for auto-redeeming a saved OpenAI Codex rate-limit
3
+ * reset, plus the process-wide coordinator that serializes attempts.
4
+ *
5
+ * WHY THIS IS REACTIVE-ONLY (never proactive):
6
+ * The only trustworthy "blocked right now" signal is a live 429 /
7
+ * `usage_limit_reached` from a request authenticated as the session's active
8
+ * Codex credential. The session hook calls this predicate from the usage-limit
9
+ * branch of the retry pipeline, *after* free remedies (sibling-account switch)
10
+ * fail and *before* model fallback. A proactive surface (the status-line usage
11
+ * poll) cannot be used: at `used_percent < 100` the account is not actually
12
+ * limited, so redeeming would be a credit-wasting no-op; at exactly 100 the
13
+ * user may be idle, so the freshly-reset weekly window would tick away with
14
+ * nobody working. Saved resets are a scarce, ~monthly, effectively
15
+ * irreversible resource — every gate here is biased to precision over recall:
16
+ * we would rather miss a redeem than waste a credit.
17
+ *
18
+ * THE DECISION-2 TRAP (status MUST NOT be used to find the blocker):
19
+ * `openai-codex.ts` applies the top-level `rate_limit.limit_reached` flag to
20
+ * BOTH the primary (5h) and secondary (weekly) `buildUsageLimit` calls, so when
21
+ * an account is blocked, *both* limit entries carry `status: "exhausted"`
22
+ * regardless of which window is actually at 100%. Only `amount.usedFraction`
23
+ * disambiguates which window is the real blocker. This module therefore keys
24
+ * eligibility off exact limit ids (`openai-codex:primary` /
25
+ * `openai-codex:secondary`) and `usedFraction`, never off `status`.
26
+ *
27
+ * ANTI-WASTE GATES (in evaluation order): the policy must be OFF unless opted
28
+ * in; the active model must be Codex (not Spark — a Spark block lives on a
29
+ * separate meter and it is unknown whether a credit even resets it); a fresh
30
+ * usage report for the active account must confirm `limitReached`; the WEEKLY
31
+ * (secondary) window must be genuinely exhausted — a 5h-only block self-heals
32
+ * within the hour, so a credit spent there buys nothing; the natural reset must be far
33
+ * enough away to justify spending a ~30-day credit yet within one plausible
34
+ * window length; a credit must be verifiably available above the reserve; and
35
+ * the same block episode must not have been attempted already (debounce +
36
+ * per-account cooldown). All of this is pure — no fetches, no IO. The only
37
+ * stateful piece is the {@link CodexAutoRedeemCoordinator} container, whose
38
+ * read-only views are passed in so the predicate itself stays deterministic.
39
+ */
40
+ import type { OAuthAccountIdentity, ResetCreditTarget, UsageReport } from "@oh-my-pi/pi-ai";
41
+ import { reportMatchesActiveAccount } from "../slash-commands/helpers/active-oauth-account";
42
+
43
+ /** Weekly window counts as exhausted at `usedFraction >= 0.999` (used_percent >= 99.9). */
44
+ export const WEEKLY_EXHAUSTED_MIN_FRACTION = 0.999;
45
+ /** A weekly reset can never be more than one window length (7d) away; +1h slack for skew. */
46
+ export const MAX_PLAUSIBLE_REMAINING_MS = 7 * 24 * 3_600_000 + 60 * 60_000;
47
+ /** Report must be no older than the 5-min usage cache TTL plus slack. */
48
+ export const REPORT_FRESHNESS_MS = 10 * 60_000;
49
+ /** Per-account cooldown that catches blockKey drift across a minute boundary. */
50
+ export const ATTEMPT_COOLDOWN_MS = 60_000;
51
+ /** Minute bucket for blockKey, absorbing `reset_after_seconds`-derived jitter. */
52
+ export const DEBOUNCE_BUCKET_MS = 60_000;
53
+
54
+ export type CodexAutoRedeemSkipReason =
55
+ | "disabled"
56
+ | "wrong-provider"
57
+ | "spark-model"
58
+ | "no-identity"
59
+ | "no-report"
60
+ | "stale-report"
61
+ | "not-limit-reached"
62
+ | "weekly-not-exhausted"
63
+ | "no-reset-time"
64
+ | "reset-too-soon"
65
+ | "reset-implausible"
66
+ | "credits-unknown"
67
+ | "reserve"
68
+ | "already-attempted"
69
+ | "cooldown";
70
+
71
+ export interface CodexAutoRedeemInput {
72
+ nowMs: number;
73
+ /** `this.model.provider`. */
74
+ provider: string;
75
+ /** `this.model.id`. */
76
+ modelId: string;
77
+ settings: { autoRedeem: boolean; minBlockedMinutes: number; keepCredits: number };
78
+ /** `getOAuthAccountIdentity("openai-codex", sessionId)`, captured at hook entry before any await. */
79
+ identity: OAuthAccountIdentity | undefined;
80
+ /** `session.fetchUsageReports()` (≤5-min cache). */
81
+ reports: UsageReport[] | null;
82
+ attemptedBlockKeys: ReadonlySet<string>;
83
+ lastAttemptAtByAccount: ReadonlyMap<string, number>;
84
+ }
85
+
86
+ export type CodexAutoRedeemDecision =
87
+ | {
88
+ redeem: true;
89
+ target: ResetCreditTarget;
90
+ accountKey: string;
91
+ blockKey: string;
92
+ weeklyResetAtMs: number;
93
+ remainingMs: number;
94
+ availableCount: number;
95
+ }
96
+ | { redeem: false; reason: CodexAutoRedeemSkipReason };
97
+
98
+ /** Trimmed lowercase, or undefined when blank. Mirrors `normalizeIdentityValue` in active-oauth-account.ts. */
99
+ function normalize(value: unknown): string | undefined {
100
+ return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
101
+ }
102
+
103
+ /**
104
+ * Decide whether to auto-redeem a saved Codex reset for the active account.
105
+ *
106
+ * Pure: every gate below is a function of the snapshot inputs only. Order
107
+ * matters — cheapest / most-decisive gates first so the common "not eligible"
108
+ * paths short-circuit before any account/report matching.
109
+ */
110
+ export function evaluateCodexAutoRedeem(input: CodexAutoRedeemInput): CodexAutoRedeemDecision {
111
+ const { nowMs, settings } = input;
112
+ if (!settings.autoRedeem) return { redeem: false, reason: "disabled" };
113
+ if (input.provider !== "openai-codex") return { redeem: false, reason: "wrong-provider" };
114
+ // Unknown #1: it is unknown whether a credit resets the separate Spark meter.
115
+ if (input.modelId.includes("-spark")) return { redeem: false, reason: "spark-model" };
116
+
117
+ const accountKey = normalize(input.identity?.accountId) ?? normalize(input.identity?.email);
118
+ if (!accountKey) return { redeem: false, reason: "no-identity" };
119
+
120
+ const report = input.reports?.find(
121
+ r => r.provider === "openai-codex" && reportMatchesActiveAccount(r, input.identity),
122
+ );
123
+ if (!report) return { redeem: false, reason: "no-report" };
124
+ if (nowMs - report.fetchedAt > REPORT_FRESHNESS_MS) return { redeem: false, reason: "stale-report" };
125
+ // The wire's own blocked flag must confirm the 429.
126
+ if (report.metadata?.limitReached !== true) return { redeem: false, reason: "not-limit-reached" };
127
+
128
+ // EXACT ids — never `status` (see the Decision-2 trap in the module docs).
129
+ // The saved reset applies to the WEEKLY window, so that is the blocker we act
130
+ // on. A 5h-only block (weekly still has headroom) self-heals within the hour,
131
+ // so spending a scarce ~monthly credit there would be wasted.
132
+ const weekly = report.limits.find(l => l.id === "openai-codex:secondary");
133
+ const wUsed = weekly?.amount.usedFraction;
134
+ if (!weekly || wUsed === undefined || wUsed < WEEKLY_EXHAUSTED_MIN_FRACTION) {
135
+ return { redeem: false, reason: "weekly-not-exhausted" };
136
+ }
137
+
138
+ const resetsAt = weekly.window?.resetsAt;
139
+ if (resetsAt === undefined) return { redeem: false, reason: "no-reset-time" };
140
+ const remainingMs = resetsAt - nowMs;
141
+ // anti-waste: too close to the natural reset — let it roll over instead of spending a credit.
142
+ if (remainingMs < settings.minBlockedMinutes * 60_000) return { redeem: false, reason: "reset-too-soon" };
143
+ if (remainingMs > MAX_PLAUSIBLE_REMAINING_MS) return { redeem: false, reason: "reset-implausible" };
144
+
145
+ const available = report.resetCredits?.availableCount;
146
+ // can't verify availability from the snapshot → don't spend (precision over recall).
147
+ if (available === undefined) return { redeem: false, reason: "credits-unknown" };
148
+ if (available - Math.max(0, Math.trunc(settings.keepCredits)) < 1) {
149
+ return { redeem: false, reason: "reserve" };
150
+ }
151
+
152
+ const blockKey = `${accountKey}|${Math.round(resetsAt / DEBOUNCE_BUCKET_MS)}`;
153
+ if (input.attemptedBlockKeys.has(blockKey)) return { redeem: false, reason: "already-attempted" };
154
+ const lastAt = input.lastAttemptAtByAccount.get(accountKey);
155
+ if (lastAt !== undefined && nowMs - lastAt < ATTEMPT_COOLDOWN_MS) return { redeem: false, reason: "cooldown" };
156
+
157
+ return {
158
+ redeem: true,
159
+ target: { accountId: input.identity?.accountId, email: input.identity?.email },
160
+ accountKey,
161
+ blockKey,
162
+ weeklyResetAtMs: resetsAt,
163
+ remainingMs,
164
+ availableCount: available,
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Process-wide (NOT per-session) coordinator state. Parallel subagent sessions
170
+ * share the same Codex accounts and must not race a double-spend, so this is a
171
+ * single shared container, not a per-session field.
172
+ *
173
+ * - `attemptedBlockKeys`: one attempt EVER per block episode, regardless of
174
+ * outcome — recorded before calling the consume so exceptions can't re-enter.
175
+ * - `lastAttemptAtByAccount`: per-account cooldown timestamps (epoch ms),
176
+ * catching blockKey drift across a minute boundary.
177
+ * - `inFlightByAccount`: serializes per account — a second session for the same
178
+ * account adopts the in-flight promise instead of starting a second consume.
179
+ */
180
+ export interface CodexAutoRedeemCoordinator {
181
+ attemptedBlockKeys: Set<string>;
182
+ lastAttemptAtByAccount: Map<string, number>;
183
+ inFlightByAccount: Map<string, Promise<boolean>>;
184
+ }
185
+
186
+ export const defaultCodexAutoRedeemCoordinator: CodexAutoRedeemCoordinator = {
187
+ attemptedBlockKeys: new Set(),
188
+ lastAttemptAtByAccount: new Map(),
189
+ inFlightByAccount: new Map(),
190
+ };