@iamoberlin/chorus 1.2.6 → 1.2.8

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 CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  import * as prayers from "./src/prayers/prayers.js";
39
39
  import * as prayerStore from "./src/prayers/store.js";
40
40
 
41
- const VERSION = "1.2.6"; // Fixed: use --message flag instead of nonexistent stdin support
41
+ const VERSION = "1.2.7"; // Fixed JSON parsing: extract JSON from output (plugin logs prefix it)
42
42
 
43
43
  const plugin = {
44
44
  id: "chorus",
@@ -208,7 +208,13 @@ const plugin = {
208
208
  isolated: true,
209
209
  timeoutSeconds: 300,
210
210
  });
211
- console.log(` ✓ ${choir.name} complete`);
211
+ const text = result?.text || result?.payloads?.[0]?.text || '';
212
+ const duration = result?.meta?.durationMs || 0;
213
+ console.log(` ✓ ${choir.name} complete (${(duration/1000).toFixed(1)}s)`);
214
+ if (text) {
215
+ const preview = text.slice(0, 150).replace(/\n/g, ' ');
216
+ console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
217
+ }
212
218
  } catch (err) {
213
219
  console.error(` ✗ ${choir.name} failed:`, err);
214
220
  }
@@ -228,7 +234,11 @@ const plugin = {
228
234
 
229
235
  if (result.status === 0) {
230
236
  try {
231
- const json = JSON.parse(result.stdout || '{}');
237
+ // Extract JSON from output (may have plugin logs before it)
238
+ const stdout = result.stdout || '';
239
+ const jsonStart = stdout.indexOf('{');
240
+ const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
241
+ const json = JSON.parse(jsonStr);
232
242
  const text = json.result?.payloads?.[0]?.text || '';
233
243
  const duration = json.result?.meta?.durationMs || 0;
234
244
  console.log(` ✓ ${choir.name} complete (${(duration/1000).toFixed(1)}s)`);
@@ -236,8 +246,8 @@ const plugin = {
236
246
  const preview = text.slice(0, 150).replace(/\n/g, ' ');
237
247
  console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
238
248
  }
239
- } catch {
240
- console.log(` ✓ ${choir.name} complete`);
249
+ } catch (parseErr) {
250
+ console.log(` ✓ ${choir.name} complete (parse error: ${parseErr})`);
241
251
  }
242
252
  } else {
243
253
  const errMsg = result.stderr || result.stdout || 'Unknown error';
@@ -327,7 +337,10 @@ const plugin = {
327
337
  });
328
338
  if (result.status === 0 && result.stdout) {
329
339
  try {
330
- const json = JSON.parse(result.stdout);
340
+ const stdout = result.stdout;
341
+ const jsonStart = stdout.indexOf('{');
342
+ const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
343
+ const json = JSON.parse(jsonStr);
331
344
  const text = json.result?.payloads?.[0]?.text || '';
332
345
  contextStore.set(`${choirId}:d${day}`, text.slice(0, 500));
333
346
  console.log(` ✓ (dry)`);
@@ -360,9 +373,12 @@ const plugin = {
360
373
  });
361
374
 
362
375
  if (result.status === 0) {
363
- // Parse the agent response
376
+ // Parse the agent response (extract JSON from output)
364
377
  try {
365
- const json = JSON.parse(result.stdout || '{}');
378
+ const stdout = result.stdout || '';
379
+ const jsonStart = stdout.indexOf('{');
380
+ const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
381
+ const json = JSON.parse(jsonStr);
366
382
  const text = json.result?.payloads?.[0]?.text || '';
367
383
  const duration = json.result?.meta?.durationMs || 0;
368
384
  contextStore.set(`${choirId}:d${day}`, text.slice(0, 2000)); // Keep 2KB of response
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
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,9 @@ 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";
12
15
 
13
16
  interface ChoirContext {
14
17
  choirId: string;
@@ -22,6 +25,64 @@ interface ChoirRunState {
22
25
  runCount: number;
23
26
  }
24
27
 
28
+ // State persistence path
29
+ const CHORUS_DIR = join(homedir(), ".chorus");
30
+ const RUN_STATE_PATH = join(CHORUS_DIR, "run-state.json");
31
+
32
+ // Load persisted run state from disk
33
+ function loadRunState(log: PluginLogger): Map<string, ChoirRunState> {
34
+ const state = new Map<string, ChoirRunState>();
35
+
36
+ // Initialize all choirs with default state
37
+ for (const choirId of Object.keys(CHOIRS)) {
38
+ state.set(choirId, { runCount: 0 });
39
+ }
40
+
41
+ // Try to load persisted state
42
+ if (existsSync(RUN_STATE_PATH)) {
43
+ try {
44
+ const data = JSON.parse(readFileSync(RUN_STATE_PATH, "utf-8"));
45
+ for (const [choirId, saved] of Object.entries(data)) {
46
+ const s = saved as any;
47
+ if (state.has(choirId)) {
48
+ state.set(choirId, {
49
+ lastRun: s.lastRun ? new Date(s.lastRun) : undefined,
50
+ lastOutput: s.lastOutput,
51
+ runCount: s.runCount || 0,
52
+ });
53
+ }
54
+ }
55
+ log.info(`[chorus] Loaded run state from disk (${Object.keys(data).length} choirs)`);
56
+ } catch (err) {
57
+ log.warn(`[chorus] Failed to load run state: ${err}`);
58
+ }
59
+ }
60
+
61
+ return state;
62
+ }
63
+
64
+ // Save run state to disk
65
+ function saveRunState(state: Map<string, ChoirRunState>, log: PluginLogger): void {
66
+ try {
67
+ // Ensure directory exists
68
+ if (!existsSync(CHORUS_DIR)) {
69
+ mkdirSync(CHORUS_DIR, { recursive: true });
70
+ }
71
+
72
+ const obj: Record<string, any> = {};
73
+ for (const [choirId, s] of state) {
74
+ obj[choirId] = {
75
+ lastRun: s.lastRun?.toISOString(),
76
+ lastOutput: s.lastOutput,
77
+ runCount: s.runCount,
78
+ };
79
+ }
80
+ writeFileSync(RUN_STATE_PATH, JSON.stringify(obj, null, 2));
81
+ } catch (err) {
82
+ log.error(`[chorus] Failed to save run state: ${err}`);
83
+ }
84
+ }
85
+
25
86
  export function createChoirScheduler(
26
87
  config: ChorusConfig,
27
88
  log: PluginLogger,
@@ -29,12 +90,9 @@ export function createChoirScheduler(
29
90
  ): OpenClawPluginService {
30
91
  let checkInterval: NodeJS.Timeout | null = null;
31
92
  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
- }
93
+
94
+ // Load persisted state instead of starting fresh
95
+ const runState = loadRunState(log);
38
96
 
39
97
  // Build the prompt with context injected
40
98
  function buildPrompt(choir: Choir): string {
@@ -101,6 +159,9 @@ export function createChoirScheduler(
101
159
  lastOutput: output.slice(0, 500),
102
160
  runCount: state.runCount + 1,
103
161
  });
162
+
163
+ // Persist state to disk after each run
164
+ saveRunState(runState, log);
104
165
 
105
166
  log.info(`[chorus] ${choir.emoji} ${choir.name} completed (${(execution.durationMs/1000).toFixed(1)}s)`);
106
167