pi-harness-runtime 1.1.430 → 1.1.431

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 (123) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/durable-review/package.json +1 -1
  30. package/packages/e2e-tester/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-sink/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/jev-judge/package.json +1 -1
  46. package/packages/knowledge-graph/package.json +1 -1
  47. package/packages/knowledge-retrieval/package.json +1 -1
  48. package/packages/laravel-plugin/package.json +1 -1
  49. package/packages/learning-engine/package.json +1 -1
  50. package/packages/logging/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/package.json +1 -1
  57. package/packages/observability/package.json +1 -1
  58. package/packages/okf-indexer/package.json +1 -1
  59. package/packages/paperclip-integration/package.json +1 -1
  60. package/packages/performance-optimizer/package.json +1 -1
  61. package/packages/permission-mode/package.json +1 -1
  62. package/packages/privilege-broker/package.json +1 -1
  63. package/packages/project-analyzer/package.json +1 -1
  64. package/packages/project-bootstrap/package.json +1 -1
  65. package/packages/projection-engine/package.json +1 -1
  66. package/packages/prompt-compiler/package.json +1 -1
  67. package/packages/prompt-versioning/package.json +1 -1
  68. package/packages/provider-adapter-sdk/package.json +1 -1
  69. package/packages/provider-router/package.json +1 -1
  70. package/packages/provider-selector/package.json +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/qdrant-skills/package.json +1 -1
  73. package/packages/quota-manager/package.json +1 -1
  74. package/packages/rate-limiter/package.json +1 -1
  75. package/packages/react-vite-plugin/package.json +1 -1
  76. package/packages/relay/package.json +1 -1
  77. package/packages/release-manager/package.json +1 -1
  78. package/packages/requirement-compiler/package.json +1 -1
  79. package/packages/runtime/package.json +1 -1
  80. package/packages/scheduler/package.json +1 -1
  81. package/packages/scheduler-adapter/package.json +1 -1
  82. package/packages/session/package.json +1 -1
  83. package/packages/session-api/package.json +1 -1
  84. package/packages/session-export/package.json +1 -1
  85. package/packages/shared-context/package.json +1 -1
  86. package/packages/skill-mcp-client/package.json +1 -1
  87. package/packages/skill-registry/package.json +1 -1
  88. package/packages/skills/package.json +1 -1
  89. package/packages/sprint-planner/package.json +1 -1
  90. package/packages/subscription-engine/package.json +1 -1
  91. package/packages/task-compiler/package.json +1 -1
  92. package/packages/tencentdb-memory/package.json +1 -1
  93. package/packages/tencentdb-sync/package.json +1 -1
  94. package/packages/test-data-generator/package.json +1 -1
  95. package/packages/test-generator/package.json +1 -1
  96. package/packages/todo-bd-sync/package.json +1 -1
  97. package/packages/token-estimation/package.json +1 -1
  98. package/packages/token-optimizer/package.json +1 -1
  99. package/packages/tooling-preference/package.json +1 -1
  100. package/packages/trajectory/package.json +1 -1
  101. package/packages/tui/package.json +1 -1
  102. package/packages/two-phase-review/package.json +1 -1
  103. package/packages/types/package.json +1 -1
  104. package/packages/voice-api/package.json +1 -1
  105. package/packages/webmcp-tester/package.json +1 -1
  106. package/packages/wiki/package.json +1 -1
  107. package/packages/workflow-events/package.json +1 -1
  108. package/packages/workspace-scanner/package.json +1 -1
  109. package/packages/worktree/package.json +1 -1
  110. package/packages/write-review/package.json +1 -1
  111. package/packages/notification/adapters/email-adapter.ts +0 -126
  112. package/packages/notification/adapters/ntfy-adapter.ts +0 -100
  113. package/packages/notification/adapters/telegram-adapter.ts +0 -107
  114. package/packages/notification/adapters/webhook-adapter.ts +0 -75
  115. package/packages/notification/base-adapter.ts +0 -75
  116. package/packages/notification/index.ts +0 -51
  117. package/packages/notification/notification-center.ts +0 -286
  118. package/packages/notification/types.ts +0 -89
  119. package/packages/quota-manager/index.ts +0 -28
  120. package/packages/quota-manager/quota-manager.ts +0 -339
  121. package/packages/quota-manager/tui-usage-monitor.ts +0 -355
  122. package/packages/types/src/ai-providers.d.ts +0 -36
  123. package/packages/types/src/runtime-types.d.ts +0 -418
