@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 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.4"; // Restore --message flag (required by openclaw agent CLI)
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
- contextStore.set(`${choirId}:d${day}`, result.stdout?.slice(-2000) || `[${choir.name} completed]`);
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
  }
@@ -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.4",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
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/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
- return minutesSinceLastRun >= choir.intervalMinutes;
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
@@ -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 { spawn } from "child_process";
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 (async to avoid blocking event loop)
277
+ // CLI fallback - use stdin to avoid arg length limits
278
278
  log.debug(`[purpose-research] Using CLI fallback for "${purpose.name}"`);
279
- result = await new Promise<any>((resolve) => {
280
- const child = spawn("openclaw", [
281
- "agent",
282
- "--session-id", `chorus:purpose:${purpose.id}`,
283
- "--message", prompt,
284
- "--json",
285
- ], { stdio: ['pipe', 'pipe', 'pipe'] });
286
-
287
- let stdout = '';
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 { spawn } from "child_process";
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 (async to avoid blocking event loop)
132
- const result = await new Promise<{ status: number | null; stdout: string; stderr: string }>((resolve) => {
133
- const child = spawn('openclaw', [
134
- 'agent',
135
- '--session-id', `chorus:${choir.id}`,
136
- '--message', prompt,
137
- '--json',
138
- ], { stdio: ['pipe', 'pipe', 'pipe'] });
139
-
140
- let stdout = '';
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;