@iamoberlin/chorus 1.3.4 → 1.3.6
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/index.ts +38 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/choirs.ts +8 -1
- package/src/purpose-research.ts +11 -19
- package/src/scheduler.ts +12 -20
package/index.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
10
10
|
import { spawnSync } from "child_process";
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
11
14
|
import { loadChorusConfig, type ChorusPluginConfig } from "./src/config.js";
|
|
12
15
|
import { createSecurityHooks } from "./src/security.js";
|
|
13
16
|
import { createChoirScheduler } from "./src/scheduler.js";
|
|
@@ -38,7 +41,37 @@ import {
|
|
|
38
41
|
import * as prayers from "./src/prayers/prayers.js";
|
|
39
42
|
import * as prayerStore from "./src/prayers/store.js";
|
|
40
43
|
|
|
41
|
-
const VERSION = "1.3.
|
|
44
|
+
const VERSION = "1.3.5"; // Vision now updates run-state.json
|
|
45
|
+
|
|
46
|
+
// Run state persistence for Vision mode
|
|
47
|
+
const CHORUS_DIR = join(homedir(), ".chorus");
|
|
48
|
+
const RUN_STATE_PATH = join(CHORUS_DIR, "run-state.json");
|
|
49
|
+
|
|
50
|
+
function updateRunState(choirId: string, output: string): void {
|
|
51
|
+
try {
|
|
52
|
+
if (!existsSync(CHORUS_DIR)) {
|
|
53
|
+
mkdirSync(CHORUS_DIR, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let state: Record<string, any> = {};
|
|
57
|
+
if (existsSync(RUN_STATE_PATH)) {
|
|
58
|
+
try {
|
|
59
|
+
state = JSON.parse(readFileSync(RUN_STATE_PATH, "utf-8"));
|
|
60
|
+
} catch { /* start fresh */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const existing = state[choirId] || { runCount: 0 };
|
|
64
|
+
state[choirId] = {
|
|
65
|
+
lastRun: new Date().toISOString(),
|
|
66
|
+
lastOutput: output.slice(0, 500),
|
|
67
|
+
runCount: (existing.runCount || 0) + 1,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
writeFileSync(RUN_STATE_PATH, JSON.stringify(state, null, 2));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Silent fail — don't break Vision for state issues
|
|
73
|
+
}
|
|
74
|
+
}
|
|
42
75
|
|
|
43
76
|
const plugin = {
|
|
44
77
|
id: "chorus",
|
|
@@ -382,10 +415,13 @@ const plugin = {
|
|
|
382
415
|
const text = json.result?.payloads?.[0]?.text || '';
|
|
383
416
|
const duration = json.result?.meta?.durationMs || 0;
|
|
384
417
|
contextStore.set(`${choirId}:d${day}`, text.slice(0, 2000)); // Keep 2KB of response
|
|
418
|
+
updateRunState(choirId, text); // Update scheduler state
|
|
385
419
|
successfulRuns++;
|
|
386
420
|
console.log(` ✓ (${(duration/1000).toFixed(1)}s)`);
|
|
387
421
|
} catch {
|
|
388
|
-
|
|
422
|
+
const output = result.stdout?.slice(-2000) || `[${choir.name} completed]`;
|
|
423
|
+
contextStore.set(`${choirId}:d${day}`, output);
|
|
424
|
+
updateRunState(choirId, output); // Update scheduler state
|
|
389
425
|
successfulRuns++;
|
|
390
426
|
console.log(` ✓`);
|
|
391
427
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "chorus",
|
|
3
3
|
"name": "CHORUS",
|
|
4
4
|
"description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement",
|
|
5
|
-
"version": "1.3
|
|
5
|
+
"version": "1.1.3",
|
|
6
6
|
"author": "Oberlin",
|
|
7
7
|
"homepage": "https://chorus.oberlin.ai",
|
|
8
8
|
"repository": "https://github.com/iamoberlin/chorus",
|
package/package.json
CHANGED
package/src/choirs.ts
CHANGED
|
@@ -413,12 +413,19 @@ export function getChoir(id: string): Choir | undefined {
|
|
|
413
413
|
return CHOIRS[id];
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
// Global minimum interval to prevent over-triggering during rapid restarts or testing
|
|
417
|
+
// Even if a choir's config says 0 or very short, enforce at least this many minutes
|
|
418
|
+
const MIN_INTERVAL_MINUTES = 30;
|
|
419
|
+
|
|
416
420
|
// Check if a choir should run based on its interval
|
|
417
421
|
export function shouldRunChoir(choir: Choir, now: Date, lastRun?: Date): boolean {
|
|
418
422
|
if (!lastRun) return true;
|
|
419
423
|
|
|
420
424
|
const minutesSinceLastRun = (now.getTime() - lastRun.getTime()) / 1000 / 60;
|
|
421
|
-
|
|
425
|
+
|
|
426
|
+
// Enforce both the choir's configured interval AND the global minimum
|
|
427
|
+
const effectiveInterval = Math.max(choir.intervalMinutes, MIN_INTERVAL_MINUTES);
|
|
428
|
+
return minutesSinceLastRun >= effectiveInterval;
|
|
422
429
|
}
|
|
423
430
|
|
|
424
431
|
// Get human-readable frequency
|
package/src/purpose-research.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { recordExecution, type ChoirExecution } from "./metrics.js";
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { homedir } from "os";
|
|
14
|
-
import {
|
|
14
|
+
import { spawnSync } from "child_process";
|
|
15
15
|
|
|
16
16
|
// Workspace path for research output
|
|
17
17
|
const WORKSPACE_PATH = process.env.OPENCLAW_WORKSPACE || join(homedir(), ".openclaw", "workspace");
|
|
@@ -274,25 +274,17 @@ CRITICAL: If sending alerts via iMessage, use PLAIN TEXT ONLY (no markdown).
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
if (!result) {
|
|
277
|
-
// CLI fallback - use stdin to avoid arg length limits
|
|
277
|
+
// CLI fallback - use stdin to avoid arg length limits
|
|
278
278
|
log.debug(`[purpose-research] Using CLI fallback for "${purpose.name}"`);
|
|
279
|
-
result =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
let stderr = '';
|
|
289
|
-
const maxBuffer = 1024 * 1024;
|
|
290
|
-
child.stdout.on('data', (d: Buffer) => { if (stdout.length < maxBuffer) stdout += d.toString(); });
|
|
291
|
-
child.stderr.on('data', (d: Buffer) => { if (stderr.length < maxBuffer) stderr += d.toString(); });
|
|
292
|
-
|
|
293
|
-
const timer = setTimeout(() => { child.kill('SIGTERM'); }, config.researchTimeoutMs);
|
|
294
|
-
child.on('close', (code) => { clearTimeout(timer); resolve({ status: code, stdout, stderr }); });
|
|
295
|
-
child.on('error', (err) => { clearTimeout(timer); resolve({ status: 1, stdout: '', stderr: String(err) }); });
|
|
279
|
+
result = spawnSync("openclaw", [
|
|
280
|
+
"agent",
|
|
281
|
+
"--session-id", `chorus:purpose:${purpose.id}`,
|
|
282
|
+
"--json",
|
|
283
|
+
], {
|
|
284
|
+
input: prompt,
|
|
285
|
+
encoding: "utf-8",
|
|
286
|
+
timeout: config.researchTimeoutMs,
|
|
287
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
296
288
|
});
|
|
297
289
|
|
|
298
290
|
if (result.status === 0 && result.stdout) {
|
package/src/scheduler.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { recordExecution, type ChoirExecution } from "./metrics.js";
|
|
|
12
12
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
13
13
|
import { join } from "path";
|
|
14
14
|
import { homedir } from "os";
|
|
15
|
-
import {
|
|
15
|
+
import { spawnSync } from "child_process";
|
|
16
16
|
|
|
17
17
|
interface ChoirContext {
|
|
18
18
|
choirId: string;
|
|
@@ -128,28 +128,20 @@ export function createChoirScheduler(
|
|
|
128
128
|
try {
|
|
129
129
|
const prompt = buildPrompt(choir);
|
|
130
130
|
|
|
131
|
-
// Use openclaw agent CLI (
|
|
132
|
-
const result =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
let stderr = '';
|
|
142
|
-
const maxBuffer = 1024 * 1024;
|
|
143
|
-
child.stdout.on('data', (d: Buffer) => { if (stdout.length < maxBuffer) stdout += d.toString(); });
|
|
144
|
-
child.stderr.on('data', (d: Buffer) => { if (stderr.length < maxBuffer) stderr += d.toString(); });
|
|
145
|
-
|
|
146
|
-
const timer = setTimeout(() => { child.kill('SIGTERM'); }, 300000); // 5 min
|
|
147
|
-
child.on('close', (code) => { clearTimeout(timer); resolve({ status: code, stdout, stderr }); });
|
|
148
|
-
child.on('error', (err) => { clearTimeout(timer); resolve({ status: 1, stdout: '', stderr: String(err) }); });
|
|
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
|
|
149
141
|
});
|
|
150
142
|
|
|
151
143
|
let output = "(no response)";
|
|
152
|
-
|
|
144
|
+
|
|
153
145
|
if (result.status === 0 && result.stdout) {
|
|
154
146
|
// Extract JSON from output (may have plugin logs before it)
|
|
155
147
|
const stdout = result.stdout;
|