@@ -1,339 +0,0 @@
1
- /**
2
- * Quota Manager — RFC-0003
3
- *
4
- * Collects quota signals from API responses, provider status,
5
- * Playwright, and local estimates. Produces provider availability state.
6
- */
7
-
8
- import type {
9
- QuotaSignal,
10
- QuotaState,
11
- } from "../../packages/types/src/runtime-types.js";
12
-
13
- export interface QuotaSignalInput {
14
- provider: string;
15
- source:
16
- | "api_response"
17
- | "provider_status"
18
- | "playwright"
19
- | "local_estimate"
20
- | "tui_message";
21
- windowType: "5h" | "daily" | "weekly" | "monthly";
22
- usedPct?: number;
23
- exhausted?: boolean;
24
- resetsAt?: string;
25
- retryAfterMs?: number;
26
- }
27
-
28
- export class QuotaManager {
29
- private signals: Map<string, QuotaSignal[]> = new Map();
30
- private readonly staleThresholdMs: number;
31
-
32
- constructor(staleThresholdMs: number = 30 * 60 * 1000) {
33
- this.staleThresholdMs = staleThresholdMs;
34
- }
35
-
36
- /**
37
- * Record a quota signal
38
- */
39
- recordSignal(input: QuotaSignalInput): void {
40
- const signal: QuotaSignal = {
41
- provider: input.provider,
42
- windowType: input.windowType,
43
- usedPct: input.usedPct ?? 0,
44
- remainingPct: 100 - (input.usedPct ?? 0),
45
- exhausted: input.exhausted ?? false,
46
- source: input.source,
47
- capturedAt: new Date().toISOString(),
48
- resetsAt: input.resetsAt,
49
- };
50
-
51
- const key = `${input.provider}:${input.windowType}`;
52
- const existing = this.signals.get(key) ?? [];
53
- this.signals.set(key, [...existing, signal]);
54
- }
55
-
56
- /**
57
- * Get the latest signal for a provider
58
- */
59
- getLatestSignal(
60
- provider: string,
61
- windowType?: "5h" | "daily" | "weekly" | "monthly",
62
- ): QuotaSignal | null {
63
- const key = windowType ? `${provider}:${windowType}` : provider;
64
-
65
- if (windowType) {
66
- const signals = this.signals.get(key) ?? [];
67
- return (
68
- signals.sort(
69
- (a, b) => Date.parse(b.capturedAt) - Date.parse(a.capturedAt),
70
- )[0] ?? null
71
- );
72
- }
73
-
74
- // Return most recent across all window types
75
- let latest: QuotaSignal | null = null;
76
- let latestTs = 0;
77
-
78
- for (const entry of Array.from(this.signals.entries())) {
79
- const [k, sigs] = entry;
80
- if (k.startsWith(`${provider}:`)) {
81
- const mostRecent = sigs.sort(
82
- (a: QuotaSignal, b: QuotaSignal) =>
83
- Date.parse(b.capturedAt) - Date.parse(a.capturedAt),
84
- )[0];
85
- if (mostRecent) {
86
- const ts = Date.parse(mostRecent.capturedAt);
87
- if (ts > latestTs) {
88
- latestTs = ts;
89
- latest = mostRecent;
90
- }
91
- }
92
- }
93
- }
94
-
95
- return latest;
96
- }
97
-
98
- /**
99
- * Get quota state for a provider
100
- */
101
- getProviderState(provider: string): QuotaState {
102
- const signals = this.getSignals(provider);
103
- const exhausted = signals.some(
104
- (s) => s.exhausted || (s.usedPct !== undefined && s.usedPct >= 100),
105
- );
106
- const limited = signals.some(
107
- (s) => s.usedPct !== undefined && s.usedPct >= 90,
108
- );
109
-
110
- // Find next reset time
111
- let nextAvailableAt: string | undefined;
112
- for (const signal of signals) {
113
- if (signal.resetsAt) {
114
- if (
115
- !nextAvailableAt ||
116
- Date.parse(signal.resetsAt) < Date.parse(nextAvailableAt)
117
- ) {
118
- nextAvailableAt = signal.resetsAt;
119
- }
120
- }
121
- }
122
-
123
- return {
124
- provider,
125
- available: !exhausted && !limited,
126
- limited,
127
- exhausted,
128
- signals,
129
- nextAvailableAt,
130
- };
131
- }
132
-
133
- /**
134
- * Get signals for a provider
135
- */
136
- getSignals(provider: string): QuotaSignal[] {
137
- const result: QuotaSignal[] = [];
138
- const cutoff = Date.now() - this.staleThresholdMs;
139
-
140
- for (const entry of Array.from(this.signals.entries())) {
141
- const [key, sigs] = entry;
142
- if (key.startsWith(`${provider}:`)) {
143
- for (const signal of sigs) {
144
- if (Date.parse(signal.capturedAt) >= cutoff) {
145
- result.push(signal);
146
- }
147
- }
148
- }
149
- }
150
-
151
- return result;
152
- }
153
-
154
- /**
155
- * Check if provider is available
156
- */
157
- isAvailable(provider: string): boolean {
158
- return this.getProviderState(provider).available;
159
- }
160
-
161
- /**
162
- * Check if provider is exhausted
163
- */
164
- isExhausted(provider: string): boolean {
165
- return this.getProviderState(provider).exhausted;
166
- }
167
-
168
- /**
169
- * Get time until provider is available again
170
- */
171
- getWaitTime(provider: string): number | null {
172
- const state = this.getProviderState(provider);
173
- if (!state.nextAvailableAt) return null;
174
-
175
- const waitMs = Date.parse(state.nextAvailableAt) - Date.now();
176
- return waitMs > 0 ? waitMs : null;
177
- }
178
-
179
- /**
180
- * Get best available provider from a list
181
- */
182
- selectBestProvider(providers: string[]): string | null {
183
- const available = providers.filter((p) => this.isAvailable(p));
184
-
185
- if (available.length === 0) return null;
186
-
187
- // Sort by remaining quota (prefer those with more remaining)
188
- available.sort((a, b) => {
189
- const signalA = this.getLatestSignal(a);
190
- const signalB = this.getLatestSignal(b);
191
- const remA = signalA?.remainingPct ?? 100;
192
- const remB = signalB?.remainingPct ?? 100;
193
- return remB - remA;
194
- });
195
-
196
- return available[0];
197
- }
198
-
199
- /**
200
- * Clear stale signals
201
- */
202
- clearStale(): void {
203
- const cutoff = Date.now() - this.staleThresholdMs;
204
-
205
- for (const entry of Array.from(this.signals.entries())) {
206
- const [key, sigs] = entry;
207
- const fresh = sigs.filter(
208
- (s: QuotaSignal) => Date.parse(s.capturedAt) >= cutoff,
209
- );
210
- if (fresh.length === 0) {
211
- this.signals.delete(key);
212
- } else {
213
- this.signals.set(key, fresh);
214
- }
215
- }
216
- }
217
-
218
- /**
219
- * Generate quota report
220
- */
221
- generateReport(providers: string[]): string {
222
- const lines = ["Quota Status Report", "=".repeat(40), ""];
223
-
224
- for (const provider of providers) {
225
- const state = this.getProviderState(provider);
226
- const latest = this.getLatestSignal(provider);
227
-
228
- const status = state.exhausted
229
- ? "EXHAUSTED"
230
- : state.limited
231
- ? "LIMITED"
232
- : "AVAILABLE";
233
-
234
- lines.push(`${provider}: ${status}`);
235
-
236
- if (latest) {
237
- lines.push(` Latest: ${latest.usedPct ?? 0}% used`);
238
- if (latest.resetsAt) {
239
- const wait = Date.parse(latest.resetsAt) - Date.now();
240
- if (wait > 0) {
241
- lines.push(` Resets: in ${Math.ceil(wait / 60000)} minutes`);
242
- }
243
- }
244
- lines.push(` Source: ${latest.source}`);
245
- }
246
-
247
- lines.push("");
248
- }
249
-
250
- return lines.join("\n");
251
- }
252
- }
253
-
254
- /**
255
- * Parse quota signal from MiniMax error
256
- */
257
- export function parseMiniMaxError(error: unknown): QuotaSignal | null {
258
- const e = error as Record<string, unknown>;
259
- const msg = String(e.message ?? e.error ?? "");
260
-
261
- // MiniMax quota error: "Error code: 2056 - Rate limit exceeded. Retry after..."
262
- if (
263
- msg.includes("2056") ||
264
- msg.includes("quota") ||
265
- msg.includes("rate limit")
266
- ) {
267
- // Try to extract retry time
268
- const retryMatch = msg.match(/retry after (\d+) (seconds?|minutes?)/i);
269
- let retryAfterMs: number | undefined;
270
-
271
- if (retryMatch) {
272
- const value = parseInt(retryMatch[1], 10);
273
- const unit = retryMatch[2].toLowerCase();
274
- retryAfterMs = unit.startsWith("minute")
275
- ? value * 60 * 1000
276
- : value * 1000;
277
- }
278
-
279
- return {
280
- provider: "minimax",
281
- windowType: "5h",
282
- usedPct: 100,
283
- remainingPct: 0,
284
- exhausted: true,
285
- source: "api_response",
286
- capturedAt: new Date().toISOString(),
287
- retryAfterMs,
288
- };
289
- }
290
-
291
- return null;
292
- }
293
-
294
- /**
295
- * Parse quota signal from OpenAI error
296
- */
297
- export function parseOpenAIError(error: unknown): QuotaSignal | null {
298
- const e = error as Record<string, unknown>;
299
- const code = String(e.code ?? "");
300
- const msg = String(e.message ?? e.error ?? "");
301
-
302
- if (code === "insufficient_quota" || code === "context_length_exceeded") {
303
- return {
304
- provider: "openai",
305
- windowType: "daily",
306
- usedPct: 100,
307
- remainingPct: 0,
308
- exhausted: true,
309
- source: "api_response",
310
- capturedAt: new Date().toISOString(),
311
- };
312
- }
313
-
314
- if (msg.includes("429")) {
315
- const retryMatch = msg.match(/retry after (\d+) (seconds?|minutes?)/i);
316
- let retryAfterMs: number | undefined;
317
-
318
- if (retryMatch) {
319
- const value = parseInt(retryMatch[1], 10);
320
- const unit = retryMatch[2].toLowerCase();
321
- retryAfterMs = unit.startsWith("minute")
322
- ? value * 60 * 1000
323
- : value * 1000;
324
- }
325
-
326
- return {
327
- provider: "openai",
328
- windowType: "5h",
329
- usedPct: 100,
330
- remainingPct: 0,
331
- exhausted: true,
332
- source: "api_response",
333
- capturedAt: new Date().toISOString(),
334
- retryAfterMs,
335
- };
336
- }
337
-
338
- return null;
339
- }
@@ -1,355 +0,0 @@
1
- /**
2
- * TUI Usage Monitor — Unified TUI Message Parser
3
- *
4
- * Hooks into pi's TUI messages to capture quota signals from all providers.
5
- * Handles OpenAI, GLM, and any other provider that reports via TUI.
6
- *
7
- * Usage:
8
- * ```typescript
9
- * const monitor = new TUIUsageMonitor(quotaManager);
10
- *
11
- * // Hook into pi
12
- * pi.on("error", (event) => monitor.processMessage(event.message));
13
- * pi.on("message", (event) => monitor.processMessage(event.message));
14
- * ```
15
- */
16
-
17
- import { EventEmitter } from "node:events";
18
- import type { QuotaManager } from "./quota-manager.js";
19
-
20
- export interface TUIUsageSignal {
21
- provider: "openai" | "glm" | "anthropic" | "openrouter" | "unknown";
22
- timestamp: string;
23
- /** Usage percentage (0-100), if available */
24
- usedPct?: number;
25
- /** Remaining percentage (0-100) */
26
- remainingPct: number;
27
- /** If exhausted */
28
- exhausted: boolean;
29
- /** Reset time */
30
- resetsAt?: string;
31
- /** Which limit was hit */
32
- limitType: "tokens" | "context_window" | "rate_limit" | "unknown";
33
- /** Original message */
34
- originalMessage: string;
35
- }
36
-
37
- export interface TUIUsageMonitorConfig {
38
- /** QuotaManager instance to feed signals into */
39
- quotaManager: QuotaManager;
40
- /** Enable debug logging */
41
- debug?: boolean;
42
- }
43
-
44
- /**
45
- * Provider-specific patterns
46
- */
47
- const PROVIDER_PATTERNS = {
48
- openai: [
49
- /(?:^|\s)OpenAI(?:$|\s)/i,
50
- /(?:^|\s)GPT(?:$|\s)/i,
51
- /openai.*(?:quota|limit|exhausted)/i,
52
- /gpt.*(?:quota|limit|exhausted)/i,
53
- /insufficient_quota/i,
54
- /context_length_exceeded/i,
55
- /openai.*rate.?limit/i,
56
- /429.*openai/i,
57
- /openai.*context.*(?:window|length)/i,
58
- ],
59
- glm: [
60
- /(?:^|\s)GLM(?:$|\s)/i,
61
- /(?:^|\s)Zhipu(?:$|\s)/i,
62
- /glm.*(?:quota|limit|exhausted)/i,
63
- /zhipu.*(?:quota|limit|exhausted)/i,
64
- /glm.*context.*(?:window|length)/i,
65
- /glm.*rate.?limit/i,
66
- ],
67
- anthropic: [
68
- /(?:^|\s)Claude(?:$|\s)/i,
69
- /(?:^|\s)Anthropic(?:$|\s)/i,
70
- /anthropic.*(?:quota|limit|exhausted)/i,
71
- /claude.*(?:quota|limit|exhausted)/i,
72
- /overloaded_error/i,
73
- /rate_limit_error/i,
74
- ],
75
- openrouter: [
76
- /(?:^|\s)OpenRouter(?:$|\s)/i,
77
- /openrouter.*(?:quota|limit|exhausted)/i,
78
- ],
79
- };
80
-
81
- const RESET_TIME_PATTERNS = [
82
- /reset(?:s)?\s+(?:at|in)\s+(\d{1,2}):(\d{2})/i,
83
- /(?:at|in)\s+(\d{1,2}):(\d{2})/i,
84
- /retry\s+after\s+(\d{1,2}):(\d{2})/i,
85
- /(\d{1,2}):(\d{2})\s*(?:am|pm)/i,
86
- ];
87
-
88
- const PERCENTAGE_PATTERN = /(\d+)%/;
89
-
90
- export class TUIUsageMonitor extends EventEmitter {
91
- private readonly quotaManager: QuotaManager;
92
- private readonly debug: boolean;
93
- private lastSignals: Map<string, TUIUsageSignal> = new Map();
94
-
95
- constructor(config: TUIUsageMonitorConfig) {
96
- super();
97
- this.quotaManager = config.quotaManager;
98
- this.debug = config.debug ?? false;
99
- }
100
-
101
- /**
102
- * Process a TUI message and extract quota signals
103
- */
104
- processMessage(message: string): TUIUsageSignal | null {
105
- // Try to identify provider
106
- const provider = this.detectProvider(message);
107
- if (!provider) {
108
- return null;
109
- }
110
-
111
- // Parse the signal
112
- const signal = this.parseSignal(message, provider);
113
- if (!signal) {
114
- return null;
115
- }
116
-
117
- // Store last signal
118
- this.lastSignals.set(provider, signal);
119
-
120
- // Record to QuotaManager
121
- this.recordToQuotaManager(signal);
122
-
123
- // Emit event
124
- this.emit("signal", signal);
125
-
126
- // Debug log
127
- if (this.debug) {
128
- console.log(`[TUIUsageMonitor] Detected ${provider}:`, signal);
129
- }
130
-
131
- return signal;
132
- }
133
-
134
- /**
135
- * Detect which provider the message is about
136
- */
137
- detectProvider(
138
- message: string,
139
- ): "openai" | "glm" | "anthropic" | "openrouter" | null {
140
- for (const [provider, patterns] of Object.entries(PROVIDER_PATTERNS)) {
141
- for (const pattern of patterns) {
142
- if (pattern.test(message)) {
143
- return provider as "openai" | "glm" | "anthropic" | "openrouter";
144
- }
145
- }
146
- }
147
- return null;
148
- }
149
-
150
- /**
151
- * Get the last signal for a provider
152
- */
153
- getLastSignal(
154
- provider: "openai" | "glm" | "anthropic" | "openrouter",
155
- ): TUIUsageSignal | null {
156
- return this.lastSignals.get(provider) ?? null;
157
- }
158
-
159
- /**
160
- * Get all last signals
161
- */
162
- getAllLastSignals(): Map<string, TUIUsageSignal> {
163
- return new Map(this.lastSignals);
164
- }
165
-
166
- /**
167
- * Clear stored signals
168
- */
169
- clear(): void {
170
- this.lastSignals.clear();
171
- }
172
-
173
- /**
174
- * Parse the quota signal from message
175
- */
176
- private parseSignal(
177
- message: string,
178
- provider: "openai" | "glm" | "anthropic" | "openrouter",
179
- ): TUIUsageSignal | null {
180
- // Determine limit type
181
- const limitType = this.detectLimitType(message);
182
-
183
- // Parse reset time
184
- const resetsAt = this.parseResetTime(message);
185
-
186
- // Parse percentage if available
187
- const pctMatch = message.match(PERCENTAGE_PATTERN);
188
- const usedPct = pctMatch ? parseInt(pctMatch[1], 10) : undefined;
189
-
190
- // Determine if exhausted
191
- const exhausted =
192
- usedPct !== undefined
193
- ? usedPct >= 100
194
- : this.containsExhaustedKeywords(message);
195
-
196
- if (!exhausted && !resetsAt) {
197
- // Not a quota signal
198
- return null;
199
- }
200
-
201
- return {
202
- provider,
203
- timestamp: new Date().toISOString(),
204
- usedPct,
205
- remainingPct: usedPct !== undefined ? 100 - usedPct : 0,
206
- exhausted,
207
- resetsAt,
208
- limitType,
209
- originalMessage: message,
210
- };
211
- }
212
-
213
- /**
214
- * Detect the type of limit that was hit
215
- */
216
- private detectLimitType(
217
- message: string,
218
- ): "tokens" | "context_window" | "rate_limit" | "unknown" {
219
- const lowerMsg = message.toLowerCase();
220
-
221
- // Context window
222
- if (
223
- lowerMsg.includes("context") &&
224
- (lowerMsg.includes("window") ||
225
- lowerMsg.includes("length") ||
226
- lowerMsg.includes("full") ||
227
- lowerMsg.includes("exceeded"))
228
- ) {
229
- return "context_window";
230
- }
231
-
232
- // Rate limit
233
- if (
234
- lowerMsg.includes("rate") ||
235
- lowerMsg.includes("429") ||
236
- lowerMsg.includes("too many requests")
237
- ) {
238
- return "rate_limit";
239
- }
240
-
241
- // Tokens/quota exhausted
242
- if (
243
- lowerMsg.includes("quota") ||
244
- lowerMsg.includes("exhausted") ||
245
- lowerMsg.includes("ran out") ||
246
- lowerMsg.includes("limit")
247
- ) {
248
- return "tokens";
249
- }
250
-
251
- return "unknown";
252
- }
253
-
254
- /**
255
- * Check if message contains exhausted keywords
256
- */
257
- private containsExhaustedKeywords(message: string): boolean {
258
- const keywords = [
259
- "exhausted",
260
- "quota",
261
- "limit",
262
- "rate limit",
263
- "429",
264
- "exceeded",
265
- "ran out",
266
- ];
267
- const lowerMsg = message.toLowerCase();
268
- return keywords.some((kw) => lowerMsg.includes(kw));
269
- }
270
-
271
- /**
272
- * Parse reset time from message
273
- */
274
- private parseResetTime(message: string): string | undefined {
275
- for (const pattern of RESET_TIME_PATTERNS) {
276
- const match = message.match(pattern);
277
- if (match) {
278
- let hour = parseInt(match[1], 10);
279
- const minute = parseInt(match[2], 10);
280
-
281
- // Handle AM/PM
282
- const matchIndex = message.indexOf(match[0]);
283
- const afterMatch = message.slice(matchIndex, matchIndex + 20);
284
- const isPM = /pm/i.test(afterMatch);
285
- const isAM = /am/i.test(afterMatch);
286
-
287
- if (isPM && hour < 12) hour += 12;
288
- if (isAM && hour === 12) hour = 0;
289
-
290
- // Convert to ISO string
291
- const now = new Date();
292
- const resetTime = new Date(now);
293
- resetTime.setHours(hour, minute, 0, 0);
294
-
295
- // If time has passed today, assume tomorrow
296
- if (resetTime <= now) {
297
- resetTime.setDate(resetTime.getDate() + 1);
298
- }
299
-
300
- return resetTime.toISOString();
301
- }
302
- }
303
- return undefined;
304
- }
305
-
306
- /**
307
- * Record signal to QuotaManager
308
- */
309
- private recordToQuotaManager(signal: TUIUsageSignal): void {
310
- // Map limit type to window type
311
- let windowType: "5h" | "daily" | "weekly" | "monthly" = "5h";
312
-
313
- if (signal.limitType === "context_window") {
314
- // Context window is usually daily or 5h depending on provider
315
- windowType = "daily";
316
- }
317
-
318
- // Record to QuotaManager
319
- this.quotaManager.recordSignal({
320
- provider: signal.provider,
321
- source: "tui_message",
322
- windowType,
323
- usedPct: signal.usedPct,
324
- exhausted: signal.exhausted,
325
- resetsAt: signal.resetsAt,
326
- });
327
- }
328
- }
329
-
330
- /**
331
- * Create a TUI usage monitor and automatically hook into pi
332
- */
333
- export function createTUIUsageMonitor(
334
- quotaManager: QuotaManager,
335
- pi: {
336
- on: (event: string, handler: (event: { message: string }) => void) => void;
337
- },
338
- config?: { debug?: boolean },
339
- ): TUIUsageMonitor {
340
- const monitor = new TUIUsageMonitor({
341
- quotaManager,
342
- debug: config?.debug,
343
- });
344
-
345
- // Hook into pi events
346
- pi.on("error", (event: { message: string }) => {
347
- monitor.processMessage(event.message);
348
- });
349
-
350
- pi.on("message", (event: { message: string }) => {
351
- monitor.processMessage(event.message);
352
- });
353
-
354
- return monitor;
355
- }