pi-harness-runtime 0.10.13 → 0.10.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.versionrc.js +32 -0
- package/cli.js +112 -0
- package/footer-status.js +155 -0
- package/harness/agent-handoff.js +123 -0
- package/harness/auto-compact.js +243 -0
- package/harness/auto-quota-resume.js +104 -0
- package/harness/blackboard.js +258 -0
- package/harness/context-compact-orchestrator.js +365 -0
- package/harness/context-window-manager.js +330 -0
- package/harness/continue-prompt.js +164 -0
- package/harness/e2e/minimax-quota-parser.js +52 -0
- package/harness/e2e/minimax-quota-scraper.js +475 -0
- package/harness/e2e/openai-quota-scraper.js +332 -0
- package/harness/e2e/playwright-runner.js +165 -0
- package/harness/e2e/quota-status.js +140 -0
- package/harness/e2e/test-engine.js +290 -0
- package/harness/forked-summarizer.js +212 -0
- package/harness/index.js +54 -0
- package/harness/job-state-machine.js +277 -0
- package/harness/loop-runtime.js +531 -0
- package/harness/master-planner.js +229 -0
- package/harness/notification-events.js +233 -0
- package/harness/output-limit-handler.js +233 -0
- package/harness/partial-recovery.js +413 -0
- package/harness/project-detector/detector.js +283 -0
- package/harness/repair-engine.js +256 -0
- package/harness/session-memory.js +293 -0
- package/harness/task-graph.js +87 -0
- package/index.js +1121 -0
- package/mirror.js +205 -0
- package/package.json +6 -4
- package/proactive-compact.js +42 -0
- package/renderer.js +134 -0
- package/status-parsers.js +63 -0
- package/tracker.js +49 -0
- package/windows.js +90 -0
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
/** Small buffer (ms) before the exact reset time to resume. */
|
|
16
|
+
const RESUME_BUFFER_MS = 10_000; // 10 seconds — resume just before reset
|
|
17
|
+
/** Minimum time to wait before auto-resuming (avoid immediate re-trigger). */
|
|
18
|
+
const MIN_RESUME_DELAY_MS = 5_000; // 5 seconds
|
|
19
|
+
/** Map of jobId → active auto-resume timer handle. */
|
|
20
|
+
const activeTimers = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* Schedule an auto-resume for a job that was paused due to quota exhaustion.
|
|
23
|
+
*
|
|
24
|
+
* Reads the reset epoch from the mirror store for the given provider,
|
|
25
|
+
* computes the resume time, sets it on the checkpoint, and schedules
|
|
26
|
+
* a setTimeout to transition back to "running".
|
|
27
|
+
*
|
|
28
|
+
* Returns the scheduled resumeAt ISO string, or null if no epoch data
|
|
29
|
+
* is available (requires human resume).
|
|
30
|
+
*/
|
|
31
|
+
export function scheduleAutoResume(provider, machine, mirrorStore) {
|
|
32
|
+
const jobId = machine.getCheckpoint()?.jobId;
|
|
33
|
+
if (!jobId)
|
|
34
|
+
return null;
|
|
35
|
+
// Cancel any existing timer for this job
|
|
36
|
+
cancelAutoResume(jobId);
|
|
37
|
+
const mirror = mirrorStore.readAll();
|
|
38
|
+
const record = mirror?.[provider];
|
|
39
|
+
if (!record)
|
|
40
|
+
return null;
|
|
41
|
+
// Only auto-resume on 5h quota exhaustion (has precise epoch)
|
|
42
|
+
if (typeof record.h5_resets_at_epoch !== "number")
|
|
43
|
+
return null;
|
|
44
|
+
const resetEpoch = record.h5_resets_at_epoch;
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const delayMs = Math.max(resetEpoch - now - RESUME_BUFFER_MS, MIN_RESUME_DELAY_MS);
|
|
47
|
+
const resumeAt = now + delayMs;
|
|
48
|
+
const resumeAtIso = new Date(resumeAt).toISOString();
|
|
49
|
+
// Persist to checkpoint so resumeAt survives worker restart
|
|
50
|
+
machine.setResumeTime(resumeAtIso);
|
|
51
|
+
// console.log(
|
|
52
|
+
// `[auto-quota-resume] ${provider} 5h quota exhausted.` +
|
|
53
|
+
// ` Auto-resume scheduled at ${resumeAtIso} (in ${Math.round(delayMs / 1000)}s)`,
|
|
54
|
+
// );
|
|
55
|
+
const timeout = setTimeout(async () => {
|
|
56
|
+
activeTimers.delete(jobId);
|
|
57
|
+
const checkpoint = machine.getCheckpoint();
|
|
58
|
+
if (!checkpoint || checkpoint.status !== "paused_quota") {
|
|
59
|
+
// Job was manually resumed or cancelled
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Check the mirror — if quota is still exhausted, shift the timer
|
|
63
|
+
const updated = mirrorStore.readAll()?.[provider];
|
|
64
|
+
if (updated?.h5_resets_at_epoch) {
|
|
65
|
+
const stillExhausted = Date.now() < updated.h5_resets_at_epoch;
|
|
66
|
+
if (stillExhausted) {
|
|
67
|
+
// console.log(
|
|
68
|
+
// `[auto-quota-resume] ${provider} still exhausted at scheduled time.` +
|
|
69
|
+
// ` Re-scheduling...`,
|
|
70
|
+
// );
|
|
71
|
+
scheduleAutoResume(provider, machine, mirrorStore);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const result = await machine.transition("running");
|
|
76
|
+
if (result.success) {
|
|
77
|
+
// console.log(
|
|
78
|
+
// `[auto-quota-resume] Job ${jobId} auto-resumed after ${provider} 5h quota reset.`,
|
|
79
|
+
// );
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.error(`[auto-quota-resume] Auto-resume failed for ${jobId}: ${result.error}`);
|
|
83
|
+
}
|
|
84
|
+
}, delayMs);
|
|
85
|
+
activeTimers.set(jobId, { timeout, resumeAt });
|
|
86
|
+
return resumeAtIso;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Cancel the auto-resume timer for a job.
|
|
90
|
+
* Call this when the job is manually resumed or cancelled.
|
|
91
|
+
*/
|
|
92
|
+
export function cancelAutoResume(jobId) {
|
|
93
|
+
const existing = activeTimers.get(jobId);
|
|
94
|
+
if (existing) {
|
|
95
|
+
clearTimeout(existing.timeout);
|
|
96
|
+
activeTimers.delete(jobId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get the scheduled resume time for a job, or null if not scheduled.
|
|
101
|
+
*/
|
|
102
|
+
export function getScheduledResume(jobId) {
|
|
103
|
+
return activeTimers.get(jobId)?.resumeAt ?? null;
|
|
104
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Blackboard — RFC-0011
|
|
3
|
+
*
|
|
4
|
+
* Durable file-based coordination so agents communicate without the human
|
|
5
|
+
* acting as message bus.
|
|
6
|
+
*
|
|
7
|
+
* Layout:
|
|
8
|
+
* harness/blackboard/
|
|
9
|
+
* status.json
|
|
10
|
+
* next_action.json
|
|
11
|
+
* tasks.json
|
|
12
|
+
* agent_registry.json
|
|
13
|
+
* locks/
|
|
14
|
+
* reports/
|
|
15
|
+
* context/
|
|
16
|
+
* events.jsonl
|
|
17
|
+
*/
|
|
18
|
+
import { writeJson, readJson, appendJsonl, ensureUsageDir } from "../cli.ts";
|
|
19
|
+
// @ts-expect-error - Bun has built-in Node.js types
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
export class SharedBlackboard {
|
|
22
|
+
jobDir;
|
|
23
|
+
record = null;
|
|
24
|
+
constructor(jobId, rootDir) {
|
|
25
|
+
this.jobDir = join(rootDir, "jobs", jobId, "blackboard");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Initialize a new blackboard for a job
|
|
29
|
+
*/
|
|
30
|
+
init(jobId, taskGraph) {
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
this.record = {
|
|
33
|
+
jobId,
|
|
34
|
+
status: "created",
|
|
35
|
+
nextAction: undefined,
|
|
36
|
+
tasks: taskGraph,
|
|
37
|
+
agentRegistry: { agents: {} },
|
|
38
|
+
reports: {},
|
|
39
|
+
locks: {},
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
};
|
|
42
|
+
this.save();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load blackboard from disk
|
|
46
|
+
*/
|
|
47
|
+
load() {
|
|
48
|
+
const path = join(this.jobDir, "status.json");
|
|
49
|
+
this.record = readJson(path);
|
|
50
|
+
return this.record;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Save blackboard to disk
|
|
54
|
+
*/
|
|
55
|
+
save() {
|
|
56
|
+
if (!this.record)
|
|
57
|
+
return;
|
|
58
|
+
this.record.updatedAt = new Date().toISOString();
|
|
59
|
+
ensureUsageDir();
|
|
60
|
+
const path = join(this.jobDir, "status.json");
|
|
61
|
+
writeJson(path, this.record);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Update job status
|
|
65
|
+
*/
|
|
66
|
+
updateStatus(status) {
|
|
67
|
+
if (!this.record)
|
|
68
|
+
return;
|
|
69
|
+
this.record.status = status;
|
|
70
|
+
this.save();
|
|
71
|
+
this.appendEvent("StatusUpdated", { status });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Set the next action for agents to pick up
|
|
75
|
+
*/
|
|
76
|
+
setNextAction(action) {
|
|
77
|
+
if (!this.record)
|
|
78
|
+
return;
|
|
79
|
+
this.record.nextAction = action;
|
|
80
|
+
this.save();
|
|
81
|
+
this.appendEvent("NextActionUpdated", {
|
|
82
|
+
taskId: action.taskId,
|
|
83
|
+
agentId: action.agentId,
|
|
84
|
+
priority: action.priority,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clear the next action (after agent picks it up)
|
|
89
|
+
*/
|
|
90
|
+
clearNextAction() {
|
|
91
|
+
if (!this.record)
|
|
92
|
+
return;
|
|
93
|
+
this.record.nextAction = undefined;
|
|
94
|
+
this.save();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register an agent
|
|
98
|
+
*/
|
|
99
|
+
registerAgent(agentId, name, provider, model) {
|
|
100
|
+
if (!this.record)
|
|
101
|
+
return;
|
|
102
|
+
this.record.agentRegistry.agents[agentId] = {
|
|
103
|
+
id: agentId,
|
|
104
|
+
name,
|
|
105
|
+
provider,
|
|
106
|
+
model,
|
|
107
|
+
status: "idle",
|
|
108
|
+
startedAt: new Date().toISOString(),
|
|
109
|
+
};
|
|
110
|
+
this.save();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Update agent status
|
|
114
|
+
*/
|
|
115
|
+
updateAgentStatus(agentId, status, currentTaskId) {
|
|
116
|
+
if (!this.record)
|
|
117
|
+
return;
|
|
118
|
+
const agent = this.record.agentRegistry.agents[agentId];
|
|
119
|
+
if (!agent)
|
|
120
|
+
return;
|
|
121
|
+
agent.status = status;
|
|
122
|
+
agent.currentTaskId = currentTaskId;
|
|
123
|
+
agent.lastHeartbeat = new Date().toISOString();
|
|
124
|
+
this.save();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Unregister an agent
|
|
128
|
+
*/
|
|
129
|
+
unregisterAgent(agentId) {
|
|
130
|
+
if (!this.record)
|
|
131
|
+
return;
|
|
132
|
+
delete this.record.agentRegistry.agents[agentId];
|
|
133
|
+
this.save();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Write an agent report
|
|
137
|
+
*/
|
|
138
|
+
writeReport(report) {
|
|
139
|
+
if (!this.record)
|
|
140
|
+
return;
|
|
141
|
+
this.record.reports[report.agentId] = report;
|
|
142
|
+
this.save();
|
|
143
|
+
this.appendEvent("AgentReportWritten", {
|
|
144
|
+
agentId: report.agentId,
|
|
145
|
+
taskId: report.taskId,
|
|
146
|
+
status: report.status,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Acquire a lock on a task
|
|
151
|
+
*/
|
|
152
|
+
acquireLock(taskId, agentId) {
|
|
153
|
+
if (!this.record)
|
|
154
|
+
return false;
|
|
155
|
+
if (this.record.locks[taskId]) {
|
|
156
|
+
return false; // Already locked
|
|
157
|
+
}
|
|
158
|
+
this.record.locks[taskId] = {
|
|
159
|
+
taskId,
|
|
160
|
+
agentId,
|
|
161
|
+
acquiredAt: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
this.save();
|
|
164
|
+
this.appendEvent("LockAcquired", { taskId, agentId });
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Release a lock on a task
|
|
169
|
+
*/
|
|
170
|
+
releaseLock(taskId, agentId) {
|
|
171
|
+
if (!this.record)
|
|
172
|
+
return false;
|
|
173
|
+
const lock = this.record.locks[taskId];
|
|
174
|
+
if (!lock || lock.agentId !== agentId) {
|
|
175
|
+
return false; // Not locked by this agent
|
|
176
|
+
}
|
|
177
|
+
delete this.record.locks[taskId];
|
|
178
|
+
this.save();
|
|
179
|
+
this.appendEvent("LockReleased", { taskId, agentId });
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Check if a task is locked
|
|
184
|
+
*/
|
|
185
|
+
isLocked(taskId) {
|
|
186
|
+
return !!this.record?.locks[taskId];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get lock info for a task
|
|
190
|
+
*/
|
|
191
|
+
getLock(taskId) {
|
|
192
|
+
return this.record?.locks[taskId] ?? null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get the current record
|
|
196
|
+
*/
|
|
197
|
+
getRecord() {
|
|
198
|
+
return this.record;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get active agents
|
|
202
|
+
*/
|
|
203
|
+
getActiveAgents() {
|
|
204
|
+
if (!this.record)
|
|
205
|
+
return [];
|
|
206
|
+
return Object.values(this.record.agentRegistry.agents);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Check for stale agents (no heartbeat in N minutes)
|
|
210
|
+
*/
|
|
211
|
+
getStaleAgents(maxAgeMinutes = 10) {
|
|
212
|
+
if (!this.record)
|
|
213
|
+
return [];
|
|
214
|
+
const cutoff = Date.now() - maxAgeMinutes * 60 * 1000;
|
|
215
|
+
return Object.values(this.record.agentRegistry.agents).filter((a) => {
|
|
216
|
+
if (!a.lastHeartbeat)
|
|
217
|
+
return false;
|
|
218
|
+
return Date.parse(a.lastHeartbeat) < cutoff;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Append an event to the event log
|
|
223
|
+
*/
|
|
224
|
+
appendEvent(type, data) {
|
|
225
|
+
if (!this.record)
|
|
226
|
+
return;
|
|
227
|
+
const event = {
|
|
228
|
+
ts: new Date().toISOString(),
|
|
229
|
+
jobId: this.record.jobId,
|
|
230
|
+
type,
|
|
231
|
+
message: `Blackboard event: ${type}`,
|
|
232
|
+
data,
|
|
233
|
+
};
|
|
234
|
+
const path = join(this.jobDir, "events.jsonl");
|
|
235
|
+
ensureUsageDir();
|
|
236
|
+
appendJsonl(path, event);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Export full blackboard state
|
|
240
|
+
*/
|
|
241
|
+
export() {
|
|
242
|
+
return JSON.stringify(this.record, null, 2);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get blackboard directory path
|
|
246
|
+
*/
|
|
247
|
+
getPath() {
|
|
248
|
+
return this.jobDir;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Create and initialize a blackboard for a job
|
|
253
|
+
*/
|
|
254
|
+
export function createBlackboard(jobId, rootDir, taskGraph) {
|
|
255
|
+
const blackboard = new SharedBlackboard(jobId, rootDir);
|
|
256
|
+
blackboard.init(jobId, taskGraph);
|
|
257
|
+
return blackboard;
|
|
258
|
+
}
|