clementine-agent 1.0.80 → 1.0.81
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 +21 -1
- package/dist/cli/dashboard.js +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -556,7 +556,7 @@ Both paths trigger an automatic daemon restart when the new agent is detected.
|
|
|
556
556
|
|
|
557
557
|
### Agent configuration
|
|
558
558
|
|
|
559
|
-
Each agent is defined by a YAML frontmatter file in `~/.clementine/agents/<slug>/agent.md`:
|
|
559
|
+
Each agent is defined by a YAML frontmatter file in `~/.clementine/vault/00-System/agents/<slug>/agent.md`:
|
|
560
560
|
|
|
561
561
|
| Field | Description |
|
|
562
562
|
|-------|-------------|
|
|
@@ -585,6 +585,26 @@ The dashboard's "The Office" page shows each agent as an animated desk station w
|
|
|
585
585
|
- Channel assignment, model badge, project badge, tool count
|
|
586
586
|
- Edit and "Let Go" (delete) actions
|
|
587
587
|
|
|
588
|
+
### Per-agent heartbeats
|
|
589
|
+
|
|
590
|
+
Each specialist (Ross / Sasha / your hires) gets their own autonomous heartbeat scheduler alongside Clementine's. The cycle:
|
|
591
|
+
|
|
592
|
+
1. **Cheap tick** every 30 min: load the agent's state, hash three signals (pending delegated tasks, latest goal update, latest cron run). If unchanged → silent tick, no LLM call, no cost.
|
|
593
|
+
2. **LLM tick** when a signal *changes* between ticks (a delegated task arrived, a goal moved, a cron deliverable to review): the scheduler invokes `assistant.heartbeat()` with the agent's profile. Output flows through their dedicated Discord bot to their channel.
|
|
594
|
+
3. **Self-adjusting cadence**: agents end their LLM-tick output with `[NEXT_CHECK: Xm]` to set when to check in next (5–720 min). Clamped at the bounds. Default 30m if omitted.
|
|
595
|
+
|
|
596
|
+
State per agent at `~/.clementine/heartbeat/agents/<slug>/state.json`. Live observability via:
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
curl -H "X-Token: $(cat ~/.clementine/.dashboard-token)" \
|
|
600
|
+
http://localhost:3030/api/agent-heartbeats | jq
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Routing rules — Clementine remains the master delegator:
|
|
604
|
+
- Inbox triage runs as Clementine, but she'll hand off via `team_message` when an item clearly belongs to a specialist (she's allowed to guess).
|
|
605
|
+
- Daily-plan goal-priorities owned by a specialist now fire goal-triggers (which run as the owner) instead of queueing as Clementine's work.
|
|
606
|
+
- Goal advancement triggers route to `goal.owner` automatically.
|
|
607
|
+
|
|
588
608
|
---
|
|
589
609
|
|
|
590
610
|
## Scheduled tasks & cron jobs
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -1049,6 +1049,55 @@ function getHeartbeat() {
|
|
|
1049
1049
|
return {};
|
|
1050
1050
|
}
|
|
1051
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Read per-agent heartbeat states from disk (one state.json per agent
|
|
1054
|
+
* under ~/.clementine/heartbeat/agents/<slug>/). Returns an array sorted
|
|
1055
|
+
* by next-due-soonest, with relative-time strings convenient for UI use.
|
|
1056
|
+
*/
|
|
1057
|
+
function getAgentHeartbeats() {
|
|
1058
|
+
const agentsRoot = path.join(BASE_DIR, 'heartbeat', 'agents');
|
|
1059
|
+
if (!existsSync(agentsRoot))
|
|
1060
|
+
return [];
|
|
1061
|
+
const out = [];
|
|
1062
|
+
let dirs = [];
|
|
1063
|
+
try {
|
|
1064
|
+
dirs = readdirSync(agentsRoot, { withFileTypes: true })
|
|
1065
|
+
.filter((d) => d.isDirectory())
|
|
1066
|
+
.map((d) => d.name);
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
return [];
|
|
1070
|
+
}
|
|
1071
|
+
const now = Date.now();
|
|
1072
|
+
for (const slug of dirs) {
|
|
1073
|
+
const stateFile = path.join(agentsRoot, slug, 'state.json');
|
|
1074
|
+
if (!existsSync(stateFile))
|
|
1075
|
+
continue;
|
|
1076
|
+
try {
|
|
1077
|
+
const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
1078
|
+
const lastTickMs = state.lastTickAt ? new Date(String(state.lastTickAt)).getTime() : 0;
|
|
1079
|
+
const nextCheckMs = state.nextCheckAt ? new Date(String(state.nextCheckAt)).getTime() : 0;
|
|
1080
|
+
out.push({
|
|
1081
|
+
slug,
|
|
1082
|
+
lastTickAt: state.lastTickAt ?? null,
|
|
1083
|
+
nextCheckAt: state.nextCheckAt ?? null,
|
|
1084
|
+
silentTickCount: Number(state.silentTickCount ?? 0),
|
|
1085
|
+
fingerprint: state.fingerprint ?? '',
|
|
1086
|
+
lastSignalSummary: state.lastSignalSummary ?? null,
|
|
1087
|
+
lastTickAgoMs: lastTickMs ? now - lastTickMs : null,
|
|
1088
|
+
nextCheckInMs: nextCheckMs ? nextCheckMs - now : null,
|
|
1089
|
+
isDue: nextCheckMs > 0 && nextCheckMs <= now,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
catch { /* skip malformed */ }
|
|
1093
|
+
}
|
|
1094
|
+
out.sort((a, b) => {
|
|
1095
|
+
const ai = typeof a.nextCheckInMs === 'number' ? a.nextCheckInMs : Number.MAX_SAFE_INTEGER;
|
|
1096
|
+
const bi = typeof b.nextCheckInMs === 'number' ? b.nextCheckInMs : Number.MAX_SAFE_INTEGER;
|
|
1097
|
+
return ai - bi;
|
|
1098
|
+
});
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1052
1101
|
async function getMemory() {
|
|
1053
1102
|
const memoryFile = path.join(VAULT_DIR, '00-System', 'MEMORY.md');
|
|
1054
1103
|
let content = '';
|
|
@@ -1925,6 +1974,9 @@ export async function cmdDashboard(opts) {
|
|
|
1925
1974
|
app.get('/api/heartbeat', (_req, res) => {
|
|
1926
1975
|
res.json(getHeartbeat());
|
|
1927
1976
|
});
|
|
1977
|
+
app.get('/api/agent-heartbeats', (_req, res) => {
|
|
1978
|
+
res.json(getAgentHeartbeats());
|
|
1979
|
+
});
|
|
1928
1980
|
app.get('/api/heartbeat/agent/:slug', (req, res) => {
|
|
1929
1981
|
const slug = req.params.slug;
|
|
1930
1982
|
const state = getHeartbeat();
|