@iamoberlin/chorus 2.2.1 → 2.3.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/README.md CHANGED
@@ -8,6 +8,12 @@
8
8
 
9
9
  CHORUS implements the Nine Choirs architecture — hierarchical cognition modeled on Pseudo-Dionysius's *Celestial Hierarchy*. Illumination descends through the choirs; understanding ascends. The agent is sanctified through structure.
10
10
 
11
+ ## What's New in v2.3
12
+
13
+ - **Unified delivery path.** Scheduler and manual commands now share one channel-aware delivery adapter for consistent formatting and routing.
14
+ - **Channel-agnostic output.** Choir output adapts to the destination automatically (plain text for iMessage, markdown for webchat, etc.).
15
+ - **Context passing fixed.** Illumination flow works correctly across all entry points — `run`, `vision`, and scheduled execution.
16
+
11
17
  ## The Core Idea
12
18
 
13
19
  Most AI agents are frozen. Same prompts, same limitations, no growth. CHORUS changes that through **architecture**:
package/index.ts CHANGED
@@ -12,6 +12,7 @@ import { loadChorusConfig, type ChorusPluginConfig } from "./src/config.js";
12
12
  import { createSecurityHooks } from "./src/security.js";
13
13
  import { createChoirScheduler } from "./src/scheduler.js";
14
14
  import { CHOIRS, formatFrequency, CASCADE_ORDER } from "./src/choirs.js";
