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.
Files changed (95) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/footer-status.ts +65 -7
  3. package/harness/e2e/glm-quota-scraper.ts +62 -10
  4. package/harness/glm-quota-countdown.ts +36 -25
  5. package/harness/glm-quota-logger.ts +172 -0
  6. package/harness/index.ts +19 -0
  7. package/package.json +1 -1
  8. package/packages/a2a-adapter/package.json +1 -1
  9. package/packages/architecture-generator/package.json +1 -1
  10. package/packages/auth/package.json +1 -1
  11. package/packages/autonomous-refactor/package.json +1 -1
  12. package/packages/autonomous-runtime/package.json +1 -1
  13. package/packages/cache-strategy/package.json +1 -1
  14. package/packages/capability-registry/package.json +1 -1
  15. package/packages/checkpoint/package.json +1 -1
  16. package/packages/cli-plugin-sdk/package.json +1 -1
  17. package/packages/clipboard/package.json +1 -1
  18. package/packages/clipboard-plugin/package.json +1 -1
  19. package/packages/code-generation/package.json +1 -1
  20. package/packages/code-review/package.json +1 -1
  21. package/packages/codex-adapter/package.json +1 -1
  22. package/packages/config-capture/package.json +1 -1
  23. package/packages/context-compiler/package.json +1 -1
  24. package/packages/context-discovery/package.json +1 -1
  25. package/packages/context-manager/package.json +1 -1
  26. package/packages/cookie-sanitizer/package.json +1 -1
  27. package/packages/cost-optimizer/package.json +1 -1
  28. package/packages/dependency-analyzer/package.json +1 -1
  29. package/packages/django-plugin/package.json +1 -1
  30. package/packages/doc-generator/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-store/package.json +1 -1
  35. package/packages/experience-replay/package.json +1 -1
  36. package/packages/feedback-collector/package.json +1 -1
  37. package/packages/file-copy-helper/package.json +1 -1
  38. package/packages/framework-detector/package.json +1 -1
  39. package/packages/framework-plugin-sdk/package.json +1 -1
  40. package/packages/frappe-plugin/package.json +1 -1
  41. package/packages/generic-web-plugin/package.json +1 -1
  42. package/packages/health-monitor/package.json +1 -1
  43. package/packages/intent-analyzer/package.json +1 -1
  44. package/packages/knowledge-graph/package.json +1 -1
  45. package/packages/knowledge-retrieval/package.json +1 -1
  46. package/packages/laravel-plugin/package.json +1 -1
  47. package/packages/learning-engine/package.json +1 -1
  48. package/packages/mcp-adapter/package.json +1 -1
  49. package/packages/memory-engine/package.json +1 -1
  50. package/packages/milestone-manager/package.json +1 -1
  51. package/packages/model-registry/package.json +1 -1
  52. package/packages/nextjs-plugin/package.json +1 -1
  53. package/packages/notification/package.json +1 -1
  54. package/packages/observability/package.json +1 -1
  55. package/packages/okf-indexer/package.json +1 -1
  56. package/packages/performance-optimizer/package.json +1 -1
  57. package/packages/privilege-broker/package.json +1 -1
  58. package/packages/project-analyzer/package.json +1 -1
  59. package/packages/project-bootstrap/package.json +1 -1
  60. package/packages/projection-engine/package.json +1 -1
  61. package/packages/prompt-compiler/package.json +1 -1
  62. package/packages/prompt-versioning/package.json +1 -1
  63. package/packages/provider-adapter-sdk/package.json +1 -1
  64. package/packages/provider-router/package.json +1 -1
  65. package/packages/provider-selector/package.json +1 -1
  66. package/packages/providers/package.json +1 -1
  67. package/packages/quota-manager/package.json +1 -1
  68. package/packages/rate-limiter/package.json +1 -1
  69. package/packages/react-vite-plugin/package.json +1 -1
  70. package/packages/release-manager/package.json +1 -1
  71. package/packages/requirement-compiler/package.json +1 -1
  72. package/packages/runtime/package.json +1 -1
  73. package/packages/scheduler/package.json +1 -1
  74. package/packages/scheduler-adapter/package.json +1 -1
  75. package/packages/session/package.json +1 -1
  76. package/packages/session-api/package.json +1 -1
  77. package/packages/session-export/package.json +1 -1
  78. package/packages/shared-context/package.json +1 -1
  79. package/packages/skill-registry/package.json +1 -1
  80. package/packages/sprint-planner/package.json +1 -1
  81. package/packages/subscription-engine/package.json +1 -1
  82. package/packages/task-compiler/package.json +1 -1
  83. package/packages/tencentdb-memory/package.json +1 -1
  84. package/packages/tencentdb-sync/package.json +1 -1
  85. package/packages/test-data-generator/package.json +1 -1
  86. package/packages/test-generator/package.json +1 -1
  87. package/packages/todo-bd-sync/package.json +1 -1
  88. package/packages/token-estimation/package.json +1 -1
  89. package/packages/token-optimizer/package.json +1 -1
  90. package/packages/tui/package.json +1 -1
  91. package/packages/types/package.json +1 -1
  92. package/packages/workflow-events/package.json +1 -1
  93. package/packages/workspace-scanner/package.json +1 -1
  94. package/packages/worktree/package.json +1 -1
  95. 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 limitType = view.limitType ?? "tokens";
