clementine-agent 1.18.142 → 1.18.144
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/self-improve.js +3 -2
- package/dist/agent/skill-store.d.ts +6 -0
- package/dist/agent/skill-store.js +7 -1
- package/dist/cli/routes/workflows.js +2 -1
- package/dist/gateway/agent-heartbeat-scheduler.d.ts +12 -0
- package/dist/gateway/agent-heartbeat-scheduler.js +32 -30
- package/dist/gateway/cron-scheduler.d.ts +21 -3
- package/dist/gateway/cron-scheduler.js +30 -18
- package/dist/gateway/heartbeat-scheduler.d.ts +15 -2
- package/dist/gateway/heartbeat-scheduler.js +18 -17
- package/dist/gateway/scheduler-state.d.ts +67 -0
- package/dist/gateway/scheduler-state.js +101 -0
- package/dist/tools/shared.d.ts +9 -0
- package/dist/tools/shared.js +21 -0
- package/dist/tools/skill-tools.js +4 -1
- package/dist/tools/team-tools.js +3 -5
- package/package.json +2 -2
|
@@ -17,6 +17,7 @@ import pino from 'pino';
|
|
|
17
17
|
import { BASE_DIR, SELF_IMPROVE_DIR, SOUL_FILE, CRON_FILE, WORKFLOWS_DIR, VAULT_DIR, MEMORY_DB_PATH, AGENTS_DIR, CRON_REFLECTIONS_DIR, GOALS_DIR, } from '../config.js';
|
|
18
18
|
import { listAllGoals } from '../tools/shared.js';
|
|
19
19
|
import { MemoryStore } from '../memory/store.js';
|
|
20
|
+
import { ANTHROPIC_SKILL_NAME_PATTERN } from './skill-store.js';
|
|
20
21
|
const logger = pino({ name: 'clementine.self-improve' });
|
|
21
22
|
// ── Defaults ─────────────────────────────────────────────────────────
|
|
22
23
|
const DEFAULT_CONFIG = {
|
|
@@ -2196,7 +2197,7 @@ export function validateProposal(area, target, proposedChange) {
|
|
|
2196
2197
|
// present + non-empty, no XML tags in description, no Anthropic-
|
|
2197
2198
|
// reserved words in name. Reuses the centralized validator that
|
|
2198
2199
|
// dashboard + MCP + auto-extract all share.
|
|
2199
|
-
if (
|
|
2200
|
+
if (!ANTHROPIC_SKILL_NAME_PATTERN.test(target)) {
|
|
2200
2201
|
return { valid: false, error: `skill target must be a valid slug (got "${target}")` };
|
|
2201
2202
|
}
|
|
2202
2203
|
let parsed;
|
|
@@ -2210,7 +2211,7 @@ export function validateProposal(area, target, proposedChange) {
|
|
|
2210
2211
|
const name = typeof fm.name === 'string' ? fm.name : '';
|
|
2211
2212
|
const description = typeof fm.description === 'string' ? fm.description : '';
|
|
2212
2213
|
const body = parsed.content || '';
|
|
2213
|
-
if (!name ||
|
|
2214
|
+
if (!name || !ANTHROPIC_SKILL_NAME_PATTERN.test(name)) {
|
|
2214
2215
|
return { valid: false, error: 'skill frontmatter "name" missing or invalid slug' };
|
|
2215
2216
|
}
|
|
2216
2217
|
if (name !== target) {
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
* crons → folder-form skills.
|
|
23
23
|
*/
|
|
24
24
|
import type { Skill, SkillScope, SkillValidationWarning, CronJobDefinition } from '../types.js';
|
|
25
|
+
/**
|
|
26
|
+
* Anthropic skill slug regex. Exported (1.18.144) so other modules
|
|
27
|
+
* (self-improve, migration tooling) don't drift their own copies.
|
|
28
|
+
* Lowercase letters/digits/dashes, must start with [a-z0-9], ≤64 chars.
|
|
29
|
+
*/
|
|
30
|
+
export declare const ANTHROPIC_SKILL_NAME_PATTERN: RegExp;
|
|
25
31
|
/** Run Anthropic-spec validations on a parsed skill. Errors are spec
|
|
26
32
|
* violations (skill would be rejected by the Anthropic API); warnings
|
|
27
33
|
* are best-practice hints (still loadable). Findings render in the
|
|
@@ -37,7 +37,13 @@ function projectSkillsDir(workDir) {
|
|
|
37
37
|
return existsSync(dir) ? dir : null;
|
|
38
38
|
}
|
|
39
39
|
// ── Anthropic spec validations ────────────────────────────────────────
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Anthropic skill slug regex. Exported (1.18.144) so other modules
|
|
42
|
+
* (self-improve, migration tooling) don't drift their own copies.
|
|
43
|
+
* Lowercase letters/digits/dashes, must start with [a-z0-9], ≤64 chars.
|
|
44
|
+
*/
|
|
45
|
+
export const ANTHROPIC_SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
46
|
+
const NAME_PATTERN = ANTHROPIC_SKILL_NAME_PATTERN;
|
|
41
47
|
const RESERVED_NAMES = new Set(['anthropic', 'claude']);
|
|
42
48
|
const NAME_MAX_LEN = 64;
|
|
43
49
|
const DESCRIPTION_MAX_LEN = 1024;
|
|
@@ -116,7 +116,8 @@ export function workflowsRouter(deps) {
|
|
|
116
116
|
const { wf, agentSlug } = candidates[0];
|
|
117
117
|
// Slugify the workflow name to the Anthropic regex
|
|
118
118
|
const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
|
|
119
|
-
|
|
119
|
+
const { ANTHROPIC_SKILL_NAME_PATTERN } = await import('../../agent/skill-store.js');
|
|
120
|
+
if (!ANTHROPIC_SKILL_NAME_PATTERN.test(slug)) {
|
|
120
121
|
res.status(400).json({ ok: false, error: `Workflow name "${name}" cannot be slugified to Anthropic regex` });
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
* the user's team. Runs autonomously alongside Clementine's own
|
|
4
4
|
* HeartbeatScheduler.
|
|
5
5
|
*
|
|
6
|
+
* Why this is its own scheduler (NOT a unified base class):
|
|
7
|
+
* - It has NO loop. Unlike HeartbeatScheduler (setInterval) and
|
|
8
|
+
* CronScheduler (node-cron), this one is ticked externally by
|
|
9
|
+
* whatever orchestrator manages the agent fleet. Its `tick()` is a
|
|
10
|
+
* pure function of state + signals.
|
|
11
|
+
* - Its scheduling decision is *adaptive* (computeNextInterval) based
|
|
12
|
+
* on the previous tick's outcome — not a fixed cadence. Forcing it
|
|
13
|
+
* into a generic "interval-based" base would lose this behavior.
|
|
14
|
+
*
|
|
15
|
+
* Shared with the other schedulers: only the JSON state-file load/save
|
|
16
|
+
* pattern, factored into ./scheduler-state.ts in 1.18.143.
|
|
17
|
+
*
|
|
6
18
|
* Phase 2 — cheap path only. No LLM call. The tick loads state, scans
|
|
7
19
|
* three signals (pending delegated tasks, recent goal updates, recent
|
|
8
20
|
* cron completions), updates fingerprint, and persists state.
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
* the user's team. Runs autonomously alongside Clementine's own
|
|
4
4
|
* HeartbeatScheduler.
|
|
5
5
|
*
|
|
6
|
+
* Why this is its own scheduler (NOT a unified base class):
|
|
7
|
+
* - It has NO loop. Unlike HeartbeatScheduler (setInterval) and
|
|
8
|
+
* CronScheduler (node-cron), this one is ticked externally by
|
|
9
|
+
* whatever orchestrator manages the agent fleet. Its `tick()` is a
|
|
10
|
+
* pure function of state + signals.
|
|
11
|
+
* - Its scheduling decision is *adaptive* (computeNextInterval) based
|
|
12
|
+
* on the previous tick's outcome — not a fixed cadence. Forcing it
|
|
13
|
+
* into a generic "interval-based" base would lose this behavior.
|
|
14
|
+
*
|
|
15
|
+
* Shared with the other schedulers: only the JSON state-file load/save
|
|
16
|
+
* pattern, factored into ./scheduler-state.ts in 1.18.143.
|
|
17
|
+
*
|
|
6
18
|
* Phase 2 — cheap path only. No LLM call. The tick loads state, scans
|
|
7
19
|
* three signals (pending delegated tasks, recent goal updates, recent
|
|
8
20
|
* cron completions), updates fingerprint, and persists state.
|
|
@@ -11,11 +23,12 @@
|
|
|
11
23
|
* agent's profile) when the fingerprint indicates a real signal change.
|
|
12
24
|
*/
|
|
13
25
|
import { createHash } from 'node:crypto';
|
|
14
|
-
import { existsSync,
|
|
26
|
+
import { existsSync, readFileSync, readdirSync, statSync, } from 'node:fs';
|
|
15
27
|
import path from 'node:path';
|
|
16
28
|
import pino from 'pino';
|
|
17
29
|
import { AGENTS_DIR, BASE_DIR } from '../config.js';
|
|
18
30
|
import { listAllGoals } from '../tools/shared.js';
|
|
31
|
+
import { loadStateFile, saveStateFile } from './scheduler-state.js';
|
|
19
32
|
const logger = pino({ name: 'clementine.agent-heartbeat' });
|
|
20
33
|
const DEFAULT_INTERVAL_MIN = 30;
|
|
21
34
|
const MIN_INTERVAL_MIN = 5;
|
|
@@ -81,43 +94,32 @@ export class AgentHeartbeatScheduler {
|
|
|
81
94
|
}
|
|
82
95
|
/** Read persisted state, or return a fresh state ready to tick now. */
|
|
83
96
|
loadState() {
|
|
84
|
-
|
|
85
|
-
if (existsSync(this.stateFile)) {
|
|
86
|
-
const raw = JSON.parse(readFileSync(this.stateFile, 'utf-8'));
|
|
87
|
-
const validKinds = ['acted', 'quiet', 'silent', 'override'];
|
|
88
|
-
const kind = validKinds.includes(raw.lastTickKind)
|
|
89
|
-
? raw.lastTickKind
|
|
90
|
-
: undefined;
|
|
91
|
-
return {
|
|
92
|
-
slug: this.slug,
|
|
93
|
-
lastTickAt: String(raw.lastTickAt ?? ''),
|
|
94
|
-
nextCheckAt: String(raw.nextCheckAt ?? new Date().toISOString()),
|
|
95
|
-
silentTickCount: Number(raw.silentTickCount ?? 0),
|
|
96
|
-
fingerprint: String(raw.fingerprint ?? ''),
|
|
97
|
-
...(raw.lastSignalSummary ? { lastSignalSummary: raw.lastSignalSummary } : {}),
|
|
98
|
-
...(kind ? { lastTickKind: kind } : {}),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
catch (err) {
|
|
103
|
-
logger.warn({ err, slug: this.slug }, 'Failed to load agent heartbeat state — starting fresh');
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
97
|
+
const fresh = {
|
|
106
98
|
slug: this.slug,
|
|
107
99
|
lastTickAt: '',
|
|
108
100
|
nextCheckAt: new Date().toISOString(),
|
|
109
101
|
silentTickCount: 0,
|
|
110
102
|
fingerprint: '',
|
|
111
103
|
};
|
|
104
|
+
return loadStateFile(this.stateFile, fresh, (raw) => {
|
|
105
|
+
const r = raw;
|
|
106
|
+
const validKinds = ['acted', 'quiet', 'silent', 'override'];
|
|
107
|
+
const kind = validKinds.includes(r.lastTickKind)
|
|
108
|
+
? r.lastTickKind
|
|
109
|
+
: undefined;
|
|
110
|
+
return {
|
|
111
|
+
slug: this.slug,
|
|
112
|
+
lastTickAt: String(r.lastTickAt ?? ''),
|
|
113
|
+
nextCheckAt: String(r.nextCheckAt ?? new Date().toISOString()),
|
|
114
|
+
silentTickCount: Number(r.silentTickCount ?? 0),
|
|
115
|
+
fingerprint: String(r.fingerprint ?? ''),
|
|
116
|
+
...(r.lastSignalSummary ? { lastSignalSummary: r.lastSignalSummary } : {}),
|
|
117
|
+
...(kind ? { lastTickKind: kind } : {}),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
112
120
|
}
|
|
113
121
|
saveState(state) {
|
|
114
|
-
|
|
115
|
-
mkdirSync(path.dirname(this.stateFile), { recursive: true });
|
|
116
|
-
writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
logger.warn({ err, slug: this.slug }, 'Failed to save agent heartbeat state — non-fatal');
|
|
120
|
-
}
|
|
122
|
+
saveStateFile(this.stateFile, state);
|
|
121
123
|
}
|
|
122
124
|
/** True if the agent is due for a tick. */
|
|
123
125
|
isDue(now = new Date()) {
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clementine TypeScript — Cron scheduler (autonomous execution).
|
|
3
3
|
*
|
|
4
|
-
* CronScheduler: precise scheduled tasks using node-cron
|
|
4
|
+
* CronScheduler: precise per-job scheduled tasks using node-cron. Each
|
|
5
|
+
* cron job (defined in CRON.md or via the schedule registry) gets its
|
|
6
|
+
* own ScheduledTask instance with that job's cron expression, so the
|
|
7
|
+
* scheduling primitive here is fundamentally different from the
|
|
8
|
+
* fixed-interval HeartbeatScheduler and the externally-ticked
|
|
9
|
+
* AgentHeartbeatScheduler.
|
|
5
10
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
11
|
+
* Why this is its own scheduler (NOT a unified base class):
|
|
12
|
+
* - One node-cron task per job. CronScheduler maintains the
|
|
13
|
+
* scheduledTasks/workflowTasks maps; a generic base class would
|
|
14
|
+
* have to model "N independent timers" which the other two don't
|
|
15
|
+
* need.
|
|
16
|
+
* - Owns crash-safe idempotency (cron-running.json), workflow
|
|
17
|
+
* execution, file watching, trigger directories, broken-job
|
|
18
|
+
* tracking. None of these belong in a generic base.
|
|
19
|
+
*
|
|
20
|
+
* Also contains shared parsers (parseCronJobs, parseAgentCronJobs,
|
|
21
|
+
* validateCronYaml), retry helpers, CronRunLog, and daily-note logging
|
|
22
|
+
* utilities used by both schedulers.
|
|
23
|
+
*
|
|
24
|
+
* Shared with the other schedulers: only the JSON state-file load/save
|
|
25
|
+
* pattern, factored into ./scheduler-state.ts in 1.18.143.
|
|
8
26
|
*/
|
|
9
27
|
import type { CronJobDefinition, CronRunEntry, SelfImproveConfig, SelfImproveExperiment, SelfImproveState, WorkflowDefinition } from '../types.js';
|
|
10
28
|
import type { NotificationDispatcher } from './notifications.js';
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clementine TypeScript — Cron scheduler (autonomous execution).
|
|
3
3
|
*
|
|
4
|
-
* CronScheduler: precise scheduled tasks using node-cron
|
|
4
|
+
* CronScheduler: precise per-job scheduled tasks using node-cron. Each
|
|
5
|
+
* cron job (defined in CRON.md or via the schedule registry) gets its
|
|
6
|
+
* own ScheduledTask instance with that job's cron expression, so the
|
|
7
|
+
* scheduling primitive here is fundamentally different from the
|
|
8
|
+
* fixed-interval HeartbeatScheduler and the externally-ticked
|
|
9
|
+
* AgentHeartbeatScheduler.
|
|
5
10
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
11
|
+
* Why this is its own scheduler (NOT a unified base class):
|
|
12
|
+
* - One node-cron task per job. CronScheduler maintains the
|
|
13
|
+
* scheduledTasks/workflowTasks maps; a generic base class would
|
|
14
|
+
* have to model "N independent timers" which the other two don't
|
|
15
|
+
* need.
|
|
16
|
+
* - Owns crash-safe idempotency (cron-running.json), workflow
|
|
17
|
+
* execution, file watching, trigger directories, broken-job
|
|
18
|
+
* tracking. None of these belong in a generic base.
|
|
19
|
+
*
|
|
20
|
+
* Also contains shared parsers (parseCronJobs, parseAgentCronJobs,
|
|
21
|
+
* validateCronYaml), retry helpers, CronRunLog, and daily-note logging
|
|
22
|
+
* utilities used by both schedulers.
|
|
23
|
+
*
|
|
24
|
+
* Shared with the other schedulers: only the JSON state-file load/save
|
|
25
|
+
* pattern, factored into ./scheduler-state.ts in 1.18.143.
|
|
8
26
|
*/
|
|
9
27
|
import { execSync } from 'node:child_process';
|
|
10
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync,
|
|
28
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, watchFile, unwatchFile, writeFileSync, } from 'node:fs';
|
|
11
29
|
import path from 'node:path';
|
|
12
30
|
import cron from 'node-cron';
|
|
13
31
|
import matter from 'gray-matter';
|
|
@@ -15,6 +33,7 @@ import pino from 'pino';
|
|
|
15
33
|
import { CRON_FILE, WORKFLOWS_DIR, AGENTS_DIR, DAILY_NOTES_DIR, BASE_DIR, DISCORD_OWNER_ID, GOALS_DIR, CRON_REFLECTIONS_DIR, ADVISOR_LOG_PATH, TIMEZONE, } from '../config.js';
|
|
16
34
|
import { listAllGoals, findGoalPath, readGoalById } from '../tools/shared.js';
|
|
17
35
|
import { listSchedules } from '../agent/schedule-registry.js';
|
|
36
|
+
import { saveStateFile } from './scheduler-state.js';
|
|
18
37
|
import { scanner } from '../security/scanner.js';
|
|
19
38
|
import { parseAllWorkflows as parseAllWorkflowsSync } from '../agent/workflow-runner.js';
|
|
20
39
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
@@ -579,20 +598,13 @@ export class CronScheduler {
|
|
|
579
598
|
* rename so a crash mid-write cannot corrupt the file.
|
|
580
599
|
*/
|
|
581
600
|
persistRunningJobs(metaByName) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const tmp = CronScheduler.RUNNING_JOBS_FILE + '.tmp';
|
|
590
|
-
writeFileSync(tmp, JSON.stringify(entries, null, 2));
|
|
591
|
-
renameSync(tmp, CronScheduler.RUNNING_JOBS_FILE);
|
|
592
|
-
}
|
|
593
|
-
catch (err) {
|
|
594
|
-
logger.debug({ err }, 'Failed to persist running-jobs file');
|
|
595
|
-
}
|
|
601
|
+
const entries = [...this.runningJobs].map(name => ({
|
|
602
|
+
jobName: name,
|
|
603
|
+
startedAt: metaByName?.get(name)?.startedAt ?? new Date().toISOString(),
|
|
604
|
+
runId: metaByName?.get(name)?.runId ?? '',
|
|
605
|
+
pid: process.pid,
|
|
606
|
+
}));
|
|
607
|
+
saveStateFile(CronScheduler.RUNNING_JOBS_FILE, entries, { atomic: true });
|
|
596
608
|
}
|
|
597
609
|
/**
|
|
598
610
|
* On startup, read the persisted running-jobs file. Any entries present
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clementine TypeScript — Heartbeat scheduler.
|
|
3
3
|
*
|
|
4
|
-
* HeartbeatScheduler: periodic general check-ins using setInterval
|
|
5
|
-
*
|
|
4
|
+
* HeartbeatScheduler: periodic general check-ins using setInterval
|
|
5
|
+
* (one tick every HEARTBEAT_INTERVAL_MINUTES, default 30 min, gated to
|
|
6
|
+
* the HEARTBEAT_ACTIVE_START..ACTIVE_END window). Channel-agnostic —
|
|
7
|
+
* sends notifications via the NotificationDispatcher.
|
|
8
|
+
*
|
|
9
|
+
* Why this is its own scheduler (NOT a unified base class):
|
|
10
|
+
* - It uses a fixed setInterval. Different scheduling primitive than
|
|
11
|
+
* CronScheduler (which uses node-cron per-job) and AgentHeartbeat
|
|
12
|
+
* Scheduler (which is ticked externally — has no loop of its own).
|
|
13
|
+
* - It owns autonomous concerns: insight engine, proactive ledger,
|
|
14
|
+
* dense-vector backfill, salience decay, episodic consolidation.
|
|
15
|
+
* None of these belong in a generic scheduler base.
|
|
16
|
+
*
|
|
17
|
+
* Shared with the other two schedulers: only the JSON state-file
|
|
18
|
+
* load/save pattern, factored into ./scheduler-state.ts in 1.18.143.
|
|
6
19
|
*/
|
|
7
20
|
import type { HeartbeatWorkItem } from '../types.js';
|
|
8
21
|
import type { CronScheduler } from './cron-scheduler.js';
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clementine TypeScript — Heartbeat scheduler.
|
|
3
3
|
*
|
|
4
|
-
* HeartbeatScheduler: periodic general check-ins using setInterval
|
|
5
|
-
*
|
|
4
|
+
* HeartbeatScheduler: periodic general check-ins using setInterval
|
|
5
|
+
* (one tick every HEARTBEAT_INTERVAL_MINUTES, default 30 min, gated to
|
|
6
|
+
* the HEARTBEAT_ACTIVE_START..ACTIVE_END window). Channel-agnostic —
|
|
7
|
+
* sends notifications via the NotificationDispatcher.
|
|
8
|
+
*
|
|
9
|
+
* Why this is its own scheduler (NOT a unified base class):
|
|
10
|
+
* - It uses a fixed setInterval. Different scheduling primitive than
|
|
11
|
+
* CronScheduler (which uses node-cron per-job) and AgentHeartbeat
|
|
12
|
+
* Scheduler (which is ticked externally — has no loop of its own).
|
|
13
|
+
* - It owns autonomous concerns: insight engine, proactive ledger,
|
|
14
|
+
* dense-vector backfill, salience decay, episodic consolidation.
|
|
15
|
+
* None of these belong in a generic scheduler base.
|
|
16
|
+
*
|
|
17
|
+
* Shared with the other two schedulers: only the JSON state-file
|
|
18
|
+
* load/save pattern, factored into ./scheduler-state.ts in 1.18.143.
|
|
6
19
|
*/
|
|
7
20
|
import { createHash, randomBytes } from 'node:crypto';
|
|
8
21
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
@@ -11,6 +24,7 @@ import matter from 'gray-matter';
|
|
|
11
24
|
import pino from 'pino';
|
|
12
25
|
import { HEARTBEAT_FILE, TASKS_FILE, INBOX_DIR, DAILY_NOTES_DIR, HEARTBEAT_INTERVAL_MINUTES, HEARTBEAT_ACTIVE_START, HEARTBEAT_ACTIVE_END, BASE_DIR, GOALS_DIR, AGENTS_DIR, HEARTBEAT_WORK_QUEUE_FILE, DISCORD_OWNER_ID, } from '../config.js';
|
|
13
26
|
import { findGoalPath, listAllGoals } from '../tools/shared.js';
|
|
27
|
+
import { loadStateFile, saveStateFile } from './scheduler-state.js';
|
|
14
28
|
import { gatherInsightSignals, buildInsightPrompt, parseInsightResponse, canSendInsight, recordInsightSent, recordInsightAcked, maybeIncreaseCooldown, } from '../agent/insight-engine.js';
|
|
15
29
|
import { decideDailyPlanPriority, decideDiscoveredWorkItem, decideGoalAdvancement, decisionShouldCreateGoalTrigger, decisionShouldQueueHeartbeatWork, } from '../agent/proactive-engine.js';
|
|
16
30
|
import { recentDecisions, recordDecision, recordDecisionOutcome, wasRecentlyDecided, } from '../agent/proactive-ledger.js';
|
|
@@ -1144,23 +1158,10 @@ export class HeartbeatScheduler {
|
|
|
1144
1158
|
return parsed.content;
|
|
1145
1159
|
}
|
|
1146
1160
|
loadState() {
|
|
1147
|
-
|
|
1148
|
-
try {
|
|
1149
|
-
return JSON.parse(readFileSync(this.stateFile, 'utf-8'));
|
|
1150
|
-
}
|
|
1151
|
-
catch {
|
|
1152
|
-
logger.warn('Failed to load heartbeat state — starting fresh');
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
return { fingerprint: '', details: {}, timestamp: '' };
|
|
1161
|
+
return loadStateFile(this.stateFile, { fingerprint: '', details: {}, timestamp: '' });
|
|
1156
1162
|
}
|
|
1157
1163
|
saveState() {
|
|
1158
|
-
|
|
1159
|
-
writeFileSync(this.stateFile, JSON.stringify(this.lastState, null, 2));
|
|
1160
|
-
}
|
|
1161
|
-
catch (err) {
|
|
1162
|
-
logger.warn({ err }, 'Failed to save heartbeat state');
|
|
1163
|
-
}
|
|
1164
|
+
saveStateFile(this.stateFile, this.lastState);
|
|
1164
1165
|
}
|
|
1165
1166
|
computeStateFingerprint() {
|
|
1166
1167
|
const details = {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler state-file helpers (1.18.143)
|
|
3
|
+
*
|
|
4
|
+
* Three schedulers each persist a JSON state file (heartbeat,
|
|
5
|
+
* cron-running-jobs, per-agent heartbeat). Before this module they
|
|
6
|
+
* each reimplemented "try parse, fall back on error" + "write JSON,
|
|
7
|
+
* log on error". The patterns are tiny but they were drifting (some
|
|
8
|
+
* had `mkdirSync`, some had atomic write-then-rename, some had
|
|
9
|
+
* neither — and a future addition would have copied yet another
|
|
10
|
+
* variant).
|
|
11
|
+
*
|
|
12
|
+
* This module is the single source of truth for both shapes:
|
|
13
|
+
*
|
|
14
|
+
* loadStateFile(path, default, validator?)
|
|
15
|
+
* — read JSON, fall back to default on missing/invalid file,
|
|
16
|
+
* optionally run a validator that can clean/coerce the parsed
|
|
17
|
+
* payload before returning it.
|
|
18
|
+
*
|
|
19
|
+
* saveStateFile(path, state, opts)
|
|
20
|
+
* — ensure parent dir exists, then either plain
|
|
21
|
+
* writeFileSync (default) or atomic write-then-rename for
|
|
22
|
+
* crash-safe persistence (set `atomic: true`).
|
|
23
|
+
*
|
|
24
|
+
* Both swallow filesystem errors and log a warning — the schedulers
|
|
25
|
+
* treat persistence as best-effort (a missing/corrupt state file
|
|
26
|
+
* means "start fresh", not "crash"). Callers that need failure
|
|
27
|
+
* surfaced should check the boolean return on saveStateFile.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Read a JSON state file. Returns `defaultValue` if the file is
|
|
31
|
+
* missing, unreadable, or fails JSON parse. Optional validator runs
|
|
32
|
+
* after parse and can return a cleaned-up version (e.g. coerce
|
|
33
|
+
* missing fields, drop invalid values).
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadStateFile<T>(filePath: string, defaultValue: T, validator?: (raw: unknown) => T): T;
|
|
36
|
+
/**
|
|
37
|
+
* 1.18.144 — Generic JSON file reader for non-scheduler call sites.
|
|
38
|
+
*
|
|
39
|
+
* Same try/parse/fallback shape as loadStateFile but with a `silent`
|
|
40
|
+
* option for callers who expect missing files to be common (e.g. a
|
|
41
|
+
* tool reading an optional config) and don't want every miss to log.
|
|
42
|
+
*
|
|
43
|
+
* Six+ files were inlining `try { JSON.parse(readFileSync(p)) } catch
|
|
44
|
+
* { return default }` before this. They now share one well-tested
|
|
45
|
+
* helper, which means the next file that needs to read a JSON sidecar
|
|
46
|
+
* doesn't add a seventh variant.
|
|
47
|
+
*/
|
|
48
|
+
export declare function loadJsonFile<T>(filePath: string, defaultValue: T, opts?: {
|
|
49
|
+
silent?: boolean;
|
|
50
|
+
validator?: (raw: unknown) => T;
|
|
51
|
+
}): T;
|
|
52
|
+
export interface SaveStateOptions {
|
|
53
|
+
/**
|
|
54
|
+
* If true, write to `<file>.tmp` then rename — guarantees the on-disk
|
|
55
|
+
* file is either the previous state or the new state, never a half-
|
|
56
|
+
* written file. Use for state that must survive crashes mid-write
|
|
57
|
+
* (cron-running.json relies on this for idempotency).
|
|
58
|
+
*/
|
|
59
|
+
atomic?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Write a JSON state file. Creates the parent directory if missing.
|
|
63
|
+
* Returns true on success, false on failure (always logs a warning
|
|
64
|
+
* on failure — caller doesn't need to log again).
|
|
65
|
+
*/
|
|
66
|
+
export declare function saveStateFile<T>(filePath: string, state: T, opts?: SaveStateOptions): boolean;
|
|
67
|
+
//# sourceMappingURL=scheduler-state.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler state-file helpers (1.18.143)
|
|
3
|
+
*
|
|
4
|
+
* Three schedulers each persist a JSON state file (heartbeat,
|
|
5
|
+
* cron-running-jobs, per-agent heartbeat). Before this module they
|
|
6
|
+
* each reimplemented "try parse, fall back on error" + "write JSON,
|
|
7
|
+
* log on error". The patterns are tiny but they were drifting (some
|
|
8
|
+
* had `mkdirSync`, some had atomic write-then-rename, some had
|
|
9
|
+
* neither — and a future addition would have copied yet another
|
|
10
|
+
* variant).
|
|
11
|
+
*
|
|
12
|
+
* This module is the single source of truth for both shapes:
|
|
13
|
+
*
|
|
14
|
+
* loadStateFile(path, default, validator?)
|
|
15
|
+
* — read JSON, fall back to default on missing/invalid file,
|
|
16
|
+
* optionally run a validator that can clean/coerce the parsed
|
|
17
|
+
* payload before returning it.
|
|
18
|
+
*
|
|
19
|
+
* saveStateFile(path, state, opts)
|
|
20
|
+
* — ensure parent dir exists, then either plain
|
|
21
|
+
* writeFileSync (default) or atomic write-then-rename for
|
|
22
|
+
* crash-safe persistence (set `atomic: true`).
|
|
23
|
+
*
|
|
24
|
+
* Both swallow filesystem errors and log a warning — the schedulers
|
|
25
|
+
* treat persistence as best-effort (a missing/corrupt state file
|
|
26
|
+
* means "start fresh", not "crash"). Callers that need failure
|
|
27
|
+
* surfaced should check the boolean return on saveStateFile.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import pino from 'pino';
|
|
32
|
+
const logger = pino({ name: 'clementine.scheduler-state' });
|
|
33
|
+
/**
|
|
34
|
+
* Read a JSON state file. Returns `defaultValue` if the file is
|
|
35
|
+
* missing, unreadable, or fails JSON parse. Optional validator runs
|
|
36
|
+
* after parse and can return a cleaned-up version (e.g. coerce
|
|
37
|
+
* missing fields, drop invalid values).
|
|
38
|
+
*/
|
|
39
|
+
export function loadStateFile(filePath, defaultValue, validator) {
|
|
40
|
+
try {
|
|
41
|
+
if (!existsSync(filePath))
|
|
42
|
+
return defaultValue;
|
|
43
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
44
|
+
return validator ? validator(raw) : raw;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
logger.warn({ err, filePath }, 'Failed to load state file — starting fresh');
|
|
48
|
+
return defaultValue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 1.18.144 — Generic JSON file reader for non-scheduler call sites.
|
|
53
|
+
*
|
|
54
|
+
* Same try/parse/fallback shape as loadStateFile but with a `silent`
|
|
55
|
+
* option for callers who expect missing files to be common (e.g. a
|
|
56
|
+
* tool reading an optional config) and don't want every miss to log.
|
|
57
|
+
*
|
|
58
|
+
* Six+ files were inlining `try { JSON.parse(readFileSync(p)) } catch
|
|
59
|
+
* { return default }` before this. They now share one well-tested
|
|
60
|
+
* helper, which means the next file that needs to read a JSON sidecar
|
|
61
|
+
* doesn't add a seventh variant.
|
|
62
|
+
*/
|
|
63
|
+
export function loadJsonFile(filePath, defaultValue, opts = {}) {
|
|
64
|
+
try {
|
|
65
|
+
if (!existsSync(filePath))
|
|
66
|
+
return defaultValue;
|
|
67
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
68
|
+
return opts.validator ? opts.validator(raw) : raw;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (!opts.silent) {
|
|
72
|
+
logger.warn({ err, filePath }, 'Failed to load JSON file — using default');
|
|
73
|
+
}
|
|
74
|
+
return defaultValue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Write a JSON state file. Creates the parent directory if missing.
|
|
79
|
+
* Returns true on success, false on failure (always logs a warning
|
|
80
|
+
* on failure — caller doesn't need to log again).
|
|
81
|
+
*/
|
|
82
|
+
export function saveStateFile(filePath, state, opts = {}) {
|
|
83
|
+
try {
|
|
84
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
85
|
+
const json = JSON.stringify(state, null, 2);
|
|
86
|
+
if (opts.atomic) {
|
|
87
|
+
const tmp = filePath + '.tmp';
|
|
88
|
+
writeFileSync(tmp, json);
|
|
89
|
+
renameSync(tmp, filePath);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
writeFileSync(filePath, json);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
logger.warn({ err, filePath }, 'Failed to save state file');
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=scheduler-state.js.map
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -676,6 +676,15 @@ export declare function agentTasksFile(slug: string | null): string;
|
|
|
676
676
|
export declare function agentWorkingMemoryFile(slug: string | null): string;
|
|
677
677
|
export declare function agentGoalsDir(slug: string | null): string;
|
|
678
678
|
export declare function agentDailyNotesDir(slug: string | null): string;
|
|
679
|
+
/**
|
|
680
|
+
* 1.18.144 — Single source of truth for "all currently-hired agent
|
|
681
|
+
* slugs." Five+ files used to inline the same readdirSync + filter
|
|
682
|
+
* pattern (skipping leading-underscore directories like _archive).
|
|
683
|
+
*
|
|
684
|
+
* Returns slugs sorted alphabetically. Returns [] when AGENTS_DIR
|
|
685
|
+
* doesn't exist or can't be read.
|
|
686
|
+
*/
|
|
687
|
+
export declare function listAgentSlugs(): string[];
|
|
679
688
|
export type GoalRecord = {
|
|
680
689
|
id: string;
|
|
681
690
|
title: string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -80,6 +80,27 @@ export function agentDailyNotesDir(slug) {
|
|
|
80
80
|
return DAILY_NOTES_DIR;
|
|
81
81
|
return path.join(AGENTS_DIR, slug, 'daily-notes');
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 1.18.144 — Single source of truth for "all currently-hired agent
|
|
85
|
+
* slugs." Five+ files used to inline the same readdirSync + filter
|
|
86
|
+
* pattern (skipping leading-underscore directories like _archive).
|
|
87
|
+
*
|
|
88
|
+
* Returns slugs sorted alphabetically. Returns [] when AGENTS_DIR
|
|
89
|
+
* doesn't exist or can't be read.
|
|
90
|
+
*/
|
|
91
|
+
export function listAgentSlugs() {
|
|
92
|
+
if (!existsSync(AGENTS_DIR))
|
|
93
|
+
return [];
|
|
94
|
+
try {
|
|
95
|
+
return readdirSync(AGENTS_DIR, { withFileTypes: true })
|
|
96
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('_'))
|
|
97
|
+
.map((d) => d.name)
|
|
98
|
+
.sort();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
83
104
|
/** Return the directory where a goal owned by `owner` should live. */
|
|
84
105
|
export function goalDirForOwner(owner) {
|
|
85
106
|
if (!owner || owner === 'clementine')
|
|
@@ -25,7 +25,10 @@ import { VAULT_DIR, textResult, logger } from './shared.js';
|
|
|
25
25
|
// 1.18.124 — name regex is the only validator skill-tools still uses
|
|
26
26
|
// directly (for update_skill's pre-flight slug check). All other
|
|
27
27
|
// validations + the file write live in skill-store.writeSkill.
|
|
28
|
-
|
|
28
|
+
// 1.18.144 — pulled the regex from skill-store's exported canonical
|
|
29
|
+
// constant so all skill-name validation now traces to one source.
|
|
30
|
+
import { ANTHROPIC_SKILL_NAME_PATTERN } from '../agent/skill-store.js';
|
|
31
|
+
const NAME_PATTERN = ANTHROPIC_SKILL_NAME_PATTERN;
|
|
29
32
|
const DESCRIPTION_MAX_LEN = 1024;
|
|
30
33
|
function globalSkillsDir() {
|
|
31
34
|
return path.join(VAULT_DIR, '00-System', 'skills');
|
package/dist/tools/team-tools.js
CHANGED
|
@@ -7,13 +7,14 @@ import path from 'node:path';
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { ACTIVE_AGENT_SLUG, AGENTS_DIR, BASE_DIR, DELEGATIONS_BASE, TEAM_COMMS_LOG, env, logger, parseTasks, textResult, } from './shared.js';
|
|
9
9
|
import { todayISO } from '../gateway/cron-scheduler.js';
|
|
10
|
+
import { listAgentSlugs } from './shared.js';
|
|
10
11
|
async function loadTeamAgents() {
|
|
11
12
|
const matterMod = await import('gray-matter');
|
|
12
13
|
const agents = [];
|
|
13
14
|
const seen = new Set();
|
|
14
15
|
if (existsSync(AGENTS_DIR)) {
|
|
15
16
|
try {
|
|
16
|
-
for (const slug of
|
|
17
|
+
for (const slug of listAgentSlugs()) {
|
|
17
18
|
const agentFile = path.join(AGENTS_DIR, slug, 'agent.md');
|
|
18
19
|
if (!existsSync(agentFile))
|
|
19
20
|
continue;
|
|
@@ -349,10 +350,7 @@ export function registerTeamTools(server) {
|
|
|
349
350
|
const agentsBase = AGENTS_DIR;
|
|
350
351
|
if (!existsSync(agentsBase))
|
|
351
352
|
return textResult('No agents found.');
|
|
352
|
-
const agentSlugs =
|
|
353
|
-
.filter(d => d.isDirectory())
|
|
354
|
-
.map(d => d.name)
|
|
355
|
-
.filter(n => !agent || n === agent);
|
|
353
|
+
const agentSlugs = listAgentSlugs().filter(n => !agent || n === agent);
|
|
356
354
|
if (!agentSlugs.length)
|
|
357
355
|
return textResult('No agents found.');
|
|
358
356
|
const matterMod = await import('gray-matter');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clementine-agent",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.144",
|
|
4
4
|
"description": "Clementine — Personal AI Assistant (TypeScript)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"postinstall": "node scripts/postinstall.js 2>/dev/null || true"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
31
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.138",
|
|
32
32
|
"@anthropic-ai/sdk": "^0.91.0",
|
|
33
33
|
"@composio/claude-agent-sdk": "^0.8.1",
|
|
34
34
|
"@composio/core": "^0.8.1",
|