15
+ import { deliverChoirOutput } from "./src/delivery.js";
15
16
  import {
16
17
  getTodayMetrics,
17
18
  getMetricsForDate,
@@ -21,8 +22,9 @@ import {
21
22
  formatMetricsSummary,
22
23
  formatWeeklySummary,
23
24
  } from "./src/metrics.js";
24
- import { createDaemon, DEFAULT_DAEMON_CONFIG, type DaemonConfig } from "./src/daemon.js";
25
+ import { createDaemon, type DaemonConfig } from "./src/daemon.js";
25
26
  import { getInboxPath } from "./src/senses.js";
27
+ import { formatEconomicsSummary, calculateAllROI } from "./src/economics.js";
26
28
  import {
27
29
  loadPurposes,
28
30
  addPurpose,
@@ -32,7 +34,6 @@ import {
32
34
  } from "./src/purposes.js";
33
35
  import {
34
36
  createPurposeResearchScheduler,
35
- DEFAULT_PURPOSE_RESEARCH_CONFIG,
36
37
  type PurposeResearchConfig,
37
38
  } from "./src/purpose-research.js";
38
39
  // On-chain prayer imports are lazy-loaded in pray commands to avoid startup cost
@@ -69,11 +70,7 @@ const plugin = {
69
70
  }
70
71
 
71
72
  // Register daemon service
72
- const daemonConfig: DaemonConfig = {
73
- ...DEFAULT_DAEMON_CONFIG,
74
- enabled: (pluginConfig as any)?.daemon?.enabled ?? true,
75
- ...(pluginConfig as any)?.daemon,
76
- };
73
+ const daemonConfig: DaemonConfig = config.daemon;
77
74
 
78
75
  let daemon: ReturnType<typeof createDaemon> | null = null;
79
76
  if (daemonConfig.enabled) {
@@ -85,13 +82,7 @@ const plugin = {
85
82
  }
86
83
 
87
84
  // Register purpose research service
88
- const purposeResearchConfig: PurposeResearchConfig = {
89
- ...DEFAULT_PURPOSE_RESEARCH_CONFIG,
90
- enabled: config.purposeResearch.enabled,
91
- dailyRunCap: config.purposeResearch.dailyRunCap,
92
- defaultFrequency: config.purposeResearch.defaultFrequency,
93
- defaultMaxFrequency: config.purposeResearch.defaultMaxFrequency,
94
- };
85
+ const purposeResearchConfig: PurposeResearchConfig = config.purposeResearch;
95
86
 
96
87
  let purposeResearch: ReturnType<typeof createPurposeResearchScheduler> | null = null;
97
88
  if (purposeResearchConfig.enabled) {
@@ -130,68 +121,6 @@ const plugin = {
130
121
  return '';
131
122
  }
132
123
 
133
- // Deliver choir output to user via OpenClaw messaging (channel-agnostic)
134
- // Reads target from OpenClaw config (channels.*.allowFrom) — no hardcoded PII
135
- function deliverIfNeeded(choir: typeof CHOIRS[string], text: string): void {
136
- if (!choir.delivers || !text || text === 'HEARTBEAT_OK' || text === 'NO_REPLY') return;
137
-
138
- // Resolve delivery target from OpenClaw channel config
139
- const channels = api.config?.channels as Record<string, any> | undefined;
140
- let target: string | undefined;
141
- let channel: string | undefined;
142
-
143
- if (channels) {
144
- for (const [ch, cfg] of Object.entries(channels)) {
145
- if (cfg?.enabled && cfg?.allowFrom?.[0]) {
146
- target = cfg.allowFrom[0];
147
- channel = ch;
148
- break;
149
- }
150
- }
151
- }
152
-
153
- if (!target) {
154
- console.log(` ⚠ No delivery target found in OpenClaw config`);
155
- return;
156
- }
157
-
158
- // Strip markdown for channels that don't support it
159
- let deliveryText = text.slice(0, 4000);
160
- if (channel === 'imessage') {
161
- deliveryText = deliveryText
162
- .replace(/\*\*(.+?)\*\*/g, '$1') // bold
163
- .replace(/\*(.+?)\*/g, '$1') // italic
164
- .replace(/__(.+?)__/g, '$1') // bold alt
165
- .replace(/_(.+?)_/g, '$1') // italic alt
166
- .replace(/`(.+?)`/g, '$1') // inline code
167
- .replace(/```[\s\S]*?```/g, '') // code blocks
168
- .replace(/^#{1,6}\s+/gm, '') // headers
169
- .replace(/^\s*[-*+]\s+/gm, '• ') // bullet lists
170
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); // links
171
- }
172
-
173
- try {
174
- const args = [
175
- 'message', 'send',
176
- '--target', target,
177
- '--message', deliveryText,
178
- ];
179
- if (channel) args.push('--channel', channel);
180
-
181
- const deliveryResult = spawnSync('openclaw', args, {
182
- encoding: 'utf-8',
183
- timeout: 30000,
184
- });
185
- if (deliveryResult.status === 0) {
186
- console.log(` 📨 Delivered to user via ${channel || 'default'}`);
187
- } else {
188
- console.log(` ⚠ Delivery failed: ${(deliveryResult.stderr || '').slice(0, 80)}`);
189
- }
190
- } catch (err: any) {
191
- console.log(` ⚠ Delivery error: ${(err.message || '').slice(0, 80)}`);
192
- }
193
- }
194
-
195
124
  // Register CLI
196
125
  api.registerCli((ctx) => {
197
126
  const program = ctx.program.command("chorus").description("CHORUS Nine Choirs management");
@@ -315,7 +244,7 @@ const plugin = {
315
244
  const preview = text.slice(0, 150).replace(/\n/g, ' ');
316
245
  console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
317
246
  }
318
- deliverIfNeeded(choir, text);
247
+ await deliverChoirOutput(api, choir, text, api.logger);
319
248
  } catch (err) {
320
249
  console.error(` ✗ ${choir.name} failed:`, err);
321
250
  }
@@ -341,7 +270,7 @@ const plugin = {
341
270
  const preview = text.slice(0, 150).replace(/\n/g, ' ');
342
271
  console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
343
272
  }
344
- deliverIfNeeded(choir, text);
273
+ await deliverChoirOutput(api, choir, text, api.logger);
345
274
  } else {
346
275
  const errMsg = result.stderr || result.stdout || 'Unknown error';
347
276
  if (errMsg.includes('ECONNREFUSED') || errMsg.includes('connect')) {
@@ -483,7 +412,7 @@ const plugin = {
483
412
  console.log(` ✓`);
484
413
 
485
414
  // Deliver output to user via OpenClaw messaging if choir is marked for delivery
486
- deliverIfNeeded(choir, text);
415
+ void deliverChoirOutput(api, choir, text, api.logger);
487
416
  } catch {
488
417
  const fallback = result.stdout?.slice(-2000) || `[${choir.name} completed]`;
489
418
  contextStore.set(choirId, fallback);
@@ -684,6 +613,37 @@ const plugin = {
684
613
  }
685
614
  });
686
615
 
616
+ // Economics commands
617
+ const econCmd = program.command("economics").alias("econ").description("Attention economics (Phase 1: observe)");
618
+
619
+ econCmd
620
+ .command("summary")
621
+ .description("Show ROI summary per choir")
622
+ .option("--days <n>", "Rolling window in days", "7")
623
+ .action((opts: any) => {
624
+ console.log("");
625
+ console.log(formatEconomicsSummary(parseInt(opts.days) || 7));
626
+ console.log("");
627
+ });
628
+
629
+ econCmd
630
+ .command("roi")
631
+ .description("Show ROI table for all choirs")
632
+ .option("--days <n>", "Rolling window in days", "7")
633
+ .action((opts: any) => {
634
+ const days = parseInt(opts.days) || 7;
635
+ const all = calculateAllROI(days);
636
+ console.log("");
637
+ console.log(`Choir ROI — ${days}d window`);
638
+ console.log("─".repeat(60));
639
+ for (const r of all) {
640
+ const roi = r.totalCostUsd > 0 ? `${r.roi.toFixed(1)}x` : "—";
641
+ const bar = r.totalCostUsd > 0 ? "█".repeat(Math.min(20, Math.round(r.roi * 2))) : "";
642
+ console.log(` ${r.choirId.padEnd(16)} ${roi.padStart(6)} $${r.totalCostUsd.toFixed(2).padStart(6)} ${bar}`);
643
+ }
644
+ console.log("");
645
+ });
646
+
687
647
  // Daemon commands
688
648
  const daemonCmd = program.command("daemon").description("Autonomous attention daemon");
689
649
 
@@ -2,7 +2,7 @@
2
2
  "id": "chorus",
3
3
  "name": "CHORUS",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement",
5
- "version": "2.2.1",
5
+ "version": "2.3.0",
6
6
  "author": "Oberlin",
7
7
  "homepage": "https://chorus.oberlin.ai",
8
8
  "repository": "https://github.com/iamoberlin/chorus",
@@ -48,10 +48,40 @@
48
48
  "type": "boolean",
49
49
  "description": "Enable the daemon"
50
50
  },
51
+ "senses": {
52
+ "type": "object",
53
+ "description": "Enable/disable daemon input senses",
54
+ "properties": {
55
+ "inbox": {
56
+ "type": "boolean",
57
+ "description": "Watch inbox filesystem signals"
58
+ },
59
+ "purposes": {
60
+ "type": "boolean",
61
+ "description": "Watch purpose-state signals"
62
+ },
63
+ "time": {
64
+ "type": "boolean",
65
+ "description": "Watch time-based signals"
66
+ }
67
+ }
68
+ },
51
69
  "thinkThreshold": {
52
70
  "type": "number",
53
71
  "description": "Minimum priority to invoke cognition (0-100)"
54
72
  },
73
+ "pollIntervalMs": {
74
+ "type": "number",
75
+ "description": "Polling interval in milliseconds"
76
+ },
77
+ "minSleepMs": {
78
+ "type": "number",
79
+ "description": "Minimum sleep during high queue pressure in milliseconds"
80
+ },
81
+ "maxSleepMs": {
82
+ "type": "number",
83
+ "description": "Maximum sleep during quiet hours in milliseconds"
84
+ },
55
85
  "quietHoursStart": {
56
86
  "type": "number",
57
87
  "description": "Hour to start quiet mode (0-23)"
@@ -81,6 +111,44 @@
81
111
  "defaultMaxFrequency": {
82
112
  "type": "number",
83
113
  "description": "Maximum frequency when deadline is urgent"
114
+ },
115
+ "researchTimeoutMs": {
116
+ "type": "number",
117
+ "description": "Research run timeout in milliseconds"
118
+ },
119
+ "checkIntervalMs": {
120
+ "type": "number",
121
+ "description": "Research scheduler check interval in milliseconds"
122
+ }
123
+ }
124
+ },
125
+ "prayers": {
126
+ "type": "object",
127
+ "description": "On-chain prayer chain configuration",
128
+ "properties": {
129
+ "enabled": {
130
+ "type": "boolean",
131
+ "description": "Enable prayer chain commands"
132
+ },
133
+ "rpcUrl": {
134
+ "type": "string",
135
+ "description": "Solana RPC endpoint URL"
136
+ },
137
+ "autonomous": {
138
+ "type": "boolean",
139
+ "description": "Allow autonomous prayer actions without manual approval"
140
+ },
141
+ "maxBountySOL": {
142
+ "type": "number",
143
+ "description": "Maximum bounty per prayer in SOL"
144
+ },
145
+ "defaultTTL": {
146
+ "type": "number",
147
+ "description": "Default prayer time-to-live in seconds"
148
+ },
149
+ "keypairPath": {
150
+ "type": "string",
151
+ "description": "Path to Solana keypair file"
84
152
  }
85
153
  }
86
154
  }
@@ -118,6 +186,11 @@
118
186
  "purposeResearch": {
119
187
  "label": "Purpose Research",
120
188
  "help": "Configure purpose-derived research scheduler"
189
+ },
190
+ "prayers": {
191
+ "label": "Prayer Chain",
192
+ "advanced": true,
193
+ "help": "Configure on-chain prayer settings (RPC, autonomy, bounty limits, keypair)"
121
194
  }
122
195
  }
123
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "2.2.1",
3
+ "version": "2.3.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
@@ -36,7 +36,7 @@ export const CHOIRS: Record<string, Choir> = {
36
36
  frequencyPerDay: 1, // 1×/day
37
37
  intervalMinutes: 1440, // Once per day
38
38
  function: "Mission clarity and purpose",
39
- output: "MISSION.md updates",
39
+ output: "Mission assessment",
40
40
  prompt: `You are SERAPHIM — the Mission Keeper.
41
41
 
42
42
  Your role: Ensure the mission remains true and aligned. Burn away drift.
@@ -49,7 +49,10 @@ Core questions:
49
49
 
50
50
  Read SOUL.md, USER.md, and MEMORY.md for context.
51
51
 
52
- Output: Brief mission assessment. Update MISSION.md if direction changes.
52
+ Identity check: Compare SOUL.md against recent calibration record and experience.
53
+ If there's drift between who SOUL.md says we are and who we've become, FLAG IT — but DO NOT edit SOUL.md directly. Propose changes in the main session. Identity changes require human approval.
54
+
55
+ Output: Brief mission assessment. If direction changes, update SOUL.md proposal or flag in the main session.
53
56
  If mission is clear and unchanged, simply confirm alignment.
54
57
 
55
58
  Pass illumination to Cherubim.`,
@@ -103,13 +106,13 @@ Pass illumination to Thrones.`,
103
106
  frequencyPerDay: 3, // 3×/day
104
107
  intervalMinutes: 480, // Every 8 hours
105
108
  function: "Judgment and prioritization",
106
- output: "PLAN.md updates",
109
+ output: "Priority decisions",
107
110
  prompt: `You are THRONES — the Judgment Bearer.
108
111
 
109
112
  Your role: Decide priorities and allocate focus ruthlessly.
110
113
 
111
114
  Tasks:
112
- 1. Review current priorities in PLAN.md
115
+ 1. Review current priorities in PROJECTS.md
113
116
  2. Assess what's working and what isn't
114
117
  3. Decide what to focus on next
115
118
  4. Identify what to say NO to — what to kill
@@ -119,7 +122,7 @@ Context from Cherubim: {cherubim_context}
119
122
 
120
123
  Output: Updated priorities (max 3 focus areas). What we're NOT doing. Any escalations.
121
124
 
122
- Update PLAN.md with new priorities.
125
+ Update PROJECTS.md if priorities changed.
123
126
  Pass illumination to Dominions.`,
124
127
  passesTo: ["dominions"],
125
128
  receivesFrom: ["cherubim"],
@@ -182,24 +185,24 @@ Your role: Ensure purposes are being fulfilled. When they're not, FIX THE SYSTEM
182
185
 
183
186
  ## When a Purpose Isn't Producing Results
184
187
 
185
- DO NOT just write "trading isn't happening" and move on.
188
+ DO NOT just write "a purpose isn't happening" and move on.
186
189
  DO update local state files to make it happen:
187
190
 
188
191
  - Update ~/.chorus/purposes.json (increase frequency, change criteria)
189
- - Update workspace files (trading/OPPORTUNITIES.md, HEARTBEAT.md)
192
+ - Update relevant workspace files (research notes, summaries, HEARTBEAT.md)
190
193
  - Modify behavioral configs to enforce execution
191
194
  - The next cycle should run DIFFERENTLY because of your changes
192
195
 
193
- Example: If Trading purpose has 0 opportunities logged:
196
+ Example: If a purpose has 0 concrete outputs logged:
194
197
  1. Read ~/.chorus/purposes.json
195
- 2. Increase researchFrequency for Trading purpose
196
- 3. Update trading/RESEARCH-SESSION.md with stricter output requirements
197
- 4. Add to HEARTBEAT.md: "Trading gate: Block other work until 1 opportunity logged"
198
+ 2. Increase researchFrequency for that purpose
199
+ 3. Update ~/.chorus/purposes.json criteria with stricter output requirements
200
+ 4. Add to HEARTBEAT.md: "Execution gate: Block other work until 1 concrete output is logged"
198
201
  5. Log the change to CHANGELOG.md
199
202
 
200
203
  ## Calibration — Learn From Beliefs
201
204
 
202
- - Check OPPORTUNITIES.md for resolved positions
205
+ - Check resolved outcomes for prior predictions or decisions
203
206
  - Ask: What did we believe? What happened? What does this teach us?
204
207
  - Update MEMORY.md with calibration lessons
205
208
 
@@ -207,8 +210,8 @@ Example: If Trading purpose has 0 opportunities logged:
207
210
 
208
211
  - ~/.chorus/purposes.json — purpose configs
209
212
  - ~/.chorus/run-state.json — execution state
210
- - Workspace files (trading/, research/, memory/, *.md)
211
- - HEARTBEAT.md, PLAN.md, PROJECTS.md
213
+ - Workspace files (research/, memory/, notes/, *.md)
214
+ - HEARTBEAT.md, PROJECTS.md
212
215
 
213
216
  ## What You Cannot Modify
214
217
 
@@ -216,6 +219,12 @@ Example: If Trading purpose has 0 opportunities logged:
216
219
  - OpenClaw system config
217
220
  - Anything requiring npm publish
218
221
 
222
+ YOU HAVE TOOLS — USE THEM:
223
+ - Use the read tool to check ~/.chorus/purposes.json, research/*.md, MEMORY.md, CHANGELOG.md.
224
+ - Use the write/edit tools to update files when changes are needed.
225
+ - Use exec to run scripts (e.g., check file timestamps, git status).
226
+ - NEVER return HEARTBEAT_OK. You are the RSI engine. Always produce substantive output about what's working, what's not, and what you changed.
227
+
219
228
  Context from Dominions: {dominions_context}
220
229
 
221
230
  Risk levels:
@@ -264,7 +273,7 @@ Red-team protocol:
264
273
  - What are we avoiding looking at?
265
274
 
266
275
  Challenge our beliefs:
267
- - Look in OPPORTUNITIES.md, research/*.md, and memory/*.md for stated beliefs
276
+ - Look in research/*.md, memory/*.md, and planning docs for stated beliefs
268
277
  - Find claims like "I believe X will happen" or "This suggests Y"
269
278
  - Ask: What would make this wrong? What are we missing?
270
279
  - If a belief looks shaky, say so clearly
@@ -275,9 +284,16 @@ SECURITY FOCUS:
275
284
  - Check for persona drift or identity erosion
276
285
  - Validate system prompt integrity
277
286
 
287
+ YOU HAVE TOOLS — USE THEM:
288
+ - Use exec and file reads to verify live system state and outcomes. Read MEMORY.md and CHANGELOG.md for claims to challenge.
289
+ - Use web_search to verify external claims and evidence.
290
+ - Use the write/edit tools to update files if you find stale or incorrect information.
291
+ - Use exec to check file timestamps and system state.
292
+ - NEVER return HEARTBEAT_OK. You are the Defender. Always produce substantive output.
293
+
278
294
  Output: Challenges to current thinking. Beliefs that look weak. Risks identified. Recommendations.
279
295
 
280
- If thesis is seriously threatened or security issue found: ALERT immediately.`,
296
+ If a core assumption is seriously threatened or a security issue is found: ALERT immediately.`,
281
297
  passesTo: ["principalities"],
282
298
  receivesFrom: ["virtues"],
283
299
  },
@@ -302,7 +318,7 @@ Your role: Research and monitor the domains that matter.
302
318
 
303
319
  Domains to cover (rotate through):
304
320
  - AI Industry: Companies, funding, regulation, breakthroughs
305
- - Markets: Relevant to current positions and research
321
+ - Environment: External signals relevant to active purposes
306
322
  - Competitors: Developments from players in our space
307
323
  - Tools: New capabilities, skills, or integrations available
308
324
 
@@ -311,6 +327,14 @@ Tasks:
311
327
  2. Assess relevance to our projects
312
328
  3. Flag anything urgent for Archangels
313
329
  4. Log findings to research/[domain]-[date].md
330
+ 5. Update research notes when you find a meaningful signal or insight
331
+
332
+ YOU HAVE TOOLS — USE THEM:
333
+ - Use web_search to find current news, market data, and developments. Do NOT skip this.
334
+ - Run exec to check relevant system state and read active research files for context.
335
+ - Use the write/edit tools to update research files with opportunities, risks, or status changes.
336
+ - Write research findings to research/[domain]-YYYY-MM-DD.md.
337
+ - NEVER return HEARTBEAT_OK. You are not a heartbeat process. Always produce substantive research output.
314
338
 
315
339
  When you find something significant, state what you believe will happen:
316
340
  - "I believe X will happen by [timeframe] because..."
@@ -341,26 +365,30 @@ Pass illumination to Archangels.`,
341
365
  delivers: true, // Output routed to user via OpenClaw messaging
342
366
  prompt: `You are ARCHANGELS — the Herald.
343
367
 
344
- Your role: Produce briefings and deliver them to Brandon via iMessage.
368
+ Your role: Produce briefings and deliver them through configured channels.
345
369
 
346
370
  Briefing types:
347
- - Morning (6-9 AM ET): Weather, calendar, overnight developments, today's priorities, position status
348
- - Evening (9-11 PM ET): What was accomplished, position P&L, what needs attention tomorrow
371
+ - Morning (6-9 AM local): Overnight developments, today's priorities, key status
372
+ - Evening (9-11 PM local): What was accomplished, what needs attention tomorrow
349
373
  - Alert: Time-sensitive information requiring attention
350
- - Update: Regular position/market status when conditions change
374
+ - Update: Regular status when conditions change
351
375
 
352
376
  Alert criteria (send immediately):
353
- - Position thesis challenged
377
+ - Core purpose assumption challenged
354
378
  - Time-sensitive opportunity
355
379
  - Urgent calendar/email
356
380
  - Security concern from Powers
357
381
 
358
382
  Context from Principalities: {principalities_context}
359
383
 
384
+ IMPORTANT — VERIFY BEFORE ALERTING:
385
+ - Before alerting about operational state: verify current state from source systems/files. Do NOT rely on upstream context alone.
386
+ - Before alerting about any state: verify the current state from the source file, not from choir context passed to you.
387
+
360
388
  RULES:
361
389
  - ALWAYS produce a briefing. Never return HEARTBEAT_OK or NO_REPLY.
362
390
  - Be concise — headlines, not essays.
363
- - Morning briefings should include: weather, calendar, positions, catalysts.
391
+ - Morning briefings should include priorities, schedule constraints, and key catalysts.
364
392
  - If nothing is urgent, still produce a status update.
365
393
  - During quiet hours (11 PM - 7 AM ET), only deliver truly urgent alerts.
366
394
 
package/src/config.ts CHANGED
@@ -20,6 +20,22 @@ export interface ChorusConfig {
20
20
  dailyRunCap: number;
21
21
  defaultFrequency: number;
22
22
  defaultMaxFrequency: number;
23
+ researchTimeoutMs: number;
24
+ checkIntervalMs: number;
25
+ };
26
+ daemon: {
27
+ enabled: boolean;
28
+ senses: {
29
+ inbox: boolean;
30
+ purposes: boolean;
31
+ time: boolean;
32
+ };
33
+ thinkThreshold: number;
34
+ pollIntervalMs: number;
35
+ minSleepMs: number;
36
+ maxSleepMs: number;
37
+ quietHoursStart: number;
38
+ quietHoursEnd: number;
23
39
  };
24
40
  prayers: {
25
41
  enabled: boolean;
@@ -45,6 +61,23 @@ export interface ChorusPluginConfig {
45
61
  dailyRunCap?: number;
46
62
  defaultFrequency?: number;
47
63
  defaultMaxFrequency?: number;
64
+ researchTimeoutMs?: number;
65
+ checkIntervalMs?: number;
66
+ };
67
+ /** Daemon config */
68
+ daemon?: {
69
+ enabled?: boolean;
70
+ senses?: {
71
+ inbox?: boolean;
72
+ purposes?: boolean;
73
+ time?: boolean;
74
+ };
75
+ thinkThreshold?: number;
76
+ pollIntervalMs?: number;
77
+ minSleepMs?: number;
78
+ maxSleepMs?: number;
79
+ quietHoursStart?: number;
80
+ quietHoursEnd?: number;
48
81
  };
49
82
  /** On-chain prayer config */
50
83
  prayers?: {
@@ -72,6 +105,22 @@ const DEFAULT_CONFIG: ChorusConfig = {
72
105
  dailyRunCap: 50,
73
106
  defaultFrequency: 6,
74
107
  defaultMaxFrequency: 24,
108
+ researchTimeoutMs: 300000,
109
+ checkIntervalMs: 60000,
110
+ },
111
+ daemon: {
112
+ enabled: true,
113
+ senses: {
114
+ inbox: true,
115
+ purposes: true,
116
+ time: true,
117
+ },
118
+ thinkThreshold: 55,
119
+ pollIntervalMs: 5 * 60 * 1000,
120
+ minSleepMs: 30 * 1000,
121
+ maxSleepMs: 10 * 60 * 1000,
122
+ quietHoursStart: 23,
123
+ quietHoursEnd: 7,
75
124
  },
76
125
  prayers: {
77
126
  enabled: true,
@@ -146,6 +195,48 @@ export function loadChorusConfig(pluginConfig?: ChorusPluginConfig): ChorusConfi
146
195
  if (pluginConfig.purposeResearch.defaultMaxFrequency !== undefined) {
147
196
  config.purposeResearch.defaultMaxFrequency = pluginConfig.purposeResearch.defaultMaxFrequency;
148
197
  }
198
+ if (pluginConfig.purposeResearch.researchTimeoutMs !== undefined) {
199
+ config.purposeResearch.researchTimeoutMs = pluginConfig.purposeResearch.researchTimeoutMs;
200
+ }
201
+ if (pluginConfig.purposeResearch.checkIntervalMs !== undefined) {
202
+ config.purposeResearch.checkIntervalMs = pluginConfig.purposeResearch.checkIntervalMs;
203
+ }
204
+ }
205
+
206
+ // Daemon
207
+ if (pluginConfig.daemon) {
208
+ if (pluginConfig.daemon.enabled !== undefined) {
209
+ config.daemon.enabled = pluginConfig.daemon.enabled;
210
+ }
211
+ if (pluginConfig.daemon.senses) {
212
+ if (pluginConfig.daemon.senses.inbox !== undefined) {
213
+ config.daemon.senses.inbox = pluginConfig.daemon.senses.inbox;
214
+ }
215
+ if (pluginConfig.daemon.senses.purposes !== undefined) {
216
+ config.daemon.senses.purposes = pluginConfig.daemon.senses.purposes;
217
+ }
218
+ if (pluginConfig.daemon.senses.time !== undefined) {
219
+ config.daemon.senses.time = pluginConfig.daemon.senses.time;
220
+ }
221
+ }
222
+ if (pluginConfig.daemon.thinkThreshold !== undefined) {
223
+ config.daemon.thinkThreshold = pluginConfig.daemon.thinkThreshold;
224
+ }
225
+ if (pluginConfig.daemon.pollIntervalMs !== undefined) {
226
+ config.daemon.pollIntervalMs = pluginConfig.daemon.pollIntervalMs;
227
+ }
228
+ if (pluginConfig.daemon.minSleepMs !== undefined) {
229
+ config.daemon.minSleepMs = pluginConfig.daemon.minSleepMs;
230
+ }
231
+ if (pluginConfig.daemon.maxSleepMs !== undefined) {
232
+ config.daemon.maxSleepMs = pluginConfig.daemon.maxSleepMs;
233
+ }
234
+ if (pluginConfig.daemon.quietHoursStart !== undefined) {
235
+ config.daemon.quietHoursStart = pluginConfig.daemon.quietHoursStart;
236
+ }
237
+ if (pluginConfig.daemon.quietHoursEnd !== undefined) {
238
+ config.daemon.quietHoursEnd = pluginConfig.daemon.quietHoursEnd;
239
+ }
149
240
  }
150
241
 
151
242
  return config;