45
- const reset = view.resets_at ?? view.h5_resets_at ?? "soon";
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} (${limitType} hit)`;
104
+ return `${label}: ⏳ ${countdown} (${limitTypeLabel})`;
50
105
  }
51
106
 
52
- return `${label}: limit hit (${limitType}), reset ${reset}`;
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 reset = view.resets_at ?? view.h5_resets_at ?? "soon";
184
- const limitType = view.limitType ?? "tokens";
185
- return `${label}: limit hit (${limitType}), reset ${reset}`;
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 = config.apiKey ?? loadApiKey(config.apiKeyFile ?? DEFAULT_API_KEY_FILE);
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(startDate?: Date, endDate?: Date): Promise<GLMQuotaData | null> {
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
- `Set ZAI_API_KEY env var or drop key into ${DEFAULT_API_KEY_FILE}`,
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(`[DEBUG GLMQuotaScraper] Fetching usage from ${startStr} to ${endStr}`);
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(`[DEBUG GLMQuotaScraper] API error: ${data.msg ?? "Unknown 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
- `Total tokens: ${result.totalTokens.toLocaleString()}, ` +
283
- `Model: ${result.modelName}`,
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
- console.log(
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
- console.warn(
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
- console.log(`[GLMQuotaCountdown] Cancelled countdown for job ${jobId}`);
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
- console.log(
326
- `[GLMQuotaCountdown] Updated mirror with reset time: ${resetAt}`,
327
- );
339
+ logMirrorUpdate(jobId, true);
328
340
  } catch (error) {
329
- console.error(`[GLMQuotaCountdown] Failed to update mirror: ${error}`);
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
- state: GLMQuotaCountdownState,
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
- console.log(
500
- `[GLMQuotaCountdown] Notification: ${formatCountdown(remainingSeconds)} until GLM quota reset`,
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
- console.log(
520
- `[GLMQuotaCountdown] Sent ${label} notification for job ${jobId}`,
521
- );
532
+ logNotificationSent(jobId, remainingSeconds);
522
533
  } catch (error) {
523
- console.error(`[GLMQuotaCountdown] Failed to send notification: ${error}`);
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
- state: GLMQuotaCountdownState,
543
+ _state: GLMQuotaCountdownState,
533
544
  machine: JobStateMachine,
534
545
  mirrorStore: MirrorStore,
535
546
  ): Promise<void> {
536
- console.log(`[GLMQuotaCountdown] Triggering auto-resume for job ${jobId}`);
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
- console.log(`[GLMQuotaCountdown] Job ${jobId} auto-resumed successfully`);
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
- console.error(`[GLMQuotaCountdown] Auto-resume failed: ${result.error}`);
583
+ logAutoResumeFailed(jobId, result.error ?? "Unknown error");
573
584
  }
574
585
  }
575
586
  } catch (error) {
576
- console.error(`[GLMQuotaCountdown] Auto-resume error: ${error}`);
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
- console.error(`[GLMQuotaCountdown] Tick callback error: ${error}`);
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-runtime",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/a2a-adapter",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/architecture-generator",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Generate ADR and Mermaid diagrams from project analysis",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "pi-harness-runtime — auth",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-refactor",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "type": "module",
5
5
  "description": "Autonomous refactoring engine",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-runtime",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cache-strategy",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "type": "module",
5
5
  "description": "Smart caching with TTL and invalidation strategies (RFC-0022)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/capability-registry",
3
- "version": "1.1.13",
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",