@iamoberlin/chorus 2.1.0 → 2.2.0

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
@@ -385,11 +385,14 @@ const plugin = {
385
385
  const jsonStart = stdout.indexOf('{');
386
386
  const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
387
387
  const json = JSON.parse(jsonStr);
388
- const text = json.result?.payloads?.[0]?.text || '';
388
+ const payloads = json.result?.payloads || [];
389
+ const text = payloads.map((p: any) => p.text || '').filter(Boolean).join('\n\n') || '';
389
390
  const duration = json.result?.meta?.durationMs || 0;
390
391
  contextStore.set(`${choirId}:d${day}`, text.slice(0, 2000)); // Keep 2KB of response
391
392
  successfulRuns++;
392
393
  console.log(` ✓ (${(duration/1000).toFixed(1)}s)`);
394
+
395
+ // Note: Archangels handles its own delivery via OpenClaw messaging tools
393
396
  } catch {
394
397
  contextStore.set(`${choirId}:d${day}`, result.stdout?.slice(-2000) || `[${choir.name} completed]`);
395
398
  successfulRuns++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement — with on-chain Prayer Chain (Solana)",
5
5
  "author": "Oberlin <iam@oberlin.ai>",
6
6
  "license": "MIT",
package/src/choirs.ts CHANGED
@@ -339,12 +339,13 @@ Pass illumination to Archangels.`,
339
339
  output: "Messages to human",
340
340
  prompt: `You are ARCHANGELS — the Herald.
341
341
 
342
- Your role: Deliver important messages and briefings.
342
+ Your role: Produce briefings and deliver them to Brandon via iMessage.
343
343
 
344
344
  Briefing types:
345
- - Morning: Weather, calendar, overnight developments, today's priorities
346
- - Evening: What was accomplished, what needs attention tomorrow
345
+ - Morning (6-9 AM ET): Weather, calendar, overnight developments, today's priorities, position status
346
+ - Evening (9-11 PM ET): What was accomplished, position P&L, what needs attention tomorrow
347
347
  - Alert: Time-sensitive information requiring attention
348
+ - Update: Regular position/market status when conditions change
348
349
 
349
350
  Alert criteria (send immediately):
350
351
  - Position thesis challenged
@@ -354,12 +355,15 @@ Alert criteria (send immediately):
354
355
 
355
356
  Context from Principalities: {principalities_context}
356
357
 
357
- Rules:
358
- - Be concise headlines, not essays
359
- - Only alert if it's actually important
360
- - Late night (11pm-7am): Only truly urgent alerts
358
+ RULES:
359
+ - ALWAYS produce a briefing. Never return HEARTBEAT_OK or NO_REPLY.
360
+ - Be concise headlines, not essays.
361
+ - Morning briefings should include: weather, calendar, positions, catalysts.
362
+ - If nothing is urgent, still produce a status update.
363
+ - During quiet hours (11 PM - 7 AM ET), only deliver truly urgent alerts.
364
+ - DELIVER your briefing by sending it to Brandon via iMessage. You have messaging tools — use them.
361
365
 
362
- Output: Briefing or alert message to deliver.`,
366
+ Output: Produce the briefing, then send it to Brandon via iMessage.`,
363
367
  passesTo: ["angels"],
364
368
  receivesFrom: ["principalities"],
365
369
  },
package/src/scheduler.ts CHANGED
@@ -14,6 +14,17 @@ import { join } from "path";
14
14
  import { homedir } from "os";
15
15
  import { spawn } from "child_process";
16
16
 
