clementine-agent 1.0.24 → 1.0.26
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.
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import pino from 'pino';
|
|
14
|
-
import { GOALS_DIR, BASE_DIR } from '../config.js';
|
|
14
|
+
import { GOALS_DIR, BASE_DIR, MEMORY_DB_PATH, VAULT_DIR } from '../config.js';
|
|
15
15
|
import { listAllGoals } from '../tools/shared.js';
|
|
16
|
+
import { computeBrokenJobs } from '../gateway/failure-monitor.js';
|
|
17
|
+
import { MemoryStore } from '../memory/store.js';
|
|
16
18
|
const logger = pino({ name: 'clementine.insight-engine' });
|
|
17
19
|
const BASE_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
18
20
|
const MAX_DAILY_INSIGHTS = 3;
|
|
@@ -172,9 +174,7 @@ export function gatherInsightSignals(gateway) {
|
|
|
172
174
|
// with a diagnosis is a real, actionable signal the owner should
|
|
173
175
|
// see proactively rather than stumble across in the dashboard.
|
|
174
176
|
try {
|
|
175
|
-
|
|
176
|
-
const fm = require('../gateway/failure-monitor.js');
|
|
177
|
-
const broken = fm.computeBrokenJobs();
|
|
177
|
+
const broken = computeBrokenJobs();
|
|
178
178
|
for (const b of broken.slice(0, 3)) {
|
|
179
179
|
const hint = b.diagnosis?.rootCause
|
|
180
180
|
? ` — ${b.diagnosis.rootCause.slice(0, 120)}`
|
|
@@ -185,14 +185,13 @@ export function gatherInsightSignals(gateway) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
-
catch {
|
|
188
|
+
catch (err) {
|
|
189
|
+
logger.debug({ err }, 'Failed to pull broken-jobs signals');
|
|
190
|
+
}
|
|
189
191
|
// 6. Claim tracker — failed claims in the last N hours erode trust.
|
|
190
192
|
// Surface them so the owner sees "Clementine said she'd do X; she
|
|
191
193
|
// didn't" instead of silently swallowing the miss.
|
|
192
194
|
try {
|
|
193
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
194
|
-
const { MemoryStore } = require('../memory/store.js');
|
|
195
|
-
const { MEMORY_DB_PATH, VAULT_DIR } = require('../config.js');
|
|
196
195
|
if (existsSync(MEMORY_DB_PATH)) {
|
|
197
196
|
const store = new MemoryStore(MEMORY_DB_PATH, VAULT_DIR);
|
|
198
197
|
store.initialize();
|
|
@@ -209,7 +208,9 @@ export function gatherInsightSignals(gateway) {
|
|
|
209
208
|
store.close();
|
|
210
209
|
}
|
|
211
210
|
}
|
|
212
|
-
catch {
|
|
211
|
+
catch (err) {
|
|
212
|
+
logger.debug({ err }, 'Failed to pull failed-claims signals');
|
|
213
|
+
}
|
|
213
214
|
return signals;
|
|
214
215
|
}
|
|
215
216
|
/**
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* so the response to a silent failure is "here's what's wrong and
|
|
11
11
|
* here's what to change" rather than "go investigate."
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import pino from 'pino';
|
|
16
16
|
import { AGENTS_DIR, BASE_DIR, CRON_FILE } from '../config.js';
|
|
@@ -54,7 +54,6 @@ function saveCache(cache) {
|
|
|
54
54
|
mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
55
55
|
const tmp = CACHE_FILE + '.tmp';
|
|
56
56
|
writeFileSync(tmp, JSON.stringify(cache, null, 2));
|
|
57
|
-
const { renameSync } = require('node:fs');
|
|
58
57
|
renameSync(tmp, CACHE_FILE);
|
|
59
58
|
}
|
|
60
59
|
catch (err) {
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* Read-only with respect to the cron run logs and advisor events; mutates
|
|
16
16
|
* only its own state file (cron/failure-monitor.json).
|
|
17
17
|
*/
|
|
18
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
|
|
18
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
+
import Database from 'better-sqlite3';
|
|
20
21
|
import pino from 'pino';
|
|
21
|
-
import { BASE_DIR } from '../config.js';
|
|
22
|
+
import { BASE_DIR, MEMORY_DB_PATH } from '../config.js';
|
|
22
23
|
const logger = pino({ name: 'clementine.failure-monitor' });
|
|
23
24
|
const RUNS_DIR = path.join(BASE_DIR, 'cron', 'runs');
|
|
24
25
|
const ADVISOR_EVENTS_FILE = path.join(BASE_DIR, 'cron', 'advisor-events.jsonl');
|
|
@@ -54,7 +55,6 @@ function saveState(state) {
|
|
|
54
55
|
// writes from corrupting the state if the process is killed mid-write.
|
|
55
56
|
const tmp = STATE_FILE + '.tmp';
|
|
56
57
|
writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
57
|
-
const { renameSync } = require('node:fs');
|
|
58
58
|
renameSync(tmp, STATE_FILE);
|
|
59
59
|
}
|
|
60
60
|
catch (err) {
|
|
@@ -105,11 +105,8 @@ function isFailure(entry, gradeCache) {
|
|
|
105
105
|
function loadGradeCache() {
|
|
106
106
|
const cache = new Map();
|
|
107
107
|
try {
|
|
108
|
-
const { MEMORY_DB_PATH } = require('../config.js');
|
|
109
108
|
if (!existsSync(MEMORY_DB_PATH))
|
|
110
109
|
return cache;
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
112
|
-
const Database = require('better-sqlite3');
|
|
113
110
|
const db = new Database(MEMORY_DB_PATH, { readonly: true });
|
|
114
111
|
try {
|
|
115
112
|
const rows = db.prepare(`SELECT job_name, started_at, passed FROM graded_runs
|
|
@@ -121,7 +118,9 @@ function loadGradeCache() {
|
|
|
121
118
|
catch { /* graded_runs may not exist on older DBs */ }
|
|
122
119
|
db.close();
|
|
123
120
|
}
|
|
124
|
-
catch {
|
|
121
|
+
catch (err) {
|
|
122
|
+
logger.warn({ err }, 'Failed to load grade cache');
|
|
123
|
+
}
|
|
125
124
|
return cache;
|
|
126
125
|
}
|
|
127
126
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* owner with the verdict — succeeded or still failing — so a self-reported
|
|
8
8
|
* "fix" can't go untested again.
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
13
|
import pino from 'pino';
|
|
@@ -31,7 +31,6 @@ function saveState(state) {
|
|
|
31
31
|
mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
32
32
|
const tmp = STATE_FILE + '.tmp';
|
|
33
33
|
writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
34
|
-
const { renameSync } = require('node:fs');
|
|
35
34
|
renameSync(tmp, STATE_FILE);
|
|
36
35
|
}
|
|
37
36
|
catch (err) {
|
package/dist/gateway/router.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Manages per-user/channel sessions for conversation continuity.
|
|
6
6
|
*/
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import pino from 'pino';
|
|
10
10
|
import { PersonalAssistant } from '../agent/assistant.js';
|
|
11
11
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
@@ -743,7 +743,15 @@ export class Gateway {
|
|
|
743
743
|
// specific agent profile, classify whether the message should go
|
|
744
744
|
// to a specialist. Direct-to-agent-bot sessions bypass this entirely.
|
|
745
745
|
// Small-talk and meta queries stay with Clementine by default.
|
|
746
|
-
|
|
746
|
+
//
|
|
747
|
+
// Also bypass structured workflow messages — button clicks, approvals,
|
|
748
|
+
// and other system-injected interactions are not free-form chat and
|
|
749
|
+
// shouldn't be reclassified. They already have an intended flow.
|
|
750
|
+
const isStructuredWorkflowMsg = text.startsWith('[Button clicked:')
|
|
751
|
+
|| text.startsWith('[Approval:')
|
|
752
|
+
|| text.startsWith('[Reaction:')
|
|
753
|
+
|| text.startsWith('[System:');
|
|
754
|
+
const routingResult = !isInternalMsg && !sess?.profile && !text.startsWith('!') && !isStructuredWorkflowMsg
|
|
747
755
|
? await this._maybeRouteToSpecialist(sessionKey, text, onText)
|
|
748
756
|
: null;
|
|
749
757
|
if (routingResult?.delegated) {
|
|
@@ -956,12 +964,36 @@ export class Gateway {
|
|
|
956
964
|
}
|
|
957
965
|
}
|
|
958
966
|
// ── Deep mode detection ─────────────────────────────────────
|
|
959
|
-
// Agent proposes background execution for complex tasks
|
|
960
|
-
|
|
967
|
+
// Agent proposes background execution for complex tasks.
|
|
968
|
+
//
|
|
969
|
+
// Syntax:
|
|
970
|
+
// [DEEP_MODE: task description]
|
|
971
|
+
// [DEEP_MODE(work_dir=/abs/path): task description] ← NEW
|
|
972
|
+
//
|
|
973
|
+
// The optional work_dir= parameter runs the unleashed task in a
|
|
974
|
+
// different project directory. Useful when the task belongs to
|
|
975
|
+
// a Claude Code project with its own CLAUDE.md / slash commands
|
|
976
|
+
// (e.g. the proposal-builder project for audit-queue approvals).
|
|
977
|
+
const deepMatch = response?.match(/^\[DEEP_MODE(?:\(([^)]+)\))?:\s*(.+?)\]\s*/s);
|
|
961
978
|
if (deepMatch) {
|
|
962
|
-
const
|
|
963
|
-
const
|
|
979
|
+
const paramsStr = deepMatch[1] ?? '';
|
|
980
|
+
const taskDesc = deepMatch[2].trim() || text;
|
|
981
|
+
const ack = response.replace(/^\[DEEP_MODE(?:\([^)]*\))?:[^\]]*\]\s*/s, '').trim();
|
|
964
982
|
logger.info({ sessionKey, task: taskDesc }, 'Deep mode triggered by agent');
|
|
983
|
+
// Parse optional work_dir parameter — strict: must be an absolute
|
|
984
|
+
// path and must exist. Anything else falls back to default.
|
|
985
|
+
let deepWorkDir;
|
|
986
|
+
const wdMatch = paramsStr.match(/work_dir=([^,\s]+)/);
|
|
987
|
+
if (wdMatch) {
|
|
988
|
+
const candidate = wdMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
989
|
+
if (path.isAbsolute(candidate) && existsSync(candidate)) {
|
|
990
|
+
deepWorkDir = candidate;
|
|
991
|
+
logger.info({ sessionKey, workDir: deepWorkDir }, 'Deep mode using custom work_dir');
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
logger.warn({ sessionKey, candidate }, 'Deep mode work_dir rejected (not absolute or does not exist)');
|
|
995
|
+
}
|
|
996
|
+
}
|
|
965
997
|
const currentSess = this.getSession(sessionKey);
|
|
966
998
|
const jobName = `deep-${Date.now()}`;
|
|
967
999
|
currentSess.deepTask = { jobName, taskDesc, startedAt: new Date().toISOString() };
|
|
@@ -969,7 +1001,7 @@ export class Gateway {
|
|
|
969
1001
|
this.assistant.runUnleashedTask(jobName, `${taskDesc}\n\nOriginal request: ${text}`, 2, // tier 2 (Bash/Write/Edit enabled)
|
|
970
1002
|
undefined, // default maxTurns (75/phase)
|
|
971
1003
|
undefined, // default model
|
|
972
|
-
|
|
1004
|
+
deepWorkDir, // honors [DEEP_MODE(work_dir=...)] if provided
|
|
973
1005
|
1).then(async (result) => {
|
|
974
1006
|
logger.info({ sessionKey, jobName, resultLen: result?.length ?? 0 }, 'Deep mode task completed');
|
|
975
1007
|
if (result && result !== '__NOTHING__') {
|
|
@@ -1511,8 +1543,6 @@ function logRouteDecision(opts) {
|
|
|
1511
1543
|
while (_routeAuditBuffer.length > 200)
|
|
1512
1544
|
_routeAuditBuffer.shift();
|
|
1513
1545
|
try {
|
|
1514
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1515
|
-
const { appendFileSync } = require('node:fs');
|
|
1516
1546
|
appendFileSync(Gateway.routeAuditLogPath(), JSON.stringify(entry) + '\n');
|
|
1517
1547
|
}
|
|
1518
1548
|
catch (err) {
|