pi-harness-runtime 1.1.12 → 1.1.14

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 (100) hide show
  1. package/CHANGELOG.md +13 -25
  2. package/footer-status.ts +49 -4
  3. package/harness/glm-quota-countdown.test.ts +323 -0
  4. package/harness/glm-quota-countdown.ts +615 -0
  5. package/harness/index.ts +12 -0
  6. package/index.ts +47 -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/dist/tui-usage-monitor.d.ts.map +1 -1
  68. package/packages/quota-manager/dist/tui-usage-monitor.js +20 -4
  69. package/packages/quota-manager/dist/tui-usage-monitor.js.map +1 -1
  70. package/packages/quota-manager/package.json +1 -1
  71. package/packages/quota-manager/src/tui-usage-monitor.ts +21 -4
  72. package/packages/rate-limiter/package.json +1 -1
  73. package/packages/react-vite-plugin/package.json +1 -1
  74. package/packages/release-manager/package.json +1 -1
  75. package/packages/requirement-compiler/package.json +1 -1
  76. package/packages/runtime/package.json +1 -1
  77. package/packages/scheduler/package.json +1 -1
  78. package/packages/scheduler-adapter/package.json +1 -1
  79. package/packages/session/package.json +1 -1
  80. package/packages/session-api/package.json +1 -1
  81. package/packages/session-export/package.json +1 -1
  82. package/packages/shared-context/package.json +1 -1
  83. package/packages/skill-registry/package.json +1 -1
  84. package/packages/sprint-planner/package.json +1 -1
  85. package/packages/subscription-engine/package.json +1 -1
  86. package/packages/task-compiler/package.json +1 -1
  87. package/packages/tencentdb-memory/package.json +1 -1
  88. package/packages/tencentdb-sync/package.json +1 -1
  89. package/packages/test-data-generator/package.json +1 -1
  90. package/packages/test-generator/package.json +1 -1
  91. package/packages/todo-bd-sync/package.json +1 -1
  92. package/packages/token-estimation/package.json +1 -1
  93. package/packages/token-optimizer/package.json +1 -1
  94. package/packages/tui/package.json +1 -1
  95. package/packages/types/package.json +1 -1
  96. package/packages/workflow-events/package.json +1 -1
  97. package/packages/workspace-scanner/package.json +1 -1
  98. package/packages/worktree/package.json +1 -1
  99. package/packages/write-review/package.json +1 -1
  100. package/proactive-compact.ts +71 -0
package/CHANGELOG.md CHANGED
@@ -2,8 +2,20 @@
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.12](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.11...v1.1.12) (2026-08-20)
5
+ ### [1.1.14](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.13...v1.1.14) (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))
6
11
 
