pi-harness-runtime 0.10.17 → 0.10.19

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 (40) hide show
  1. package/package.json +3 -4
  2. package/packages/auth/src/run-minimax-auth.ts +0 -0
  3. package/prompts/tencentdb-connect.md +35 -0
  4. package/prompts/tencentdb-setup.md +101 -0
  5. package/prompts/tencentdb.md +88 -0
  6. package/.versionrc.js +0 -32
  7. package/cli.js +0 -112
  8. package/footer-status.js +0 -155
  9. package/harness/agent-handoff.js +0 -123
  10. package/harness/auto-compact.js +0 -243
  11. package/harness/auto-quota-resume.js +0 -104
  12. package/harness/blackboard.js +0 -258
  13. package/harness/context-compact-orchestrator.js +0 -365
  14. package/harness/context-window-manager.js +0 -330
  15. package/harness/continue-prompt.js +0 -164
  16. package/harness/e2e/minimax-quota-parser.js +0 -52
  17. package/harness/e2e/minimax-quota-scraper.js +0 -475
  18. package/harness/e2e/openai-quota-scraper.js +0 -332
  19. package/harness/e2e/playwright-runner.js +0 -165
  20. package/harness/e2e/quota-status.js +0 -140
  21. package/harness/e2e/test-engine.js +0 -290
  22. package/harness/forked-summarizer.js +0 -212
  23. package/harness/index.js +0 -54
  24. package/harness/job-state-machine.js +0 -277
  25. package/harness/loop-runtime.js +0 -531
  26. package/harness/master-planner.js +0 -229
  27. package/harness/notification-events.js +0 -233
  28. package/harness/output-limit-handler.js +0 -233
  29. package/harness/partial-recovery.js +0 -413
  30. package/harness/project-detector/detector.js +0 -283
  31. package/harness/repair-engine.js +0 -256
  32. package/harness/session-memory.js +0 -293
  33. package/harness/task-graph.js +0 -88
  34. package/index.js +0 -1121
  35. package/mirror.js +0 -205
  36. package/proactive-compact.js +0 -42
  37. package/renderer.js +0 -134
  38. package/status-parsers.js +0 -63
  39. package/tracker.js +0 -49
  40. package/windows.js +0 -90
