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,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair Engine — RFC-0018
|
|
3
|
+
*
|
|
4
|
+
* Converts failures into repair tasks with retry policy and escalation.
|
|
5
|
+
*/
|
|
6
|
+
import { appendJsonl } from "../cli.ts";
|
|
7
|
+
// @ts-expect-error - Bun has built-in Node.js types
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
// @ts-expect-error - Bun has built-in Node.js types
|
|
10
|
+
import { mkdirSync } from "node:fs";
|
|
11
|
+
const DEFAULT_RETRY_POLICY = {
|
|
12
|
+
maxRetries: 3,
|
|
13
|
+
backoffMs: 5000,
|
|
14
|
+
backoffMultiplier: 2,
|
|
15
|
+
escalationAfter: 2,
|
|
16
|
+
};
|
|
17
|
+
const FAILURE_PATTERNS = {
|
|
18
|
+
"test failed": "test_failure",
|
|
19
|
+
"assertion failed": "test_failure",
|
|
20
|
+
TypeError: "type_error",
|
|
21
|
+
SyntaxError: "build_error",
|
|
22
|
+
ESLint: "lint_error",
|
|
23
|
+
"build failed": "build_error",
|
|
24
|
+
"compilation error": "build_error",
|
|
25
|
+
quota: "quota_exhausted",
|
|
26
|
+
"rate limit": "quota_exhausted",
|
|
27
|
+
"429": "quota_exhausted",
|
|
28
|
+
"500": "provider_error",
|
|
29
|
+
"502": "provider_error",
|
|
30
|
+
"503": "provider_error",
|
|
31
|
+
connection: "provider_error",
|
|
32
|
+
timeout: "provider_error",
|
|
33
|
+
};
|
|
34
|
+
export class RepairEngine {
|
|
35
|
+
repairTasks = new Map();
|
|
36
|
+
rootDir;
|
|
37
|
+
constructor(rootDir) {
|
|
38
|
+
this.rootDir = rootDir;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Classify a failure type from error message
|
|
42
|
+
*/
|
|
43
|
+
classifyFailure(errorMessage) {
|
|
44
|
+
const lower = errorMessage.toLowerCase();
|
|
45
|
+
for (const [pattern, type] of Object.entries(FAILURE_PATTERNS)) {
|
|
46
|
+
if (lower.includes(pattern.toLowerCase())) {
|
|
47
|
+
return type;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a repair task from a failed task
|
|
54
|
+
*/
|
|
55
|
+
createRepairTask(originalTaskId, failureType, errorMessage, options) {
|
|
56
|
+
const id = `repair-${originalTaskId}-${Date.now()}`;
|
|
57
|
+
const retryPolicy = {
|
|
58
|
+
...DEFAULT_RETRY_POLICY,
|
|
59
|
+
...options?.retryPolicy,
|
|
60
|
+
};
|
|
61
|
+
const task = {
|
|
62
|
+
id,
|
|
63
|
+
originalTaskId,
|
|
64
|
+
failureType,
|
|
65
|
+
description: `Fix failure in task ${originalTaskId}: ${errorMessage}`,
|
|
66
|
+
attemptedFixes: [],
|
|
67
|
+
status: "pending",
|
|
68
|
+
retryPolicy,
|
|
69
|
+
createdAt: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
this.repairTasks.set(id, task);
|
|
72
|
+
return task;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Record an attempted fix
|
|
76
|
+
*/
|
|
77
|
+
recordAttempt(repairTaskId, description, success, output) {
|
|
78
|
+
const task = this.repairTasks.get(repairTaskId);
|
|
79
|
+
if (!task)
|
|
80
|
+
return;
|
|
81
|
+
const attempt = {
|
|
82
|
+
attempt: task.attemptedFixes.length + 1,
|
|
83
|
+
description,
|
|
84
|
+
success,
|
|
85
|
+
output,
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
task.attemptedFixes.push(attempt);
|
|
89
|
+
if (success) {
|
|
90
|
+
task.status = "resolved";
|
|
91
|
+
task.resolvedAt = new Date().toISOString();
|
|
92
|
+
}
|
|
93
|
+
else if (attempt.attempt >= task.retryPolicy.maxRetries) {
|
|
94
|
+
// Check if should escalate
|
|
95
|
+
if (task.retryPolicy.escalationAfter &&
|
|
96
|
+
attempt.attempt >= task.retryPolicy.escalationAfter) {
|
|
97
|
+
task.status = "escalated";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this.repairTasks.set(repairTaskId, task);
|
|
101
|
+
this.saveRepairTask(task);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get the next retry delay in milliseconds
|
|
105
|
+
*/
|
|
106
|
+
getNextRetryDelay(repairTaskId) {
|
|
107
|
+
const task = this.repairTasks.get(repairTaskId);
|
|
108
|
+
if (!task)
|
|
109
|
+
return null;
|
|
110
|
+
const attempt = task.attemptedFixes.length;
|
|
111
|
+
if (attempt >= task.retryPolicy.maxRetries)
|
|
112
|
+
return null;
|
|
113
|
+
const { backoffMs, backoffMultiplier } = task.retryPolicy;
|
|
114
|
+
return backoffMs * backoffMultiplier ** attempt;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if repair task should escalate to human
|
|
118
|
+
*/
|
|
119
|
+
shouldEscalate(repairTaskId) {
|
|
120
|
+
const task = this.repairTasks.get(repairTaskId);
|
|
121
|
+
if (!task)
|
|
122
|
+
return false;
|
|
123
|
+
return task.status === "escalated";
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get all repair tasks for a job
|
|
127
|
+
*/
|
|
128
|
+
getRepairTasks(jobId) {
|
|
129
|
+
// const path = join(this.rootDir, "jobs", jobId, "repair-tasks.jsonl");
|
|
130
|
+
// In practice, this would read from file. For now, return in-memory.
|
|
131
|
+
return Array.from(this.repairTasks.values()).filter((t) => t.originalTaskId.startsWith(`task-${jobId}`));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get repair summary
|
|
135
|
+
*/
|
|
136
|
+
getSummary(jobId) {
|
|
137
|
+
const tasks = this.getRepairTasks(jobId);
|
|
138
|
+
return {
|
|
139
|
+
total: tasks.length,
|
|
140
|
+
pending: tasks.filter((t) => t.status === "pending" || t.status === "in_progress").length,
|
|
141
|
+
resolved: tasks.filter((t) => t.status === "resolved").length,
|
|
142
|
+
escalated: tasks.filter((t) => t.status === "escalated").length,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Analyze a failure and create repair task with guidance
|
|
147
|
+
*/
|
|
148
|
+
analyzeAndRepair(originalTaskId, errorMessage, options) {
|
|
149
|
+
const failureType = this.classifyFailure(errorMessage);
|
|
150
|
+
const repairTask = this.createRepairTask(originalTaskId, failureType, errorMessage, options);
|
|
151
|
+
const guidance = this.generateGuidance(repairTask);
|
|
152
|
+
return { repairTask, guidance };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Generate repair guidance based on failure type
|
|
156
|
+
*/
|
|
157
|
+
generateGuidance(repairTask) {
|
|
158
|
+
switch (repairTask.failureType) {
|
|
159
|
+
case "test_failure":
|
|
160
|
+
return `Analyze the failing test. Check:
|
|
161
|
+
1. Is the test correct? (Does it test what it claims?)
|
|
162
|
+
2. Is the implementation correct? (Does it match the expected behavior?)
|
|
163
|
+
3. Are there any recent changes that broke the test?
|
|
164
|
+
4. Are there any environment-specific issues?`;
|
|
165
|
+
case "build_error":
|
|
166
|
+
return `Check the build error details:
|
|
167
|
+
1. Look at the exact error message and line number
|
|
168
|
+
2. Check for missing imports or type mismatches
|
|
169
|
+
3. Verify all dependencies are installed
|
|
170
|
+
4. Check for circular dependencies`;
|
|
171
|
+
case "type_error":
|
|
172
|
+
return `Fix TypeScript/JavaScript type errors:
|
|
173
|
+
1. Check the type of each variable
|
|
174
|
+
2. Ensure type assertions are correct
|
|
175
|
+
3. Verify interface/type definitions match usage
|
|
176
|
+
4. Consider adding type guards if needed`;
|
|
177
|
+
case "lint_error":
|
|
178
|
+
return `Fix linting errors:
|
|
179
|
+
1. Review the linting rules being violated
|
|
180
|
+
2. Check if code style matches project conventions
|
|
181
|
+
3. Run linter with --fix if available
|
|
182
|
+
4. Update eslint/prettier config if needed`;
|
|
183
|
+
case "quota_exhausted":
|
|
184
|
+
return `Quota exhausted. Options:
|
|
185
|
+
1. Wait for quota reset
|
|
186
|
+
2. Switch to an alternative provider
|
|
187
|
+
3. Optimize prompts to use fewer tokens
|
|
188
|
+
4. Reduce task scope to fit within quota`;
|
|
189
|
+
case "provider_error":
|
|
190
|
+
return `Provider error. Actions:
|
|
191
|
+
1. Retry with exponential backoff
|
|
192
|
+
2. Check provider status page
|
|
193
|
+
3. Switch to fallback provider if available
|
|
194
|
+
4. Log error details for debugging`;
|
|
195
|
+
case "e2e_failure":
|
|
196
|
+
return `E2E test failed. Check:
|
|
197
|
+
1. Is the page/app in the expected state?
|
|
198
|
+
2. Are selectors still valid?
|
|
199
|
+
3. Is there a timing issue (need to wait longer)?
|
|
200
|
+
4. Is the test assertion correct?`;
|
|
201
|
+
default:
|
|
202
|
+
return `Unknown failure. Gather more information:
|
|
203
|
+
1. Collect full error stack trace
|
|
204
|
+
2. Check system logs
|
|
205
|
+
3. Reproduce the issue locally
|
|
206
|
+
4. Consider asking for human assistance`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Save repair task to file
|
|
211
|
+
*/
|
|
212
|
+
saveRepairTask(task) {
|
|
213
|
+
const path = join(this.rootDir, "jobs", task.originalTaskId.split("-")[1] ?? "unknown", "repair-tasks.jsonl");
|
|
214
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
215
|
+
appendJsonl(path, task);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Load repair tasks from file
|
|
219
|
+
*/
|
|
220
|
+
loadRepairTasks(_jobId) {
|
|
221
|
+
// const path = join(this.rootDir, "jobs", _jobId, "repair-tasks.jsonl");
|
|
222
|
+
// In practice, this would read from file and populate the map
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Export repair report
|
|
226
|
+
*/
|
|
227
|
+
exportReport(jobId) {
|
|
228
|
+
const tasks = this.getRepairTasks(jobId);
|
|
229
|
+
const summary = this.getSummary(jobId);
|
|
230
|
+
const lines = [
|
|
231
|
+
`Repair Report for Job ${jobId}`,
|
|
232
|
+
"=".repeat(50),
|
|
233
|
+
`Total: ${summary.total}`,
|
|
234
|
+
`Pending: ${summary.pending}`,
|
|
235
|
+
`Resolved: ${summary.resolved}`,
|
|
236
|
+
`Escalated: ${summary.escalated}`,
|
|
237
|
+
"",
|
|
238
|
+
"Details:",
|
|
239
|
+
"-".repeat(50),
|
|
240
|
+
];
|
|
241
|
+
for (const task of tasks) {
|
|
242
|
+
lines.push(`\n[${task.status.toUpperCase()}] ${task.id}`);
|
|
243
|
+
lines.push(` Original: ${task.originalTaskId}`);
|
|
244
|
+
lines.push(` Type: ${task.failureType}`);
|
|
245
|
+
lines.push(` Description: ${task.description}`);
|
|
246
|
+
lines.push(` Attempts: ${task.attemptedFixes.length}/${task.retryPolicy.maxRetries}`);
|
|
247
|
+
if (task.attemptedFixes.length > 0) {
|
|
248
|
+
lines.push(" Attempt History:");
|
|
249
|
+
for (const fix of task.attemptedFixes) {
|
|
250
|
+
lines.push(` - [${fix.success ? "✓" : "✗"}] ${fix.description}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return lines.join("\n");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Memory — Persistent Knowledge Extraction
|
|
3
|
+
*
|
|
4
|
+
* Extracts key facts and decisions to persistent storage.
|
|
5
|
+
* On resume, re-injects memory as context without re-sending full history.
|
|
6
|
+
*
|
|
7
|
+
* Based on the reference implementation's sessionMemoryCompact pattern.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
// --- Session Memory Manager -------------------------------------------------
|
|
12
|
+
export class SessionMemoryManager {
|
|
13
|
+
memory;
|
|
14
|
+
memoryPath;
|
|
15
|
+
maxFacts = 50;
|
|
16
|
+
maxDecisions = 20;
|
|
17
|
+
constructor(jobId, rootDir) {
|
|
18
|
+
this.memoryPath = join(rootDir ?? join(process.env.HOME ?? ".", ".pi", "harness", jobId), "session-memory.json");
|
|
19
|
+
this.memory = this.load() ?? {
|
|
20
|
+
jobId,
|
|
21
|
+
extractedFacts: [],
|
|
22
|
+
decisions: [],
|
|
23
|
+
filesModified: [],
|
|
24
|
+
testsStatus: [],
|
|
25
|
+
artifacts: {},
|
|
26
|
+
lastUpdated: new Date().toISOString(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// --- Public API -----------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Extract key facts from recent messages
|
|
32
|
+
*/
|
|
33
|
+
extractFacts(messages) {
|
|
34
|
+
for (const msg of messages) {
|
|
35
|
+
const content = msg.content;
|
|
36
|
+
// File creation patterns
|
|
37
|
+
for (const match of content.matchAll(/(?:created|created file|created module):?\s*([^\n.]+)/gi)) {
|
|
38
|
+
if (match[1]) {
|
|
39
|
+
this.addFileReference(match[1].trim(), "created");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// File modification patterns
|
|
43
|
+
for (const match of content.matchAll(/(?:modified|updated|changed):?\s*([^\n.]+)/gi)) {
|
|
44
|
+
if (match[1] && !/(?:file|module|code)/i.test(match[0])) {
|
|
45
|
+
this.addFileReference(match[1].trim(), "modified");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Test result patterns
|
|
49
|
+
for (const match of content.matchAll(/(?:test|spec):?\s*([^\n.]+)\s*(passed|failed|pass|fail)/gi)) {
|
|
50
|
+
if (match[1] && match[2]) {
|
|
51
|
+
this.addTestResult(match[1].trim(), /^pass$/i.test(match[2]));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Decision patterns
|
|
55
|
+
for (const match of content.matchAll(/(?:decided|choosing|chose|decision|approach|using):?\s*([^\n.]+)/gi)) {
|
|
56
|
+
if (match[1] && match[1].length > 10) {
|
|
57
|
+
this.recordDecision(match[1].trim());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Key facts patterns
|
|
61
|
+
for (const match of content.matchAll(/(?:important|key|note|facts?:)\s*([^\n]+)/gi)) {
|
|
62
|
+
if (match[1] && match[1].length > 20) {
|
|
63
|
+
this.recordFact(match[1].trim());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.memory.lastUpdated = new Date().toISOString();
|
|
68
|
+
this.save();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Add a decision to memory
|
|
72
|
+
*/
|
|
73
|
+
recordDecision(text, rationale) {
|
|
74
|
+
// Avoid exact duplicates
|
|
75
|
+
if (this.memory.decisions.some((d) => d.text === text)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.memory.decisions.push({
|
|
79
|
+
text,
|
|
80
|
+
rationale,
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
// Trim to max
|
|
84
|
+
if (this.memory.decisions.length > this.maxDecisions) {
|
|
85
|
+
this.memory.decisions = this.memory.decisions.slice(-this.maxDecisions);
|
|
86
|
+
}
|
|
87
|
+
this.save();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Add a fact to memory
|
|
91
|
+
*/
|
|
92
|
+
recordFact(fact) {
|
|
93
|
+
// Avoid duplicates
|
|
94
|
+
if (this.memory.extractedFacts.includes(fact)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.memory.extractedFacts.push(fact);
|
|
98
|
+
// Trim to max
|
|
99
|
+
if (this.memory.extractedFacts.length > this.maxFacts) {
|
|
100
|
+
this.memory.extractedFacts = this.memory.extractedFacts.slice(-this.maxFacts);
|
|
101
|
+
}
|
|
102
|
+
this.save();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Add a file reference
|
|
106
|
+
*/
|
|
107
|
+
addFileReference(path, action) {
|
|
108
|
+
// Avoid exact duplicates
|
|
109
|
+
if (this.memory.filesModified.some((f) => f.path === path)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.memory.filesModified.push({
|
|
113
|
+
path,
|
|
114
|
+
action,
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
});
|
|
117
|
+
this.save();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Add a test result
|
|
121
|
+
*/
|
|
122
|
+
addTestResult(name, passed) {
|
|
123
|
+
this.memory.testsStatus.push({
|
|
124
|
+
name,
|
|
125
|
+
passed,
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
});
|
|
128
|
+
this.save();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Store an artifact (e.g., generated code snippet)
|
|
132
|
+
*/
|
|
133
|
+
storeArtifact(key, content) {
|
|
134
|
+
this.memory.artifacts[key] = content;
|
|
135
|
+
this.save();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get memory for re-injection as context
|
|
139
|
+
*/
|
|
140
|
+
getMemoryForContext() {
|
|
141
|
+
const parts = [];
|
|
142
|
+
// Files modified
|
|
143
|
+
if (this.memory.filesModified.length > 0) {
|
|
144
|
+
parts.push("## Files Modified");
|
|
145
|
+
for (const file of this.memory.filesModified.slice(-10)) {
|
|
146
|
+
parts.push(`- [${file.action}] ${file.path}`);
|
|
147
|
+
}
|
|
148
|
+
parts.push("");
|
|
149
|
+
}
|
|
150
|
+
// Key decisions
|
|
151
|
+
if (this.memory.decisions.length > 0) {
|
|
152
|
+
parts.push("## Key Decisions");
|
|
153
|
+
for (const decision of this.memory.decisions.slice(-5)) {
|
|
154
|
+
parts.push(`- ${decision.text}`);
|
|
155
|
+
if (decision.rationale) {
|
|
156
|
+
parts.push(` Reason: ${decision.rationale}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
parts.push("");
|
|
160
|
+
}
|
|
161
|
+
// Extracted facts
|
|
162
|
+
if (this.memory.extractedFacts.length > 0) {
|
|
163
|
+
parts.push("## Key Facts");
|
|
164
|
+
for (const fact of this.memory.extractedFacts.slice(-10)) {
|
|
165
|
+
parts.push(`- ${fact}`);
|
|
166
|
+
}
|
|
167
|
+
parts.push("");
|
|
168
|
+
}
|
|
169
|
+
// Test status
|
|
170
|
+
if (this.memory.testsStatus.length > 0) {
|
|
171
|
+
const passed = this.memory.testsStatus.filter((t) => t.passed).length;
|
|
172
|
+
const total = this.memory.testsStatus.length;
|
|
173
|
+
parts.push(`## Tests: ${passed}/${total} passed`);
|
|
174
|
+
parts.push("");
|
|
175
|
+
}
|
|
176
|
+
if (parts.length === 0) {
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
return ["## Session Memory", "", ...parts.slice(0, -1)].join("\n");
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get compact-worthy content (for session memory compaction)
|
|
183
|
+
*/
|
|
184
|
+
getCompactContent() {
|
|
185
|
+
const lines = [];
|
|
186
|
+
if (this.memory.filesModified.length > 0) {
|
|
187
|
+
lines.push("Files modified:");
|
|
188
|
+
for (const file of this.memory.filesModified) {
|
|
189
|
+
lines.push(` ${file.action}: ${file.path}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (this.memory.decisions.length > 0) {
|
|
193
|
+
lines.push("Decisions:");
|
|
194
|
+
for (const d of this.memory.decisions) {
|
|
195
|
+
lines.push(` - ${d.text}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (this.memory.extractedFacts.length > 0) {
|
|
199
|
+
lines.push("Facts:");
|
|
200
|
+
for (const f of this.memory.extractedFacts) {
|
|
201
|
+
lines.push(` - ${f}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get test summary
|
|
208
|
+
*/
|
|
209
|
+
getTestSummary() {
|
|
210
|
+
const passed = this.memory.testsStatus.filter((t) => t.passed).length;
|
|
211
|
+
const total = this.memory.testsStatus.length;
|
|
212
|
+
return `${passed}/${total}`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check if all tests passed
|
|
216
|
+
*/
|
|
217
|
+
allTestsPassed() {
|
|
218
|
+
return (this.memory.testsStatus.length > 0 &&
|
|
219
|
+
this.memory.testsStatus.every((t) => t.passed));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Clear memory (e.g., after successful task completion)
|
|
223
|
+
*/
|
|
224
|
+
clear() {
|
|
225
|
+
this.memory.extractedFacts = [];
|
|
226
|
+
this.memory.decisions = [];
|
|
227
|
+
this.memory.filesModified = [];
|
|
228
|
+
this.memory.testsStatus = [];
|
|
229
|
+
this.memory.artifacts = {};
|
|
230
|
+
this.memory.lastUpdated = new Date().toISOString();
|
|
231
|
+
this.save();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get raw memory object
|
|
235
|
+
*/
|
|
236
|
+
getMemory() {
|
|
237
|
+
return { ...this.memory };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Update from another memory object (for merge on resume)
|
|
241
|
+
*/
|
|
242
|
+
merge(other) {
|
|
243
|
+
if (other.extractedFacts) {
|
|
244
|
+
for (const fact of other.extractedFacts) {
|
|
245
|
+
if (!this.memory.extractedFacts.includes(fact)) {
|
|
246
|
+
this.memory.extractedFacts.push(fact);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (other.decisions) {
|
|
251
|
+
for (const decision of other.decisions) {
|
|
252
|
+
if (!this.memory.decisions.some((d) => d.text === decision.text)) {
|
|
253
|
+
this.memory.decisions.push(decision);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (other.filesModified) {
|
|
258
|
+
for (const file of other.filesModified) {
|
|
259
|
+
if (!this.memory.filesModified.some((f) => f.path === file.path)) {
|
|
260
|
+
this.memory.filesModified.push(file);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
this.memory.lastUpdated = new Date().toISOString();
|
|
265
|
+
this.save();
|
|
266
|
+
}
|
|
267
|
+
// --- Private Methods --------------------------------------------------
|
|
268
|
+
load() {
|
|
269
|
+
if (!existsSync(this.memoryPath)) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(readFileSync(this.memoryPath, "utf-8"));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
save() {
|
|
280
|
+
const dir = this.memoryPath.substring(0, this.memoryPath.lastIndexOf("/"));
|
|
281
|
+
if (!existsSync(dir)) {
|
|
282
|
+
mkdirSync(dir, { recursive: true });
|
|
283
|
+
}
|
|
284
|
+
writeFileSync(this.memoryPath, JSON.stringify(this.memory, null, 2), "utf-8");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// --- Factory ----------------------------------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* Create a SessionMemoryManager for a job
|
|
290
|
+
*/
|
|
291
|
+
export function createSessionMemoryManager(jobId, rootDir) {
|
|
292
|
+
return new SessionMemoryManager(jobId, rootDir);
|
|
293
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Graph Manager — RFC-0028 Phase 3
|
|
3
|
+
*
|
|
4
|
+
* Manages task dependency graph for compact-aware scheduling.
|
|
5
|
+
* Currently a stub — integrates with CompactOrchestrator for
|
|
6
|
+
* dependency-aware compaction prioritization.
|
|
7
|
+
*
|
|
8
|
+
* Future work:
|
|
9
|
+
* - Track task dependencies (blocking/ready tasks)
|
|
10
|
+
* - Prioritize compaction to keep blocking path messages
|
|
11
|
+
* - Track which tasks depend on which checkpoint artifacts
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new TaskGraphManager instance.
|
|
15
|
+
* Stub implementation — returns minimal working graph.
|
|
16
|
+
*/
|
|
17
|
+
export function createTaskGraphManager() {
|
|
18
|
+
const nodes = new Map();
|
|
19
|
+
return {
|
|
20
|
+
addTask(task) {
|
|
21
|
+
if (!nodes.has(task.id)) {
|
|
22
|
+
nodes.set(task.id, {
|
|
23
|
+
task,
|
|
24
|
+
dependencies: new Set(),
|
|
25
|
+
dependents: new Set(),
|
|
26
|
+
status: "pending",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
completeTask(taskId) {
|
|
31
|
+
const node = nodes.get(taskId);
|
|
32
|
+
if (!node)
|
|
33
|
+
return;
|
|
34
|
+
node.status = "done";
|
|
35
|
+
// Update dependents to ready
|
|
36
|
+
for (const depId of node.dependents) {
|
|
37
|
+
const depNode = nodes.get(depId);
|
|
38
|
+
if (!depNode)
|
|
39
|
+
continue;
|
|
40
|
+
// Check if all dependencies are met
|
|
41
|
+
const allDepsMet = [...depNode.dependencies].every((depId) => nodes.get(depId)?.status === "done");
|
|
42
|
+
if (allDepsMet) {
|
|
43
|
+
depNode.status = "ready";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
getReadyTasks() {
|
|
48
|
+
return [...nodes.values()]
|
|
49
|
+
.filter((n) => n.status === "ready")
|
|
50
|
+
.map((n) => n.task);
|
|
51
|
+
},
|
|
52
|
+
getBlockedTasks(_taskId) {
|
|
53
|
+
// Stub: return empty array
|
|
54
|
+
return [];
|
|
55
|
+
},
|
|
56
|
+
getPendingTasks() {
|
|
57
|
+
return [...nodes.values()]
|
|
58
|
+
.filter((n) => n.status === "pending")
|
|
59
|
+
.map((n) => n.task);
|
|
60
|
+
},
|
|
61
|
+
getAllTasks() {
|
|
62
|
+
return [...nodes.values()].map((n) => n.task);
|
|
63
|
+
},
|
|
64
|
+
getProgressSummary() {
|
|
65
|
+
const allNodes = [...nodes.values()];
|
|
66
|
+
return {
|
|
67
|
+
total: allNodes.length,
|
|
68
|
+
done: allNodes.filter((n) => n.status === "done").length,
|
|
69
|
+
running: allNodes.filter((n) => n.status === "running").length,
|
|
70
|
+
failed: allNodes.filter((n) => n.status === "blocked").length,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
getCompactPriority() {
|
|
74
|
+
// Stub: keep done tasks, prune pending ones
|
|
75
|
+
const done = [...nodes.values()]
|
|
76
|
+
.filter((n) => n.status === "done")
|
|
77
|
+
.map((n) => n.task.id);
|
|
78
|
+
const pending = [...nodes.values()]
|
|
79
|
+
.filter((n) => n.status === "pending")
|
|
80
|
+
.map((n) => n.task.id);
|
|
81
|
+
return {
|
|
82
|
+
keep: done, // Keep done task context (they're relevant)
|
|
83
|
+
prune: pending, // Prune pending task messages first
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|