pi-harness-runtime 0.9.13 → 0.9.15

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 (84) hide show
  1. package/README.md +54 -45
  2. package/harness/auto-quota-resume.ts +129 -0
  3. package/harness/e2e/minimax-quota-scraper.ts +10 -4
  4. package/harness/index.ts +8 -0
  5. package/harness/loop-runtime.ts +57 -0
  6. package/index.ts +47 -11
  7. package/mirror.ts +40 -18
  8. package/package.json +1 -1
  9. package/packages/a2a-adapter/package.json +1 -1
  10. package/packages/auth/package.json +1 -1
  11. package/packages/autonomous-refactor/package.json +1 -1
  12. package/packages/cache-strategy/package.json +1 -1
  13. package/packages/capability-registry/package.json +1 -1
  14. package/packages/checkpoint/package.json +1 -1
  15. package/packages/cli-plugin-sdk/package.json +1 -1
  16. package/packages/code-generation/package.json +1 -1
  17. package/packages/code-review/package.json +1 -1
  18. package/packages/codex-adapter/package.json +1 -1
  19. package/packages/context-compiler/package.json +1 -1
  20. package/packages/context-manager/package.json +1 -1
  21. package/packages/cookie-sanitizer/package.json +1 -1
  22. package/packages/cost-optimizer/package.json +1 -1
  23. package/packages/django-plugin/package.json +1 -1
  24. package/packages/doc-generator/package.json +1 -1
  25. package/packages/evaluation-engine/package.json +1 -1
  26. package/packages/evaluation-runner/package.json +1 -1
  27. package/packages/experience-replay/package.json +1 -1
  28. package/packages/feedback-collector/package.json +1 -1
  29. package/packages/framework-detector/package.json +1 -1
  30. package/packages/framework-plugin-sdk/package.json +1 -1
  31. package/packages/frappe-plugin/package.json +1 -1
  32. package/packages/generic-web-plugin/package.json +1 -1
  33. package/packages/health-monitor/package.json +1 -1
  34. package/packages/laravel-plugin/package.json +1 -1
  35. package/packages/learning-engine/package.json +1 -1
  36. package/packages/mcp-adapter/package.json +1 -1
  37. package/packages/memory-engine/package.json +1 -1
  38. package/packages/model-registry/package.json +1 -1
  39. package/packages/nextjs-plugin/package.json +1 -1
  40. package/packages/notification/package.json +1 -1
  41. package/packages/observability/package.json +1 -1
  42. package/packages/performance-optimizer/package.json +1 -1
  43. package/packages/project-analyzer/package.json +1 -1
  44. package/packages/prompt-compiler/package.json +1 -1
  45. package/packages/prompt-versioning/package.json +1 -1
  46. package/packages/provider-adapter-sdk/package.json +1 -1
  47. package/packages/provider-router/package.json +1 -1
  48. package/packages/provider-selector/package.json +1 -1
  49. package/packages/providers/dist/provider-id.d.ts +2 -0
  50. package/packages/providers/dist/provider-id.d.ts.map +1 -1
  51. package/packages/providers/dist/provider-id.js +9 -0
  52. package/packages/providers/dist/provider-id.js.map +1 -1
  53. package/packages/providers/package.json +1 -1
  54. package/packages/providers/src/provider-id.ts +12 -0
  55. package/packages/quota-manager/package.json +1 -1
  56. package/packages/rate-limiter/package.json +1 -1
  57. package/packages/react-vite-plugin/package.json +1 -1
  58. package/packages/release-manager/package.json +1 -1
  59. package/packages/requirement-compiler/package.json +1 -1
  60. package/packages/runtime/package.json +1 -1
  61. package/packages/scheduler/package.json +1 -1
  62. package/packages/session/package.json +1 -1
  63. package/packages/shared-context/package.json +1 -1
  64. package/packages/skill-registry/package.json +1 -1
  65. package/packages/sprint-planner/package.json +1 -1
  66. package/packages/task-compiler/package.json +1 -1
  67. package/packages/test-data-generator/package.json +1 -1
  68. package/packages/test-generator/package.json +1 -1
  69. package/packages/token-estimation/package.json +1 -1
  70. package/packages/token-optimizer/package.json +1 -1
  71. package/packages/tui/package.json +1 -1
  72. package/packages/types/dist/ai-providers.d.ts +36 -0
  73. package/packages/types/dist/ai-providers.d.ts.map +1 -0
  74. package/packages/types/dist/ai-providers.js +66 -0
  75. package/packages/types/dist/ai-providers.js.map +1 -0
  76. package/packages/types/dist/runtime-types.d.ts +6 -0
  77. package/packages/types/dist/runtime-types.d.ts.map +1 -1
  78. package/packages/types/dist/runtime-types.js +2 -1
  79. package/packages/types/dist/runtime-types.js.map +1 -1
  80. package/packages/types/package.json +1 -1
  81. package/packages/types/src/ai-providers.ts +73 -0
  82. package/packages/types/src/runtime-types.ts +15 -0
  83. package/packages/types/tsconfig.tsbuildinfo +1 -1
  84. package/packages/worktree/package.json +1 -1
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Pi Harness Runtime
2
2
 
