pi-harness-runtime 0.10.16 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "0.10.16",
3
+ "version": "0.10.19",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,16 +16,15 @@
16
16
  "auth:minimax:scrape": "bun packages/auth/src/run-minimax-auth.ts scrape",
17
17
  "skills:sync": "bun scripts/skills-sync.ts",
18
18
  "skills:sync:check": "bun scripts/skills-sync.ts --check-only",
19
- "build": "for pkg in packages/*/; do [ -f \"${pkg}tsconfig.json\" ] && [ \"$pkg\" != \"packages/provider-router/\" ] && node_modules/.bin/tsc -p \"${pkg}tsconfig.json\" --skipLibCheck > /dev/null 2>&1 || true; done && node_modules/.bin/tsc -p tsconfig.harness.json --skipLibCheck > /dev/null 2>&1"
19
+ "build": "for pkg in packages/*/; do [ -f \"${pkg}tsconfig.json\" ] && [ \"$pkg\" != \"packages/provider-router/\" ] && node_modules/.bin/tsc -p \"${pkg}tsconfig.json\" --skipLibCheck > /dev/null 2>&1 || true; done",
20
+ "prepublishOnly": "bun run build"
20
21
  },
21
22
  "bin": {
22
23
  "harness-auth": "packages/auth/src/run-minimax-auth.ts"
23
24
  },
