pi-harness-runtime 0.9.14 → 0.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/autonomous-runtime/README.md +48 -0
- package/packages/autonomous-runtime/dist/inbox.d.ts +75 -0
- package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/inbox.js +212 -0
- package/packages/autonomous-runtime/dist/inbox.js.map +1 -0
- package/packages/autonomous-runtime/dist/index.d.ts +17 -0
- package/packages/autonomous-runtime/dist/index.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/index.js +17 -0
- package/packages/autonomous-runtime/dist/index.js.map +1 -0
- package/packages/autonomous-runtime/dist/lease.d.ts +78 -0
- package/packages/autonomous-runtime/dist/lease.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/lease.js +273 -0
- package/packages/autonomous-runtime/dist/lease.js.map +1 -0
- package/packages/autonomous-runtime/dist/types.d.ts +160 -0
- package/packages/autonomous-runtime/dist/types.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/types.js +31 -0
- package/packages/autonomous-runtime/dist/types.js.map +1 -0
- package/packages/autonomous-runtime/dist/worker.d.ts +63 -0
- package/packages/autonomous-runtime/dist/worker.d.ts.map +1 -0
- package/packages/autonomous-runtime/dist/worker.js +133 -0
- package/packages/autonomous-runtime/dist/worker.js.map +1 -0
- package/packages/autonomous-runtime/package.json +34 -0
- package/packages/autonomous-runtime/src/inbox.ts +266 -0
- package/packages/autonomous-runtime/src/index.ts +22 -0
- package/packages/autonomous-runtime/src/lease.ts +318 -0
- package/packages/autonomous-runtime/src/types.ts +257 -0
- package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +194 -0
- package/packages/autonomous-runtime/test/inbox.test.mjs +266 -0
- package/packages/autonomous-runtime/test/inbox.test.ts +80 -0
- package/packages/autonomous-runtime/test/types.test.ts +68 -0
- package/packages/autonomous-runtime/tsconfig.json +22 -0
- package/packages/autonomous-runtime/tsconfig.tsbuildinfo +1 -0
- 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/privilege-broker/README.md +45 -0
- package/packages/privilege-broker/config/privileges.yaml.example +172 -0
- package/packages/privilege-broker/dist/approval.d.ts +23 -0
- package/packages/privilege-broker/dist/approval.d.ts.map +1 -0
- package/packages/privilege-broker/dist/approval.js +45 -0
- package/packages/privilege-broker/dist/approval.js.map +1 -0
- package/packages/privilege-broker/dist/audit.d.ts +38 -0
- package/packages/privilege-broker/dist/audit.d.ts.map +1 -0
- package/packages/privilege-broker/dist/audit.js +125 -0
- package/packages/privilege-broker/dist/audit.js.map +1 -0
- package/packages/privilege-broker/dist/executor.d.ts +25 -0
- package/packages/privilege-broker/dist/executor.d.ts.map +1 -0
- package/packages/privilege-broker/dist/executor.js +83 -0
- package/packages/privilege-broker/dist/executor.js.map +1 -0
- package/packages/privilege-broker/dist/index.d.ts +30 -0
- package/packages/privilege-broker/dist/index.d.ts.map +1 -0
- package/packages/privilege-broker/dist/index.js +5 -0
- package/packages/privilege-broker/dist/index.js.map +1 -0
- package/packages/privilege-broker/dist/registry.d.ts +15 -0
- package/packages/privilege-broker/dist/registry.d.ts.map +1 -0
- package/packages/privilege-broker/dist/registry.js +76 -0
- package/packages/privilege-broker/dist/registry.js.map +1 -0
- package/packages/privilege-broker/dist/types.d.ts +77 -0
- package/packages/privilege-broker/dist/types.d.ts.map +1 -0
- package/packages/privilege-broker/dist/types.js +11 -0
- package/packages/privilege-broker/dist/types.js.map +1 -0
- package/packages/privilege-broker/package.json +35 -0
- package/packages/privilege-broker/src/approval.ts +70 -0
- package/packages/privilege-broker/src/audit.ts +142 -0
- package/packages/privilege-broker/src/executor.ts +110 -0
- package/packages/privilege-broker/src/index.ts +46 -0
- package/packages/privilege-broker/src/registry.ts +98 -0
- package/packages/privilege-broker/src/types.ts +105 -0
- package/packages/privilege-broker/tsconfig.json +22 -0
- package/packages/privilege-broker/tsconfig.tsbuildinfo +1 -0
- 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/scheduler-adapter/README.md +36 -0
- package/packages/scheduler-adapter/dist/cron.d.ts +21 -0
- package/packages/scheduler-adapter/dist/cron.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/cron.js +116 -0
- package/packages/scheduler-adapter/dist/cron.js.map +1 -0
- package/packages/scheduler-adapter/dist/index.d.ts +9 -0
- package/packages/scheduler-adapter/dist/index.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/index.js +8 -0
- package/packages/scheduler-adapter/dist/index.js.map +1 -0
- package/packages/scheduler-adapter/dist/interface.d.ts +61 -0
- package/packages/scheduler-adapter/dist/interface.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/interface.js +2 -0
- package/packages/scheduler-adapter/dist/interface.js.map +1 -0
- package/packages/scheduler-adapter/dist/internal.d.ts +30 -0
- package/packages/scheduler-adapter/dist/internal.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/internal.js +82 -0
- package/packages/scheduler-adapter/dist/internal.js.map +1 -0
- package/packages/scheduler-adapter/dist/systemd.d.ts +31 -0
- package/packages/scheduler-adapter/dist/systemd.d.ts.map +1 -0
- package/packages/scheduler-adapter/dist/systemd.js +203 -0
- package/packages/scheduler-adapter/dist/systemd.js.map +1 -0
- package/packages/scheduler-adapter/package.json +32 -0
- package/packages/scheduler-adapter/src/cron.ts +123 -0
- package/packages/scheduler-adapter/src/index.ts +9 -0
- package/packages/scheduler-adapter/src/interface.ts +61 -0
- package/packages/scheduler-adapter/src/internal.ts +99 -0
- package/packages/scheduler-adapter/src/systemd.ts +221 -0
- package/packages/scheduler-adapter/tsconfig.json +22 -0
- package/packages/scheduler-adapter/tsconfig.tsbuildinfo +1 -0
- 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/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
|
@@ -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
|
|
package/mirror.ts
CHANGED
|
@@ -32,6 +32,27 @@
|
|
|
32
32
|
|
|
33
33
|
import { getMirrorPath, readJson, writeJson } from "./cli.ts";
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Canonical lowercase provider ids — mirrors the KNOWN_AI_PROVIDERS from
|
|
37
|
+
* packages/types/src/ai-providers.ts. Duplicated here to avoid import path
|
|
38
|
+
* resolution issues in the root workspace.
|
|
39
|
+
*/
|
|
40
|
+
const KNOWN_PROVIDER_IDS = [
|
|
41
|
+
"minimax",
|
|
42
|
+
"openai",
|
|
43
|
+
"anthropic",
|
|
44
|
+
"glm",
|
|
45
|
+
"openrouter",
|
|
46
|
+
"openai-codex",
|
|
47
|
+
"deepseek",
|
|
48
|
+
"gemini",
|
|
49
|
+
"kimi",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
function isKnownProvider(id: string): boolean {
|
|
53
|
+
return (KNOWN_PROVIDER_IDS as readonly string[]).includes(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
/** Where a piece of data came from. */
|
|
36
57
|
export type MirrorSource = "scrape" | "tui-signal" | "manual";
|
|
37
58
|
|
|
@@ -49,10 +70,14 @@ export interface ProviderMirrorRecord {
|
|
|
49
70
|
h5_used_pct?: number;
|
|
50
71
|
/** When the 5h window resets — human-readable or ISO. */
|
|
51
72
|
h5_resets_at?: string;
|
|
73
|
+
/** Epoch ms when the 5h window resets — precise UTC timestamp. */
|
|
74
|
+
h5_resets_at_epoch?: number;
|
|
52
75
|
/** Weekly used percent. */
|
|
53
76
|
weekly_used_pct?: number;
|
|
54
77
|
/** When the weekly window resets. */
|
|
55
78
|
weekly_resets_at?: string;
|
|
79
|
+
/** Epoch ms when the weekly window resets — precise UTC timestamp. */
|
|
80
|
+
weekly_resets_at_epoch?: number;
|
|
56
81
|
/** Set when a limit was hit and we have the exhaustion signal. */
|
|
57
82
|
exhausted?: boolean;
|
|
58
83
|
/** Limit type from TUI signal: tokens, context_window, rate_limit, unknown. */
|
|
@@ -81,15 +106,7 @@ export interface MirrorRecord {
|
|
|
81
106
|
weekly_resets_at?: string;
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
export type KnownProviderId =
|
|
86
|
-
| "minimax"
|
|
87
|
-
| "openai"
|
|
88
|
-
| "openai-codex"
|
|
89
|
-
| "glm"
|
|
90
|
-
| "anthropic"
|
|
91
|
-
| "openrouter"
|
|
92
|
-
| (string & {}); // allow extension
|
|
109
|
+
|
|
93
110
|
|
|
94
111
|
const STALE_WARN_MS = 30 * 60 * 1000; // 30 min → orange
|
|
95
112
|
const STALE_ERROR_MS = 2 * 60 * 60 * 1000; // 2 h → red
|
|
@@ -112,11 +129,12 @@ function isLegacyShape(raw: unknown): raw is MirrorRecord & {
|
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
/** Convert a legacy single-row record to per-provider shape. */
|
|
115
|
-
function upgradeLegacyRecord(legacy: MirrorRecord): PerProviderMirror {
|
|
132
|
+
function upgradeLegacyRecord(legacy: MirrorRecord): PerProviderMirror | null {
|
|
116
133
|
const provider = legacy.provider ?? "minimax";
|
|
134
|
+
const known = isKnownProvider(provider) ? provider : "minimax";
|
|
117
135
|
const rec: ProviderMirrorRecord = {
|
|
118
136
|
synced_at: legacy.synced_at ?? new Date().toISOString(),
|
|
119
|
-
provider,
|
|
137
|
+
provider: known,
|
|
120
138
|
source: "scrape", // legacy was always scrape
|
|
121
139
|
model: legacy.model,
|
|
122
140
|
h5_used_pct: legacy.h5_used_pct,
|
|
@@ -124,7 +142,7 @@ function upgradeLegacyRecord(legacy: MirrorRecord): PerProviderMirror {
|
|
|
124
142
|
weekly_used_pct: legacy.weekly_used_pct,
|
|
125
143
|
weekly_resets_at: legacy.weekly_resets_at,
|
|
126
144
|
};
|
|
127
|
-
return { [
|
|
145
|
+
return { [known]: rec };
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
export class MirrorStore {
|
|
@@ -152,13 +170,17 @@ export class MirrorStore {
|
|
|
152
170
|
|
|
153
171
|
if (isLegacyShape(raw)) {
|
|
154
172
|
const upgraded = upgradeLegacyRecord(raw as MirrorRecord);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
if (upgraded) {
|
|
174
|
+
// Persist the upgrade so subsequent reads are fast.
|
|
175
|
+
try {
|
|
176
|
+
writeJson(this.path, upgraded);
|
|
177
|
+
} catch {
|
|
178
|
+
// best-effort
|
|
179
|
+
}
|
|
180
|
+
return upgraded;
|
|
160
181
|
}
|
|
161
|
-
|
|
182
|
+
// Unknown legacy provider — treat as absent.
|
|
183
|
+
return null;
|
|
162
184
|
}
|
|
163
185
|
|
|
164
186
|
// Already per-provider shape.
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Autonomous Runtime — RFC-0101
|
|
2
|
+
|
|
3
|
+
Task Inbox, Task Lease, and worker supervision for autonomous post-session execution.
|
|
4
|
+
|
|
5
|
+
## Package
|
|
6
|
+
|
|
7
|
+
`@pi-harness/autonomous-runtime`
|
|
8
|
+
|
|
9
|
+
## Concepts
|
|
10
|
+
|
|
11
|
+
### Task Inbox
|
|
12
|
+
|
|
13
|
+
Durable queue of tasks submitted by the harness master planner. Survives worker crashes, host reboots, and provider quota exhaustion.
|
|
14
|
+
|
|
15
|
+
### Task Lease
|
|
16
|
+
|
|
17
|
+
Atomic exclusive claim on a task. Guarantees **single-execution** under crash recovery. Other workers skip a leased task.
|
|
18
|
+
|
|
19
|
+
### Worker
|
|
20
|
+
|
|
21
|
+
Supervised process that reads from the inbox, claims tasks via leases, executes, and reports results.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { TaskInbox, TaskLeaseManager, WORKER_ROOT } from "@pi-harness/autonomous-runtime";
|
|
27
|
+
|
|
28
|
+
const inbox = new TaskInbox();
|
|
29
|
+
const leaseMgr = new TaskLeaseManager();
|
|
30
|
+
|
|
31
|
+
// Submit a task
|
|
32
|
+
await inbox.submit({
|
|
33
|
+
title: "Fix login bug",
|
|
34
|
+
description: "Users cannot log in with SSO",
|
|
35
|
+
capability: "write",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Worker: claim and work
|
|
39
|
+
const task = await inbox.claimNext("worker-1", leaseMgr);
|
|
40
|
+
if (task) {
|
|
41
|
+
// ... do work ...
|
|
42
|
+
await inbox.complete(task.id, { success: true });
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Status
|
|
47
|
+
|
|
48
|
+
**Phase 1 done.** Phases 2–10 pending implementation.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { TaskRecord, TaskStatus, TaskEvent } from "./types.js";
|
|
2
|
+
export declare class InboxError extends Error {
|
|
3
|
+
constructor(msg: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Manages the append-only task inbox at `~/.pi/harness/inbox/tasks.jsonl`.
|
|
7
|
+
*
|
|
8
|
+
* Storage layout:
|
|
9
|
+
* ```
|
|
10
|
+
* ~/.pi/harness/inbox/
|
|
11
|
+
* ├── tasks.jsonl # append-only log (one JSON line per task)
|
|
12
|
+
* ├── claimed/ # active leases (one JSON file per task)
|
|
13
|
+
* └── BACKLOG.md # human-friendly authoring surface (optional)
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Operations are:
|
|
17
|
+
* - **append** — append a new task to tasks.jsonl
|
|
18
|
+
* - **list** — read all tasks, filterable by status
|
|
19
|
+
* - **transition** — update a task's status in-place (rewrite)
|
|
20
|
+
* - **get** — read one task by id
|
|
21
|
+
* - **reindex** — compact tasks.jsonl by rewriting it from in-memory state
|
|
22
|
+
*/
|
|
23
|
+
export declare class TaskInbox {
|
|
24
|
+
private readonly tasksPath;
|
|
25
|
+
private readonly inboxDir;
|
|
26
|
+
private _cache;
|
|
27
|
+
constructor(options?: {
|
|
28
|
+
tasksPath?: string;
|
|
29
|
+
inboxDir?: string;
|
|
30
|
+
});
|
|
31
|
+
/** Ensure the inbox directory exists. Call before any write. */
|
|
32
|
+
ensureDir(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Append a new task to tasks.jsonl.
|
|
35
|
+
* The task is validated before writing. Duplicate ids are rejected.
|
|
36
|
+
*
|
|
37
|
+
* @throws {InboxError} if the task id already exists or validation fails
|
|
38
|
+
*/
|
|
39
|
+
append(task: TaskRecord): void;
|
|
40
|
+
/**
|
|
41
|
+
* Read all tasks from tasks.jsonl.
|
|
42
|
+
* Results are sorted by priority ASC then createdAt ASC (oldest first).
|
|
43
|
+
*
|
|
44
|
+
* @param filter Optionally filter by status. If omitted, returns all.
|
|
45
|
+
*/
|
|
46
|
+
list(filter?: {
|
|
47
|
+
status?: TaskStatus | TaskStatus[];
|
|
48
|
+
}): TaskRecord[];
|
|
49
|
+
/** Retrieve a single task by id, or null if not found. */
|
|
50
|
+
get(id: string): TaskRecord | null;
|
|
51
|
+
/**
|
|
52
|
+
* Update a task's status in-place inside tasks.jsonl.
|
|
53
|
+
*
|
|
54
|
+
* Uses read→parse→update→rewrite to avoid corrupting the JSONL file.
|
|
55
|
+
* The rewrite is atomic (write-tmp-then-rename).
|
|
56
|
+
*
|
|
57
|
+
* @param taskId The id of the task to transition
|
|
58
|
+
* @param newStatus The target status
|
|
59
|
+
* @param extra Optional additional fields to merge into the task record
|
|
60
|
+
* @param event Optional TaskEvent to append to the history
|
|
61
|
+
* @throws {InboxError} if the task does not exist
|
|
62
|
+
*/
|
|
63
|
+
transition(taskId: string, newStatus: TaskStatus, extra?: Partial<TaskRecord>, event?: TaskEvent): TaskRecord;
|
|
64
|
+
/** Convenience: mark a task as completed with a result. */
|
|
65
|
+
complete(taskId: string, result: TaskRecord["result"]): TaskRecord;
|
|
66
|
+
/** Convenience: mark a task as failed with a reason. */
|
|
67
|
+
fail(taskId: string, reason: string): TaskRecord;
|
|
68
|
+
/** Rewrite tasks.jsonl from the given task list. */
|
|
69
|
+
private _rewriteAll;
|
|
70
|
+
/** Force a full reindex. Call after manual edits to tasks.jsonl. */
|
|
71
|
+
reindex(): void;
|
|
72
|
+
/** Return counts of tasks grouped by status. */
|
|
73
|
+
stats(): Record<TaskStatus, number>;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=inbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAKpE,qBAAa,UAAW,SAAQ,KAAK;IACpC,YAAY,GAAG,EAAE,MAAM,EAGtB;CACD;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,SAAS;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,MAAM,CAA6B;IAE3C,YAAY,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,EAGlE;IAED,gEAAgE;IAChE,SAAS,IAAI,IAAI,CAIhB;IAID;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAc7B;IAID;;;;;OAKG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,UAAU,GAAG,UAAU,EAAE,CAAA;KAAE,GAAG,UAAU,EAAE,CAoClE;IAID,0DAA0D;IAC1D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAGjC;IAID;;;;;;;;;;;OAWG;IACH,UAAU,CACT,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,UAAU,EACrB,KAAK,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAC3B,KAAK,CAAC,EAAE,SAAS,GACf,UAAU,CAqCZ;IAID,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,UAAU,CAWjE;IAED,wDAAwD;IACxD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAe/C;IAID,oDAAoD;IACpD,OAAO,CAAC,WAAW;IAUnB,oEAAoE;IACpE,OAAO,IAAI,IAAI,CAKd;IAID,gDAAgD;IAChD,KAAK,IAAI,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAOlC;CACD"}
|