Skip to content

Commit bdc602a

Browse files
jlia0claude
andauthored
fix(heartbeat): persist per-agent enabled and interval settings to heartbeat overrides (#230)
The heartbeat API endpoints were not reading or persisting per-agent heartbeat overrides (enabled, interval) that the cron script relies on. The GET endpoint now returns enabled and interval from agent settings, and the PUT endpoint persists them to settings.agents[id].heartbeat. The UI now correctly saves these fields to the per-agent endpoint instead of the global monitoring settings. Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 8c8059a commit bdc602a

3 files changed

Lines changed: 39 additions & 32 deletions

File tree

packages/server/src/routes/agents.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ app.get('/api/agents/:id/memory', (c) => {
265265
return c.json({ index, files, memoryDir });
266266
});
267267

268-
// GET /api/agents/:id/heartbeat — read heartbeat.md from workspace
268+
// GET /api/agents/:id/heartbeat — read heartbeat.md and settings from workspace
269269
app.get('/api/agents/:id/heartbeat', (c) => {
270270
const agentId = c.req.param('id');
271271
const settings = getSettings();
@@ -277,19 +277,39 @@ app.get('/api/agents/:id/heartbeat', (c) => {
277277
if (fs.existsSync(heartbeatMd)) {
278278
try { content = fs.readFileSync(heartbeatMd, 'utf8'); } catch { /* skip */ }
279279
}
280-
return c.json({ content, path: heartbeatMd });
280+
return c.json({
281+
content,
282+
path: heartbeatMd,
283+
enabled: agent.heartbeat?.enabled ?? true,
284+
interval: agent.heartbeat?.interval,
285+
});
281286
});
282287

283-
// PUT /api/agents/:id/heartbeat — write heartbeat.md to workspace
288+
// PUT /api/agents/:id/heartbeat — write heartbeat.md and settings to workspace
284289
app.put('/api/agents/:id/heartbeat', async (c) => {
285290
const agentId = c.req.param('id');
286291
const settings = getSettings();
287292
const agent = settings.agents?.[agentId];
288293
if (!agent) return c.json({ error: `agent '${agentId}' not found` }, 404);
289294

290-
const body = await c.req.json() as { content: string };
291-
const heartbeatMd = path.join(agent.working_directory, 'heartbeat.md');
292-
fs.writeFileSync(heartbeatMd, body.content || '', 'utf8');
295+
const body = await c.req.json() as { content?: string; enabled?: boolean; interval?: number };
296+
297+
// Write heartbeat.md if content provided
298+
if (body.content != null) {
299+
const heartbeatMd = path.join(agent.working_directory, 'heartbeat.md');
300+
fs.writeFileSync(heartbeatMd, body.content || '', 'utf8');
301+
}
302+
303+
// Persist heartbeat overrides to settings.json
304+
if (body.enabled != null || body.interval != null) {
305+
mutateSettings(s => {
306+
if (!s.agents?.[agentId]) return;
307+
if (!s.agents[agentId].heartbeat) s.agents[agentId].heartbeat = {};
308+
if (body.enabled != null) s.agents[agentId].heartbeat!.enabled = body.enabled;
309+
if (body.interval != null) s.agents[agentId].heartbeat!.interval = body.interval;
310+
});
311+
}
312+
293313
return c.json({ ok: true });
294314
});
295315

tinyoffice/src/app/agents/[id]/page.tsx

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@ import { useState, useCallback, useEffect, use } from "react";
44
import { usePolling } from "@/lib/hooks";
55
import {
66
getAgents,
7-
getSettings,
8-
updateSettings,
97
getAgentSkills,
108
getAgentSystemPrompt,
119
saveAgentSystemPrompt,
1210
getAgentMemory,
1311
getAgentHeartbeat,
1412
saveAgentHeartbeat,
1513
type AgentConfig,
16-
type Settings,
1714
type WorkspaceSkill,
1815
} from "@/lib/api";
1916
import { cn } from "@/lib/utils";
@@ -74,7 +71,6 @@ export default function AgentConfigPage({
7471
getAgents,
7572
0,
7673
);
77-
const { data: settings } = usePolling<Settings>(getSettings, 0);
7874

7975
const [activeTab, setActiveTab] = useState<TabId>("chat");
8076
const [spSaving, setSpSaving] = useState(false);
@@ -130,20 +126,15 @@ export default function AgentConfigPage({
130126
.then((data) => {
131127
setHeartbeatContent(data.content);
132128
setHeartbeatPath(data.path);
129+
setHeartbeatEnabled(data.enabled);
130+
if (data.interval != null) {
131+
setHeartbeatInterval(String(data.interval));
132+
}
133133
setHeartbeatLoaded(true);
134134
})
135135
.catch(() => setHeartbeatLoaded(true));
136136
}, [agent, agentId]);
137137

138-
// Sync heartbeat interval from settings unless the user has edited it
139-
useEffect(() => {
140-
if (!settings?.monitoring) return;
141-
const interval = settings.monitoring.heartbeat_interval;
142-
if (typeof interval === "number") {
143-
setHeartbeatInterval(String(interval));
144-
}
145-
}, [settings]);
146-
147138
// Convert workspace skills to SkillEntry format for constellation
148139
const constellationSkills: SkillEntry[] = workspaceSkills.map((s) => ({
149140
id: s.id,
@@ -169,23 +160,19 @@ export default function AgentConfigPage({
169160
if (!agent) return;
170161
setHbSaving(true);
171162
try {
172-
await saveAgentHeartbeat(agentId, heartbeatContent);
173-
if (settings?.monitoring) {
174-
await updateSettings({
175-
monitoring: {
176-
...settings.monitoring,
177-
heartbeat_interval: parseInt(heartbeatInterval) || 300,
178-
},
179-
});
180-
}
163+
await saveAgentHeartbeat(agentId, {
164+
content: heartbeatContent,
165+
enabled: heartbeatEnabled,
166+
interval: parseInt(heartbeatInterval) || 300,
167+
});
181168
setHbSaved(true);
182169
setTimeout(() => setHbSaved(false), 2000);
183170
} catch {
184171
// Error handling
185172
} finally {
186173
setHbSaving(false);
187174
}
188-
}, [agent, agentId, heartbeatContent, heartbeatEnabled, heartbeatInterval, settings]);
175+
}, [agent, agentId, heartbeatContent, heartbeatEnabled, heartbeatInterval]);
189176

190177
const refreshWorkspaceData = useCallback(() => {
191178
getAgentSkills(agentId)

tinyoffice/src/lib/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,14 @@ export async function getAgentMemory(agentId: string): Promise<{ index: string;
215215
return apiFetch(`/api/agents/${encodeURIComponent(agentId)}/memory`);
216216
}
217217

218-
export async function getAgentHeartbeat(agentId: string): Promise<{ content: string; path: string }> {
218+
export async function getAgentHeartbeat(agentId: string): Promise<{ content: string; path: string; enabled: boolean; interval?: number }> {
219219
return apiFetch(`/api/agents/${encodeURIComponent(agentId)}/heartbeat`);
220220
}
221221

222-
export async function saveAgentHeartbeat(agentId: string, content: string): Promise<{ ok: boolean }> {
222+
export async function saveAgentHeartbeat(agentId: string, data: { content?: string; enabled?: boolean; interval?: number }): Promise<{ ok: boolean }> {
223223
return apiFetch(`/api/agents/${encodeURIComponent(agentId)}/heartbeat`, {
224224
method: "PUT",
225-
body: JSON.stringify({ content }),
225+
body: JSON.stringify(data),
226226
});
227227
}
228228

0 commit comments

Comments
 (0)