@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/scheduler.ts +101 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement — with Prayer Requests social network",
5
5
  "author": "Oberlin <iam@oberlin.ai>",
6
6
  "license": "MIT",
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
- const runState: Map<string, ChoirRunState> = new Map();
33
-
34
- // Initialize run state for all choirs
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 OpenClaw's session system to run an agent turn
73
- const result = await api.runAgentTurn?.({
74
- sessionLabel: `chorus:${choir.id}`,
75
- message: prompt,
76
- isolated: true,
77
- timeoutSeconds: 300, // 5 min max
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
- const output = result?.response || "(no response)";
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 = true;
164
+ execution.success = result.status === 0;
83
165
  execution.outputLength = output.length;
84
- execution.tokensUsed = result?.meta?.tokensUsed || estimateTokens(output);
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