clementine-agent 1.0.13 → 1.0.15
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 +32 -2
- package/dist/agent/self-improve.js +23 -0
- package/dist/agent/skill-extractor.d.ts +10 -0
- package/dist/agent/skill-extractor.js +61 -0
- package/dist/channels/discord-agent-bot.d.ts +4 -0
- package/dist/channels/discord-agent-bot.js +35 -0
- package/dist/channels/discord-bot-manager.d.ts +4 -0
- package/dist/channels/discord-bot-manager.js +16 -0
- package/dist/channels/discord.js +141 -0
- package/dist/channels/slack.js +51 -1
- package/dist/channels/telegram.js +28 -1
- package/dist/cli/dashboard.js +299 -5
- package/dist/gateway/cron-scheduler.d.ts +5 -0
- package/dist/gateway/cron-scheduler.js +32 -5
- package/dist/gateway/failure-monitor.d.ts +40 -0
- package/dist/gateway/failure-monitor.js +416 -0
- package/dist/gateway/fix-verification.d.ts +39 -0
- package/dist/gateway/fix-verification.js +144 -0
- package/dist/gateway/heartbeat-scheduler.js +61 -4
- package/dist/gateway/notifications.js +26 -1
- package/dist/gateway/router.js +2 -2
- package/dist/memory/store.d.ts +20 -0
- package/dist/memory/store.js +64 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -10,6 +10,20 @@ import { DeliveryQueue } from './delivery-queue.js';
|
|
|
10
10
|
const logger = pino({ name: 'clementine.notifications' });
|
|
11
11
|
/** Safety cap — prevent runaway messages, but each channel handles its own chunking/limits. */
|
|
12
12
|
const MAX_MESSAGE_LENGTH = 8000;
|
|
13
|
+
/** Map a sessionKey prefix to the registered channel name that owns it. */
|
|
14
|
+
function channelForSessionKey(sessionKey) {
|
|
15
|
+
if (sessionKey.startsWith('discord:'))
|
|
16
|
+
return 'discord';
|
|
17
|
+
if (sessionKey.startsWith('slack:'))
|
|
18
|
+
return 'slack';
|
|
19
|
+
if (sessionKey.startsWith('telegram:'))
|
|
20
|
+
return 'telegram';
|
|
21
|
+
if (sessionKey.startsWith('whatsapp:'))
|
|
22
|
+
return 'whatsapp';
|
|
23
|
+
if (sessionKey.startsWith('dashboard:'))
|
|
24
|
+
return 'dashboard';
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
13
27
|
export class NotificationDispatcher {
|
|
14
28
|
senders = new Map();
|
|
15
29
|
_retryQueue;
|
|
@@ -52,9 +66,20 @@ export class NotificationDispatcher {
|
|
|
52
66
|
const capped = text.length > MAX_MESSAGE_LENGTH
|
|
53
67
|
? text.slice(0, MAX_MESSAGE_LENGTH - 20) + '\n\n_(truncated)_'
|
|
54
68
|
: text;
|
|
69
|
+
// If sessionKey is set, route only to the channel that owns it.
|
|
70
|
+
// Fan out to all channels only when no originating channel is known.
|
|
71
|
+
const targetChannel = context?.sessionKey ? channelForSessionKey(context.sessionKey) : null;
|
|
72
|
+
const scopedSenders = [];
|
|
73
|
+
if (targetChannel && this.senders.has(targetChannel)) {
|
|
74
|
+
scopedSenders.push([targetChannel, this.senders.get(targetChannel)]);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
for (const entry of this.senders)
|
|
78
|
+
scopedSenders.push(entry);
|
|
79
|
+
}
|
|
55
80
|
const channelErrors = {};
|
|
56
81
|
let anySuccess = false;
|
|
57
|
-
for (const [name, sender] of
|
|
82
|
+
for (const [name, sender] of scopedSenders) {
|
|
58
83
|
try {
|
|
59
84
|
await sender(capped, context);
|
|
60
85
|
anySuccess = true;
|
package/dist/gateway/router.js
CHANGED
|
@@ -185,14 +185,14 @@ export class Gateway {
|
|
|
185
185
|
try {
|
|
186
186
|
const agentReply = await this.handleMessage(sessionKey, syntheticPrompt);
|
|
187
187
|
if (agentReply?.trim()) {
|
|
188
|
-
await this._dispatcher?.send(agentReply);
|
|
188
|
+
await this._dispatcher?.send(agentReply, { sessionKey });
|
|
189
189
|
logger.info({ sessionKey }, 'Deep mode result delivered via agent follow-up + dispatcher');
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
catch (err) {
|
|
193
193
|
logger.warn({ err, sessionKey }, 'Deep mode agent follow-up failed — using raw fallback');
|
|
194
194
|
if (rawFallback.trim()) {
|
|
195
|
-
await this._dispatcher?.send(rawFallback.slice(0, 1500))
|
|
195
|
+
await this._dispatcher?.send(rawFallback.slice(0, 1500), { sessionKey })
|
|
196
196
|
.catch(async (e) => {
|
|
197
197
|
// Both paths failed — surface it instead of swallowing at debug level.
|
|
198
198
|
logger.warn({ err: e, sessionKey }, 'Deep mode fallback delivery failed — persisting to daily note');
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -22,6 +22,26 @@ export declare class MemoryStore {
|
|
|
22
22
|
* Create the database and schema if needed.
|
|
23
23
|
*/
|
|
24
24
|
initialize(): void;
|
|
25
|
+
private _stmtLogSkillUse;
|
|
26
|
+
/**
|
|
27
|
+
* Record that a skill was retrieved and injected into a query context.
|
|
28
|
+
* Outcome is left null; a follow-up could backfill from reflection scores.
|
|
29
|
+
*/
|
|
30
|
+
logSkillUse(row: {
|
|
31
|
+
skillName: string;
|
|
32
|
+
sessionKey?: string | null;
|
|
33
|
+
queryText?: string | null;
|
|
34
|
+
score?: number | null;
|
|
35
|
+
agentSlug?: string | null;
|
|
36
|
+
}): void;
|
|
37
|
+
/** Aggregate skill usage stats keyed by skill_name. */
|
|
38
|
+
skillUsageStats(windowDays?: number): Map<string, {
|
|
39
|
+
retrievals: number;
|
|
40
|
+
lastRetrievedAt: string | null;
|
|
41
|
+
avgScore: number | null;
|
|
42
|
+
}>;
|
|
43
|
+
/** Number of times a skill has been retrieved (all time). */
|
|
44
|
+
skillRetrievalCount(skillName: string): number;
|
|
25
45
|
/**
|
|
26
46
|
* Close the database connection.
|
|
27
47
|
*/
|
package/dist/memory/store.js
CHANGED
|
@@ -405,8 +405,72 @@ export class MemoryStore {
|
|
|
405
405
|
CREATE INDEX IF NOT EXISTS idx_sf_sync_local ON sf_sync_log(local_table, local_id);
|
|
406
406
|
CREATE INDEX IF NOT EXISTS idx_sf_sync_sfid ON sf_sync_log(sf_id);
|
|
407
407
|
CREATE INDEX IF NOT EXISTS idx_sf_sync_status ON sf_sync_log(sync_status);
|
|
408
|
+
|
|
409
|
+
CREATE TABLE IF NOT EXISTS skill_usage (
|
|
410
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
411
|
+
skill_name TEXT NOT NULL,
|
|
412
|
+
session_key TEXT,
|
|
413
|
+
query_text TEXT,
|
|
414
|
+
retrieved_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
415
|
+
score REAL,
|
|
416
|
+
outcome TEXT,
|
|
417
|
+
agent_slug TEXT
|
|
418
|
+
);
|
|
419
|
+
CREATE INDEX IF NOT EXISTS idx_skill_usage_name ON skill_usage(skill_name, retrieved_at DESC);
|
|
420
|
+
CREATE INDEX IF NOT EXISTS idx_skill_usage_time ON skill_usage(retrieved_at DESC);
|
|
408
421
|
`);
|
|
409
422
|
}
|
|
423
|
+
// ── Skill usage telemetry ─────────────────────────────────────────
|
|
424
|
+
_stmtLogSkillUse = null;
|
|
425
|
+
/**
|
|
426
|
+
* Record that a skill was retrieved and injected into a query context.
|
|
427
|
+
* Outcome is left null; a follow-up could backfill from reflection scores.
|
|
428
|
+
*/
|
|
429
|
+
logSkillUse(row) {
|
|
430
|
+
try {
|
|
431
|
+
if (!this._stmtLogSkillUse) {
|
|
432
|
+
this._stmtLogSkillUse = this.conn.prepare('INSERT INTO skill_usage (skill_name, session_key, query_text, score, agent_slug) VALUES (?, ?, ?, ?, ?)');
|
|
433
|
+
}
|
|
434
|
+
this._stmtLogSkillUse.run(row.skillName, row.sessionKey ?? null, row.queryText ? row.queryText.slice(0, 200) : null, row.score ?? null, row.agentSlug ?? null);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Best-effort — telemetry must never break retrieval.
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/** Aggregate skill usage stats keyed by skill_name. */
|
|
441
|
+
skillUsageStats(windowDays = 7) {
|
|
442
|
+
const out = new Map();
|
|
443
|
+
try {
|
|
444
|
+
const rows = this.conn.prepare(`SELECT skill_name,
|
|
445
|
+
COUNT(*) AS retrievals,
|
|
446
|
+
MAX(retrieved_at) AS last_retrieved_at,
|
|
447
|
+
AVG(score) AS avg_score
|
|
448
|
+
FROM skill_usage
|
|
449
|
+
WHERE retrieved_at >= datetime('now', ?)
|
|
450
|
+
GROUP BY skill_name`).all(`-${Math.max(1, Math.floor(windowDays))} days`);
|
|
451
|
+
for (const r of rows) {
|
|
452
|
+
out.set(r.skill_name, {
|
|
453
|
+
retrievals: r.retrievals,
|
|
454
|
+
lastRetrievedAt: r.last_retrieved_at,
|
|
455
|
+
avgScore: r.avg_score,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Table may not exist yet on legacy DBs — caller should tolerate empty.
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
/** Number of times a skill has been retrieved (all time). */
|
|
465
|
+
skillRetrievalCount(skillName) {
|
|
466
|
+
try {
|
|
467
|
+
const row = this.conn.prepare('SELECT COUNT(*) AS cnt FROM skill_usage WHERE skill_name = ?').get(skillName);
|
|
468
|
+
return row?.cnt ?? 0;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
410
474
|
/**
|
|
411
475
|
* Close the database connection.
|
|
412
476
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -104,6 +104,8 @@ export type OnTextCallback = (text: string) => Promise<void>;
|
|
|
104
104
|
export type OnToolActivityCallback = (toolName: string, toolInput: Record<string, unknown>) => Promise<void>;
|
|
105
105
|
export interface NotificationContext {
|
|
106
106
|
agentSlug?: string;
|
|
107
|
+
/** When set, the dispatcher routes the message back to the channel that owns this session. */
|
|
108
|
+
sessionKey?: string;
|
|
107
109
|
}
|
|
108
110
|
export type NotificationSender = (text: string, context?: NotificationContext) => Promise<void>;
|
|
109
111
|
/** Policy governing autonomous outbound email sending for an agent. */
|
|
@@ -192,6 +194,7 @@ export interface HeartbeatState {
|
|
|
192
194
|
lastSelfImproveDate?: string;
|
|
193
195
|
lastConsolidationDate?: string;
|
|
194
196
|
lastAgentSiRuns?: Record<string, string>;
|
|
197
|
+
lastSkillDecayDate?: string;
|
|
195
198
|
/** Proactive insight engine state */
|
|
196
199
|
insightState?: {
|
|
197
200
|
sentToday: string[];
|