clementine-agent 1.0.76 → 1.0.78

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.
@@ -5,6 +5,15 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
5
5
  import path from 'node:path';
6
6
  import { z } from 'zod';
7
7
  import { BASE_DIR, HANDOFFS_DIR, INBOX_DIR, TASKS_FILE, logger, textResult, todayStr, listAllGoals, } from './shared.js';
8
+ import { decideDiscoveredWorkItem } from '../agent/proactive-engine.js';
9
+ import { recentDecisions } from '../agent/proactive-ledger.js';
10
+ const ACTION_LABEL = {
11
+ act_now: 'Doing now',
12
+ queue: 'Queued',
13
+ ask_user: 'Needs you',
14
+ snooze: 'Snoozed',
15
+ ignore: 'Skipped',
16
+ };
8
17
  function ensureHandoffsDir() {
9
18
  if (!existsSync(HANDOFFS_DIR))
10
19
  mkdirSync(HANDOFFS_DIR, { recursive: true });
@@ -78,7 +87,7 @@ export function registerSessionTools(server) {
78
87
  const staleThreshold = goal.reviewFrequency === 'daily' ? 1 : goal.reviewFrequency === 'weekly' ? 7 : 30;
79
88
  if (daysSinceUpdate > staleThreshold) {
80
89
  const urgency = Math.min(5, Math.floor(daysSinceUpdate / staleThreshold) + (goal.priority === 'high' ? 2 : goal.priority === 'medium' ? 1 : 0));
81
- items.push({ type: 'stale-goal', urgency, description: `Goal "${goal.title}" stale for ${daysSinceUpdate}d (${goal.priority} priority)` });
90
+ items.push({ type: 'stale-goal', id: goal.id, urgency, description: `Goal "${goal.title}" stale for ${daysSinceUpdate}d (${goal.priority} priority)` });
82
91
  }
83
92
  }
84
93
  // 2. Failing cron jobs
@@ -92,7 +101,7 @@ export function registerSessionTools(server) {
92
101
  const errCount = consecutiveErrors === -1 ? recent.length : consecutiveErrors;
93
102
  if (errCount >= 2) {
94
103
  const jobName = recent[0]?.jobName ?? f.replace('.jsonl', '');
95
- items.push({ type: 'cron-failure', urgency: Math.min(5, errCount), description: `Job "${jobName}" has ${errCount} consecutive failures` });
104
+ items.push({ type: 'cron-failure', id: jobName, urgency: Math.min(5, errCount), description: `Job "${jobName}" has ${errCount} consecutive failures` });
96
105
  }
97
106
  }
98
107
  catch {
@@ -107,13 +116,13 @@ export function registerSessionTools(server) {
107
116
  const overdue = (content.match(/- \[ \].*?📅\s*(\d{4}-\d{2}-\d{2})/g) ?? [])
108
117
  .filter(line => { const m = line.match(/📅\s*(\d{4}-\d{2}-\d{2})/); return m && m[1] < today; });
109
118
  if (overdue.length > 0)
110
- items.push({ type: 'overdue-tasks', urgency: 4, description: `${overdue.length} overdue task(s) in TASKS.md` });
119
+ items.push({ type: 'overdue-tasks', id: 'tasks-overdue', urgency: 4, description: `${overdue.length} overdue task(s) in TASKS.md` });
111
120
  }
112
121
  // 4. Inbox items
113
122
  if (existsSync(INBOX_DIR)) {
114
123
  const inboxFiles = readdirSync(INBOX_DIR).filter(f => f.endsWith('.md') && !f.startsWith('_'));
115
124
  if (inboxFiles.length > 0)
116
- items.push({ type: 'inbox', urgency: 2, description: `${inboxFiles.length} unprocessed inbox item(s)` });
125
+ items.push({ type: 'inbox', id: 'inbox', urgency: 2, description: `${inboxFiles.length} unprocessed inbox item(s)` });
117
126
  }
118
127
  // 5. Daily plan priorities
119
128
  const planFile = path.join(BASE_DIR, 'plans', 'daily', `${todayStr()}.json`);
@@ -123,7 +132,7 @@ export function registerSessionTools(server) {
123
132
  for (const p of (plan.priorities ?? []).slice(0, 5)) {
124
133
  const alreadyListed = items.some(i => i.description.includes(p.id) || i.description.includes(p.action?.slice(0, 20)));
125
134
  if (!alreadyListed)
126
- items.push({ type: `plan-${p.type}`, urgency: p.urgency ?? 3, description: `[From daily plan] ${p.action}` });
135
+ items.push({ type: `plan-${p.type}`, id: p.id, urgency: p.urgency ?? 3, description: `[From daily plan] ${p.action}` });
127
136
  }
128
137
  }
129
138
  catch { /* non-fatal */ }
@@ -132,8 +141,56 @@ export function registerSessionTools(server) {
132
141
  const topItems = items.slice(0, maxItems);
133
142
  if (topItems.length === 0)
134
143
  return textResult('No work items discovered. All goals on track, no failures, inbox clear.');
135
- const lines = topItems.map(i => `- [${i.type}] Urgency ${i.urgency}/5: ${i.description}`);
144
+ // Decisions here are advisory the agent surveys, then chooses whether
145
+ // to act. We deliberately do NOT recordDecision: the autonomous paths
146
+ // (processInbox, daily-plan loop, goal advancement) record their own
147
+ // decisions when they actually act, so the ledger reflects committed
148
+ // decisions and proactive_stats stays signal-rich.
149
+ const lines = topItems.map((i) => {
150
+ const decision = decideDiscoveredWorkItem(i);
151
+ const label = ACTION_LABEL[decision.action];
152
+ return `- [${i.type}] Urgency ${i.urgency}/5 → ${label}: ${i.description}`;
153
+ });
136
154
  return textResult(`## Discovered Work Items (${topItems.length})\n${lines.join('\n')}`);
137
155
  });
156
+ server.tool('proactive_stats', 'Report how the proactive engine has been deciding and what those decisions led to. Use to tune urgency thresholds.', {
157
+ since_hours: z.number().optional().describe('Window in hours (default: 168 = 7 days)'),
158
+ }, async ({ since_hours }) => {
159
+ const windowMs = (since_hours ?? 168) * 60 * 60 * 1000;
160
+ const records = recentDecisions({ sinceMs: windowMs }, undefined);
161
+ if (records.length === 0)
162
+ return textResult('No proactive decisions recorded in window.');
163
+ const bySource = new Map();
164
+ const outcomeByKey = new Map();
165
+ for (const r of records) {
166
+ if (r.outcome)
167
+ outcomeByKey.set(r.decision.idempotencyKey, r.outcome.status);
168
+ }
169
+ for (const r of records) {
170
+ if (r.outcome)
171
+ continue;
172
+ const sourceMap = bySource.get(r.decision.source) ?? new Map();
173
+ const bucket = sourceMap.get(r.decision.action) ?? { decided: 0, outcomes: {} };
174
+ bucket.decided++;
175
+ const outcome = outcomeByKey.get(r.decision.idempotencyKey);
176
+ if (outcome)
177
+ bucket.outcomes[outcome] = (bucket.outcomes[outcome] ?? 0) + 1;
178
+ sourceMap.set(r.decision.action, bucket);
179
+ bySource.set(r.decision.source, sourceMap);
180
+ }
181
+ const lines = [`## Proactive Stats (last ${since_hours ?? 168}h, ${records.length} records)\n`];
182
+ for (const [source, actionMap] of bySource) {
183
+ lines.push(`### ${source}`);
184
+ for (const [action, bucket] of actionMap) {
185
+ const outcomes = Object.entries(bucket.outcomes);
186
+ const outcomeStr = outcomes.length === 0
187
+ ? 'no outcomes recorded'
188
+ : outcomes.map(([k, v]) => `${k}=${v}`).join(', ');
189
+ lines.push(`- ${ACTION_LABEL[action]} (${action}): decided ${bucket.decided}× → ${outcomeStr}`);
190
+ }
191
+ lines.push('');
192
+ }
193
+ return textResult(lines.join('\n'));
194
+ });
138
195
  }
139
196
  //# sourceMappingURL=session-tools.js.map
package/dist/types.d.ts CHANGED
@@ -210,6 +210,7 @@ export interface HeartbeatWorkItem {
210
210
  description: string;
211
211
  prompt: string;
212
212
  source: string;
213
+ idempotencyKey?: string;
213
214
  priority: 'high' | 'normal';
214
215
  queuedAt: string;
215
216
  maxTurns: number;
@@ -220,6 +221,19 @@ export interface HeartbeatWorkItem {
220
221
  error?: string;
221
222
  agentSlug?: string;
222
223
  }
224
+ /**
225
+ * State for one specialist agent's heartbeat scheduler. Persisted at
226
+ * ~/.clementine/heartbeat/agents/<slug>/state.json. Manager reads
227
+ * `nextCheckAt` to decide whether the agent is due for a tick.
228
+ */
229
+ export interface AgentHeartbeatState {
230
+ slug: string;
231
+ lastTickAt: string;
232
+ nextCheckAt: string;
233
+ silentTickCount: number;
234
+ fingerprint: string;
235
+ lastSignalSummary?: string;
236
+ }
223
237
  export interface CronJobDefinition {
224
238
  name: string;
225
239
  schedule: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.76",
3
+ "version": "1.0.78",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",