clementine-agent 1.0.83 → 1.0.85
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/agent-manager.js +15 -0
- package/dist/cli/dashboard.js +73 -0
- package/dist/gateway/agent-heartbeat-manager.js +9 -18
- package/dist/gateway/agent-heartbeat-scheduler.d.ts +10 -0
- package/dist/gateway/agent-heartbeat-scheduler.js +90 -8
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
|
@@ -165,6 +165,20 @@ export class AgentManager {
|
|
|
165
165
|
catch { /* migration failed — continue with plaintext */ }
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
// Parse active_hours from frontmatter ("HH:MM-HH:MM" → decimal hours).
|
|
169
|
+
// Same-day windows only; midnight-crossing strings are ignored.
|
|
170
|
+
let activeHours;
|
|
171
|
+
const ahRaw = meta.active_hours ?? meta.activeHours;
|
|
172
|
+
if (typeof ahRaw === 'string') {
|
|
173
|
+
const m = ahRaw.match(/^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$/);
|
|
174
|
+
if (m) {
|
|
175
|
+
const start = Number(m[1]) + Number(m[2]) / 60;
|
|
176
|
+
const end = Number(m[3]) + Number(m[4]) / 60;
|
|
177
|
+
if (start < end && start >= 0 && end <= 24) {
|
|
178
|
+
activeHours = { start, end };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
168
182
|
// Parse sendPolicy from frontmatter
|
|
169
183
|
let sendPolicy;
|
|
170
184
|
if (meta.sendPolicy && typeof meta.sendPolicy === 'object') {
|
|
@@ -205,6 +219,7 @@ export class AgentManager {
|
|
|
205
219
|
status: (['active', 'paused', 'error', 'terminated'].includes(meta.status) ? meta.status : 'active'),
|
|
206
220
|
budgetMonthlyCents: meta.budgetMonthlyCents ? Number(meta.budgetMonthlyCents) : undefined,
|
|
207
221
|
strictMemoryIsolation: meta.strictMemoryIsolation === false ? false : true, // default true for all agents
|
|
222
|
+
activeHours,
|
|
208
223
|
};
|
|
209
224
|
}
|
|
210
225
|
// ── ProfileManager-compatible interface ───────────────────────────
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -1084,6 +1084,7 @@ function getAgentHeartbeats() {
|
|
|
1084
1084
|
silentTickCount: Number(state.silentTickCount ?? 0),
|
|
1085
1085
|
fingerprint: state.fingerprint ?? '',
|
|
1086
1086
|
lastSignalSummary: state.lastSignalSummary ?? null,
|
|
1087
|
+
lastTickKind: state.lastTickKind ?? null,
|
|
1087
1088
|
lastTickAgoMs: lastTickMs ? now - lastTickMs : null,
|
|
1088
1089
|
nextCheckInMs: nextCheckMs ? nextCheckMs - now : null,
|
|
1089
1090
|
isDue: nextCheckMs > 0 && nextCheckMs <= now,
|
|
@@ -8977,6 +8978,26 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
8977
8978
|
.desk-kpi-strip .dss-icon { font-size: 12px; opacity: 0.7; }
|
|
8978
8979
|
.desk-kpi-strip .dss-val { font-weight: 700; color: var(--accent); }
|
|
8979
8980
|
|
|
8981
|
+
/* ── Heartbeat Strip ────────────── */
|
|
8982
|
+
.desk-hb-strip {
|
|
8983
|
+
display: flex;
|
|
8984
|
+
justify-content: center;
|
|
8985
|
+
gap: 6px;
|
|
8986
|
+
padding: 2px 12px;
|
|
8987
|
+
font-size: 10px;
|
|
8988
|
+
color: var(--text-muted);
|
|
8989
|
+
min-height: 0;
|
|
8990
|
+
}
|
|
8991
|
+
.desk-hb-strip:empty { display: none; }
|
|
8992
|
+
.desk-hb-strip .dss-item {
|
|
8993
|
+
display: flex;
|
|
8994
|
+
align-items: center;
|
|
8995
|
+
gap: 4px;
|
|
8996
|
+
white-space: nowrap;
|
|
8997
|
+
}
|
|
8998
|
+
.desk-hb-strip .dss-icon { font-size: 11px; opacity: 0.85; }
|
|
8999
|
+
.desk-hb-strip .dss-val { font-weight: 600; color: var(--text-secondary); }
|
|
9000
|
+
|
|
8980
9001
|
/* ── Health Badge ──────────────────── */
|
|
8981
9002
|
.desk-health-badge {
|
|
8982
9003
|
display: none;
|
|
@@ -17272,6 +17293,9 @@ async function refreshTeam() {
|
|
|
17272
17293
|
'</details>';
|
|
17273
17294
|
}
|
|
17274
17295
|
|
|
17296
|
+
// Heartbeat strip — last/next tick, populated async from /api/agent-heartbeats
|
|
17297
|
+
var hbStrip = '<div class="desk-hb-strip" id="hb-' + a.slug + '"></div>';
|
|
17298
|
+
|
|
17275
17299
|
// SDR KPI strip (fetched async, populated by data attribute)
|
|
17276
17300
|
var kpiStrip = '<div class="desk-kpi-strip" id="kpi-' + a.slug + '"></div>';
|
|
17277
17301
|
|
|
@@ -17299,6 +17323,7 @@ async function refreshTeam() {
|
|
|
17299
17323
|
(actions ? '<div class="desk-actions">' + actions + '</div>' : '') +
|
|
17300
17324
|
'</div>' +
|
|
17301
17325
|
statsStrip +
|
|
17326
|
+
hbStrip +
|
|
17302
17327
|
kpiStrip +
|
|
17303
17328
|
cronDetails +
|
|
17304
17329
|
'</div>';
|
|
@@ -17312,6 +17337,54 @@ async function refreshTeam() {
|
|
|
17312
17337
|
|
|
17313
17338
|
grid.innerHTML = cards.join('');
|
|
17314
17339
|
|
|
17340
|
+
// Heartbeat strip — single batch fetch, populate per-agent (avoids N requests).
|
|
17341
|
+
apiFetch('/api/agent-heartbeats').then(function(r) { return r.json(); }).then(function(hbList) {
|
|
17342
|
+
if (!Array.isArray(hbList)) return;
|
|
17343
|
+
var byAgent = {};
|
|
17344
|
+
for (var i = 0; i < hbList.length; i++) byAgent[hbList[i].slug] = hbList[i];
|
|
17345
|
+
agents.forEach(function(a) {
|
|
17346
|
+
var hb = byAgent[a.slug];
|
|
17347
|
+
var el = document.getElementById('hb-' + a.slug);
|
|
17348
|
+
if (!el || !hb) return;
|
|
17349
|
+
// "in 12 min" / "due now" / "10s ago tick"
|
|
17350
|
+
var nextLabel;
|
|
17351
|
+
if (hb.isDue) {
|
|
17352
|
+
nextLabel = 'due now';
|
|
17353
|
+
} else if (typeof hb.nextCheckInMs === 'number') {
|
|
17354
|
+
var mins = Math.max(1, Math.round(hb.nextCheckInMs / 60000));
|
|
17355
|
+
nextLabel = mins < 60 ? 'in ' + mins + 'm' : 'in ' + Math.floor(mins / 60) + 'h';
|
|
17356
|
+
} else {
|
|
17357
|
+
nextLabel = '—';
|
|
17358
|
+
}
|
|
17359
|
+
var lastLabel = hb.lastTickAt ? fmtTimeAgo(hb.lastTickAt) : 'never';
|
|
17360
|
+
// Tick-kind icon — small visual signal
|
|
17361
|
+
var kindIcon = '';
|
|
17362
|
+
var kindTitle = '';
|
|
17363
|
+
switch (hb.lastSignalSummary && hb.lastSignalSummary.indexOf('llm tick error') === 0
|
|
17364
|
+
? 'error' : '') {
|
|
17365
|
+
case 'error':
|
|
17366
|
+
kindIcon = '⚠'; kindTitle = 'Last tick errored — fell back to quiet cadence';
|
|
17367
|
+
break;
|
|
17368
|
+
}
|
|
17369
|
+
// Use a literal field if available — backend exposes lastTickKind via state file
|
|
17370
|
+
// (we read it through getAgentHeartbeats which preserves the field)
|
|
17371
|
+
if (!kindIcon) {
|
|
17372
|
+
switch (hb.lastTickKind) {
|
|
17373
|
+
case 'acted': kindIcon = '⚡'; kindTitle = 'Active — agent took action last tick'; break;
|
|
17374
|
+
case 'quiet': kindIcon = '◌'; kindTitle = 'Quiet — agent saw signals but had nothing to do'; break;
|
|
17375
|
+
case 'silent': kindIcon = '·'; kindTitle = 'Silent — fingerprint unchanged, backing off'; break;
|
|
17376
|
+
case 'override': kindIcon = '◇'; kindTitle = 'Override — cadence set explicitly by the agent'; break;
|
|
17377
|
+
default: kindIcon = '·'; kindTitle = 'No tick recorded yet';
|
|
17378
|
+
}
|
|
17379
|
+
}
|
|
17380
|
+
el.innerHTML =
|
|
17381
|
+
'<span class="dss-item" title="' + kindTitle + '">' +
|
|
17382
|
+
'<span class="dss-icon">' + kindIcon + '</span> ' +
|
|
17383
|
+
'<span class="dss-val">' + lastLabel + '</span> · next ' + nextLabel +
|
|
17384
|
+
'</span>';
|
|
17385
|
+
});
|
|
17386
|
+
}).catch(function() { /* heartbeat endpoint missing or daemon old — silently skip */ });
|
|
17387
|
+
|
|
17315
17388
|
// Async-fetch KPIs and health for each agent
|
|
17316
17389
|
agents.forEach(function(a) {
|
|
17317
17390
|
// KPI strip
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* out. Per-agent failures are caught so one buggy agent can't crash the
|
|
11
11
|
* daemon or stall others.
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync, mkdirSync, watch } from 'node:fs';
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, watch } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import pino from 'pino';
|
|
16
16
|
import { AGENTS_DIR, BASE_DIR } from '../config.js';
|
|
@@ -107,22 +107,15 @@ export class AgentHeartbeatManager {
|
|
|
107
107
|
if (!filename.endsWith('.json'))
|
|
108
108
|
return;
|
|
109
109
|
const slug = filename.replace(/\.json$/, '');
|
|
110
|
-
// Consume the sentinel + wake the agent
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// best-effort: import unlinkSync inline
|
|
117
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
118
|
-
const fs = require('node:fs');
|
|
119
|
-
try {
|
|
120
|
-
fs.unlinkSync(sentinelPath);
|
|
121
|
-
}
|
|
122
|
-
catch { /* ignore */ }
|
|
110
|
+
// Consume the sentinel + wake the agent. Best-effort delete so the
|
|
111
|
+
// same sentinel can't fire repeatedly.
|
|
112
|
+
const sentinelPath = path.join(wakeDir, filename);
|
|
113
|
+
if (existsSync(sentinelPath)) {
|
|
114
|
+
try {
|
|
115
|
+
unlinkSync(sentinelPath);
|
|
123
116
|
}
|
|
117
|
+
catch { /* ignore */ }
|
|
124
118
|
}
|
|
125
|
-
catch { /* non-fatal */ }
|
|
126
119
|
this.scheduleWake(slug, 'wake-sentinel');
|
|
127
120
|
});
|
|
128
121
|
}
|
|
@@ -197,9 +190,7 @@ export class AgentHeartbeatManager {
|
|
|
197
190
|
const triggerPath = path.join(BASE_DIR, 'cron', 'goal-triggers', filename);
|
|
198
191
|
if (!existsSync(triggerPath))
|
|
199
192
|
return; // file was already consumed by cron-scheduler
|
|
200
|
-
|
|
201
|
-
const fs = require('node:fs');
|
|
202
|
-
const trigger = JSON.parse(fs.readFileSync(triggerPath, 'utf-8'));
|
|
193
|
+
const trigger = JSON.parse(readFileSync(triggerPath, 'utf-8'));
|
|
203
194
|
if (!trigger.goalId)
|
|
204
195
|
return;
|
|
205
196
|
const lookup = listAllGoals().find((g) => g.goal && g.goal.id === trigger.goalId);
|
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { AgentHeartbeatState } from '../types.js';
|
|
14
14
|
import type { AgentManager } from '../agent/agent-manager.js';
|
|
15
|
+
/**
|
|
16
|
+
* Compute the next-check interval (minutes) for an upcoming tick. Pure
|
|
17
|
+
* function of the inputs — exported for tests.
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeNextInterval(opts: {
|
|
20
|
+
kind: 'acted' | 'quiet' | 'silent' | 'override';
|
|
21
|
+
silentStreak: number;
|
|
22
|
+
isActiveHours: boolean;
|
|
23
|
+
overrideMin?: number;
|
|
24
|
+
}): number;
|
|
15
25
|
/**
|
|
16
26
|
* Minimal gateway surface the scheduler needs for the LLM tick path.
|
|
17
27
|
* Kept narrow so tests can mock it without pulling in the full Gateway.
|
|
@@ -20,6 +20,50 @@ const logger = pino({ name: 'clementine.agent-heartbeat' });
|
|
|
20
20
|
const DEFAULT_INTERVAL_MIN = 30;
|
|
21
21
|
const MIN_INTERVAL_MIN = 5;
|
|
22
22
|
const MAX_INTERVAL_MIN = 12 * 60;
|
|
23
|
+
// ── Adaptive cadence ─────────────────────────────────────────────────
|
|
24
|
+
// Tick-outcome → next-check interval (minutes). Plain numbers tuned for
|
|
25
|
+
// the typical "specialist running on a goal" workload — fast when active,
|
|
26
|
+
// gradually slower when idle.
|
|
27
|
+
const ACTED_INTERVAL_MIN = 10;
|
|
28
|
+
const QUIET_INTERVAL_MIN = 60;
|
|
29
|
+
// Exponential backoff schedule for consecutive silent ticks. Index = streak
|
|
30
|
+
// length (capped at last entry). 0 silent → unused; 1st → 30, 2nd → 60, ...
|
|
31
|
+
const SILENT_BACKOFF_MIN = [30, 60, 120, 240, 480, 720];
|
|
32
|
+
// Off-hours multiplier. Applied after kind-based interval, capped at MAX.
|
|
33
|
+
const OFF_HOURS_MULTIPLIER = 4;
|
|
34
|
+
// Default active hours when an agent's profile doesn't specify
|
|
35
|
+
const DEFAULT_ACTIVE_START_HOUR = 8;
|
|
36
|
+
const DEFAULT_ACTIVE_END_HOUR = 22;
|
|
37
|
+
/**
|
|
38
|
+
* Compute the next-check interval (minutes) for an upcoming tick. Pure
|
|
39
|
+
* function of the inputs — exported for tests.
|
|
40
|
+
*/
|
|
41
|
+
export function computeNextInterval(opts) {
|
|
42
|
+
if (opts.kind === 'override' && typeof opts.overrideMin === 'number') {
|
|
43
|
+
// User-set override always wins, still clamped to bounds.
|
|
44
|
+
return Math.max(MIN_INTERVAL_MIN, Math.min(MAX_INTERVAL_MIN, Math.floor(opts.overrideMin)));
|
|
45
|
+
}
|
|
46
|
+
let base;
|
|
47
|
+
if (opts.kind === 'acted') {
|
|
48
|
+
base = ACTED_INTERVAL_MIN;
|
|
49
|
+
}
|
|
50
|
+
else if (opts.kind === 'quiet') {
|
|
51
|
+
base = QUIET_INTERVAL_MIN;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// silent
|
|
55
|
+
const idx = Math.min(Math.max(0, opts.silentStreak - 1), SILENT_BACKOFF_MIN.length - 1);
|
|
56
|
+
base = SILENT_BACKOFF_MIN[idx];
|
|
57
|
+
}
|
|
58
|
+
if (!opts.isActiveHours)
|
|
59
|
+
base *= OFF_HOURS_MULTIPLIER;
|
|
60
|
+
return Math.max(MIN_INTERVAL_MIN, Math.min(MAX_INTERVAL_MIN, base));
|
|
61
|
+
}
|
|
62
|
+
function isWithinActiveHours(now, profile) {
|
|
63
|
+
const hours = profile.activeHours ?? { start: DEFAULT_ACTIVE_START_HOUR, end: DEFAULT_ACTIVE_END_HOUR };
|
|
64
|
+
const localHour = now.getHours() + now.getMinutes() / 60;
|
|
65
|
+
return localHour >= hours.start && localHour < hours.end;
|
|
66
|
+
}
|
|
23
67
|
export class AgentHeartbeatScheduler {
|
|
24
68
|
slug;
|
|
25
69
|
agentManager;
|
|
@@ -40,6 +84,10 @@ export class AgentHeartbeatScheduler {
|
|
|
40
84
|
try {
|
|
41
85
|
if (existsSync(this.stateFile)) {
|
|
42
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;
|
|
43
91
|
return {
|
|
44
92
|
slug: this.slug,
|
|
45
93
|
lastTickAt: String(raw.lastTickAt ?? ''),
|
|
@@ -47,6 +95,7 @@ export class AgentHeartbeatScheduler {
|
|
|
47
95
|
silentTickCount: Number(raw.silentTickCount ?? 0),
|
|
48
96
|
fingerprint: String(raw.fingerprint ?? ''),
|
|
49
97
|
...(raw.lastSignalSummary ? { lastSignalSummary: raw.lastSignalSummary } : {}),
|
|
98
|
+
...(kind ? { lastTickKind: kind } : {}),
|
|
50
99
|
};
|
|
51
100
|
}
|
|
52
101
|
}
|
|
@@ -190,18 +239,33 @@ export class AgentHeartbeatScheduler {
|
|
|
190
239
|
const prior = this.loadState();
|
|
191
240
|
const { fingerprint, signals } = this.buildFingerprint();
|
|
192
241
|
const changed = fingerprint !== prior.fingerprint;
|
|
193
|
-
let nextCheckMinutes = DEFAULT_INTERVAL_MIN;
|
|
194
242
|
let lastSignalSummary;
|
|
243
|
+
let llmResult = {
|
|
244
|
+
nextCheckMinutes: undefined,
|
|
245
|
+
summary: '',
|
|
246
|
+
ranLlm: false,
|
|
247
|
+
tookAction: false,
|
|
248
|
+
};
|
|
195
249
|
const shouldRunLlm = changed && prior.fingerprint !== '' && this.gateway !== null;
|
|
196
250
|
if (shouldRunLlm) {
|
|
197
251
|
try {
|
|
198
252
|
const result = await this.runLlmTick(profile, signals, prior, now);
|
|
199
|
-
|
|
200
|
-
|
|
253
|
+
// Heuristic: a meaningful response means the agent took action. Short
|
|
254
|
+
// "all quiet" / "[ALL_QUIET]" responses don't count.
|
|
255
|
+
const summary = result.summary ?? '';
|
|
256
|
+
const tookAction = summary.length > 200 && !/\[ALL_QUIET\]/i.test(summary);
|
|
257
|
+
llmResult = {
|
|
258
|
+
nextCheckMinutes: result.nextCheckMinutes,
|
|
259
|
+
summary,
|
|
260
|
+
ranLlm: true,
|
|
261
|
+
tookAction,
|
|
262
|
+
};
|
|
263
|
+
lastSignalSummary = summary.slice(0, 240);
|
|
201
264
|
}
|
|
202
265
|
catch (err) {
|
|
203
266
|
logger.warn({ err, slug: this.slug }, 'Agent LLM tick failed — using default cadence');
|
|
204
267
|
lastSignalSummary = `llm tick error: ${String(err).slice(0, 200)}`;
|
|
268
|
+
llmResult.ranLlm = true; // we attempted, treat as 'quiet' for backoff
|
|
205
269
|
}
|
|
206
270
|
}
|
|
207
271
|
else if (changed) {
|
|
@@ -210,22 +274,40 @@ export class AgentHeartbeatScheduler {
|
|
|
210
274
|
else {
|
|
211
275
|
lastSignalSummary = prior.lastSignalSummary;
|
|
212
276
|
}
|
|
213
|
-
|
|
214
|
-
const
|
|
277
|
+
// Classify outcome → drives adaptive cadence.
|
|
278
|
+
const newSilentStreak = changed ? 0 : prior.silentTickCount + 1;
|
|
279
|
+
const tickKind = (() => {
|
|
280
|
+
if (typeof llmResult.nextCheckMinutes === 'number')
|
|
281
|
+
return 'override';
|
|
282
|
+
if (!changed)
|
|
283
|
+
return 'silent';
|
|
284
|
+
if (llmResult.ranLlm && llmResult.tookAction)
|
|
285
|
+
return 'acted';
|
|
286
|
+
return 'quiet';
|
|
287
|
+
})();
|
|
288
|
+
const isActive = isWithinActiveHours(now, profile);
|
|
289
|
+
const computedMin = computeNextInterval({
|
|
290
|
+
kind: tickKind,
|
|
291
|
+
silentStreak: newSilentStreak,
|
|
292
|
+
isActiveHours: isActive,
|
|
293
|
+
overrideMin: llmResult.nextCheckMinutes,
|
|
294
|
+
});
|
|
295
|
+
const next = new Date(now.getTime() + computedMin * 60_000);
|
|
215
296
|
const state = {
|
|
216
297
|
slug: this.slug,
|
|
217
298
|
lastTickAt: now.toISOString(),
|
|
218
299
|
nextCheckAt: next.toISOString(),
|
|
219
|
-
silentTickCount:
|
|
300
|
+
silentTickCount: newSilentStreak,
|
|
220
301
|
fingerprint,
|
|
302
|
+
lastTickKind: tickKind,
|
|
221
303
|
...(lastSignalSummary ? { lastSignalSummary } : {}),
|
|
222
304
|
};
|
|
223
305
|
this.saveState(state);
|
|
224
306
|
if (changed) {
|
|
225
|
-
logger.info({ slug: this.slug, signals, fingerprint, ranLlm: shouldRunLlm, nextCheckMin:
|
|
307
|
+
logger.info({ slug: this.slug, signals, fingerprint, ranLlm: shouldRunLlm, kind: tickKind, nextCheckMin: computedMin, isActive }, 'Agent heartbeat tick');
|
|
226
308
|
}
|
|
227
309
|
else {
|
|
228
|
-
logger.debug({ slug: this.slug, silentTicks: state.silentTickCount }, 'Agent heartbeat: silent tick');
|
|
310
|
+
logger.debug({ slug: this.slug, silentTicks: state.silentTickCount, nextCheckMin: computedMin, isActive }, 'Agent heartbeat: silent tick');
|
|
229
311
|
}
|
|
230
312
|
return state;
|
|
231
313
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -183,6 +183,17 @@ export interface AgentProfile {
|
|
|
183
183
|
budgetMonthlyCents?: number;
|
|
184
184
|
spentMonthlyCents?: number;
|
|
185
185
|
strictMemoryIsolation?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* Active-hours window for adaptive heartbeat cadence. Decimal hours in
|
|
188
|
+
* the local timezone, e.g., { start: 8, end: 18 } = 8:00am–6:00pm.
|
|
189
|
+
* When the current time is outside this window, the agent's next-check
|
|
190
|
+
* interval is multiplied by 4. Parsed from `active_hours: "HH:MM-HH:MM"`
|
|
191
|
+
* in agent.md frontmatter; same-day windows only.
|
|
192
|
+
*/
|
|
193
|
+
activeHours?: {
|
|
194
|
+
start: number;
|
|
195
|
+
end: number;
|
|
196
|
+
};
|
|
186
197
|
}
|
|
187
198
|
export type AgentStatus = 'active' | 'paused' | 'error' | 'terminated';
|
|
188
199
|
export interface HeartbeatReportedTopic {
|
|
@@ -240,6 +251,15 @@ export interface AgentHeartbeatState {
|
|
|
240
251
|
silentTickCount: number;
|
|
241
252
|
fingerprint: string;
|
|
242
253
|
lastSignalSummary?: string;
|
|
254
|
+
/**
|
|
255
|
+
* Outcome of the last tick. Drives adaptive cadence:
|
|
256
|
+
* - 'acted' → next check at active-mode interval (10 min default)
|
|
257
|
+
* - 'quiet' → next check at quiet interval (60 min)
|
|
258
|
+
* - 'silent' → exponential backoff (30 → 60 → 120 → 240 → 480, capped)
|
|
259
|
+
* - 'override' → agent explicitly set [NEXT_CHECK: Xm], honored as-is
|
|
260
|
+
* - undefined → first tick or pre-1.0.84 state
|
|
261
|
+
*/
|
|
262
|
+
lastTickKind?: 'acted' | 'quiet' | 'silent' | 'override';
|
|
243
263
|
}
|
|
244
264
|
export interface CronJobDefinition {
|
|
245
265
|
name: string;
|