@@ -1,243 +0,0 @@
1
- /**
2
- * Auto Compact and Continue — RFC-0019
3
- *
4
- * Automatically resume work after model/session compaction without requiring
5
- * human intervention. Keeps the human out of the message-bus role.
6
- *
7
- * This module integrates with:
8
- * - `continue-prompt.ts` for prompt generation
9
- * - `context-compact-orchestrator.ts` for compact decisions
10
- * - `partial-recovery.ts` for artifact persistence
11
- *
12
- * Artifact Layout:
13
- * harness/context/
14
- * compaction_events.jsonl
15
- * latest_compaction_summary.md
16
- * continue_prompt.md
17
- */
18
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
19
- import { join } from "node:path";
20
- import { homedir } from "node:os";
21
- import { continuePromptGenerator, } from "./continue-prompt.js";
22
- const COMPACTION_PATTERNS = [
23
- /\[compaction\]/i,
24
- /compacted from \d+[,.]?\d* tokens/i,
25
- /model stopped because it reached the maximum output token limit/i,
26
- /error.*output.*token.*limit/i,
27
- /context.*truncated/i,
28
- /session.*compact/i,
29
- /\[Earlier conversation summarized\]/i,
30
- ];
31
- const CONTINUE_MARKERS = [
32
- /would you like me to continue/i,
33
- /should i continue/i,
34
- /type.*continue/i,
35
- /\[continue\]/i,
36
- /waiting for your response/i,
37
- /do not repeat work already completed/i,
38
- ];
39
- export class AutoCompactEngine {
40
- rootDir;
41
- jobId;
42
- requirement;
43
- maxAttempts;
44
- continueAttempts = 0;
45
- constructor(config) {
46
- this.rootDir =
47
- config.rootDir ??
48
- join(homedir(), ".pi", "harness", config.jobId, "context");
49
- this.jobId = config.jobId;
50
- this.requirement = config.requirement ?? "";
51
- this.maxAttempts = config.maxContinueAttempts ?? 5;
52
- }
53
- /**
54
- * Detect if output contains compaction markers
55
- */
56
- detectCompaction(output) {
57
- return COMPACTION_PATTERNS.some((pattern) => pattern.test(output));
58
- }
59
- /**
60
- * Detect if output is waiting for continue signal
61
- */
62
- isWaitingForContinue(output) {
63
- return CONTINUE_MARKERS.some((pattern) => pattern.test(output));
64
- }
65
- /**
66
- * Parse compaction details from output
67
- */
68
- parseCompactionEvent(output, taskId) {
69
- let compactedFromTokens = 0;
70
- let reason = "unknown";
71
- let errorMessage;
72
- // Extract compacted token count
73
- const tokenMatch = output.match(/compacted from ([,\d]+(?:\.\d+)?)\s*tokens/i);
74
- if (tokenMatch) {
75
- compactedFromTokens = parseInt(tokenMatch[1].replace(/,/g, ""), 10);
76
- }
77
- // Extract reason
78
- if (/output.*token.*limit/i.test(output)) {
79
- reason = "output_token_limit";
80
- }
81
- else if (/context.*truncated/i.test(output)) {
82
- reason = "context_truncated";
83
- }
84
- else if (/session.*compact/i.test(output)) {
85
- reason = "session_compact";
86
- }
87
- else if (/\[Earlier conversation summarized\]/i.test(output)) {
88
- reason = "context_compact";
89
- }
90
- // Extract error message
91
- const errorMatch = output.match(/error[:\s]+(.+)/i);
92
- if (errorMatch) {
93
- errorMessage = errorMatch[1].trim();
94
- }
95
- return {
96
- timestamp: new Date().toISOString(),
97
- jobId: this.jobId,
98
- taskId,
99
- compactedFromTokens,
100
- reason,
101
- errorMessage,
102
- partialOutput: output,
103
- };
104
- }
105
- /**
106
- * Save compaction artifact
107
- */
108
- saveCompactionArtifact(event) {
109
- this.ensureDir();
110
- // Append to events log
111
- const eventsPath = join(this.rootDir, "compaction_events.jsonl");
112
- appendFileSync(eventsPath, JSON.stringify(event) + "\n", "utf-8");
113
- // Write latest summary
114
- const summaryPath = join(this.rootDir, "latest_compaction_summary.md");
115
- const summary = this.generateSummary(event);
116
- writeFileSync(summaryPath, summary, "utf-8");
117
- }
118
- /**
119
- * Generate continue prompt using ContinuePromptGenerator
120
- */
121
- generateContinuePrompt(taskId, completedWork, remainingWork) {
122
- const context = {
123
- taskId,
124
- requirement: this.requirement || "Continue from previous session",
125
- whatWasCompleted: completedWork,
126
- whatNeedsToBeDone: remainingWork,
127
- partialFiles: this.findPartialFiles(),
128
- decisions: [],
129
- };
130
- const prompt = continuePromptGenerator.generate(context);
131
- // Also save to file for persistence
132
- const promptPath = join(this.rootDir, "continue_prompt.md");
133
- writeFileSync(promptPath, prompt, "utf-8");
134
- return prompt;
135
- }
136
- /**
137
- * Generate minimal continue prompt
138
- */
139
- generateMinimalContinuePrompt(summary, recentContent) {
140
- return [
141
- "Continue from where you left off.",
142
- "",
143
- "## Summary",
144
- summary,
145
- "",
146
- "## Recent Context",
147
- recentContent.substring(0, 2000),
148
- "",
149
- "**Do not repeat work already completed.** Focus on completing the remaining work.",
150
- ].join("\n");
151
- }
152
- /**
153
- * Check if we should continue (respects max attempts)
154
- */
155
- shouldContinue() {
156
- this.continueAttempts++;
157
- return this.continueAttempts <= this.maxAttempts;
158
- }
159
- /**
160
- * Get current continue attempt count
161
- */
162
- getContinueAttempts() {
163
- return this.continueAttempts;
164
- }
165
- /**
166
- * Reset continue attempts
167
- */
168
- resetAttempts() {
169
- this.continueAttempts = 0;
170
- }
171
- /**
172
- * Build the continue message for the next turn
173
- */
174
- buildContinueMessage() {
175
- const promptPath = join(this.rootDir, "continue_prompt.md");
176
- if (existsSync(promptPath)) {
177
- const prompt = readFileSync(promptPath, "utf-8");
178
- return `continue\n\n${prompt}`;
179
- }
180
- return "continue";
181
- }
182
- /**
183
- * Load continue prompt from file
184
- */
185
- loadContinuePrompt() {
186
- const promptPath = join(this.rootDir, "continue_prompt.md");
187
- if (existsSync(promptPath)) {
188
- return readFileSync(promptPath, "utf-8");
189
- }
190
- return null;
191
- }
192
- /**
193
- * Check if there's a pending continue prompt
194
- */
195
- hasContinuePrompt() {
196
- return existsSync(join(this.rootDir, "continue_prompt.md"));
197
- }
198
- // --- Private Methods ------------------------------------------------
199
- ensureDir() {
200
- if (!existsSync(this.rootDir)) {
201
- mkdirSync(this.rootDir, { recursive: true });
202
- }
203
- }
204
- generateSummary(event) {
205
- return `# Compaction Summary
206
-
207
- **Time:** ${event.timestamp}
208
- **Job:** ${event.jobId}
209
- **Task:** ${event.taskId}
210
- **Reason:** ${event.reason}
211
- **Tokens Compacted:** ${event.compactedFromTokens.toLocaleString()}
212
-
213
- ${event.errorMessage ? `**Error:** ${event.errorMessage}` : ""}
214
-
215
- ## What Happened
216
-
217
- Session was compacted due to ${event.reason}.
218
-
219
- ${event.compactedFromTokens > 0
220
- ? `Context was reduced from approximately ${event.compactedFromTokens.toLocaleString()} tokens.`
221
- : "Context was compacted."}
222
-
223
- ## Next Step
224
-
225
- Runtime will automatically continue this task.
226
-
227
- Continue attempts: ${this.continueAttempts}/${this.maxAttempts}
228
- `;
229
- }
230
- findPartialFiles() {
231
- const partialDir = join(this.rootDir, "..", "partial", this.jobId);
232
- if (!existsSync(partialDir)) {
233
- return [];
234
- }
235
- try {
236
- const files = readFileSync(join(partialDir, "files.json"), "utf-8");
237
- return JSON.parse(files);
238
- }
239
- catch {
240
- return [];
241
- }
242
- }
243
- }
@@ -1,104 +0,0 @@
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
- }
@@ -1,258 +0,0 @@
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
- }