@iamoberlin/chorus 1.2.7 → 1.2.9
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 +1 -1
- package/src/scheduler.ts +101 -16
package/package.json
CHANGED
package/src/scheduler.ts
CHANGED
|
@@ -9,6 +9,10 @@ import type { OpenClawPluginService, PluginLogger } from "openclaw/plugin-sdk";
|
|
|
9
9
|
import type { ChorusConfig } from "./config.js";
|
|
10
10
|
import { CHOIRS, shouldRunChoir, CASCADE_ORDER, type Choir } from "./choirs.js";
|
|
11
11
|
import { recordExecution, type ChoirExecution } from "./metrics.js";
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { spawnSync } from "child_process";
|
|
12
16
|
|
|
13
17
|
interface ChoirContext {
|
|
14
18
|
choirId: string;
|
|
@@ -22,6 +26,64 @@ interface ChoirRunState {
|
|
|
22
26
|
runCount: number;
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
// State persistence path
|
|
30
|
+
const CHORUS_DIR = join(homedir(), ".chorus");
|
|
31
|
+
const RUN_STATE_PATH = join(CHORUS_DIR, "run-state.json");
|
|
32
|
+
|
|
33
|
+
// Load persisted run state from disk
|
|
34
|
+
function loadRunState(log: PluginLogger): Map<string, ChoirRunState> {
|
|
35
|
+
const state = new Map<string, ChoirRunState>();
|
|
36
|
+
|
|
37
|
+
// Initialize all choirs with default state
|
|
38
|
+
for (const choirId of Object.keys(CHOIRS)) {
|
|
39
|
+
state.set(choirId, { runCount: 0 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try to load persisted state
|
|
43
|
+
if (existsSync(RUN_STATE_PATH)) {
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(readFileSync(RUN_STATE_PATH, "utf-8"));
|
|
46
|
+
for (const [choirId, saved] of Object.entries(data)) {
|
|
47
|
+
const s = saved as any;
|
|
48
|
+
if (state.has(choirId)) {
|
|
49
|
+
state.set(choirId, {
|
|
50
|
+
lastRun: s.lastRun ? new Date(s.lastRun) : undefined,
|
|
51
|
+
lastOutput: s.lastOutput,
|
|
52
|
+
runCount: s.runCount || 0,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
log.info(`[chorus] Loaded run state from disk (${Object.keys(data).length} choirs)`);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log.warn(`[chorus] Failed to load run state: ${err}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Save run state to disk
|
|
66
|
+
function saveRunState(state: Map<string, ChoirRunState>, log: PluginLogger): void {
|
|
67
|
+
try {
|
|
68
|
+
// Ensure directory exists
|
|
69
|
+
if (!existsSync(CHORUS_DIR)) {
|
|
70
|
+
mkdirSync(CHORUS_DIR, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const obj: Record<string, any> = {};
|
|
74
|
+
for (const [choirId, s] of state) {
|
|
75
|
+
obj[choirId] = {
|
|
76
|
+
lastRun: s.lastRun?.toISOString(),
|
|
77
|
+
lastOutput: s.lastOutput,
|
|
78
|
+
runCount: s.runCount,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
writeFileSync(RUN_STATE_PATH, JSON.stringify(obj, null, 2));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
log.error(`[chorus] Failed to save run state: ${err}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
25
87
|
export function createChoirScheduler(
|
|
26
88
|
config: ChorusConfig,
|
|
27
89
|
log: PluginLogger,
|
|
@@ -29,12 +91,9 @@ export function createChoirScheduler(
|
|
|
29
91
|
): OpenClawPluginService {
|
|
30
92
|
let checkInterval: NodeJS.Timeout | null = null;
|
|
31
93
|
const contextStore: Map<string, ChoirContext> = new Map();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (const choirId of Object.keys(CHOIRS)) {
|
|
36
|
-
runState.set(choirId, { runCount: 0 });
|
|
37
|
-
}
|
|
94
|
+
|
|
95
|
+
// Load persisted state instead of starting fresh
|
|
96
|
+
const runState = loadRunState(log);
|
|
38
97
|
|
|
39
98
|
// Build the prompt with context injected
|
|
40
99
|
function buildPrompt(choir: Choir): string {
|
|
@@ -51,7 +110,7 @@ export function createChoirScheduler(
|
|
|
51
110
|
return prompt;
|
|
52
111
|
}
|
|
53
112
|
|
|
54
|
-
// Execute a choir
|
|
113
|
+
// Execute a choir using openclaw agent CLI
|
|
55
114
|
async function executeChoir(choir: Choir): Promise<void> {
|
|
56
115
|
const state = runState.get(choir.id) || { runCount: 0 };
|
|
57
116
|
const startTime = Date.now();
|
|
@@ -69,19 +128,42 @@ export function createChoirScheduler(
|
|
|
69
128
|
try {
|
|
70
129
|
const prompt = buildPrompt(choir);
|
|
71
130
|
|
|
72
|
-
// Use
|
|
73
|
-
const result =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
131
|
+
// Use openclaw agent CLI (same as Vision mode)
|
|
132
|
+
const result = spawnSync('openclaw', [
|
|
133
|
+
'agent',
|
|
134
|
+
'--session-id', `chorus:${choir.id}`,
|
|
135
|
+
'--message', prompt,
|
|
136
|
+
'--json',
|
|
137
|
+
], {
|
|
138
|
+
encoding: 'utf-8',
|
|
139
|
+
timeout: 300000, // 5 min
|
|
140
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
78
141
|
});
|
|
79
142
|
|
|
80
|
-
|
|
143
|
+
let output = "(no response)";
|
|
144
|
+
|
|
145
|
+
if (result.status === 0 && result.stdout) {
|
|
146
|
+
// Extract JSON from output (may have plugin logs before it)
|
|
147
|
+
const stdout = result.stdout;
|
|
148
|
+
const jsonStart = stdout.indexOf('{');
|
|
149
|
+
if (jsonStart >= 0) {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(stdout.slice(jsonStart));
|
|
152
|
+
output = parsed.response || parsed.content || stdout;
|
|
153
|
+
} catch {
|
|
154
|
+
output = stdout;
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
output = stdout;
|
|
158
|
+
}
|
|
159
|
+
} else if (result.stderr) {
|
|
160
|
+
log.warn(`[chorus] ${choir.name} stderr: ${result.stderr.slice(0, 200)}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
81
163
|
execution.durationMs = Date.now() - startTime;
|
|
82
|
-
execution.success =
|
|
164
|
+
execution.success = result.status === 0;
|
|
83
165
|
execution.outputLength = output.length;
|
|
84
|
-
execution.tokensUsed =
|
|
166
|
+
execution.tokensUsed = estimateTokens(output);
|
|
85
167
|
|
|
86
168
|
// Parse output for metrics (findings, alerts, improvements)
|
|
87
169
|
execution.findings = countFindings(output);
|
|
@@ -101,6 +183,9 @@ export function createChoirScheduler(
|
|
|
101
183
|
lastOutput: output.slice(0, 500),
|
|
102
184
|
runCount: state.runCount + 1,
|
|
103
185
|
});
|
|
186
|
+
|
|
187
|
+
// Persist state to disk after each run
|
|
188
|
+
saveRunState(runState, log);
|
|
104
189
|
|
|
105
190
|
log.info(`[chorus] ${choir.emoji} ${choir.name} completed (${(execution.durationMs/1000).toFixed(1)}s)`);
|
|
106
191
|
|