clementine-agent 1.0.10 → 1.0.12
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/dist/agent/assistant.js +22 -26
- package/dist/agent/daily-planner.js +5 -15
- package/dist/agent/insight-engine.js +4 -14
- package/dist/agent/prompt-evolver.js +7 -15
- package/dist/agent/self-improve.js +20 -26
- package/dist/agent/strategic-planner.js +5 -26
- package/dist/agent/team-bus.js +5 -14
- package/dist/channels/discord-utils.js +25 -8
- package/dist/cli/dashboard.js +2 -3
- package/dist/cli/routes/digest.d.ts +0 -1
- package/dist/cli/routes/digest.js +6 -11
- package/dist/cli/routes/goals.d.ts +5 -2
- package/dist/cli/routes/goals.js +85 -74
- package/dist/gateway/cron-scheduler.js +31 -20
- package/dist/gateway/heartbeat-scheduler.d.ts +3 -5
- package/dist/gateway/heartbeat-scheduler.js +18 -17
- package/dist/gateway/router.d.ts +5 -2
- package/dist/gateway/router.js +59 -10
- package/dist/tools/goal-tools.d.ts +4 -2
- package/dist/tools/goal-tools.js +46 -58
- package/dist/tools/session-tools.js +14 -21
- package/dist/tools/shared.d.ts +40 -0
- package/dist/tools/shared.js +84 -0
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -13,11 +13,11 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { query as rawQuery, listSubagents, getSubagentMessages, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, PROFILES_DIR, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE,
|
|
16
|
+
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, PROFILES_DIR, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, ENABLE_1M_CONTEXT, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, } from '../config.js';
|
|
17
17
|
import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
|
|
18
18
|
import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, setProfileTier, setProfileAllowedTools, setAgentDir, setSendPolicy, setInteractionSource, } from './hooks.js';
|
|
19
19
|
import { scanner } from '../security/scanner.js';
|
|
20
|
-
import { agentWorkingMemoryFile } from '../tools/shared.js';
|
|
20
|
+
import { agentWorkingMemoryFile, listAllGoals } from '../tools/shared.js';
|
|
21
21
|
import { AgentManager } from './agent-manager.js';
|
|
22
22
|
import { extractLinks } from './link-extractor.js';
|
|
23
23
|
import { StallGuard } from './stall-guard.js';
|
|
@@ -1491,17 +1491,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1491
1491
|
}
|
|
1492
1492
|
const goals = [];
|
|
1493
1493
|
try {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
try {
|
|
1498
|
-
const goal = JSON.parse(fs.readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
1499
|
-
if (goal.status === 'active')
|
|
1500
|
-
goals.push({ goal, file: f });
|
|
1501
|
-
}
|
|
1502
|
-
catch {
|
|
1503
|
-
continue;
|
|
1504
|
-
}
|
|
1494
|
+
for (const { goal, filePath } of listAllGoals()) {
|
|
1495
|
+
if (goal.status === 'active')
|
|
1496
|
+
goals.push({ goal, file: path.basename(filePath) });
|
|
1505
1497
|
}
|
|
1506
1498
|
}
|
|
1507
1499
|
catch { /* non-fatal */ }
|
|
@@ -2041,11 +2033,21 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
2041
2033
|
catch (e) {
|
|
2042
2034
|
const errStr = String(e).toLowerCase();
|
|
2043
2035
|
if (errStr.includes('abort') || errStr.includes('cancel')) {
|
|
2044
|
-
// Query was aborted.
|
|
2045
|
-
//
|
|
2036
|
+
// Query was aborted. Four sources: timeout, user cancel, StallGuard
|
|
2037
|
+
// tripped (runaway loop), or interrupted by a new user message.
|
|
2046
2038
|
const stallAbort = !!stallGuard?.isBreakerActive();
|
|
2047
|
-
|
|
2048
|
-
|
|
2039
|
+
const abortReason = abortController?.signal.reason;
|
|
2040
|
+
const interruptAbort = abortReason === 'interrupted-by-new-message';
|
|
2041
|
+
logger.warn({ sessionKey, stallAbort, interruptAbort }, 'Chat query aborted');
|
|
2042
|
+
if (interruptAbort) {
|
|
2043
|
+
// New message came in — let the next query answer. Just mark
|
|
2044
|
+
// the partial response so the user knows this one was cut off.
|
|
2045
|
+
// (The next handleMessage call will fold this partial into its prompt.)
|
|
2046
|
+
responseText = responseText
|
|
2047
|
+
? responseText + '\n\n*(interrupted — answering your new message…)*'
|
|
2048
|
+
: '*(interrupted — switching to your new message…)*';
|
|
2049
|
+
}
|
|
2050
|
+
else if (stallAbort) {
|
|
2049
2051
|
const reason = stallGuard?.getBreakerReason() ?? 'runaway loop';
|
|
2050
2052
|
const stallMsg = `I got stuck in a loop — ${reason} ` +
|
|
2051
2053
|
`I stopped to save budget. Options:\n` +
|
|
@@ -3029,15 +3031,9 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3029
3031
|
// ── Goal context: inject linked goal info ───────────────────────
|
|
3030
3032
|
let goalContext = '';
|
|
3031
3033
|
try {
|
|
3032
|
-
|
|
3033
|
-
const
|
|
3034
|
-
|
|
3035
|
-
.map(f => { try {
|
|
3036
|
-
return JSON.parse(fs.readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
3037
|
-
}
|
|
3038
|
-
catch {
|
|
3039
|
-
return null;
|
|
3040
|
-
} })
|
|
3034
|
+
{
|
|
3035
|
+
const linkedGoals = listAllGoals()
|
|
3036
|
+
.map(({ goal }) => goal)
|
|
3041
3037
|
.filter(g => g && g.status === 'active' && g.linkedCronJobs?.includes(jobName));
|
|
3042
3038
|
if (linkedGoals.length > 0) {
|
|
3043
3039
|
const goalLines = linkedGoals.map((g) => {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import pino from 'pino';
|
|
11
|
-
import { BASE_DIR,
|
|
11
|
+
import { BASE_DIR, CRON_REFLECTIONS_DIR, TASKS_FILE, INBOX_DIR, MODELS, } from '../config.js';
|
|
12
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
12
13
|
const logger = pino({ name: 'clementine.daily-planner' });
|
|
13
14
|
const PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
|
|
14
15
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
@@ -95,21 +96,10 @@ export class DailyPlanner {
|
|
|
95
96
|
return sections.join('\n\n') || 'No context available — all clear.';
|
|
96
97
|
}
|
|
97
98
|
loadActiveGoals() {
|
|
98
|
-
if (!existsSync(GOALS_DIR))
|
|
99
|
-
return [];
|
|
100
99
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const goal = JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
106
|
-
if (goal.status === 'active')
|
|
107
|
-
goals.push(goal);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
100
|
+
const goals = listAllGoals()
|
|
101
|
+
.map(({ goal }) => goal)
|
|
102
|
+
.filter(g => g && g.status === 'active');
|
|
113
103
|
return goals.sort((a, b) => {
|
|
114
104
|
const p = { high: 0, medium: 1, low: 2 };
|
|
115
105
|
return (p[a.priority] ?? 2) - (p[b.priority] ?? 2);
|
|
@@ -12,6 +12,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import pino from 'pino';
|
|
14
14
|
import { GOALS_DIR, BASE_DIR } from '../config.js';
|
|
15
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
15
16
|
const logger = pino({ name: 'clementine.insight-engine' });
|
|
16
17
|
const BASE_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
17
18
|
const MAX_DAILY_INSIGHTS = 3;
|
|
@@ -133,20 +134,9 @@ export function gatherInsightSignals(gateway) {
|
|
|
133
134
|
if (sessionCount === 0) {
|
|
134
135
|
// No recent activity — could note quiet period if there are pending goals
|
|
135
136
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const g = JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
141
|
-
return g.status === 'active' && g.priority === 'high';
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
if (activeHighPriority.length > 0) {
|
|
148
|
-
signals.push(`Quiet period: ${activeHighPriority.length} high-priority goal(s) active but no recent user interaction`);
|
|
149
|
-
}
|
|
137
|
+
const activeHighPriority = listAllGoals().filter(({ goal }) => goal.status === 'active' && goal.priority === 'high');
|
|
138
|
+
if (activeHighPriority.length > 0) {
|
|
139
|
+
signals.push(`Quiet period: ${activeHighPriority.length} high-priority goal(s) active but no recent user interaction`);
|
|
150
140
|
}
|
|
151
141
|
}
|
|
152
142
|
catch { /* non-fatal */ }
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* reflections, progress state, and goal context. Returns the enrichment
|
|
6
6
|
* string to be appended to the original prompt.
|
|
7
7
|
*/
|
|
8
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import pino from 'pino';
|
|
11
|
-
import { BASE_DIR, CRON_REFLECTIONS_DIR, CRON_PROGRESS_DIR
|
|
11
|
+
import { BASE_DIR, CRON_REFLECTIONS_DIR, CRON_PROGRESS_DIR } from '../config.js';
|
|
12
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
12
13
|
const logger = pino({ name: 'clementine.prompt-evolver' });
|
|
13
14
|
/**
|
|
14
15
|
* Evolve a static cron prompt by enriching it with lessons from reflections,
|
|
@@ -225,22 +226,13 @@ function extractProgressInsights(jobName) {
|
|
|
225
226
|
* Find goals that reference this cron job and inject alignment guidance.
|
|
226
227
|
*/
|
|
227
228
|
function extractGoalGuidance(jobName) {
|
|
228
|
-
if (!existsSync(GOALS_DIR))
|
|
229
|
-
return null;
|
|
230
229
|
try {
|
|
231
|
-
const files = readdirSync(GOALS_DIR).filter(f => f.endsWith('.json'));
|
|
232
230
|
const linkedGoals = [];
|
|
233
|
-
for (const
|
|
234
|
-
|
|
235
|
-
const goal = JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
236
|
-
if (goal.status !== 'active')
|
|
237
|
-
continue;
|
|
238
|
-
if (goal.linkedCronJobs?.includes(jobName)) {
|
|
239
|
-
linkedGoals.push(goal);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
231
|
+
for (const { goal } of listAllGoals()) {
|
|
232
|
+
if (goal.status !== 'active')
|
|
243
233
|
continue;
|
|
234
|
+
if (goal.linkedCronJobs?.includes(jobName)) {
|
|
235
|
+
linkedGoals.push(goal);
|
|
244
236
|
}
|
|
245
237
|
}
|
|
246
238
|
if (linkedGoals.length === 0)
|
|
@@ -14,6 +14,7 @@ import matter from 'gray-matter';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import pino from 'pino';
|
|
16
16
|
import { BASE_DIR, SELF_IMPROVE_DIR, SOUL_FILE, AGENTS_FILE, CRON_FILE, WORKFLOWS_DIR, VAULT_DIR, MEMORY_DB_PATH, AGENTS_DIR, PKG_DIR, CRON_REFLECTIONS_DIR, GOALS_DIR, } from '../config.js';
|
|
17
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
17
18
|
const logger = pino({ name: 'clementine.self-improve' });
|
|
18
19
|
// ── Defaults ─────────────────────────────────────────────────────────
|
|
19
20
|
const DEFAULT_CONFIG = {
|
|
@@ -481,34 +482,27 @@ export class SelfImproveLoop {
|
|
|
481
482
|
}
|
|
482
483
|
}
|
|
483
484
|
catch { /* non-fatal */ }
|
|
484
|
-
// Gather goal health data
|
|
485
|
+
// Gather goal health data (walks global + per-agent goals dirs)
|
|
485
486
|
const goalHealth = [];
|
|
486
487
|
try {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
isStale: goal.status === 'active' && daysSinceUpdate > staleThreshold,
|
|
506
|
-
linkedCronJobs: goal.linkedCronJobs || [],
|
|
507
|
-
progressCount: goal.progressNotes?.length ?? 0,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
catch { /* skip malformed */ }
|
|
511
|
-
}
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
const DAY_MS = 86_400_000;
|
|
490
|
+
for (const { goal, owner } of listAllGoals()) {
|
|
491
|
+
const lastUpdate = goal.updatedAt ? new Date(goal.updatedAt).getTime() : 0;
|
|
492
|
+
const daysSinceUpdate = Math.floor((now - lastUpdate) / DAY_MS);
|
|
493
|
+
const staleThreshold = goal.reviewFrequency === 'daily' ? 1 : goal.reviewFrequency === 'weekly' ? 7 : 30;
|
|
494
|
+
goalHealth.push({
|
|
495
|
+
id: goal.id,
|
|
496
|
+
title: goal.title,
|
|
497
|
+
status: goal.status,
|
|
498
|
+
owner: owner,
|
|
499
|
+
priority: goal.priority,
|
|
500
|
+
daysSinceUpdate,
|
|
501
|
+
reviewFrequency: goal.reviewFrequency,
|
|
502
|
+
isStale: goal.status === 'active' && daysSinceUpdate > staleThreshold,
|
|
503
|
+
linkedCronJobs: goal.linkedCronJobs || [],
|
|
504
|
+
progressCount: goal.progressNotes?.length ?? 0,
|
|
505
|
+
});
|
|
512
506
|
}
|
|
513
507
|
}
|
|
514
508
|
catch { /* non-fatal */ }
|
|
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import pino from 'pino';
|
|
15
15
|
import { BASE_DIR, GOALS_DIR, MODELS } from '../config.js';
|
|
16
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
16
17
|
const logger = pino({ name: 'clementine.strategic-planner' });
|
|
17
18
|
const DAILY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
|
|
18
19
|
const WEEKLY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'weekly');
|
|
@@ -265,34 +266,12 @@ export class StrategicPlanner {
|
|
|
265
266
|
}).filter(Boolean);
|
|
266
267
|
}
|
|
267
268
|
loadActiveGoals() {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.filter(f => f.endsWith('.json'))
|
|
272
|
-
.map(f => {
|
|
273
|
-
try {
|
|
274
|
-
return JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
})
|
|
280
|
-
.filter((g) => g && g.status === 'active');
|
|
269
|
+
return listAllGoals()
|
|
270
|
+
.map(({ goal }) => goal)
|
|
271
|
+
.filter(g => g && g.status === 'active');
|
|
281
272
|
}
|
|
282
273
|
loadAllGoals() {
|
|
283
|
-
|
|
284
|
-
return [];
|
|
285
|
-
return readdirSync(GOALS_DIR)
|
|
286
|
-
.filter(f => f.endsWith('.json'))
|
|
287
|
-
.map(f => {
|
|
288
|
-
try {
|
|
289
|
-
return JSON.parse(readFileSync(path.join(GOALS_DIR, f), 'utf-8'));
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
})
|
|
295
|
-
.filter(Boolean);
|
|
274
|
+
return listAllGoals().map(({ goal }) => goal);
|
|
296
275
|
}
|
|
297
276
|
}
|
|
298
277
|
// ── Date helpers ───────────────────────────────────────────────────────
|
package/dist/agent/team-bus.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Logs to JSONL and optionally mirrors to a Discord channel.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash, randomBytes } from 'node:crypto';
|
|
8
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync,
|
|
9
|
-
import os from 'node:os';
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
9
|
import path from 'node:path';
|
|
11
10
|
import pino from 'pino';
|
|
11
|
+
import { listAllGoals } from '../tools/shared.js';
|
|
12
12
|
const logger = pino({ name: 'clementine.team-bus' });
|
|
13
13
|
/** Max inter-agent message depth before rejection (anti-loop). */
|
|
14
14
|
const MAX_DEPTH = 3;
|
|
@@ -247,19 +247,10 @@ export class TeamBus {
|
|
|
247
247
|
}
|
|
248
248
|
/** Broadcast a message to all team agents (optionally scoped to a goal). */
|
|
249
249
|
async broadcast(fromSlug, content, goalId, _sessionKey) {
|
|
250
|
-
const goalsDir = path.join(process.env.CLEMENTINE_HOME || path.join(os.homedir(), '.clementine'), 'goals');
|
|
251
250
|
let targetSlugs = [];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const goal = JSON.parse(readFileSync(path.join(goalsDir, f), 'utf-8'));
|
|
256
|
-
if (goal.id === goalId && goal.owner && goal.owner !== fromSlug) {
|
|
257
|
-
targetSlugs.push(goal.owner);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
251
|
+
for (const { goal, owner } of listAllGoals()) {
|
|
252
|
+
if (goal.id === goalId && owner && owner !== fromSlug) {
|
|
253
|
+
targetSlugs.push(owner);
|
|
263
254
|
}
|
|
264
255
|
}
|
|
265
256
|
const allAgents = this.teamRouter.listTeamAgents();
|
|
@@ -161,20 +161,37 @@ export class DiscordStreamingMessage {
|
|
|
161
161
|
this.progressTimer = null;
|
|
162
162
|
}
|
|
163
163
|
if (!text)
|
|
164
|
-
text =
|
|
164
|
+
text = "*(I didn't have anything to respond with — try rephrasing or giving me more context.)*";
|
|
165
165
|
text = sanitizeResponse(text);
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
try {
|
|
167
|
+
if (this.message) {
|
|
168
|
+
if (text.length <= 1900) {
|
|
169
|
+
await this.message.edit(text);
|
|
170
|
+
this.messageId = this.message.id;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
await this.message.delete().catch(() => { });
|
|
174
|
+
await sendChunked(this.channel, text);
|
|
175
|
+
}
|
|
170
176
|
}
|
|
171
177
|
else {
|
|
172
|
-
await this.message.delete().catch(() => { });
|
|
173
178
|
await sendChunked(this.channel, text);
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
catch (err) {
|
|
182
|
+
// Delivery failed after the agent already generated a response.
|
|
183
|
+
// Log loudly + persist the response text to the daily note so it isn't
|
|
184
|
+
// lost silently. Don't re-throw — the callers don't have try/catch
|
|
185
|
+
// around finalize() and we don't want to introduce crashes.
|
|
186
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
187
|
+
try {
|
|
188
|
+
const pino = (await import('pino')).default;
|
|
189
|
+
pino({ name: 'clementine.discord' }).warn({ err: errMsg, channelId: this.channel.id }, 'Discord delivery failed — response text saved to daily note');
|
|
190
|
+
const { logToDailyNote } = await import('../gateway/cron-scheduler.js');
|
|
191
|
+
const preview = text.slice(0, 1500);
|
|
192
|
+
logToDailyNote(`**[Discord delivery failed]** Channel \`${this.channel.id ?? 'unknown'}\` — response was:\n\n${preview}`);
|
|
193
|
+
}
|
|
194
|
+
catch { /* best-effort */ }
|
|
178
195
|
}
|
|
179
196
|
}
|
|
180
197
|
/** Format elapsed milliseconds as human-readable duration. */
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5684,15 +5684,14 @@ export async function cmdDashboard(opts) {
|
|
|
5684
5684
|
res.json(analytics);
|
|
5685
5685
|
});
|
|
5686
5686
|
// ── Goals, Delegations, Workflows, Digest — extracted routers ──
|
|
5687
|
-
const GOALS_DIR = path.join(BASE_DIR, 'goals');
|
|
5688
5687
|
const CRON_RUNS_DIR = path.join(BASE_DIR, 'cron', 'runs');
|
|
5689
5688
|
const AGENTS_BASE = path.join(VAULT_DIR, '00-System', 'agents');
|
|
5690
5689
|
const WORKFLOWS_DIR = path.join(VAULT_DIR, '00-System', 'workflows');
|
|
5691
5690
|
const WORKFLOW_RUNS_DIR = path.join(BASE_DIR, 'workflows', 'runs');
|
|
5692
|
-
app.use('/api/goals', goalsRouter({
|
|
5691
|
+
app.use('/api/goals', goalsRouter({ cronRunsDir: CRON_RUNS_DIR, vaultDir: VAULT_DIR, cronFile: CRON_FILE, getGateway }));
|
|
5693
5692
|
app.use('/api/delegations', delegationsRouter({ agentsBase: AGENTS_BASE, getGateway, broadcastEvent }));
|
|
5694
5693
|
app.use('/api/workflows', workflowsRouter({ workflowsDir: WORKFLOWS_DIR, workflowRunsDir: WORKFLOW_RUNS_DIR, agentsBase: AGENTS_BASE, getGateway, broadcastEvent, cachedAsync }));
|
|
5695
|
-
app.use('/api/digest', digestRouter({ baseDir: BASE_DIR, vaultDir: VAULT_DIR,
|
|
5694
|
+
app.use('/api/digest', digestRouter({ baseDir: BASE_DIR, vaultDir: VAULT_DIR, memoryDbPath: MEMORY_DB_PATH, parseEnvFile, getGateway, cached }));
|
|
5696
5695
|
// Voice audio route (served from digest router but needs top-level mount for /api/voice/ path)
|
|
5697
5696
|
app.get('/api/voice/audio/:hash', (req, res) => {
|
|
5698
5697
|
const hash = req.params.hash.replace(/[^a-f0-9]/g, '');
|
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, } from
|
|
|
7
7
|
import { randomBytes } from 'node:crypto';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import matter from 'gray-matter';
|
|
10
|
+
import { listAllGoals } from '../../tools/shared.js';
|
|
10
11
|
export function getDigestPrefs(prefsFile) {
|
|
11
12
|
const defaults = {
|
|
12
13
|
enabled: false,
|
|
@@ -27,7 +28,7 @@ export function getDigestPrefs(prefsFile) {
|
|
|
27
28
|
}
|
|
28
29
|
export function digestRouter(deps) {
|
|
29
30
|
const router = Router();
|
|
30
|
-
const { baseDir, vaultDir,
|
|
31
|
+
const { baseDir, vaultDir, memoryDbPath, parseEnvFile, getGateway, cached } = deps;
|
|
31
32
|
const prefsFile = path.join(baseDir, 'digest-preferences.json');
|
|
32
33
|
const voiceCacheDir = path.join(baseDir, 'cache', 'voice');
|
|
33
34
|
// Graph API helper for email
|
|
@@ -101,16 +102,10 @@ export function digestRouter(deps) {
|
|
|
101
102
|
}
|
|
102
103
|
if (secs.goals !== false) {
|
|
103
104
|
try {
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return null;
|
|
111
|
-
} }).filter(Boolean);
|
|
112
|
-
const active = goals.filter((g) => g.status === 'active');
|
|
113
|
-
const blocked = goals.filter((g) => g.status === 'blocked');
|
|
105
|
+
{
|
|
106
|
+
const goals = listAllGoals().map(e => e.goal);
|
|
107
|
+
const active = goals.filter(g => g.status === 'active');
|
|
108
|
+
const blocked = goals.filter(g => g.status === 'blocked');
|
|
114
109
|
let goalText = `${active.length} active, ${blocked.length} blocked\n`;
|
|
115
110
|
active.slice(0, 5).forEach((g) => {
|
|
116
111
|
goalText += ` - ${g.title} [${g.priority}]`;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Goals API routes — extracted from dashboard.ts
|
|
2
|
+
* Goals API routes — extracted from dashboard.ts.
|
|
3
|
+
*
|
|
4
|
+
* Goals live per-owner: Clementine's at ~/.clementine/goals/, each agent's at
|
|
5
|
+
* ~/.clementine/vault/00-System/agents/{slug}/goals/. Uses the goal-store
|
|
6
|
+
* helpers in tools/shared.ts so the dashboard doesn't need to know the layout.
|
|
3
7
|
*/
|
|
4
8
|
import { Router } from 'express';
|
|
5
9
|
import type { Gateway } from '../../gateway/router.js';
|
|
6
10
|
export interface GoalsRouterDeps {
|
|
7
|
-
goalsDir: string;
|
|
8
11
|
cronRunsDir: string;
|
|
9
12
|
vaultDir: string;
|
|
10
13
|
cronFile: string;
|