3
- [![Beta](https://img.shields.io/badge/version-beta-orange?style=for-the-badge)](https://github.com/ManotLuijiu/pi-harness-runtime)
3
+ [![Beta](https://img.shields.io/badge/version-0.9.13-orange?style=for-the-badge)](https://github.com/ManotLuijiu/pi-harness-runtime)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT)
5
5
  [![Platform](https://img.shields.io/badge/Platform-macOS%20|Linux-blue?style=for-the-badge)]()
6
6
 
7
7
  > ⚠️ **Beta Notice — Not Production Ready ⚠️**
8
- > This project is in active development. Core features work but E2E testing is not yet implemented.
8
+ > This project is in active development. Core features work.
9
9
 
10
10
  **Autonomous AI coding harness for pi: local token tracking + provider mirror + task orchestration.**
11
11
 
@@ -31,7 +31,7 @@ Requires Pi v0.37.3+.
31
31
 
32
32
  ```bash
33
33
  /usage # show full status (local tracking + provider mirror)
34
- /usage refresh # force refresh quota from MiniMax console
34
+ /usage refresh # force refresh quota from provider console
35
35
  /usage today # today's usage + 5h window
36
36
  /usage week # this week's usage + lifetime totals
37
37
  /usage reset # clear provider mirror
@@ -82,6 +82,8 @@ Requires Pi v0.37.3+.
82
82
  ```
83
83
  pi-harness-runtime/
84
84
  ├── index.ts # Extension entry point
85
+ ├── footer-status.ts # Footer bar renderer (local vs. mirror mode)
86
+ ├── mirror.ts # Per-provider quota mirror (mirror.json)
85
87
  ├── harness/
86
88
  │ ├── job-state-machine.ts # 14-state lifecycle machine
87
89
  │ ├── task-graph.ts # DAG-based task management
@@ -92,13 +94,15 @@ pi-harness-runtime/
92
94
  │ ├── context-window-manager.ts # Context tracking
93
95
  │ ├── agent-handoff.ts # Clean agent transitions
94
96
  │ ├── e2e/
95
- │ │ ├── test-engine.ts # E2E test runner (coming soon)
96
- │ │ └── playwright-runner.ts # Browser automation
97
+ │ │ ├── minimax-quota-scraper.ts # Playwright MiniMax scraper
98
+ │ │ └── playwright-runner.ts # Browser automation
97
99
  │ └── project-detector/
98
100
  │ └── detector.ts # Auto-detect project type
99
101
  ├── packages/
100
- │ ├── providers/adapters.ts # MiniMax, OpenAI adapters
102
+ │ ├── cookie-sanitizer/ # Live cookie-file sync via chokidar
103
+ │ ├── providers/ # MiniMax, OpenAI, Codex adapters
101
104
  │ ├── quota-manager/ # Quota signal collection
105
+ │ ├── auth/ # MiniMax browser auth
102
106
  │ └── worktree/ # Git worktree per task
103
107
  └── skills/
104
108
  └── harness-runtime/
@@ -124,6 +128,7 @@ cancelled blocked waiting_human repairing ready_for_client
124
128
  - **Auto-repair**: Failure classification + retry with exponential backoff
125
129
  - **E2E testing**: Scenario-based Playwright integration (coming soon)
126
130
  - **Project detection**: Auto-detects Frappe, Next.js, React, Django, Laravel
131
+ - **Per-provider mirror**: Each LLM provider maintains its own quota record
127
132
 
128
133
  ## Data Directory
129
134
 
@@ -133,7 +138,7 @@ All data stored locally in `~/.pi/`:
133
138
  ~/.pi/
134
139
  ├── usage-status/ # /usage data
135
140
  │ ├── usage.jsonl
136
- │ └── mirror.json
141
+ │ └── mirror.json # per-provider quota mirror
137
142
  ├── harness/ # /harness data
138
143
  │ └── jobs/
139
144
  │ └── <job-id>/
@@ -193,59 +198,58 @@ bun test # 131+ tests passing
193
198
 
194
199
  Quota data is automatically surfaced per-provider:
195
200
 
196
- | Provider | Source | Cadence |
201
+ | Provider | Source | Notes |
197
202
  |---|---|---|
198
- | MiniMax | Playwright + cookies (continuous scrape) | every ~15 min |
199
- | OpenAI / OpenAI Codex | TUI message capture (signal on limit) | on first limit hit |
200
- | GLM (Z.ai / Zhipu) | TUI message capture (signal on limit) | on first limit hit |
201
- | Anthropic | TUI message capture (signal on limit) | on first limit hit |
202
- | OpenRouter | TUI message capture (signal on limit) | on first limit hit |
203
+ | MiniMax | Live cookie-file watch + Playwright scrape | Drop folder: `~/.pi-harness-runtime/cookies/` |
204
+ | OpenAI / OpenAI Codex | TUI message capture | Signal on first limit hit |
205
+ | GLM (Z.ai / Zhipu) | TUI message capture | Signal on first limit hit |
206
+ | Anthropic | TUI message capture | Signal on first limit hit |
207
+ | OpenRouter | TUI message capture | Signal on first limit hit |
203
208
  | Other / unknown | (no data — footer shows hint) | — |
204
209
 
205
210
  The footer line carries the provider label, so you always know which LLM the data is from:
206
211
 
207
212
  ```
208
- MiniMax: 5h: 92% left · week: 80% left # continuous (scrape)
209
- OpenAI: 5h: -- · week: -- (no signal yet) # pending first limit hit
213
+ MiniMax: 5h: 92% left · week: 80% left # continuous (cookie scrape)
214
+ OpenAI: 5h: -- · week: -- (no signal yet) # pending first limit hit
210
215
  GLM: limit hit (tokens), reset 2026-07-23T17:28:00Z # TUI signal observed
216
+ MiniMax: cookies not found — drop any cookie file into
217
+ ~/.pi-harness-runtime/cookies/
211
218
  ```
212
219
 
213
- 1. **MiniMax setup** (one-time):
220
+ ### MiniMax Setup (one-time)
221
+
222
+ The runtime watches `~/.pi-harness-runtime/cookies/` for cookie files. Drop any cookie export there — it syncs within ~1 second:
223
+
224
+ 1. **Install EditThisCookie** Chrome extension → export from `platform.minimax.io`
225
+ 2. **Create the drop folder:**
214
226
 
215
227
  ```bash
216
- # Install EditThisCookie Chrome extension
217
- # Go to platform.minimax.io
218
- # Export cookies → Netscape format
219
- # Save to ~/.config/minimax-cookies.txt
228
+ mkdir -p ~/.pi-harness-runtime/cookies
220
229
  ```
221
230
 
222
- **Recommended:** drop the file into the dedicated cookie folder. The runtime
223
- normalizes any format (Netscape or EditThisCookie JSON, any filename) on its own:
231
+ 3. **Drop your cookie file in the folder** any filename, any format (Netscape `.txt` or EditThisCookie JSON `.json`):
224
232
 
225
233
  ```bash
226
- # Save cookies as any name, any format into:
227
- mkdir -p ~/.pi-harness-runtime/cookies
228
- cp ~/Downloads/platform.minimax.io_cookies.txt ~/.pi-harness-runtime/cookies/
229
- # …or use the EditThisCookie extension export (JSON) — same folder works.
234
+ cp ~/Downloads/platform.minimax.io_cookies.txt \
235
+ ~/.pi-harness-runtime/cookies/
236
+ # …or use the EditThisCookie JSON export — same folder works.
230
237
  ```
231
238
 
232
- The `cookie-sanitizer` package (`@pi-harness/cookie-sanitizer`) runs chokidar v5 on this
239
+ The `cookie-sanitizer` package (`@pi-harness/cookie-sanitizer`) runs chokidar on this
233
240
  folder, normalizes everything to standard Netscape, and writes the canonical cache at
234
241
  `~/.config/minimax-cookies.txt`. Live update latency: <1 second after drop.
235
242
 
236
- 2. **Other providers**: no setup needed. The runtime reads messages from the active
237
- session and captures quota-exhaustion signals from the TUI. Once any limit is hit
238
- (e.g. `OpenAI: context length exceeded, reset in 3 hr 27 min`), the footer updates
239
- immediately and the data persists across `~/.pi/usage-status/mirror.json`.
243
+ 4. **Done!** Quota auto-refreshes every ~15 minutes. Cookie file changes sync immediately.
240
244
 
241
- 3. **Environment** (optional override):
245
+ **Override:** set `QUOTA_COOKIE_FILE=/path/to/your/cookies.txt` to bypass the sanitizer
246
+ and read a specific file directly.
242
247
 
243
- ```bash
244
- export QUOTA_COOKIE_FILE=/path/to/your/cookies.txt
245
- # When set, the sanitizer is bypassed and the scraper reads this file directly.
246
- ```
248
+ ### Other Providers
247
249
 
248
- 4. **Done!** Quota auto-refreshes every 5 minutes. Cookie file changes sync within ~1 second.
250
+ No setup needed. The runtime reads TUI messages and captures quota-exhaustion signals.
251
+ Once any limit is hit (e.g. `OpenAI: context length exceeded, reset in 3 hr 27 min`), the
252
+ footer updates immediately and the data persists in `mirror.json`.
249
253
 
250
254
  ## Roadmap
251
255
 
@@ -254,9 +258,13 @@ GLM: limit hit (tokens), reset 2026-07-23T17:28:00Z # TUI signal observed
254
258
  - [x] Repair engine
255
259
  - [x] Shared blackboard
256
260
  - [x] MiniMax Web automatic quota detection
261
+ - [x] Cookie-file drop folder + live sync (`cookie-sanitizer`)
262
+ - [x] Per-provider quota mirror (`mirror.json` keyed by provider)
263
+ - [x] Multi-provider TUI signal capture (OpenAI, GLM, Anthropic, OpenRouter)
257
264
  - [ ] E2E test engine integration
258
265
  - [ ] Claude adapter
259
266
  - [ ] Production stress testing
267
+ - [ ] RFC-0101: Autonomous Operations Runtime (background execution, task inbox, lease protocol)
260
268
 
261
269
  ## License
262
270
 
@@ -268,17 +276,17 @@ MIT © 2026 MooCoding
268
276
  - [context-mode](https://github.com/MiniMax-AI/context-mode) — Context window tracking
269
277
  - [pi-web-access](https://github.com/nicobailon/pi-web-access) — Web search for pi
270
278
 
271
- Day 6 adds runtime recovery and remote-observability architecture.
272
-
273
279
  ## RFCs
274
280
 
275
- - RFC-0019 Auto Compact and Continue
276
- - RFC-0020 Output Token Limit Handler
277
- - RFC-0021 Partial Response Recovery
278
- - RFC-0022 Notification Center
279
- - RFC-0023 Local Browser Agent
281
+ The project tracks 101 design documents:
282
+
283
+ | RFC | Title | Status |
284
+ |-----|-------|--------|
285
+ | 0001–0034 | Loop Runtime, Provider Adapter, Quota Manager, Playwright, Git Worktree, Checkpoint, Scheduler, Provider Router, Shared Context, Context Window Manager, Shared Blackboard, Agent Handoff, E2E Test Engine, Project Detector, Job State Machine, Task Graph, Master Planner, Repair Engine, Auto Compact & Continue, Output Token Limit, Partial Response Recovery, Notification Center, Local Browser Agent, Local Runtime Agent, Command Executor, Workspace Manager, Runtime API, Policy Engine, DeepAgents Adapter, Agent Worker Interface, Provider Adapter SDK, Checkpoint Engine, Session Manager | Proposed |
286
+ | 0035–0100 | Observability, Roadmap V1, Code Review, Code Generation, Framework Detector, Test Data Generator, Framework Plugin SDK, Prompt Compiler, Context Compiler, Requirement Compiler, Task Compiler, Project Analyzer, Dependency Analyzer, Repo Knowledge Graph, Code Merge Engine, Conflict Resolution, Documentation Generator, Capability Registry, Skill Registry, Model Registry, Cost Optimizer, Performance Optimizer, Evaluation Engine, Learning Engine, Experience Replay, Memory Engine OKF, Frappe/Next.js/React/Django/Laravel/Generic Web Plugins, CLI Plugin SDK, MCP Adapter, A2A Adapter, Codex Adapter, Requirement Intake, Project Bootstrap, Architecture Generator, Sprint Planner, Milestone Manager, Backlog Manager, Human Approval Gate, Release Manager, Version Manager, Deployment Pipeline, Multi-User Runtime, RBAC, Audit Trail, Dashboard, Analytics, Distributed Runtime, Cluster Manager, Runtime Federation, High Availability, Disaster Recovery, + 17 Autonomous Architectures | Proposed |
287
+ | 0101 | [Autonomous Operations Runtime](RFC/0101_AUTONOMOUS_OPERATIONS_RUNTIME.md) — durable Task Inbox, Background Worker, Task Lease protocol, Privilege Broker, Approval Class policy engine, abstract Scheduler, Notification pipeline, Recovery semantics | Proposed |
280
288
 
281
- ## Why this matters
289
+ ## Why This Matters
282
290
 
283
291
  These RFCs address real runtime failures:
284
292
 
@@ -287,3 +295,4 @@ These RFCs address real runtime failures:
287
295
  - Partial responses must be preserved and continued.
288
296
  - The human needs mobile/tablet notifications.
289
297
  - Headless servers cannot open interactive login windows for MiniMax usage console.
298
+ - The runtime must continue executing after the chat session ends, the SSH session disconnects, or the provider quota exhausts.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Auto Quota Resume — schedules automatic job resume when quota resets.
3
+ *
4
+ * Flow:
5
+ * quota exhaustion detected
6
+ * → read h5_resets_at_epoch from mirror
7
+ * → compute resumeAt ISO timestamp
8
+ * → call machine.setResumeTime(resumeAt)
9
+ * → schedule setTimeout for auto-resume
10
+ * → at timeout: machine.transition("running")
11
+ *
12
+ * Only the 5h window is auto-resumed (provider-reported, predictable).
13
+ * Weekly/monthly quota exhaustion requires human monitoring.
14
+ */
15
+
16
+ import type { MirrorStore } from "../mirror.js";
17
+ import type { JobStateMachine } from "./job-state-machine.js";
18
+
19
+ /** Small buffer (ms) before the exact reset time to resume. */
20
+ const RESUME_BUFFER_MS = 10_000; // 10 seconds — resume just before reset
21
+
22
+ /** Minimum time to wait before auto-resuming (avoid immediate re-trigger). */
23
+ const MIN_RESUME_DELAY_MS = 5_000; // 5 seconds
24
+
25
+ /** Map of jobId → active auto-resume timer handle. */
26
+ const activeTimers = new Map<
27
+ string,
28
+ { timeout: ReturnType<typeof setTimeout>; resumeAt: number }
29
+ >();
30
+
31
+ /**
32
+ * Schedule an auto-resume for a job that was paused due to quota exhaustion.
33
+ *
34
+ * Reads the reset epoch from the mirror store for the given provider,
35
+ * computes the resume time, sets it on the checkpoint, and schedules
36
+ * a setTimeout to transition back to "running".
37
+ *
38
+ * Returns the scheduled resumeAt ISO string, or null if no epoch data
39
+ * is available (requires human resume).
40
+ */
41
+ export function scheduleAutoResume(
42
+ provider: string,
43
+ machine: JobStateMachine,
44
+ mirrorStore: MirrorStore,
45
+ ): string | null {
46
+ const jobId = machine.getCheckpoint()?.jobId;
47
+ if (!jobId) return null;
48
+
49
+ // Cancel any existing timer for this job
50
+ cancelAutoResume(jobId);
51
+
52
+ const mirror = mirrorStore.readAll();
53
+ const record = mirror?.[provider];
54
+ if (!record) return null;
55
+
56
+ // Only auto-resume on 5h quota exhaustion (has precise epoch)
57
+ if (typeof record.h5_resets_at_epoch !== "number") return null;
58
+
59
+ const resetEpoch = record.h5_resets_at_epoch;
60
+ const now = Date.now();
61
+ const delayMs = Math.max(resetEpoch - now - RESUME_BUFFER_MS, MIN_RESUME_DELAY_MS);
62
+ const resumeAt = now + delayMs;
63
+ const resumeAtIso = new Date(resumeAt).toISOString();
64
+
65
+ // Persist to checkpoint so resumeAt survives worker restart
66
+ machine.setResumeTime(resumeAtIso);
67
+
68
+ console.log(
69
+ `[auto-quota-resume] ${provider} 5h quota exhausted.` +
70
+ ` Auto-resume scheduled at ${resumeAtIso} (in ${Math.round(delayMs / 1000)}s)`,
71
+ );
72
+
73
+ const timeout = setTimeout(async () => {
74
+ activeTimers.delete(jobId);
75
+
76
+ const checkpoint = machine.getCheckpoint();
77
+ if (!checkpoint || checkpoint.status !== "paused_quota") {
78
+ // Job was manually resumed or cancelled
79
+ return;
80
+ }
81
+
82
+ // Check the mirror — if quota is still exhausted, shift the timer
83
+ const updated = mirrorStore.readAll()?.[provider];
84
+ if (updated?.h5_resets_at_epoch) {
85
+ const stillExhausted = Date.now() < updated.h5_resets_at_epoch;
86
+ if (stillExhausted) {
87
+ console.log(
88
+ `[auto-quota-resume] ${provider} still exhausted at scheduled time.` +
89
+ ` Re-scheduling...`,
90
+ );
91
+ scheduleAutoResume(provider, machine, mirrorStore);
92
+ return;
93
+ }
94
+ }
95
+
96
+ const result = await machine.transition("running");
97
+ if (result.success) {
98
+ console.log(
99
+ `[auto-quota-resume] Job ${jobId} auto-resumed after ${provider} 5h quota reset.`,
100
+ );
101
+ } else {
102
+ console.error(
103
+ `[auto-quota-resume] Auto-resume failed for ${jobId}: ${result.error}`,
104
+ );
105
+ }
106
+ }, delayMs);
107
+
108
+ activeTimers.set(jobId, { timeout, resumeAt });
109
+ return resumeAtIso;
110
+ }
111
+
112
+ /**
113
+ * Cancel the auto-resume timer for a job.
114
+ * Call this when the job is manually resumed or cancelled.
115
+ */
116
+ export function cancelAutoResume(jobId: string): void {
117
+ const existing = activeTimers.get(jobId);
118
+ if (existing) {
119
+ clearTimeout(existing.timeout);
120
+ activeTimers.delete(jobId);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get the scheduled resume time for a job, or null if not scheduled.
126
+ */
127
+ export function getScheduledResume(jobId: string): number | null {
128
+ return activeTimers.get(jobId)?.resumeAt ?? null;
129
+ }
@@ -21,10 +21,14 @@ export interface MiniMaxQuotaData {
21
21
  h5UsedPct: number;
22
22
  /** When the 5h window resets (e.g., "in 4 hr 56 min") */
23
23
  h5ResetsAt?: string;
24
+ /** Epoch ms when the 5h window resets — precise UTC timestamp. */
25
+ h5ResetsAtEpoch?: number;
24
26
  /** Weekly usage percentage (0-100) */
25
27
  weeklyUsedPct: number;
26
28
  /** When the weekly window resets */
27
29
  weeklyResetsAt?: string;
30
+ /** Epoch ms when the weekly window resets — precise UTC timestamp. */
31
+ weeklyResetsAtEpoch?: number;
28
32
  /** Monthly usage percentage (0-100) */
29
33
  monthlyUsedPct?: number;
30
34
  /** When the monthly window resets */
@@ -301,10 +305,10 @@ export class MiniMaxQuotaScraper {
301
305
  const weeklyUsedPct = parsePctStr(
302
306
  general.current_weekly_used_percent as unknown,
303
307
  );
304
- const h5ResetsAt = formatRemainsMs(general.end_time as number | undefined);
305
- const weeklyResetsAt = formatRemainsMs(
306
- general.weekly_end_time as number | undefined,
307
- );
308
+ const h5ResetsAtEpoch = general.end_time as number | undefined;
309
+ const h5ResetsAt = formatRemainsMs(h5ResetsAtEpoch);
310
+ const weeklyResetsAtEpoch = general.weekly_end_time as number | undefined;
311
+ const weeklyResetsAt = formatRemainsMs(weeklyResetsAtEpoch);
308
312
 
309
313
  // Best-effort: token usage summary
310
314
  let tokenUsage: MiniMaxQuotaData["tokenUsage"];
@@ -357,8 +361,10 @@ export class MiniMaxQuotaScraper {
357
361
  provider: "minimax",
358
362
  h5UsedPct: h5UsedPct ?? 0,
359
363
  h5ResetsAt,
364
+ h5ResetsAtEpoch,
360
365
  weeklyUsedPct: weeklyUsedPct ?? 0,
361
366
  weeklyResetsAt,
367
+ weeklyResetsAtEpoch,
362
368
  creditBalance,
363
369
  tokenUsage,
364
370
  apiEndpoints: [
package/harness/index.ts CHANGED
@@ -54,6 +54,14 @@ export type { LoopResult, LoopCallbacks } from "./loop-runtime.js";
54
54
  export { RepairEngine } from "./repair-engine.js";
55
55
  export type { RepairResult } from "./repair-engine.js";
56
56
 
57
+ // Auto Quota Resume (5h window auto-resume)
58
+ export {
59
+ scheduleAutoResume,
60
+ cancelAutoResume,
61
+ getScheduledResume,
62
+ } from "./auto-quota-resume.js";
63
+ export type {} from "./auto-quota-resume.js";
64
+
57
65
  // Master Planner
58
66
  export { MasterPlanner } from "./master-planner.js";
59
67
  export type { PlanResult } from "./master-planner.js";
@@ -26,6 +26,7 @@ import type {
26
26
  CompactResult,
27
27
  RuntimeEvent,
28
28
  } from "../packages/types/src/runtime-types.js";
29
+ import { MirrorStore } from "../mirror.js";
29
30
  import {
30
31
  JobStateMachine,
31
32
  type CheckpointManager,
@@ -80,6 +81,14 @@ export interface LoopCallbacks {
80
81
  event: "paused" | "resumed" | "exhausted",
81
82
  provider?: string,
82
83
  ) => Promise<void>;
84
+ /** Get current checkpoint (used by auto-resume to read resumeAt) */
85
+ onGetCheckpoint?: () => Promise<RuntimeCheckpoint | null>;
86
+ /** Check mirror for a provider (used by auto-resume to detect early reset) */
87
+ onCheckMirror?: (
88
+ provider: string,
89
+ ) => Promise<{ h5_used_pct?: number } | null>;
90
+ /** Generic notify (used by auto-resume to tell user wait time) */
91
+ onNotify?: (msg: string, type?: string) => Promise<void>;
83
92
  /** Save checkpoint (call persistence layer) */
84
93
  onCheckpoint?: (checkpoint: RuntimeCheckpoint) => Promise<void>;
85
94
  /** Notify each loop iteration */
@@ -106,6 +115,7 @@ export class LoopRuntime {
106
115
  private readonly config: Required<LoopConfig>;
107
116
  private readonly state: LoopState;
108
117
  private readonly jobState: JobStateMachine;
118
+ private readonly mirrorStore: MirrorStore;
109
119
  private readonly callbacks: LoopCallbacks;
110
120
  private running = false;
111
121
  private paused = false;
@@ -172,6 +182,7 @@ export class LoopRuntime {
172
182
  },
173
183
  },
174
184
  });
185
+ this.mirrorStore = new MirrorStore();
175
186
 
176
187
  // Initialize session memory
177
188
  this.sessionMemory = createSessionMemoryManager(config.jobId);
@@ -214,6 +225,35 @@ export class LoopRuntime {
214
225
 
215
226
  async run(): Promise<LoopResult> {
216
227
  this.running = true;
228
+
229
+ // ─── Auto-resume from quota pause ──────────────────────────────────
230
+ const checkpoint = await this.callbacks.onGetCheckpoint?.();
231
+ if (checkpoint?.status === "paused_quota" && checkpoint.resumeAt) {
232
+ const resumeMs = new Date(checkpoint.resumeAt).getTime();
233
+ const now = Date.now();
234
+ const waitMs = resumeMs - now;
235
+ if (waitMs > 0) {
236
+ const waitMin = Math.ceil(waitMs / 60_000);
237
+ const notify = this.callbacks.onNotify ?? (() => {});
238
+ await notify(
239
+ `Quota reset in ${waitMin} min — auto-resuming at ${checkpoint.resumeAt}`,
240
+ "info",
241
+ );
242
+ // Wait in 5-minute chunks so we don't block forever if quota resets early
243
+ while (this.running && Date.now() < resumeMs) {
244
+ await new Promise((r) => setTimeout(r, 5 * 60_000)); // 5 min
245
+ // Re-check mirror — quota may have reset early
246
+ const fresh = await this.callbacks.onCheckMirror?.("minimax");
247
+ if (fresh && fresh.h5_used_pct !== undefined && fresh.h5_used_pct < 100) {
248
+ break; // quota reset early, resume now
249
+ }
250
+ }
251
+ // Transition back to running
252
+ await this.jobState.transition("running");
253
+ this.callbacks.onQuotaEvent?.("resumed");
254
+ }
255
+ }
256
+
217
257
  this.state.status = "running";
218
258
 
219
259
  try {
@@ -652,6 +692,23 @@ export class LoopRuntime {
652
692
  this.paused = true;
653
693
  this.state.status = "paused_quota";
654
694
  this.jobState.transition("paused_quota");
695
+
696
+ // Set resumeAt from the mirror's known reset epoch so the auto-resume
697
+ // timer knows when to wake this job up.
698
+ const mirror = this.mirrorStore.readProvider("minimax");
699
+ const epoch = mirror?.h5_resets_at_epoch;
700
+ if (epoch) {
701
+ const resumeAt = new Date(epoch).toISOString();
702
+ await this.jobState.setResumeTime(resumeAt);
703
+ console.log(
704
+ `[LoopRuntime] Paused for 5h quota. Auto-resume at ${resumeAt}`,
705
+ );
706
+ } else {
707
+ console.log(
708
+ "[LoopRuntime] Paused for quota but no reset time known yet.",
709
+ );
710
+ }
711
+
655
712
  await this.callbacks.onQuotaEvent?.("paused");
656
713
  await this.saveCheckpoint();
657
714
  }
package/index.ts CHANGED
@@ -62,6 +62,10 @@ import {
62
62
  type SharedBlackboard,
63
63
  createBlackboard,
64
64
  } from "./harness/blackboard.ts";
65
+ import {
66
+ scheduleAutoResume,
67
+ cancelAutoResume,
68
+ } from "./harness/index.js";
65
69
  import { homedir } from "node:os";
66
70
  import { existsSync, mkdirSync } from "node:fs";
67
71
  import { join } from "node:path";
@@ -469,8 +473,10 @@ export default function (pi: ExtensionAPI) {
469
473
  source: "scrape",
470
474
  h5_used_pct: data.h5UsedPct,
471
475
  h5_resets_at: data.h5ResetsAt,
476
+ h5_resets_at_epoch: data.h5ResetsAtEpoch,
472
477
  weekly_used_pct: data.weeklyUsedPct,
473
478
  weekly_resets_at: data.weeklyResetsAt,
479
+ weekly_resets_at_epoch: data.weeklyResetsAtEpoch,
474
480
  });
475
481
  return true;
476
482
  } catch (error) {
@@ -794,25 +800,51 @@ export default function (pi: ExtensionAPI) {
794
800
  return;
795
801
  }
796
802
 
797
- const checkpoint = currentSession.machine.getCheckpoint();
798
- if (!checkpoint) {
799
- ctx.ui.notify("Failed to get checkpoint.", "error");
800
- return;
801
- }
803
+ const checkpoint = currentSession.machine.getCheckpoint();
804
+ if (!checkpoint) {
805
+ ctx.ui.notify("Failed to get checkpoint.", "error");
806
+ return;
807
+ }
802
808
 
803
- const result = await currentSession.machine.transition("paused_quota");
804
- if (!result.success) {
805
- ctx.ui.notify(`Failed to pause: ${result.error}`, "error");
806
- return;
807
- }
809
+ // Read 5h reset epoch from mirror so we can schedule auto-resume
810
+ const mirror = mirrorStore.readProvider("minimax");
811
+ const resumeAtIso = mirror?.h5_resets_at_epoch
812
+ ? new Date(mirror.h5_resets_at_epoch).toISOString()
813
+ : undefined;
814
+
815
+ const result = await currentSession.machine.transition("paused_quota");
816
+ if (!result.success) {
817
+ ctx.ui.notify(`Failed to pause: ${result.error}`, "error");
818
+ return;
819
+ }
808
820
 
821
+ // Persist resumeAt so auto-resume survives worker restart
822
+ if (resumeAtIso) {
823
+ await currentSession.machine.setResumeTime(resumeAtIso);
824
+ }
825
+
826
+ // Schedule auto-resume for 5h quota
827
+ if (resumeAtIso) {
828
+ const scheduled = scheduleAutoResume(
829
+ "minimax",
830
+ currentSession.machine,
831
+ mirrorStore,
832
+ );
833
+ ctx.ui.notify(
834
+ `Job paused.\n` +
835
+ `Auto-resume at ${scheduled ?? resumeAtIso} (5h quota exhausted).\n` +
836
+ `Run /harness cancel to abort.`,
837
+ "info",
838
+ );
839
+ } else {
809
840
  ctx.ui.notify(
810
841
  `Job ${currentSession.jobId} paused.\n` +
811
842
  `Current status: paused_quota\n` +
812
843
  `Run /harness resume to continue.`,
813
844
  "info",
814
845
  );
815
- },
846
+ }
847
+ }
816
848
  });
817
849
 
818
850
  // ─── /harness resume — Resume the harness job ───────────────────────
@@ -839,6 +871,9 @@ export default function (pi: ExtensionAPI) {
839
871
  return;
840
872
  }
841
873
 
874
+ // Cancel any scheduled auto-resume for this job
875
+ cancelAutoResume(currentSession.jobId);
876
+
842
877
  ctx.ui.notify(
843
878
  `Job ${currentSession.jobId} resumed.\n` +
844
879
  `Current status: running\n` +
@@ -873,6 +908,7 @@ export default function (pi: ExtensionAPI) {
873
908
  return;
874
909
  }
875
910
 
911
+ cancelAutoResume(currentSession.jobId);
876
912
  const sessionJobId = currentSession.jobId;
877
913
  currentSession = null;
878
914