12
+ ### [1.1.13](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.12...v1.1.13) (2026-08-21)
13
+
14
+ ### Bug Fixes
15
+
16
+ * **runtime:** auto-resume after provider overload ([7bd98bb](https://github.com/ManotLuijiu/pi-harness-runtime/commit/7bd98bb885536667a5eb35cfe6de45a06dc77269))
17
+
18
+ ### [1.1.12](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.11...v1.1.12) (2026-08-20)
7
19
 
8
20
  ### Bug Fixes
9
21
 
@@ -11,7 +23,6 @@ All notable changes to this project will be documented in this file. See [standa
11
23
 
12
24
  ### [1.1.11](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.42...v1.1.11) (2026-08-20)
13
25
 
14
-
15
26
  ### Features
16
27
 
17
28
  * add quota scrapers for ChatGPT and GLM providers ([17f9d0e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/17f9d0e7d6f486210a30244f2fecc0131e8b7d57))
@@ -19,7 +30,6 @@ All notable changes to this project will be documented in this file. See [standa
19
30
  * index.ts ([d0fc002](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d0fc002d6d16e59318155677cb6128846bc993a4))
20
31
  * update workflow run name to show version number ([ee8c59f](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ee8c59fec4c0116a5948b2ae04ad4609edaed3ac))
21
32
 
22
-
23
33
  ### Bug Fixes
24
34
 
25
35
  * add typescript as devDependency, build notification dist ([c76b522](https://github.com/ManotLuijiu/pi-harness-runtime/commit/c76b5221c33262c38615c6117ecf9db13ab7b5d2))
@@ -42,7 +52,6 @@ All notable changes to this project will be documented in this file. See [standa
42
52
 
43
53
  ### [1.1.10](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.42...v1.1.10) (2026-08-20)
44
54
 
45
-
46
55
  ### Features
47
56
 
48
57
  * add quota scrapers for ChatGPT and GLM providers ([17f9d0e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/17f9d0e7d6f486210a30244f2fecc0131e8b7d57))
@@ -50,7 +59,6 @@ All notable changes to this project will be documented in this file. See [standa
50
59
  * index.ts ([d0fc002](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d0fc002d6d16e59318155677cb6128846bc993a4))
51
60
  * update workflow run name to show version number ([ee8c59f](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ee8c59fec4c0116a5948b2ae04ad4609edaed3ac))
52
61
 
53
-
54
62
  ### Bug Fixes
55
63
 
56
64
  * add typescript as devDependency, build notification dist ([c76b522](https://github.com/ManotLuijiu/pi-harness-runtime/commit/c76b5221c33262c38615c6117ecf9db13ab7b5d2))
@@ -72,7 +80,6 @@ All notable changes to this project will be documented in this file. See [standa
72
80
 
73
81
  ### [0.10.30](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.29...v0.10.30) (2026-08-11)
74
82
 
75
-
76
83
  ### Bug Fixes
77
84
 
78
85
  * **file-copy-helper:** require explicit intent for copy rule injection ([ddfece0](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ddfece0bcbf440dfd4edc0d442b078e84e906c68))
@@ -91,13 +98,11 @@ All notable changes to this project will be documented in this file. See [standa
91
98
 
92
99
  ### [0.10.27](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.27) (2026-08-10)
93
100
 
94
-
95
101
  ### Features
96
102
 
97
103
  * create packages for missing RFC implementations ([e6ce2ff](https://github.com/ManotLuijiu/pi-harness-runtime/commit/e6ce2ffdd455b33c55212ec2b61eedcdcfb43b5e))
98
104
  * **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
99
105
 
100
-
101
106
  ### Bug Fixes
102
107
 
103
108
  * align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
@@ -113,12 +118,10 @@ All notable changes to this project will be documented in this file. See [standa
113
118
 
114
119
  ### [0.10.26](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.26) (2026-08-10)
115
120
 
116
-
117
121
  ### Features
118
122
 
119
123
  * **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
120
124
 
121
-
122
125
  ### Bug Fixes
123
126
 
124
127
  * align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
@@ -134,12 +137,10 @@ All notable changes to this project will be documented in this file. See [standa
134
137
 
135
138
  ### [0.10.25](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.25) (2026-08-10)
136
139
 
137
-
138
140
  ### Features
139
141
 
140
142
  * **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
141
143
 
142
-
143
144
  ### Bug Fixes
144
145
 
145
146
  * align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
@@ -154,12 +155,10 @@ All notable changes to this project will be documented in this file. See [standa
154
155
 
155
156
  ### [0.10.24](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.24) (2026-08-09)
156
157
 
157
-
158
158
  ### Features
159
159
 
160
160
  * **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
161
161
 
162
-
163
162
  ### Bug Fixes
164
163
 
165
164
  * align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
@@ -173,12 +172,10 @@ All notable changes to this project will be documented in this file. See [standa
173
172
 
174
173
  ### [0.10.23](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.23) (2026-08-09)
175
174
 
176
-
177
175
  ### Features
178
176
 
179
177
  * **file-copy-helper:** inject cp rule when user asks to mimic/copy files ([f089f0d](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f089f0d5e3e933231c99ee38cf18fa208c8eb8b2))
180
178
 
181
-
182
179
  ### Bug Fixes
183
180
 
184
181
  * align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
@@ -191,7 +188,6 @@ All notable changes to this project will be documented in this file. See [standa
191
188
 
192
189
  ### [0.10.22](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.22) (2026-08-09)
193
190
 
194
-
195
191
  ### Bug Fixes
196
192
 
197
193
  * align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
@@ -204,7 +200,6 @@ All notable changes to this project will be documented in this file. See [standa
204
200
 
205
201
  ### [0.10.21](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.21) (2026-08-08)
206
202
 
207
-
208
203
  ### Bug Fixes
209
204
 
210
205
  * align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
@@ -216,7 +211,6 @@ All notable changes to this project will be documented in this file. See [standa
216
211
 
217
212
  ### [0.10.20](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.14...v0.10.20) (2026-08-08)
218
213
 
219
-
220
214
  ### Bug Fixes
221
215
 
222
216
  * align TS formatting with main (remove 3-line logError) ([2515ad5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/2515ad54f43f0566123c686d3ec4f92ad363ea13))
@@ -227,14 +221,12 @@ All notable changes to this project will be documented in this file. See [standa
227
221
 
228
222
  ### [0.10.11](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.10.9...v0.10.11) (2026-08-03)
229
223
 
230
-
231
224
  ### Features
232
225
 
233
226
  * add knowledge-retrieval package for TencentDB-Agent-Memory MCP (RFC-0105) ([a4558d0](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a4558d098c7b662b01d9502c8560219eafbf1941))
234
227
  * add okf-indexer package for SKILL.md indexing (RFC-0106) ([a3cc7da](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a3cc7da0a390d5c8176d0d871418780facabcb5c))
235
228
  * extend a2a-adapter with discovery server (RFC-0104) ([0c4fc18](https://github.com/ManotLuijiu/pi-harness-runtime/commit/0c4fc1826fd55f10305bfbd894b32b5b55f1ab86))
236
229
 
237
-
238
230
  ### Bug Fixes
239
231
 
240
232
  * act on CONFLICTING immediately even when mergeStateStatus=DIRTY ([b7f290e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/b7f290ece8e0e989635aacc456fffd4ee19183ec))
@@ -256,14 +248,12 @@ All notable changes to this project will be documented in this file. See [standa
256
248
 
257
249
  ## [0.10.0](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.9.24...v0.10.0) (2026-07-30)
258
250
 
259
-
260
251
  ### Features
261
252
 
262
253
  * blackboard-coordinated loop controller with early exit ([f3cf3ea](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f3cf3ea479170dd75b7e9a7c3515dde230bb0da7))
263
254
  * herdr inter-agent event bus for autonomous code review ([1490cdd](https://github.com/ManotLuijiu/pi-harness-runtime/commit/1490cddec80dac812065c3c681984524ad3194e1))
264
255
  * loop controller for write-review cycles with early exit ([95066ce](https://github.com/ManotLuijiu/pi-harness-runtime/commit/95066ce8cb58c55eaa14d69fd826b5aaff680c16))
265
256
 
266
-
267
257
  ### Bug Fixes
268
258
 
269
259
  * **clipboard:** pass UTF-8 bytes directly to xclip via stdin, avoid pipeline corruption ([051ca18](https://github.com/ManotLuijiu/pi-harness-runtime/commit/051ca18fb1deb1169a8a4da16aa9314d2e6ec66d))
@@ -276,7 +266,6 @@ All notable changes to this project will be documented in this file. See [standa
276
266
 
277
267
  ### [0.9.25](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.9.21...v0.9.25) (2026-07-27)
278
268
 
279
-
280
269
  ### Bug Fixes
281
270
 
282
271
  * **ci:** explicitly list test files to exclude cookie auth tests in release workflow ([1c96002](https://github.com/ManotLuijiu/pi-harness-runtime/commit/1c9600232282a1ef9323300fbd763c962aff49f6))
@@ -287,7 +276,6 @@ All notable changes to this project will be documented in this file. See [standa
287
276
 
288
277
  ### [0.9.24](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v0.9.21...v0.9.24) (2026-07-27)
289
278
 
290
-
291
279
  ### Bug Fixes
292
280
 
293
281
  * **ci:** skip browser auth tests in release workflow ([6117ed5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/6117ed5af11357c6f12d11f4cd8d3441de519043))
package/footer-status.ts CHANGED
@@ -6,6 +6,51 @@ import {
6
6
  type ParsedContextWindowUsage,
7
7
  type ParsedQuotaUsageData,
8
8
  } from "./status-parsers.ts";
9
+
10
+ /**
11
+ * Format countdown seconds into a human-readable string for TUI display.
12
+ * Examples: "4h 32m", "45m 12s", "12s"
13
+ */
14
+ export function formatCountdownForDisplay(seconds: number): string {
15
+ if (seconds <= 0) return "RESET";
16
+
17
+ const days = Math.floor(seconds / 86400);
18
+ const hours = Math.floor((seconds % 86400) / 3600);
19
+ const minutes = Math.floor((seconds % 3600) / 60);
20
+ const secs = seconds % 60;
21
+
22
+ if (days > 0) {
23
+ return `${days}d ${hours}h`;
24
+ }
25
+ if (hours > 0) {
26
+ return `${hours}h ${minutes}m`;
27
+ }
28
+ if (minutes > 0) {
29
+ return `${minutes}m ${secs}s`;
30
+ }
31
+ return `${secs}s`;
32
+ }
33
+
34
+ /**
35
+ * Build a TUI-signal exhaustion status line with optional countdown.
36
+ * This is used when a provider (like GLM) hits its limit.
37
+ */
38
+ export function buildExhaustionStatusLine(
39
+ provider: string,
40
+ view: ProviderMirrorRecord,
41
+ countdownSeconds?: number,
42
+ ): string {
43
+ const label = providerDisplayName(provider);
44
+ const limitType = view.limitType ?? "tokens";
45
+ const reset = view.resets_at ?? view.h5_resets_at ?? "soon";
46
+
47
+ if (countdownSeconds !== undefined && countdownSeconds > 0) {
48
+ const countdown = formatCountdownForDisplay(countdownSeconds);
49
+ return `${label}: ⏳ ${countdown} (${limitType} hit)`;
50
+ }
51
+
52
+ return `${label}: limit hit (${limitType}), reset ${reset}`;
53
+ }
9
54
  import { getProviderLabel } from "./packages/types/src/ai-providers.js";
10
55
  import {
11
56
  providerHasContinuousScrape,
@@ -153,14 +198,14 @@ export function buildFooterStatusValue(
153
198
  const weeklyResets = view.weekly_resets_at ?? "soon";
154
199
 
155
200
  let statusLine: string;
156
- if (view.h5_used_pct !== undefined) {
201
+ if (view.h5_used_pct === undefined) {
202
+ // OpenAI: weekly-only (no 5h window)
203
+ statusLine = `week: ${weeklyLeft.toFixed(0)}% left (resets ${weeklyResets})`;
204
+ } else {
157
205
  // MiniMax: has both 5h and weekly windows
158
206
  const h5Pct = view.h5_used_pct;
159
207
  const h5Left = Math.max(0, 100 - h5Pct);
160
208
  statusLine = `5h: ${h5Left.toFixed(0)}% left · week: ${weeklyLeft.toFixed(0)}% left`;
161
- } else {
162
- // OpenAI: weekly-only (no 5h window)
163
- statusLine = `week: ${weeklyLeft.toFixed(0)}% left (resets ${weeklyResets})`;
164
209
  }
165
210
 
166
211
  const freshnessSuffix =
@@ -0,0 +1,323 @@
1
+ /**
2
+ * GLM Quota Countdown Tests
3
+ *
4
+ * Run with: bun test harness/glm-quota-countdown.test.ts
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
8
+ import {
9
+ parseGLMResetTime,
10
+ formatCountdown,
11
+ GLMQuotaCountdown,
12
+ } from "./glm-quota-countdown.js";
13
+
14
+ describe("parseGLMResetTime", () => {
15
+ it("parses full datetime format 'reset at 2026-08-25 01:47:16'", () => {
16
+ const result = parseGLMResetTime(
17
+ 'Error: 429: {"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2026-08-25 01:47:16"}',
18
+ );
19
+ expect(result).not.toBeNull();
20
+ const date = new Date(result!);
21
+ expect(date.getUTCFullYear()).toBe(2026);
22
+ expect(date.getUTCMonth()).toBe(7); // August is 7 (0-indexed)
23
+ expect(date.getUTCDate()).toBe(25);
24
+ });
25
+
26
+ it("parses full datetime format with T separator", () => {
27
+ const result = parseGLMResetTime(
28
+ "Your limit will reset at 2026-08-25T01:47:16",
29
+ );
30
+ expect(result).not.toBeNull();
31
+ const date = new Date(result!);
32
+ expect(date.getUTCFullYear()).toBe(2026);
33
+ });
34
+
35
+ it("parses time-only format when date is implied", () => {
36
+ const result = parseGLMResetTime("will reset at 14:30:00");
37
+ expect(result).not.toBeNull();
38
+ const date = new Date(result!);
39
+ expect(date.getUTCHours()).toBe(14);
40
+ expect(date.getUTCMinutes()).toBe(30);
41
+ });
42
+
43
+ it("returns null for non-GLM messages", () => {
44
+ expect(parseGLMResetTime("Hello world")).toBeNull();
45
+ expect(parseGLMResetTime("OpenAI rate limit exceeded")).toBeNull();
46
+ });
47
+
48
+ it("handles GLM error with code 1308", () => {
49
+ const result = parseGLMResetTime(
50
+ '{"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2026-08-25 01:47:16"}',
51
+ );
52
+ expect(result).not.toBeNull();
53
+ });
54
+
55
+ it("parses alternative format 'reset at 2026-08-25 01:47:16'", () => {
56
+ const result = parseGLMResetTime(
57
+ "reset at 2026-08-25 01:47:16 - please retry later",
58
+ );
59
+ expect(result).not.toBeNull();
60
+ });
61
+ });
62
+
63
+ describe("formatCountdown", () => {
64
+ it("formats days, hours, minutes, seconds", () => {
65
+ expect(formatCountdown(90061)).toBe("1d 1h 1m 1s"); // 1 day, 1 hour, 1 min, 1 sec
66
+ });
67
+
68
+ it("formats hours and minutes when no days", () => {
69
+ expect(formatCountdown(3661)).toBe("1h 1m 1s");
70
+ });
71
+
72
+ it("formats minutes and seconds when no hours", () => {
73
+ expect(formatCountdown(125)).toBe("2m 5s");
74
+ });
75
+
76
+ it("formats seconds only when less than a minute", () => {
77
+ expect(formatCountdown(45)).toBe("45s");
78
+ });
79
+
80
+ it("returns 'RESET NOW' for zero or negative", () => {
81
+ expect(formatCountdown(0)).toBe("RESET NOW");
82
+ expect(formatCountdown(-100)).toBe("RESET NOW");
83
+ });
84
+
85
+ it("handles exactly 24 hours", () => {
86
+ expect(formatCountdown(86400)).toBe("1d 0h 0m 0s");
87
+ });
88
+
89
+ it("handles exactly 1 hour", () => {
90
+ expect(formatCountdown(3600)).toBe("1h 0m 0s");
91
+ });
92
+
93
+ it("handles exactly 1 minute", () => {
94
+ expect(formatCountdown(60)).toBe("1m 0s");
95
+ });
96
+ });
97
+
98
+ describe("GLMQuotaCountdown", () => {
99
+ let countdown: GLMQuotaCountdown;
100
+ let mockCheckpoint: any;
101
+ let mockTransition: any;
102
+ let mockReadProvider: any;
103
+
104
+ beforeEach(() => {
105
+ countdown = new GLMQuotaCountdown();
106
+
107
+ // Track calls for verification
108
+ mockCheckpoint = { jobId: "test-job", status: "paused_quota" };
109
+ mockTransition = { success: true };
110
+ mockReadProvider = null;
111
+ });
112
+
113
+ afterEach(() => {
114
+ countdown.dispose();
115
+ });
116
+
117
+ describe("hasCountdown / getCountdown", () => {
118
+ it("returns false initially for unknown job", () => {
119
+ expect(countdown.hasCountdown("unknown")).toBe(false);
120
+ expect(countdown.getCountdown("unknown")).toBeNull();
121
+ });
122
+ });
123
+
124
+ describe("startCountdown", () => {
125
+ it("starts countdown and stores state", async () => {
126
+ const futureDate = new Date(Date.now() + 3600000).toISOString(); // 1 hour from now
127
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
128
+ const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
129
+
130
+ const state = await countdown.startCountdown(
131
+ "job-1",
132
+ futureDate,
133
+ mockMirror,
134
+ mockMachine,
135
+ );
136
+
137
+ expect(state.jobId).toBe("job-1");
138
+ expect(state.resetAt).toBe(futureDate);
139
+ expect(state.totalSeconds).toBeGreaterThan(3500); // ~1 hour
140
+ expect(countdown.hasCountdown("job-1")).toBe(true);
141
+ });
142
+
143
+ it("cancels existing countdown before starting new one", async () => {
144
+ const futureDate1 = new Date(Date.now() + 3600000).toISOString();
145
+ const futureDate2 = new Date(Date.now() + 7200000).toISOString();
146
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
147
+ const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
148
+
149
+ await countdown.startCountdown(
150
+ "job-1",
151
+ futureDate1,
152
+ mockMirror,
153
+ mockMachine,
154
+ );
155
+
156
+ const state = await countdown.startCountdown(
157
+ "job-1",
158
+ futureDate2,
159
+ mockMirror,
160
+ mockMachine,
161
+ );
162
+
163
+ // Should have only one countdown
164
+ expect(countdown.getAllCountdowns().length).toBe(1);
165
+ expect(state.resetAt).toBe(futureDate2);
166
+ });
167
+
168
+ it("updates mirror store with reset time", async () => {
169
+ const futureDate = new Date(Date.now() + 3600000).toISOString();
170
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
171
+ let capturedRecord: any = null;
172
+ const mockMirror = createMockMirror(
173
+ () => mockReadProvider,
174
+ (_p, r) => {
175
+ capturedRecord = r;
176
+ },
177
+ );
178
+
179
+ await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
180
+
181
+ expect(capturedRecord).not.toBeNull();
182
+ expect(capturedRecord.provider).toBe("glm");
183
+ expect(capturedRecord.exhausted).toBe(true);
184
+ expect(capturedRecord.h5_resets_at).toBe(futureDate);
185
+ });
186
+ });
187
+
188
+ describe("startFromError", () => {
189
+ it("parses reset time from error message", async () => {
190
+ const errorMsg =
191
+ 'Error: 429: {"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at 2099-12-31 23:59:59"}';
192
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
193
+ const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
194
+
195
+ const state = await countdown.startFromError(
196
+ "job-1",
197
+ errorMsg,
198
+ mockMirror,
199
+ mockMachine,
200
+ );
201
+
202
+ expect(state).not.toBeNull();
203
+ expect(state!.jobId).toBe("job-1");
204
+ expect(countdown.hasCountdown("job-1")).toBe(true);
205
+ });
206
+
207
+ it("returns null when reset time cannot be parsed", async () => {
208
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
209
+ const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
210
+
211
+ const state = await countdown.startFromError(
212
+ "job-1",
213
+ "Some unrelated error message",
214
+ mockMirror,
215
+ mockMachine,
216
+ );
217
+
218
+ expect(state).toBeNull();
219
+ });
220
+ });
221
+
222
+ describe("cancelCountdown", () => {
223
+ it("cancels active countdown", async () => {
224
+ const futureDate = new Date(Date.now() + 3600000).toISOString();
225
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
226
+ const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
227
+
228
+ await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
229
+
230
+ expect(countdown.hasCountdown("job-1")).toBe(true);
231
+
232
+ countdown.cancelCountdown("job-1");
233
+
234
+ expect(countdown.hasCountdown("job-1")).toBe(false);
235
+ });
236
+
237
+ it("handles cancelling non-existent countdown", () => {
238
+ // Should not throw
239
+ expect(() => countdown.cancelCountdown("non-existent")).not.toThrow();
240
+ });
241
+ });
242
+
243
+ describe("onTick callback", () => {
244
+ it("receives tick events", async () => {
245
+ const ticks: any[] = [];
246
+ const futureDate = new Date(Date.now() + 3600000).toISOString();
247
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
248
+ const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
249
+
250
+ const unsubscribe = countdown.onTick((event) => {
251
+ ticks.push(event);
252
+ });
253
+
254
+ await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
255
+
256
+ // Wait a bit for initial tick
257
+ await new Promise((r) => setTimeout(r, 100));
258
+
259
+ expect(ticks.length).toBeGreaterThan(0);
260
+ expect(ticks[0].jobId).toBe("job-1");
261
+ expect(ticks[0].remainingFormatted).toBeDefined();
262
+
263
+ unsubscribe();
264
+ });
265
+ });
266
+
267
+ describe("getAllCountdowns", () => {
268
+ it("returns all active countdowns", async () => {
269
+ const futureDate = new Date(Date.now() + 3600000).toISOString();
270
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
271
+ const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
272
+
273
+ await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
274
+
275
+ await countdown.startCountdown("job-2", futureDate, mockMirror, mockMachine);
276
+
277
+ const all = countdown.getAllCountdowns();
278
+ expect(all.length).toBe(2);
279
+ expect(all.map((c) => c.jobId)).toContain("job-1");
280
+ expect(all.map((c) => c.jobId)).toContain("job-2");
281
+ });
282
+ });
283
+
284
+ describe("dispose", () => {
285
+ it("clears all timers and countdowns", async () => {
286
+ const futureDate = new Date(Date.now() + 3600000).toISOString();
287
+ const mockMachine = createMockMachine(mockCheckpoint, mockTransition);
288
+ const mockMirror = createMockMirror(() => mockReadProvider, noopWrite);
289
+
290
+ await countdown.startCountdown("job-1", futureDate, mockMirror, mockMachine);
291
+
292
+ await countdown.startCountdown("job-2", futureDate, mockMirror, mockMachine);
293
+
294
+ expect(countdown.getAllCountdowns().length).toBe(2);
295
+
296
+ countdown.dispose();
297
+
298
+ expect(countdown.getAllCountdowns().length).toBe(0);
299
+ });
300
+ });
301
+ });
302
+
303
+ // No-op write function for tests that don't need to verify write calls
304
+ function noopWrite(_provider: string, _record: any): void {}
305
+
306
+ // Helper: Create mock JobStateMachine
307
+ function createMockMachine(getCheckpointResult: any, transitionResult: any) {
308
+ return {
309
+ getCheckpoint: () => getCheckpointResult,
310
+ transition: async () => transitionResult,
311
+ } as any;
312
+ }
313
+
314
+ // Helper: Create mock MirrorStore
315
+ function createMockMirror(
316
+ readProviderFn: () => any,
317
+ writeProviderFn: (provider: string, record: any) => void,
318
+ ) {
319
+ return {
320
+ readProvider: readProviderFn,
321
+ writeProvider: writeProviderFn,
322
+ } as any;
323
+ }