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.
- package/README.md +54 -45
- package/harness/auto-quota-resume.ts +129 -0
- package/harness/e2e/minimax-quota-scraper.ts +10 -4
- package/harness/index.ts +8 -0
- package/harness/loop-runtime.ts +57 -0
- package/index.ts +47 -11
- package/mirror.ts +40 -18
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/dist/provider-id.d.ts +2 -0
- package/packages/providers/dist/provider-id.d.ts.map +1 -1
- package/packages/providers/dist/provider-id.js +9 -0
- package/packages/providers/dist/provider-id.js.map +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/providers/src/provider-id.ts +12 -0
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/dist/ai-providers.d.ts +36 -0
- package/packages/types/dist/ai-providers.d.ts.map +1 -0
- package/packages/types/dist/ai-providers.js +66 -0
- package/packages/types/dist/ai-providers.js.map +1 -0
- package/packages/types/dist/runtime-types.d.ts +6 -0
- package/packages/types/dist/runtime-types.d.ts.map +1 -1
- package/packages/types/dist/runtime-types.js +2 -1
- package/packages/types/dist/runtime-types.js.map +1 -1
- package/packages/types/package.json +1 -1
- package/packages/types/src/ai-providers.ts +73 -0
- package/packages/types/src/runtime-types.ts +15 -0
- package/packages/types/tsconfig.tsbuildinfo +1 -1
- package/packages/worktree/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Pi Harness Runtime
|
|
2
2
|
|
|
3
|
-
[](https://github.com/ManotLuijiu/pi-harness-runtime)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[]()
|
|
6
6
|
|
|
7
7
|
> ⚠️ **Beta Notice — Not Production Ready ⚠️**
|
|
8
|
-
> This project is in active development. Core features work
|
|
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
|
|
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
|
-
│ │ ├──
|
|
96
|
-
│ │ └── playwright-runner.ts
|
|
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
|
-
│ ├──
|
|
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 |
|
|
201
|
+
| Provider | Source | Notes |
|
|
197
202
|
|---|---|---|
|
|
198
|
-
| MiniMax |
|
|
199
|
-
| OpenAI / OpenAI Codex | TUI message capture
|
|
200
|
-
| GLM (Z.ai / Zhipu) | TUI message capture
|
|
201
|
-
| Anthropic | TUI message capture
|
|
202
|
-
| OpenRouter | TUI message capture
|
|
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
|
|
209
|
-
OpenAI: 5h: -- · week: -- (no signal yet)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
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
|
|
305
|
-
const
|
|
306
|
-
|
|
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";
|
package/harness/loop-runtime.ts
CHANGED
|
@@ -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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
|