clementine-agent 1.0.82 → 1.0.84
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/channels/discord.js +1 -1
- package/dist/gateway/agent-heartbeat-scheduler.d.ts +10 -0
- package/dist/gateway/agent-heartbeat-scheduler.js +90 -8
- package/dist/gateway/router.d.ts +2 -2
- package/dist/gateway/router.js +30 -3
- package/dist/types.d.ts +27 -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/channels/discord.js
CHANGED
|
@@ -1122,7 +1122,7 @@ export async function startDiscord(gateway, heartbeat, cronScheduler, dispatcher
|
|
|
1122
1122
|
const streamer = new DiscordStreamingMessage(message.channel);
|
|
1123
1123
|
await streamer.start();
|
|
1124
1124
|
try {
|
|
1125
|
-
const response = await gateway.handleMessage(sessionKey, effectiveText, (t) => streamer.update(t), oneOffModel, oneOffMaxTurns, (toolName, toolInput) => { streamer.setToolStatus(friendlyToolName(toolName, toolInput)); return Promise.resolve(); });
|
|
1125
|
+
const response = await gateway.handleMessage(sessionKey, effectiveText, (t) => streamer.update(t), oneOffModel, oneOffMaxTurns, (toolName, toolInput) => { streamer.setToolStatus(friendlyToolName(toolName, toolInput)); return Promise.resolve(); }, (status) => { streamer.setToolStatus(status); return Promise.resolve(); });
|
|
1126
1126
|
await streamer.finalize(response);
|
|
1127
1127
|
updatePresence(sessionKey);
|
|
1128
1128
|
// Track bot message for feedback reactions
|
|
@@ -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/gateway/router.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Manages per-user/channel sessions for conversation continuity.
|
|
6
6
|
*/
|
|
7
7
|
import { PersonalAssistant, type ProjectMeta } from '../agent/assistant.js';
|
|
8
|
-
import type { OnTextCallback, OnToolActivityCallback, PlanProgressUpdate, PlanStep, SelfImproveConfig, SelfImproveExperiment, SessionProvenance, TeamMessage, VerboseLevel, WorkflowDefinition } from '../types.js';
|
|
8
|
+
import type { OnProgressCallback, OnTextCallback, OnToolActivityCallback, PlanProgressUpdate, PlanStep, SelfImproveConfig, SelfImproveExperiment, SessionProvenance, TeamMessage, VerboseLevel, WorkflowDefinition } from '../types.js';
|
|
9
9
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
10
10
|
import { TeamRouter } from '../agent/team-router.js';
|
|
11
11
|
import { TeamBus } from '../agent/team-bus.js';
|
|
@@ -149,7 +149,7 @@ export declare class Gateway {
|
|
|
149
149
|
* or correct the agent mid-response instead of queuing behind a long query.
|
|
150
150
|
*/
|
|
151
151
|
private acquireSessionLock;
|
|
152
|
-
handleMessage(sessionKey: string, text: string, onText?: OnTextCallback, model?: string, maxTurns?: number, onToolActivity?: OnToolActivityCallback): Promise<string>;
|
|
152
|
+
handleMessage(sessionKey: string, text: string, onText?: OnTextCallback, model?: string, maxTurns?: number, onToolActivity?: OnToolActivityCallback, onProgress?: OnProgressCallback): Promise<string>;
|
|
153
153
|
private _handleMessageInner;
|
|
154
154
|
handleHeartbeat(standingInstructions: string, changesSummary?: string, timeContext?: string, dedupContext?: string, profile?: import('../types.js').AgentProfile | null): Promise<string>;
|
|
155
155
|
handleCronJob(jobName: string, jobPrompt: string, tier?: number, maxTurns?: number, model?: string, workDir?: string, mode?: 'standard' | 'unleashed', maxHours?: number, timeoutMs?: number, successCriteria?: string[], agentSlug?: string): Promise<string>;
|
package/dist/gateway/router.js
CHANGED
|
@@ -691,7 +691,7 @@ export class Gateway {
|
|
|
691
691
|
};
|
|
692
692
|
}
|
|
693
693
|
// ── Message handling ────────────────────────────────────────────────
|
|
694
|
-
async handleMessage(sessionKey, text, onText, model, maxTurns, onToolActivity) {
|
|
694
|
+
async handleMessage(sessionKey, text, onText, model, maxTurns, onToolActivity, onProgress) {
|
|
695
695
|
if (this.draining) {
|
|
696
696
|
return "I'm restarting momentarily — your message will be processed after I'm back online.";
|
|
697
697
|
}
|
|
@@ -713,7 +713,7 @@ export class Gateway {
|
|
|
713
713
|
text_len: text.length,
|
|
714
714
|
});
|
|
715
715
|
try {
|
|
716
|
-
const result = await this._handleMessageInner(sessionKey, text, onText, model, maxTurns, onToolActivity);
|
|
716
|
+
const result = await this._handleMessageInner(sessionKey, text, onText, model, maxTurns, onToolActivity, onProgress);
|
|
717
717
|
logAuditJsonl({
|
|
718
718
|
event_type: 'message_completed',
|
|
719
719
|
duration_ms: Date.now() - traceStart,
|
|
@@ -731,7 +731,7 @@ export class Gateway {
|
|
|
731
731
|
}
|
|
732
732
|
});
|
|
733
733
|
}
|
|
734
|
-
async _handleMessageInner(sessionKey, text, onText, model, maxTurns, onToolActivity) {
|
|
734
|
+
async _handleMessageInner(sessionKey, text, onText, model, maxTurns, onToolActivity, onProgress) {
|
|
735
735
|
// ── Auth circuit breaker — stop spamming error messages ────────
|
|
736
736
|
if (this.authCircuitOpen) {
|
|
737
737
|
if (!this.shouldProbeAuth()) {
|
|
@@ -743,10 +743,31 @@ export class Gateway {
|
|
|
743
743
|
// Allow this one message through as a probe to see if auth recovered
|
|
744
744
|
logger.info({ sessionKey }, 'Auth circuit open — allowing probe message');
|
|
745
745
|
}
|
|
746
|
+
// Show "queued" status if either lane or session lock is contended,
|
|
747
|
+
// so the user doesn't stare at "thinking..." for up to 60s while a
|
|
748
|
+
// previous message is still processing.
|
|
749
|
+
const laneWaitStart = Date.now();
|
|
750
|
+
let queuedStatusShown = false;
|
|
751
|
+
const queuedTimer = onProgress
|
|
752
|
+
? setTimeout(() => {
|
|
753
|
+
queuedStatusShown = true;
|
|
754
|
+
onProgress('waiting for previous message to finish...').catch(() => { });
|
|
755
|
+
}, 750)
|
|
756
|
+
: null;
|
|
746
757
|
const releaseLane = await lanes.acquire('chat');
|
|
758
|
+
if (queuedTimer)
|
|
759
|
+
clearTimeout(queuedTimer);
|
|
747
760
|
try {
|
|
748
761
|
const release = await this.acquireSessionLock(sessionKey);
|
|
749
762
|
try {
|
|
763
|
+
if (queuedStatusShown && onProgress) {
|
|
764
|
+
// Lane was busy — clear the wait notice now that we're moving
|
|
765
|
+
await onProgress('thinking...').catch(() => { });
|
|
766
|
+
}
|
|
767
|
+
const laneWaitMs = Date.now() - laneWaitStart;
|
|
768
|
+
if (laneWaitMs > 1000) {
|
|
769
|
+
logger.info({ sessionKey, laneWaitMs }, 'Chat lane wait was non-trivial');
|
|
770
|
+
}
|
|
750
771
|
logger.info(`Message from ${sessionKey}: ${text.slice(0, 100)}...`);
|
|
751
772
|
events.emit('message:received', { sessionKey, text, timestamp: Date.now() });
|
|
752
773
|
// ── Register provenance on first interaction ────────────────
|
|
@@ -849,6 +870,9 @@ export class Gateway {
|
|
|
849
870
|
|| text.startsWith('[Approval:')
|
|
850
871
|
|| text.startsWith('[Reaction:')
|
|
851
872
|
|| text.startsWith('[System:');
|
|
873
|
+
if (!isInternalMsg && !sess?.profile && !text.startsWith('!') && !isStructuredWorkflowMsg && onProgress) {
|
|
874
|
+
await onProgress('checking if a teammate should handle this...').catch(() => { });
|
|
875
|
+
}
|
|
852
876
|
const routingResult = !isInternalMsg && !sess?.profile && !text.startsWith('!') && !isStructuredWorkflowMsg
|
|
853
877
|
? await this._maybeRouteToSpecialist(sessionKey, text, onText)
|
|
854
878
|
: null;
|
|
@@ -1058,6 +1082,9 @@ export class Gateway {
|
|
|
1058
1082
|
// Primary guardrail is cost budget (maxBudgetUsd in buildOptions).
|
|
1059
1083
|
// Wall clock (CHAT_MAX_WALL_MS) and StallGuard are safety nets.
|
|
1060
1084
|
events.emit('query:start', { sessionKey, model: effectiveModel, maxTurns: maxTurns, timestamp: Date.now() });
|
|
1085
|
+
if (onProgress) {
|
|
1086
|
+
await onProgress('thinking...').catch(() => { });
|
|
1087
|
+
}
|
|
1061
1088
|
const queryStartMs = Date.now();
|
|
1062
1089
|
const [response] = await Promise.race([
|
|
1063
1090
|
this.assistant.chat(chatPrompt, effectiveSessionKey, { onText: wrappedOnText, onToolActivity: wrappedOnToolActivity, model: effectiveModel, maxTurns: maxTurns, securityAnnotation, projectOverride, profile: resolvedProfile, verboseLevel, abortController: chatAc }),
|
package/dist/types.d.ts
CHANGED
|
@@ -103,6 +103,13 @@ export interface ChannelCapabilities {
|
|
|
103
103
|
export declare const DEFAULT_CHANNEL_CAPABILITIES: ChannelCapabilities;
|
|
104
104
|
export type OnTextCallback = (text: string) => Promise<void>;
|
|
105
105
|
export type OnToolActivityCallback = (toolName: string, toolInput: Record<string, unknown>) => Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Pre-query progress callback. Fired at stage transitions BEFORE the SDK
|
|
108
|
+
* query starts (routing, complexity classification, lock waits, etc.) so
|
|
109
|
+
* the user sees the indicator change instead of staring at "thinking..."
|
|
110
|
+
* for several seconds.
|
|
111
|
+
*/
|
|
112
|
+
export type OnProgressCallback = (status: string) => Promise<void>;
|
|
106
113
|
export interface NotificationContext {
|
|
107
114
|
agentSlug?: string;
|
|
108
115
|
/** When set, the dispatcher routes the message back to the channel that owns this session. */
|
|
@@ -176,6 +183,17 @@ export interface AgentProfile {
|
|
|
176
183
|
budgetMonthlyCents?: number;
|
|
177
184
|
spentMonthlyCents?: number;
|
|
178
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
|
+
};
|
|
179
197
|
}
|
|
180
198
|
export type AgentStatus = 'active' | 'paused' | 'error' | 'terminated';
|
|
181
199
|
export interface HeartbeatReportedTopic {
|
|
@@ -233,6 +251,15 @@ export interface AgentHeartbeatState {
|
|
|
233
251
|
silentTickCount: number;
|
|
234
252
|
fingerprint: string;
|
|
235
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';
|
|
236
263
|
}
|
|
237
264
|
export interface CronJobDefinition {
|
|
238
265
|
name: string;
|