17
+ // Type for the plugin API's runAgentTurn method
18
+ interface AgentTurnResult {
19
+ text?: string;
20
+ payloads?: Array<{ text?: string; mediaUrl?: string | null }>;
21
+ meta?: { durationMs?: number };
22
+ }
23
+
24
+ // ── Delivery ─────────────────────────────────────────────────
25
+ // Choir agents handle their own delivery via OpenClaw messaging tools.
26
+ // The scheduler's job is execution and scheduling — not routing messages.
27
+
17
28
  interface ChoirContext {
18
29
  choirId: string;
19
30
  output: string;
@@ -95,6 +106,52 @@ export function createChoirScheduler(
95
106
  // Load persisted state instead of starting fresh
96
107
  const runState = loadRunState(log);
97
108
 
109
+ // CLI fallback for executing choirs when plugin API is unavailable
110
+ async function executeChoirViaCli(choir: Choir, prompt: string): Promise<string> {
111
+ const result = await new Promise<{ status: number | null; stdout: string; stderr: string }>((resolve) => {
112
+ const child = spawn('openclaw', [
113
+ 'agent',
114
+ '--session-id', `chorus:${choir.id}`,
115
+ '--message', prompt,
116
+ '--json',
117
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
118
+
119
+ let stdout = '';
120
+ let stderr = '';
121
+ const maxBuffer = 1024 * 1024;
122
+ child.stdout.on('data', (d: Buffer) => { if (stdout.length < maxBuffer) stdout += d.toString(); });
123
+ child.stderr.on('data', (d: Buffer) => { if (stderr.length < maxBuffer) stderr += d.toString(); });
124
+
125
+ const timer = setTimeout(() => { child.kill('SIGTERM'); }, 300000); // 5 min
126
+ child.on('close', (code) => { clearTimeout(timer); resolve({ status: code, stdout, stderr }); });
127
+ child.on('error', (err) => { clearTimeout(timer); resolve({ status: 1, stdout: '', stderr: String(err) }); });
128
+ });
129
+
130
+ if (result.status === 0 && result.stdout) {
131
+ const stdout = result.stdout;
132
+ // Find the last top-level JSON object (skip plugin log noise)
133
+ for (let i = stdout.length - 1; i >= 0; i--) {
134
+ if (stdout[i] === '{') {
135
+ try {
136
+ const parsed = JSON.parse(stdout.slice(i));
137
+ return parsed.response ||
138
+ parsed.content ||
139
+ parsed.result?.payloads?.slice(-1)?.[0]?.text ||
140
+ parsed.result?.text ||
141
+ (typeof parsed.result === "string" ? parsed.result : null) ||
142
+ stdout;
143
+ } catch { /* keep searching */ }
144
+ }
145
+ }
146
+ return stdout;
147
+ }
148
+
149
+ if (result.stderr) {
150
+ log.warn(`[chorus] ${choir.name} CLI stderr: ${result.stderr.slice(0, 200)}`);
151
+ }
152
+ return "(no response)";
153
+ }
154
+
98
155
  // Build the prompt with context injected
99
156
  function buildPrompt(choir: Choir): string {
100
157
  let prompt = choir.prompt;
@@ -110,7 +167,7 @@ export function createChoirScheduler(
110
167
  return prompt;
111
168
  }
112
169
 
113
- // Execute a choir using openclaw agent CLI
170
+ // Execute a choir using the plugin API (fast, in-process) with CLI fallback
114
171
  async function executeChoir(choir: Choir): Promise<void> {
115
172
  const state = runState.get(choir.id) || { runCount: 0 };
116
173
  const startTime = Date.now();
@@ -127,49 +184,40 @@ export function createChoirScheduler(
127
184
 
128
185
  try {
129
186
  const prompt = buildPrompt(choir);
187
+ let output = "(no response)";
130
188
 
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) }); });
149
- });
189
+ // Prefer plugin API (in-process, no CLI spawn overhead)
190
+ if (typeof api.runAgentTurn === 'function') {
191
+ try {
192
+ const result: AgentTurnResult = await api.runAgentTurn({
193
+ sessionLabel: `chorus:${choir.id}`,
194
+ message: prompt,
195
+ isolated: true,
196
+ timeoutSeconds: 300,
197
+ });
150
198
 
151
- let output = "(no response)";
199
+ // Extract text from payloads — concatenate all payload texts
200
+ const payloadTexts = (result?.payloads || [])
201
+ .map((p: any) => p?.text || '')
202
+ .filter((t: string) => t.length > 0);
152
203
 
153
- if (result.status === 0 && result.stdout) {
154
- // Extract JSON from output (may have plugin logs before it)
155
- const stdout = result.stdout;
156
- const jsonStart = stdout.indexOf('{');
157
- if (jsonStart >= 0) {
158
- try {
159
- const parsed = JSON.parse(stdout.slice(jsonStart));
160
- output = parsed.response || parsed.content || stdout;
161
- } catch {
162
- output = stdout;
204
+ if (payloadTexts.length > 0) {
205
+ // Use the last substantive payload (earlier ones are often thinking-out-loud)
206
+ output = payloadTexts[payloadTexts.length - 1];
207
+ } else if (result?.text) {
208
+ output = result.text;
163
209
  }
164
- } else {
165
- output = stdout;
210
+ } catch (apiErr) {
211
+ log.warn(`[chorus] API runAgentTurn failed for ${choir.name}, falling back to CLI: ${apiErr}`);
212
+ output = await executeChoirViaCli(choir, prompt);
166
213
  }
167
- } else if (result.stderr) {
168
- log.warn(`[chorus] ${choir.name} stderr: ${result.stderr.slice(0, 200)}`);
214
+ } else {
215
+ // Fallback: spawn CLI process
216
+ output = await executeChoirViaCli(choir, prompt);
169
217
  }
170
218
 
171
219
  execution.durationMs = Date.now() - startTime;
172
- execution.success = result.status === 0;
220
+ execution.success = output !== "(no response)";
173
221
  execution.outputLength = output.length;
174
222
  execution.tokensUsed = estimateTokens(output);
175
223