24
25
  "files": [
25
26
  "*.ts",
26
- "*.js",
27
27
  "harness/**/*.ts",
28
- "harness/**/*.js",
29
28
  "packages/**/src/**/*.{ts,js}",
30
29
  "packages/**/dist/**/*.{js,d.ts,js.map,d.ts.map}",
31
30
  "skills/**/*",
package/.versionrc.js DELETED
@@ -1,32 +0,0 @@
1
- // standard-version config
2
- // https://github.com/conventional-changelog/standard-version
3
- //
4
- // ESM version (project uses "type": "module")
5
-
6
- export default {
7
- types: [
8
- { type: "feat", section: "Features" },
9
- { type: "fix", section: "Bug Fixes" },
10
- { type: "perf", section: "Performance" },
11
- { type: "refactor", section: "Refactoring" },
12
- { type: "docs", section: "Documentation" },
13
- { type: "test", section: "Tests" },
14
- { type: "ci", section: "CI/CD" },
15
- { type: "chore", section: "Maintenance", hidden: false },
16
- ],
17
- bumpFiles: [
18
- {
19
- filename: "package.json",
20
- type: "json",
21
- },
22
- ],
23
- packageFiles: ["package.json"],
24
- bumpInChangelog: "package.json",
25
- tagPrefix: "v",
26
- commitUrlFormat:
27
- "https://github.com/ManotLuijiu/pi-harness-runtime/commit/{{hash}}",
28
- compareUrlFormat:
29
- "https://github.com/ManotLuijiu/pi-harness-runtime/compare/v{{previousTag}}...v{{currentTag}}",
30
- issueUrlFormat:
31
- "https://github.com/ManotLuijiu/pi-harness-runtime/issues/{{id}}",
32
- };
package/cli.js DELETED
@@ -1,112 +0,0 @@
1
- /**
2
- * Pure helper functions — testable without pi context.
3
- * Imported by tracker.ts, mirror.ts, windows.ts, renderer.ts.
4
- */
5
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
- import { dirname, join } from "node:path";
7
- import { homedir } from "node:os";
8
- /** Resolve the ~/.pi/usage-status/ directory. Override with PI_USAGE_DIR env var (testing). */
9
- export function getUsageDir() {
10
- const override = process.env.PI_USAGE_DIR;
11
- if (override)
12
- return override;
13
- return join(homedir(), ".pi", "usage-status");
14
- }
15
- /** Resolve the JSONL usage log path. */
16
- export function getUsageLogPath() {
17
- return join(getUsageDir(), "usage.jsonl");
18
- }
19
- /** Resolve the mirror JSON path. */
20
- export function getMirrorPath() {
21
- return join(getUsageDir(), "mirror.json");
22
- }
23
- /** Ensure the directory exists. Idempotent. */
24
- export function ensureUsageDir() {
25
- const dir = getUsageDir();
26
- if (!existsSync(dir)) {
27
- mkdirSync(dir, { recursive: true });
28
- }
29
- }
30
- /** Read the JSONL usage log as an array of UsageRecord. Returns [] if missing. */
31
- export function readJsonl(path) {
32
- if (!existsSync(path))
33
- return [];
34
- const text = readFileSync(path, "utf-8");
35
- const out = [];
36
- for (const line of text.split("\n")) {
37
- const trimmed = line.trim();
38
- if (!trimmed)
39
- continue;
40
- try {
41
- out.push(JSON.parse(trimmed));
42
- }
43
- catch {
44
- // skip corrupted line
45
- }
46
- }
47
- return out;
48
- }
49
- /** Append a single JSON line to a JSONL file. */
50
- export function appendJsonl(path, record) {
51
- ensureUsageDir();
52
- const line = JSON.stringify(record) + "\n";
53
- appendFileSync(path, line, "utf-8");
54
- }
55
- /** Read JSON file safely. Returns null if missing or corrupted. */
56
- export function readJson(path) {
57
- if (!existsSync(path))
58
- return null;
59
- try {
60
- return JSON.parse(readFileSync(path, "utf-8"));
61
- }
62
- catch {
63
- return null;
64
- }
65
- }
66
- /** Write JSON file (creates parent dir if needed). */
67
- export function writeJson(path, data) {
68
- ensureUsageDir();
69
- if (!existsSync(dirname(path))) {
70
- mkdirSync(dirname(path), { recursive: true });
71
- }
72
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
73
- }
74
- /** Format milliseconds as "Xh Ym" or "Xd Yh" or "Xm" or "Xs". */
75
- export function formatDuration(ms) {
76
- const abs = Math.max(0, Math.floor(ms / 1000));
77
- const sec = abs % 60;
78
- const min = Math.floor(abs / 60) % 60;
79
- const hr = Math.floor(abs / 3600) % 24;
80
- const day = Math.floor(abs / 86400);
81
- if (day > 0)
82
- return `${day}d ${hr}h`;
83
- if (hr > 0)
84
- return `${hr}h ${min}m`;
85
- if (min > 0)
86
- return `${min}m`;
87
- return `${sec}s`;
88
- }
89
- /** Format a timestamp as relative time "X min ago". */
90
- export function formatRelative(fromIso, nowMs) {
91
- const fromMs = Date.parse(fromIso);
92
- if (isNaN(fromMs))
93
- return "unknown";
94
- const deltaMs = nowMs - fromMs;
95
- return formatDuration(deltaMs) + " ago";
96
- }
97
- /** Format a token count with k/M suffix. */
98
- export function formatTokens(n) {
99
- if (n < 1000)
100
- return String(n);
101
- if (n < 1_000_000)
102
- return `${(n / 1000).toFixed(1)}k`;
103
- return `${(n / 1_000_000).toFixed(2)}M`;
104
- }
105
- /** Format USD cost with 2-4 decimals depending on size. */
106
- export function formatUsd(n) {
107
- if (n < 0.01)
108
- return `$${n.toFixed(4)}`;
109
- if (n < 100)
110
- return `$${n.toFixed(2)}`;
111
- return `$${n.toFixed(0)}`;
112
- }
package/footer-status.js DELETED
@@ -1,155 +0,0 @@
1
- import { parseContextWindowStatusLine, parseQuotaUsageStatusLine, } from "./status-parsers.ts";
2
- import { getProviderLabel } from "./packages/types/src/ai-providers.js";
3
- import { providerHasContinuousScrape, providerHasTUISignal, } from "./packages/providers/src/provider-id.js";
4
- export function parseFooterStatusValue(value) {
5
- const quota = parseQuotaUsageStatusLine(value);
6
- if (quota) {
7
- return { kind: "quota", value: quota };
8
- }
9
- const contextWindow = parseContextWindowStatusLine(value);
10
- if (contextWindow) {
11
- return { kind: "context-window", value: contextWindow };
12
- }
13
- const todayMatch = value.match(/^today:\s*([\d,.]+)k tok\s*·\s*\$([\d,.]+)/i);
14
- if (todayMatch) {
15
- const tokens = Number.parseFloat(todayMatch[1].replace(/,/g, "")) * 1000;
16
- const cost = Number.parseFloat(todayMatch[2].replace(/,/g, ""));
17
- if (Number.isFinite(tokens) && Number.isFinite(cost)) {
18
- return {
19
- kind: "today",
20
- value: { tokens, cost },
21
- };
22
- }
23
- }
24
- return { kind: "unknown", value: null };
25
- }
26
- /**
27
- * Get display label for a provider id.
28
- * Uses the canonical getProviderLabel() from packages/types/src/ai-providers.ts
29
- * which handles all 9 known providers: minimax, openai, anthropic, glm,
30
- * openrouter, openai-codex, deepseek, gemini, kimi.
31
- */
32
- function providerDisplayName(provider) {
33
- if (!provider)
34
- return "Provider";
35
- return getProviderLabel(provider);
36
- }
37
- /**
38
- * Hint shown when the provider has no usage data yet.
39
- * Covers all 9 known providers from KNOWN_AI_PROVIDERS.
40
- */
41
- function missingDataHint(provider, hasCookieSource) {
42
- if (!provider)
43
- return "no usage source configured";
44
- // MiniMax: needs cookies for continuous scrape
45
- if (provider === "minimax") {
46
- return hasCookieSource
47
- ? "no data yet (updates after first scrape)"
48
- : "drop minimax cookies into ~/.pi-harness-runtime/cookies/";
49
- }
50
- // TUI signal providers: one-shot signal on limit hit
51
- if (providerHasTUISignal(provider)) {
52
- return "no signal yet (updates on first limit hit)";
53
- }
54
- // Providers without any tracking implementation yet
55
- // deepseek, gemini, kimi, or any unknown provider
56
- if (provider === "deepseek") {
57
- return "deepseek usage tracking not yet implemented";
58
- }
59
- if (provider === "gemini") {
60
- return "gemini usage tracking not yet implemented";
61
- }
62
- if (provider === "kimi") {
63
- return "kimi usage tracking not yet implemented";
64
- }
65
- // Generic fallback for any other unknown provider
66
- if (!hasCookieSource)
67
- return "no usage source configured";
68
- return "no data yet";
69
- }
70
- /** Normalize an unknown mirror record into a ProviderMirrorRecord view. */
71
- function toProviderView(mirror) {
72
- if (!mirror)
73
- return null;
74
- const m = mirror;
75
- if (typeof m.provider === "string")
76
- return m;
77
- // Legacy flat-shape: already a single record without a provider wrapper.
78
- return {
79
- synced_at: m.synced_at ?? new Date().toISOString(),
80
- provider: m.provider ?? "",
81
- source: "scrape",
82
- model: m.model,
83
- h5_used_pct: m.h5_used_pct,
84
- h5_resets_at: m.h5_resets_at,
85
- weekly_used_pct: m.weekly_used_pct,
86
- weekly_resets_at: m.weekly_resets_at,
87
- };
88
- }
89
- export function buildFooterStatusValue(local, mirror, freshness, hasCookieSource = true, activeProvider = null) {
90
- const view = toProviderView(mirror);
91
- const provider = activeProvider ?? view?.provider ?? null;
92
- const label = providerDisplayName(provider);
93
- // TUI-signal exhaustion path: fires when limit is hit (one-shot signal).
94
- // This takes priority over continuous data to show the most recent state.
95
- if (provider &&
96
- view &&
97
- view.exhausted &&
98
- (view.limitType !== undefined ||
99
- view.resets_at !== undefined ||
100
- view.h5_resets_at !== undefined)) {
101
- const reset = view.resets_at ?? view.h5_resets_at ?? "soon";
102
- const limitType = view.limitType ?? "tokens";
103
- return `${label}: limit hit (${limitType}), reset ${reset}`;
104
- }
105
- // Continuous data path: providers with continuous scrape (MiniMax has 5h+weekly, OpenAI has weekly-only)
106
- if (provider &&
107
- providerHasContinuousScrape(provider) &&
108
- view &&
109
- freshness !== "expired" &&
110
- (view.h5_used_pct !== undefined || view.weekly_used_pct !== undefined)) {
111
- const weeklyPct = view.weekly_used_pct ?? 0;
112
- const weeklyLeft = Math.max(0, 100 - weeklyPct);
113
- const weeklyResets = view.weekly_resets_at ?? "soon";
114
- let statusLine;
115
- if (view.h5_used_pct !== undefined) {
116
- // MiniMax: has both 5h and weekly windows
117
- const h5Pct = view.h5_used_pct;
118
- const h5Left = Math.max(0, 100 - h5Pct);
119
- statusLine = `5h: ${h5Left.toFixed(0)}% left · week: ${weeklyLeft.toFixed(0)}% left`;
120
- }
121
- else {
122
- // OpenAI: weekly-only (no 5h window)
123
- statusLine = `week: ${weeklyLeft.toFixed(0)}% left (resets ${weeklyResets})`;
124
- }
125
- const freshnessSuffix = freshness === "fresh" || freshness === "missing" ? "" : ` · ${freshness}`;
126
- return `${label}: ${statusLine}${freshnessSuffix}`;
127
- }
128
- // TUI signal providers (OpenAI, Anthropic, GLM, OpenRouter):
129
- // Show monitoring status when we have a record but haven't hit limits.
130
- // These providers only emit data when a limit is hit.
131
- if (provider &&
132
- providerHasTUISignal(provider) &&
133
- view &&
134
- freshness !== "expired") {
135
- // If exhausted is not set, we're monitoring normally
136
- if (!view.exhausted) {
137
- return `${label}: monitoring (no limits hit)`;
138
- }
139
- }
140
- // Discoverable hint when we have no data yet.
141
- if (provider &&
142
- (!view || freshness === "expired" || freshness === "missing")) {
143
- const hint = missingDataHint(provider, hasCookieSource);
144
- // MiniMax-on-fresh-machine still gets the cookie hint; for others
145
- // the hint explains the signal-driven design.
146
- if (provider === "minimax" && !hasCookieSource) {
147
- return `${label}: 5h: -- (${hint})`;
148
- }
149
- return `${label}: 5h: -- · week: -- (${hint})`;
150
- }
151
- // Silent fallback: only reached if we genuinely don't know the provider.
152
- // No "5h/week: --" line here because we'd be making up data.
153
- const todayStr = `${(local.today.tokens / 1000).toFixed(1)}k tok · $${local.today.cost.toFixed(3)}`;
154
- return `today: ${todayStr}`;
155
- }
@@ -1,123 +0,0 @@
1
- /**
2
- * Agent Handoff Protocol — RFC-0012
3
- *
4
- * Clean handoff between agents with context transfer.
5
- * Ensures continuity when switching agents mid-task.
6
- */
7
- import { writeJson, readJson } from "../cli.ts";
8
- // @ts-expect-error - Bun has built-in Node.js types
9
- import { join } from "node:path";
10
- export class AgentHandoffProtocol {
11
- rootDir;
12
- constructor(rootDir) {
13
- this.rootDir = rootDir;
14
- }
15
- /**
16
- * Create a handoff context for switching agents
17
- */
18
- createHandoff(jobId, taskId, fromAgent, toAgent, currentState) {
19
- const events = this.loadHandoffHistory(jobId, taskId);
20
- return {
21
- jobId,
22
- taskId,
23
- fromAgent,
24
- toAgent,
25
- sharedFiles: [],
26
- taskHistory: events,
27
- summary: this.generateSummary(taskId, currentState),
28
- };
29
- }
30
- /**
31
- * Record a handoff event
32
- */
33
- recordHandoff(context, result) {
34
- const path = join(this.rootDir, "jobs", context.jobId, "handoffs", `${context.taskId}.json`);
35
- const event = {
36
- ts: new Date().toISOString(),
37
- agentId: context.toAgent,
38
- action: "handoff_received",
39
- result,
40
- };
41
- context.taskHistory.push(event);
42
- writeJson(path, context);
43
- }
44
- /**
45
- * Generate handoff prompt for the receiving agent
46
- */
47
- generateHandoffPrompt(context) {
48
- const lines = [
49
- `## Agent Handoff`,
50
- ``,
51
- `**From Agent:** ${context.fromAgent}`,
52
- `**To Agent:** ${context.toAgent}`,
53
- `**Task:** ${context.taskId}`,
54
- ``,
55
- `### Task History`,
56
- ];
57
- for (const event of context.taskHistory) {
58
- lines.push(`- [${event.ts}] ${event.agentId}: ${event.action}`);
59
- if (event.result) {
60
- lines.push(` Result: ${event.result}`);
61
- }
62
- }
63
- lines.push(``);
64
- lines.push(`### Summary`);
65
- lines.push(context.summary);
66
- if (context.sharedFiles.length > 0) {
67
- lines.push(``);
68
- lines.push(`### Shared Files`);
69
- for (const file of context.sharedFiles) {
70
- lines.push(`- ${file}`);
71
- }
72
- }
73
- return lines.join("\n");
74
- }
75
- /**
76
- * Validate handoff readiness
77
- */
78
- validateHandoff(context) {
79
- const issues = [];
80
- if (!context.summary) {
81
- issues.push("Task summary is empty");
82
- }
83
- if (context.taskHistory.length === 0) {
84
- issues.push("No task history recorded");
85
- }
86
- // Check for recent handoffs
87
- const recentHandoffs = context.taskHistory.filter((h) => {
88
- const age = Date.now() - Date.parse(h.ts);
89
- return age < 5 * 60 * 1000; // 5 minutes
90
- });
91
- if (recentHandoffs.length > 3) {
92
- issues.push(`Too many recent handoffs (${recentHandoffs.length}). Possible ping-pong.`);
93
- }
94
- return { valid: issues.length === 0, issues };
95
- }
96
- /**
97
- * Load handoff history for a task
98
- */
99
- loadHandoffHistory(jobId, taskId) {
100
- const path = join(this.rootDir, "jobs", jobId, "handoffs", `${taskId}.json`);
101
- const data = readJson(path);
102
- return data?.taskHistory ?? [];
103
- }
104
- /**
105
- * Generate a summary of the task state
106
- */
107
- generateSummary(taskId, currentState) {
108
- if (!currentState) {
109
- return `Task ${taskId} requires continuation. Check task files for current state.`;
110
- }
111
- const lines = [`Task ${taskId} is in progress.`];
112
- if (currentState.filesModified) {
113
- lines.push(`Files modified: ${currentState.filesModified.join(", ")}`);
114
- }
115
- if (currentState.lastAction) {
116
- lines.push(`Last action: ${currentState.lastAction}`);
117
- }
118
- if (currentState.blockers) {
119
- lines.push(`Blockers: ${currentState.blockers}`);
120
- }
121
- return lines.join("\n");
122
- }
123
- }
@@ -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
- }