pi-harness-runtime 1.1.14 → 1.1.16
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.
- package/CHANGELOG.md +25 -0
- package/footer-status.ts +65 -7
- package/harness/e2e/glm-quota-scraper.ts +62 -10
- package/harness/glm-quota-countdown.ts +36 -25
- package/harness/glm-quota-logger.ts +172 -0
- package/harness/index.ts +19 -0
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.1.16](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.13...v1.1.16) (2026-08-24)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **glm-quota:** add robust GLM quota countdown with auto-resume ([cb78a2b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/cb78a2bdf5ecf79fc028030d43e3ce40767253cb))
|
|
11
|
+
* **glm:** add structured logging for GLM quota events ([09e692e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/09e692ec17b9c21a448c72f081d18fb35b8636ae))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **glm:** improve footer status display with specific 5h quota and reset time ([ab991c3](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ab991c329bdb4097e77503d3d7f574edc6cdc1c4))
|
|
17
|
+
|
|
18
|
+
### [1.1.15](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.13...v1.1.15) (2026-08-24)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **glm-quota:** add robust GLM quota countdown with auto-resume ([cb78a2b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/cb78a2bdf5ecf79fc028030d43e3ce40767253cb))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **glm:** improve footer status display with specific 5h quota and reset time ([ab991c3](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ab991c329bdb4097e77503d3d7f574edc6cdc1c4))
|
|
29
|
+
|
|
5
30
|
### [1.1.14](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.13...v1.1.14) (2026-08-24)
|
|
6
31
|
|
|
7
32
|
|
package/footer-status.ts
CHANGED
|
@@ -31,6 +31,61 @@ export function formatCountdownForDisplay(seconds: number): string {
|
|
|
31
31
|
return `${secs}s`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Get a human-readable limit type label.
|
|
36
|
+
* Special cases for GLM's 5h quota limit.
|
|
37
|
+
*/
|
|
38
|
+
function getLimitTypeLabel(provider: string, limitType?: string): string {
|
|
39
|
+
// GLM specific: 5h quota limit
|
|
40
|
+
if (provider === "glm" && limitType === "rate_limit") {
|
|
41
|
+
return "5h quota";
|
|
42
|
+
}
|
|
43
|
+
return limitType ?? "tokens";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format reset time for display.
|
|
48
|
+
* Converts ISO strings or human-readable times to a compact format.
|
|
49
|
+
*/
|
|
50
|
+
function formatResetTime(resetAt?: string): string {
|
|
51
|
+
if (!resetAt) return "soon";
|
|
52
|
+
|
|
53
|
+
// If it's already a short human-readable string, return as-is
|
|
54
|
+
if (/^\d+\s*(hr|min|day|d|h|m|s)/i.test(resetAt)) {
|
|
55
|
+
return resetAt;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Try to parse as ISO datetime
|
|
59
|
+
const date = new Date(resetAt);
|
|
60
|
+
if (!isNaN(date.getTime())) {
|
|
61
|
+
// Format as "Aug 25 01:47" or similar
|
|
62
|
+
const now = new Date();
|
|
63
|
+
const sameYear = date.getFullYear() === now.getFullYear();
|
|
64
|
+
const sameDay = date.toDateString() === now.toDateString();
|
|
65
|
+
|
|
66
|
+
const timeStr = date.toLocaleTimeString("en-US", {
|
|
67
|
+
hour: "2-digit",
|
|
68
|
+
minute: "2-digit",
|
|
69
|
+
hour12: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (sameDay) {
|
|
73
|
+
return `today ${timeStr}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const month = date.toLocaleString("en-US", { month: "short" });
|
|
77
|
+
const day = date.getDate();
|
|
78
|
+
|
|
79
|
+
if (sameYear) {
|
|
80
|
+
return `${month} ${day} ${timeStr}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return `${month} ${day} ${date.getFullYear()} ${timeStr}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return resetAt;
|
|
87
|
+
}
|
|
88
|
+
|
|
34
89
|
/**
|
|
35
90
|
* Build a TUI-signal exhaustion status line with optional countdown.
|
|
36
91
|
* This is used when a provider (like GLM) hits its limit.
|
|
@@ -41,15 +96,15 @@ export function buildExhaustionStatusLine(
|
|
|
41
96
|
countdownSeconds?: number,
|
|
42
97
|
): string {
|
|
43
98
|
const label = providerDisplayName(provider);
|
|
44
|
-
const
|
|
45
|
-
const
|
|
99
|
+
const limitTypeLabel = getLimitTypeLabel(provider, view.limitType);
|
|
100
|
+
const resetFormatted = formatResetTime(view.resets_at ?? view.h5_resets_at);
|
|
46
101
|
|
|
47
102
|
if (countdownSeconds !== undefined && countdownSeconds > 0) {
|
|
48
103
|
const countdown = formatCountdownForDisplay(countdownSeconds);
|
|
49
|
-
return `${label}: ⏳ ${countdown} (${
|
|
104
|
+
return `${label}: ⏳ ${countdown} (${limitTypeLabel})`;
|
|
50
105
|
}
|
|
51
106
|
|
|
52
|
-
return `${label}:
|
|
107
|
+
return `${label}: ${limitTypeLabel} hit, resets ${resetFormatted}`;
|
|
53
108
|
}
|
|
54
109
|
import { getProviderLabel } from "./packages/types/src/ai-providers.js";
|
|
55
110
|
import {
|
|
@@ -180,9 +235,12 @@ export function buildFooterStatusValue(
|
|
|
180
235
|
view.resets_at !== undefined ||
|
|
181
236
|
view.h5_resets_at !== undefined)
|
|
182
237
|
) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
238
|
+
const limitTypeLabel = getLimitTypeLabel(
|
|
239
|
+
provider,
|
|
240
|
+
view.limitType ?? undefined,
|
|
241
|
+
);
|
|
242
|
+
const resetFormatted = formatResetTime(view.resets_at ?? view.h5_resets_at);
|
|
243
|
+
return `${label}: ${limitTypeLabel} hit, resets ${resetFormatted}`;
|
|
186
244
|
}
|
|
187
245
|
|
|
188
246
|
// Continuous data path: providers with continuous scrape (MiniMax has 5h+weekly, OpenAI has weekly-only)
|
|
@@ -143,7 +143,8 @@ export class GLMQuotaScraper {
|
|
|
143
143
|
|
|
144
144
|
constructor(config: GLMScraperConfig = {}) {
|
|
145
145
|
// Resolve API key: direct value > file > null
|
|
146
|
-
const apiKey =
|
|
146
|
+
const apiKey =
|
|
147
|
+
config.apiKey ?? loadApiKey(config.apiKeyFile ?? DEFAULT_API_KEY_FILE);
|
|
147
148
|
|
|
148
149
|
this.config = {
|
|
149
150
|
apiKeyFile: config.apiKeyFile ?? DEFAULT_API_KEY_FILE,
|
|
@@ -191,13 +192,16 @@ export class GLMQuotaScraper {
|
|
|
191
192
|
/**
|
|
192
193
|
* Fetch usage data from z.ai API
|
|
193
194
|
*/
|
|
194
|
-
async fetchUsage(
|
|
195
|
+
async fetchUsage(
|
|
196
|
+
startDate?: Date,
|
|
197
|
+
endDate?: Date,
|
|
198
|
+
): Promise<GLMQuotaData | null> {
|
|
195
199
|
const apiKey = this.getApiKey();
|
|
196
200
|
if (!apiKey) {
|
|
197
201
|
if (!this.config.quiet) {
|
|
198
202
|
console.error(
|
|
199
203
|
`[DEBUG GLMQuotaScraper] No API key found. ` +
|
|
200
|
-
|
|
204
|
+
`Set ZAI_API_KEY env var or drop key into ${DEFAULT_API_KEY_FILE}`,
|
|
201
205
|
);
|
|
202
206
|
}
|
|
203
207
|
return null;
|
|
@@ -214,7 +218,9 @@ export class GLMQuotaScraper {
|
|
|
214
218
|
|
|
215
219
|
try {
|
|
216
220
|
if (!this.config.quiet) {
|
|
217
|
-
console.log(
|
|
221
|
+
console.log(
|
|
222
|
+
`[DEBUG GLMQuotaScraper] Fetching usage from ${startStr} to ${endStr}`,
|
|
223
|
+
);
|
|
218
224
|
}
|
|
219
225
|
|
|
220
226
|
const response = await fetch(url, {
|
|
@@ -234,7 +240,7 @@ export class GLMQuotaScraper {
|
|
|
234
240
|
return null;
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
const data = await response.json() as {
|
|
243
|
+
const data = (await response.json()) as {
|
|
238
244
|
code?: number;
|
|
239
245
|
success?: boolean;
|
|
240
246
|
data?: {
|
|
@@ -255,7 +261,9 @@ export class GLMQuotaScraper {
|
|
|
255
261
|
|
|
256
262
|
if (!data.success || !data.data) {
|
|
257
263
|
if (!this.config.quiet) {
|
|
258
|
-
console.error(
|
|
264
|
+
console.error(
|
|
265
|
+
`[DEBUG GLMQuotaScraper] API error: ${data.msg ?? "Unknown error"}`,
|
|
266
|
+
);
|
|
259
267
|
}
|
|
260
268
|
return null;
|
|
261
269
|
}
|
|
@@ -279,8 +287,8 @@ export class GLMQuotaScraper {
|
|
|
279
287
|
if (!this.config.quiet) {
|
|
280
288
|
console.log(
|
|
281
289
|
`[DEBUG GLMQuotaScraper] Total calls: ${result.totalCalls}, ` +
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
`Total tokens: ${result.totalTokens.toLocaleString()}, ` +
|
|
291
|
+
`Model: ${result.modelName}`,
|
|
284
292
|
);
|
|
285
293
|
}
|
|
286
294
|
|
|
@@ -320,7 +328,7 @@ export class GLMQuotaScraper {
|
|
|
320
328
|
return null;
|
|
321
329
|
}
|
|
322
330
|
|
|
323
|
-
const data = await response.json() as {
|
|
331
|
+
const data = (await response.json()) as {
|
|
324
332
|
code?: number;
|
|
325
333
|
success?: boolean;
|
|
326
334
|
data?: Array<{
|
|
@@ -407,6 +415,50 @@ export class GLMQuotaScraper {
|
|
|
407
415
|
}
|
|
408
416
|
}
|
|
409
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Parse reset time from GLM 429 error message.
|
|
420
|
+
* Extracts the reset timestamp from messages like:
|
|
421
|
+
* '{"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2026-08-25 01:47:16"}'
|
|
422
|
+
*/
|
|
423
|
+
export function parseGLMErrorResetTime(errorMessage: string): string | null {
|
|
424
|
+
// Try to find the JSON object in the message
|
|
425
|
+
const jsonMatch = errorMessage.match(/\{[^{}]*\}/);
|
|
426
|
+
if (jsonMatch) {
|
|
427
|
+
try {
|
|
428
|
+
const json = JSON.parse(jsonMatch[0]);
|
|
429
|
+
if (json.code === 1308 && json.message) {
|
|
430
|
+
// Extract datetime from message
|
|
431
|
+
const datetimeMatch = json.message.match(
|
|
432
|
+
/reset at (\d{4}-\d{2}-\d{2}[T ]?\d{2}:\d{2}:\d{2})/i,
|
|
433
|
+
);
|
|
434
|
+
if (datetimeMatch) {
|
|
435
|
+
const datetimeStr = datetimeMatch[1].replace(" ", "T");
|
|
436
|
+
const date = new Date(datetimeStr);
|
|
437
|
+
if (!isNaN(date.getTime())) {
|
|
438
|
+
return date.toISOString();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
// Not JSON, try plain text parsing
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Fallback: try plain text parsing
|
|
448
|
+
const plainMatch = errorMessage.match(
|
|
449
|
+
/reset at (\d{4}-\d{2}-\d{2}[T ]?\d{2}:\d{2}:\d{2})/i,
|
|
450
|
+
);
|
|
451
|
+
if (plainMatch) {
|
|
452
|
+
const datetimeStr = plainMatch[1].replace(" ", "T");
|
|
453
|
+
const date = new Date(datetimeStr);
|
|
454
|
+
if (!isNaN(date.getTime())) {
|
|
455
|
+
return date.toISOString();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
410
462
|
/**
|
|
411
463
|
* Integration helper for index.ts
|
|
412
464
|
*/
|
|
@@ -435,7 +487,7 @@ export class GLMQuotaManager {
|
|
|
435
487
|
return this.lastQuota;
|
|
436
488
|
}
|
|
437
489
|
|
|
438
|
-
this.lastQuota = await this.scraper.fetchUsage() ?? undefined;
|
|
490
|
+
this.lastQuota = (await this.scraper.fetchUsage()) ?? undefined;
|
|
439
491
|
this.lastFetchTime = now;
|
|
440
492
|
return this.lastQuota ?? null;
|
|
441
493
|
}
|
|
@@ -21,6 +21,24 @@ import type { JobStateMachine } from "./job-state-machine.js";
|
|
|
21
21
|
import type { MirrorStore } from "../mirror.js";
|
|
22
22
|
import type { NotificationCenter } from "../packages/notification/dist/notification-center.js";
|
|
23
23
|
|
|
24
|
+
import {
|
|
25
|
+
logInfo,
|
|
26
|
+
logWarn,
|
|
27
|
+
logError,
|
|
28
|
+
logCountdownStarted,
|
|
29
|
+
logCountdownTick,
|
|
30
|
+
logCountdownComplete,
|
|
31
|
+
logAutoResumeSuccess,
|
|
32
|
+
logAutoResumeFailed,
|
|
33
|
+
logNotificationSent,
|
|
34
|
+
logNotificationFailed,
|
|
35
|
+
logMirrorUpdate,
|
|
36
|
+
logQuotaExhausted,
|
|
37
|
+
logCountdownCancelled,
|
|
38
|
+
logResetTimeParseError,
|
|
39
|
+
getLogFilePath,
|
|
40
|
+
} from "./glm-quota-logger.js";
|
|
41
|
+
|
|
24
42
|
/** Notification intervals before reset (in seconds) */
|
|
25
43
|
const NOTIFICATION_INTERVALS = [
|
|
26
44
|
15 * 60, // 15 minutes
|
|
@@ -245,9 +263,7 @@ export class GLMQuotaCountdown {
|
|
|
245
263
|
isResetTime: totalSeconds <= 0,
|
|
246
264
|
});
|
|
247
265
|
|
|
248
|
-
|
|
249
|
-
`[GLMQuotaCountdown] Started countdown for job ${jobId}: ${formatCountdown(totalSeconds)} until reset at ${resetAt}`,
|
|
250
|
-
);
|
|
266
|
+
logCountdownStarted(jobId, resetAt, totalSeconds);
|
|
251
267
|
|
|
252
268
|
return state;
|
|
253
269
|
}
|
|
@@ -263,9 +279,7 @@ export class GLMQuotaCountdown {
|
|
|
263
279
|
): Promise<GLMQuotaCountdownState | null> {
|
|
264
280
|
const resetAt = parseGLMResetTime(errorMessage);
|
|
265
281
|
if (!resetAt) {
|
|
266
|
-
|
|
267
|
-
`[GLMQuotaCountdown] Could not parse reset time from error for job ${jobId}`,
|
|
268
|
-
);
|
|
282
|
+
logResetTimeParseError(jobId);
|
|
269
283
|
return null;
|
|
270
284
|
}
|
|
271
285
|
|
|
@@ -282,7 +296,7 @@ export class GLMQuotaCountdown {
|
|
|
282
296
|
this.timers.delete(jobId);
|
|
283
297
|
}
|
|
284
298
|
this.activeCountdowns.delete(jobId);
|
|
285
|
-
|
|
299
|
+
logCountdownCancelled(jobId);
|
|
286
300
|
}
|
|
287
301
|
|
|
288
302
|
/**
|
|
@@ -322,11 +336,10 @@ export class GLMQuotaCountdown {
|
|
|
322
336
|
};
|
|
323
337
|
|
|
324
338
|
mirrorStore.writeProvider("glm", updated);
|
|
325
|
-
|
|
326
|
-
`[GLMQuotaCountdown] Updated mirror with reset time: ${resetAt}`,
|
|
327
|
-
);
|
|
339
|
+
logMirrorUpdate(jobId, true);
|
|
328
340
|
} catch (error) {
|
|
329
|
-
|
|
341
|
+
logMirrorUpdate(jobId, false);
|
|
342
|
+
logError(jobId, "Failed to update mirror", error);
|
|
330
343
|
}
|
|
331
344
|
}
|
|
332
345
|
|
|
@@ -476,7 +489,7 @@ export class GLMQuotaCountdown {
|
|
|
476
489
|
* Get seconds until next notification
|
|
477
490
|
*/
|
|
478
491
|
private getNextNotificationIn(
|
|
479
|
-
|
|
492
|
+
_state: GLMQuotaCountdownState,
|
|
480
493
|
remainingSeconds: number,
|
|
481
494
|
): number | undefined {
|
|
482
495
|
for (const interval of NOTIFICATION_INTERVALS) {
|
|
@@ -496,13 +509,13 @@ export class GLMQuotaCountdown {
|
|
|
496
509
|
interval: number,
|
|
497
510
|
): Promise<void> {
|
|
498
511
|
if (!this.notificationCenter) {
|
|
499
|
-
|
|
500
|
-
|
|
512
|
+
logInfo(
|
|
513
|
+
jobId,
|
|
514
|
+
`Notification: ${formatCountdown(remainingSeconds)} until GLM quota reset`,
|
|
501
515
|
);
|
|
502
516
|
return;
|
|
503
517
|
}
|
|
504
518
|
|
|
505
|
-
const minutes = Math.floor(remainingSeconds / 60);
|
|
506
519
|
const label =
|
|
507
520
|
interval === 15 * 60
|
|
508
521
|
? "15 minutes"
|
|
@@ -516,11 +529,9 @@ export class GLMQuotaCountdown {
|
|
|
516
529
|
requirement: this.jobContext?.requirement ?? "GLM Quota",
|
|
517
530
|
error: `GLM quota reset in ${label} (${formatCountdown(remainingSeconds)}). Auto-resume pending.`,
|
|
518
531
|
});
|
|
519
|
-
|
|
520
|
-
`[GLMQuotaCountdown] Sent ${label} notification for job ${jobId}`,
|
|
521
|
-
);
|
|
532
|
+
logNotificationSent(jobId, remainingSeconds);
|
|
522
533
|
} catch (error) {
|
|
523
|
-
|
|
534
|
+
logNotificationFailed(jobId, String(error));
|
|
524
535
|
}
|
|
525
536
|
}
|
|
526
537
|
|
|
@@ -529,11 +540,11 @@ export class GLMQuotaCountdown {
|
|
|
529
540
|
*/
|
|
530
541
|
private async triggerAutoResume(
|
|
531
542
|
jobId: string,
|
|
532
|
-
|
|
543
|
+
_state: GLMQuotaCountdownState,
|
|
533
544
|
machine: JobStateMachine,
|
|
534
545
|
mirrorStore: MirrorStore,
|
|
535
546
|
): Promise<void> {
|
|
536
|
-
|
|
547
|
+
logCountdownComplete(jobId);
|
|
537
548
|
|
|
538
549
|
// Clear timers
|
|
539
550
|
this.cancelCountdown(jobId);
|
|
@@ -547,7 +558,7 @@ export class GLMQuotaCountdown {
|
|
|
547
558
|
if (checkpoint && checkpoint.status === "paused_quota") {
|
|
548
559
|
const result = await machine.transition("running");
|
|
549
560
|
if (result.success) {
|
|
550
|
-
|
|
561
|
+
logAutoResumeSuccess(jobId);
|
|
551
562
|
|
|
552
563
|
// Update mirror to clear exhaustion
|
|
553
564
|
const record = mirrorStore.readProvider("glm");
|
|
@@ -569,11 +580,11 @@ export class GLMQuotaCountdown {
|
|
|
569
580
|
isResetTime: true,
|
|
570
581
|
});
|
|
571
582
|
} else {
|
|
572
|
-
|
|
583
|
+
logAutoResumeFailed(jobId, result.error ?? "Unknown error");
|
|
573
584
|
}
|
|
574
585
|
}
|
|
575
586
|
} catch (error) {
|
|
576
|
-
|
|
587
|
+
logError(jobId, "Auto-resume error", error);
|
|
577
588
|
}
|
|
578
589
|
}
|
|
579
590
|
|
|
@@ -585,7 +596,7 @@ export class GLMQuotaCountdown {
|
|
|
585
596
|
try {
|
|
586
597
|
callback(event);
|
|
587
598
|
} catch (error) {
|
|
588
|
-
|
|
599
|
+
logError(event.jobId, "Tick callback error", error);
|
|
589
600
|
}
|
|
590
601
|
}
|
|
591
602
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLM Quota Logger — Structured logging for GLM quota events
|
|
3
|
+
*
|
|
4
|
+
* Logs are written to:
|
|
5
|
+
* ~/.pi/harness-logs/glm-quota.log
|
|
6
|
+
*
|
|
7
|
+
* Log levels:
|
|
8
|
+
* INFO - Normal events (countdown started, resume triggered)
|
|
9
|
+
* WARN - Warnings (quota still exhausted, notification failed)
|
|
10
|
+
* ERROR - Errors (auto-resume failed, mirror update failed)
|
|
11
|
+
*
|
|
12
|
+
* Format: [ISO_TIMESTAMP] [LEVEL] [jobId] message
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
|
|
19
|
+
export type LogLevel = "INFO" | "WARN" | "ERROR";
|
|
20
|
+
|
|
21
|
+
const LOG_DIR = join(homedir(), ".pi", "harness-logs");
|
|
22
|
+
const LOG_FILE = join(LOG_DIR, "glm-quota.log");
|
|
23
|
+
|
|
24
|
+
/** Ensure log directory exists */
|
|
25
|
+
function ensureLogDir(): void {
|
|
26
|
+
if (!existsSync(LOG_DIR)) {
|
|
27
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Format log entry */
|
|
32
|
+
function formatLog(level: LogLevel, jobId: string | null, message: string): string {
|
|
33
|
+
const timestamp = new Date().toISOString();
|
|
34
|
+
const job = jobId ? `[${jobId}]` : "[system]";
|
|
35
|
+
return `[${timestamp}] [${level}] ${job} ${message}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write a log entry to file
|
|
40
|
+
*/
|
|
41
|
+
function writeLog(level: LogLevel, jobId: string | null, message: string): void {
|
|
42
|
+
try {
|
|
43
|
+
ensureLogDir();
|
|
44
|
+
const entry = formatLog(level, jobId, message) + "\n";
|
|
45
|
+
appendFileSync(LOG_FILE, entry);
|
|
46
|
+
} catch {
|
|
47
|
+
// Non-fatal - just log to console
|
|
48
|
+
console.error(`[GLMQuotaLogger] Failed to write log: ${message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Public API ---
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Log info level event
|
|
56
|
+
*/
|
|
57
|
+
export function logInfo(jobId: string | null, message: string): void {
|
|
58
|
+
console.log(`[GLMQuota] ${jobId ? `[${jobId}] ` : ""}${message}`);
|
|
59
|
+
writeLog("INFO", jobId, message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Log warning level event
|
|
64
|
+
*/
|
|
65
|
+
export function logWarn(jobId: string | null, message: string): void {
|
|
66
|
+
console.warn(`[GLMQuota] ${jobId ? `[${jobId}] ` : ""}${message}`);
|
|
67
|
+
writeLog("WARN", jobId, message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Log error level event
|
|
72
|
+
*/
|
|
73
|
+
export function logError(jobId: string | null, message: string, error?: unknown): void {
|
|
74
|
+
const errorMsg = error instanceof Error ? error.message : String(error ?? "");
|
|
75
|
+
const fullMessage = errorMsg ? `${message}: ${errorMsg}` : message;
|
|
76
|
+
console.error(`[GLMQuota] ${jobId ? `[${jobId}] ` : ""}${fullMessage}`);
|
|
77
|
+
writeLog("ERROR", jobId, fullMessage);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Event-specific log helpers ---
|
|
81
|
+
|
|
82
|
+
/** Log countdown started */
|
|
83
|
+
export function logCountdownStarted(
|
|
84
|
+
jobId: string,
|
|
85
|
+
resetAt: string,
|
|
86
|
+
totalSeconds: number,
|
|
87
|
+
): void {
|
|
88
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
89
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
90
|
+
logInfo(
|
|
91
|
+
jobId,
|
|
92
|
+
`Countdown started: ${hours}h ${minutes}m until ${resetAt}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Log countdown tick */
|
|
97
|
+
export function logCountdownTick(
|
|
98
|
+
jobId: string,
|
|
99
|
+
remainingSeconds: number,
|
|
100
|
+
): void {
|
|
101
|
+
const hours = Math.floor(remainingSeconds / 3600);
|
|
102
|
+
const minutes = Math.floor((remainingSeconds % 3600) / 60);
|
|
103
|
+
const secs = remainingSeconds % 60;
|
|
104
|
+
logInfo(
|
|
105
|
+
jobId,
|
|
106
|
+
`Countdown tick: ${hours}h ${minutes}m ${secs}s remaining`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Log countdown completed */
|
|
111
|
+
export function logCountdownComplete(jobId: string): void {
|
|
112
|
+
logInfo(jobId, "Countdown complete, triggering auto-resume");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Log auto-resume success */
|
|
116
|
+
export function logAutoResumeSuccess(jobId: string): void {
|
|
117
|
+
logInfo(jobId, "Auto-resume succeeded, job resumed");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Log auto-resume failed */
|
|
121
|
+
export function logAutoResumeFailed(jobId: string, error: string): void {
|
|
122
|
+
logError(jobId, "Auto-resume failed", error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Log notification sent */
|
|
126
|
+
export function logNotificationSent(
|
|
127
|
+
jobId: string,
|
|
128
|
+
beforeReset: number,
|
|
129
|
+
): void {
|
|
130
|
+
const minutes = Math.floor(beforeReset / 60);
|
|
131
|
+
logInfo(jobId, `Notification sent: ${minutes} minutes before reset`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Log notification failed */
|
|
135
|
+
export function logNotificationFailed(
|
|
136
|
+
jobId: string,
|
|
137
|
+
error: string,
|
|
138
|
+
): void {
|
|
139
|
+
logWarn(jobId, `Notification failed: ${error}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Log mirror update */
|
|
143
|
+
export function logMirrorUpdate(jobId: string, success: boolean): void {
|
|
144
|
+
if (success) {
|
|
145
|
+
logInfo(jobId, "Mirror store updated with reset time");
|
|
146
|
+
} else {
|
|
147
|
+
logWarn(jobId, "Failed to update mirror store");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Log quota exhaustion detected */
|
|
152
|
+
export function logQuotaExhausted(
|
|
153
|
+
jobId: string,
|
|
154
|
+
resetAt: string,
|
|
155
|
+
): void {
|
|
156
|
+
logInfo(jobId, `GLM 5h quota exhausted, reset at ${resetAt}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Log countdown cancelled */
|
|
160
|
+
export function logCountdownCancelled(jobId: string): void {
|
|
161
|
+
logInfo(jobId, "Countdown cancelled");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Log error parsing reset time */
|
|
165
|
+
export function logResetTimeParseError(jobId: string): void {
|
|
166
|
+
logWarn(jobId, "Could not parse reset time from error message");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Get the log file path */
|
|
170
|
+
export function getLogFilePath(): string {
|
|
171
|
+
return LOG_FILE;
|
|
172
|
+
}
|
package/harness/index.ts
CHANGED
|
@@ -222,6 +222,25 @@ export type {
|
|
|
222
222
|
CountdownTickEvent,
|
|
223
223
|
} from "./glm-quota-countdown.js";
|
|
224
224
|
|
|
225
|
+
// --- GLM Quota Logger ---------------------------------------------
|
|
226
|
+
export {
|
|
227
|
+
logInfo,
|
|
228
|
+
logWarn,
|
|
229
|
+
logError,
|
|
230
|
+
logCountdownStarted,
|
|
231
|
+
logCountdownTick,
|
|
232
|
+
logCountdownComplete,
|
|
233
|
+
logAutoResumeSuccess,
|
|
234
|
+
logAutoResumeFailed,
|
|
235
|
+
logNotificationSent,
|
|
236
|
+
logNotificationFailed,
|
|
237
|
+
logMirrorUpdate,
|
|
238
|
+
logQuotaExhausted,
|
|
239
|
+
logCountdownCancelled,
|
|
240
|
+
logResetTimeParseError,
|
|
241
|
+
getLogFilePath,
|
|
242
|
+
} from "./glm-quota-logger.js";
|
|
243
|
+
|
|
225
244
|
// --- RFC-0059: Experience Replay ------------------------------------------
|
|
226
245
|
export {
|
|
227
246
|
ExperienceReplay,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/capability-registry",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|