@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 +4 -1
- package/package.json +1 -1
- package/src/choirs.ts +12 -8
- package/src/scheduler.ts +84 -36
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
|
|
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
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:
|
|
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
|
-
|
|
358
|
-
-
|
|
359
|
-
-
|
|
360
|
-
-
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
}
|
|
165
|
-
|
|
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
|
|
168
|
-
|
|
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 =
|
|
220
|
+
execution.success = output !== "(no response)";
|
|
173
221
|
execution.outputLength = output.length;
|
|
174
222
|
execution.tokensUsed = estimateTokens(output);
|
|
175
223
|
|