Skip to content

Commit eb4a9d0

Browse files
Merge pull request #2278 from netease-youdao/feat/openclaw-heartbeat-toggle
feat(openclaw): add heartbeat toggle setting
2 parents ec98344 + de47eca commit eb4a9d0

14 files changed

Lines changed: 127 additions & 1 deletion

src/main/coworkStore.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,12 @@ test('getConfig defaults skipMissedJobs to true when config is missing', () => {
953953
expect(config.skipMissedJobs).toBe(true);
954954
});
955955

956+
test('getConfig defaults OpenClaw heartbeat to enabled when config is missing', () => {
957+
const config = store.getConfig();
958+
959+
expect(config.openClawHeartbeatEnabled).toBe(true);
960+
});
961+
956962
test('backfillEmptyAgentModels assigns the current default model to empty agents only', () => {
957963
const now = Date.now();
958964
db.prepare(

src/main/coworkStore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ export interface CoworkConfig {
544544
memoryGuardLevel: CoworkMemoryGuardLevel;
545545
memoryUserMemoriesMaxItems: number;
546546
skipMissedJobs: boolean;
547+
openClawHeartbeatEnabled: boolean;
547548
embeddingEnabled: boolean;
548549
embeddingProvider: string;
549550
embeddingModel: string;
@@ -568,6 +569,7 @@ CoworkConfig,
568569
| 'memoryGuardLevel'
569570
| 'memoryUserMemoriesMaxItems'
570571
| 'skipMissedJobs'
572+
| 'openClawHeartbeatEnabled'
571573
| 'embeddingEnabled'
572574
| 'embeddingProvider'
573575
| 'embeddingModel'
@@ -1999,6 +2001,7 @@ export class CoworkStore {
19992001
'memoryGuardLevel',
20002002
'memoryUserMemoriesMaxItems',
20012003
'skipMissedJobs',
2004+
'openClawHeartbeatEnabled',
20022005
'embeddingEnabled',
20032006
'embeddingProvider',
20042007
'embeddingModel',
@@ -2036,6 +2039,7 @@ export class CoworkStore {
20362039
Number(cfg.get('memoryUserMemoriesMaxItems')),
20372040
),
20382041
skipMissedJobs: parseBooleanConfig(cfg.get('skipMissedJobs'), true),
2042+
openClawHeartbeatEnabled: parseBooleanConfig(cfg.get('openClawHeartbeatEnabled'), true),
20392043
embeddingEnabled: parseBooleanConfig(cfg.get('embeddingEnabled'), DEFAULT_EMBEDDING_ENABLED),
20402044
embeddingProvider: cfg.get('embeddingProvider') || DEFAULT_EMBEDDING_PROVIDER,
20412045
embeddingModel: cfg.get('embeddingModel') || DEFAULT_EMBEDDING_MODEL,
@@ -2080,6 +2084,9 @@ export class CoworkStore {
20802084
if (config.skipMissedJobs !== undefined) {
20812085
this.upsertConfig('skipMissedJobs', config.skipMissedJobs ? '1' : '0', now);
20822086
}
2087+
if (config.openClawHeartbeatEnabled !== undefined) {
2088+
this.upsertConfig('openClawHeartbeatEnabled', config.openClawHeartbeatEnabled ? '1' : '0', now);
2089+
}
20832090
if (config.embeddingEnabled !== undefined) {
20842091
this.upsertConfig('embeddingEnabled', config.embeddingEnabled ? '1' : '0', now);
20852092
}

src/main/libs/openclawConfigImpact.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ describe('OpenClaw config impact classification', () => {
165165
});
166166
});
167167

168+
test('classifies OpenClaw heartbeat changes as sync', () => {
169+
const result = classifyCoworkConfigChange(
170+
{ openClawHeartbeatEnabled: true },
171+
{ openClawHeartbeatEnabled: false },
172+
);
173+
174+
expect(result).toEqual({
175+
impact: OpenClawConfigImpact.Sync,
176+
reasons: [OpenClawConfigImpactReason.CoworkOpenClawConfig],
177+
});
178+
});
179+
168180
test('classifies dreaming changes as restart', () => {
169181
const result = classifyCoworkConfigChange(
170182
{ dreamingEnabled: false, dreamingFrequency: '0 3 * * *' },

src/main/libs/openclawConfigImpact.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const COWORK_SYNC_FIELDS = new Set([
6363
'agentEngine',
6464
'workingDirectory',
6565
'skipMissedJobs',
66+
'openClawHeartbeatEnabled',
6667
'embeddingEnabled',
6768
'embeddingProvider',
6869
'embeddingModel',

src/main/libs/openclawConfigSync.runtime.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,50 @@ describe('OpenClawConfigSync runtime config output', () => {
231231
expect(config.agents.defaults.mediaMaxMb).toBe(30);
232232
});
233233

234+
test('enables optimized OpenClaw heartbeat by default', async () => {
235+
const sync = await createSync();
236+
237+
const result = sync.sync('heartbeat-enabled-default');
238+
expect(result.ok).toBe(true);
239+
240+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
241+
expect(config.agents.defaults.heartbeat).toEqual({
242+
every: '1h',
243+
target: 'none',
244+
lightContext: true,
245+
isolatedSession: true,
246+
});
247+
});
248+
249+
test('writes disabled OpenClaw heartbeat cadence when user disables heartbeat', async () => {
250+
const sync = await createSync({
251+
getCoworkConfig: () => ({
252+
workingDirectory: tmpDir,
253+
systemPrompt: '',
254+
executionMode: 'local',
255+
agentEngine: 'openclaw',
256+
memoryEnabled: false,
257+
memoryImplicitUpdateEnabled: false,
258+
memoryLlmJudgeEnabled: false,
259+
memoryGuardLevel: 'balanced',
260+
memoryUserMemoriesMaxItems: 100,
261+
skipMissedJobs: false,
262+
openClawHeartbeatEnabled: false,
263+
}),
264+
});
265+
266+
const result = sync.sync('heartbeat-disabled');
267+
expect(result.ok).toBe(true);
268+
269+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
270+
expect(config.agents.defaults.heartbeat).toEqual({
271+
every: '0m',
272+
target: 'none',
273+
lightContext: true,
274+
isolatedSession: true,
275+
});
276+
});
277+
234278
test('writes model provider env-proxy transport when system proxy is enabled', async () => {
235279
const { setSystemProxyEnabled } = await import('./systemProxy');
236280
setSystemProxyEnabled(true);

src/main/libs/openclawConfigSync.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ const mapExecutionModeToSandboxMode = (
8787
* Also used by the runtime adapter's client-side timeout watchdog.
8888
*/
8989
export const OPENCLAW_AGENT_TIMEOUT_SECONDS = 3600;
90+
export const OPENCLAW_HEARTBEAT_EVERY_ENABLED = '1h';
91+
export const OPENCLAW_HEARTBEAT_EVERY_DISABLED = '0m';
9092
const DINGTALK_OPENCLAW_CHANNEL = 'dingtalk-connector';
9193
export const OPENCLAW_BINDING_ANY_ACCOUNT_ID = '*';
9294
const OPENCLAW_DEFAULT_MODEL_MAX_TOKENS = 8192;
@@ -1853,7 +1855,9 @@ loopDetection: MANAGED_TOOL_LOOP_DETECTION,
18531855
},
18541856
} : {}),
18551857
heartbeat: {
1856-
every: '1h',
1858+
every: coworkConfig.openClawHeartbeatEnabled === false
1859+
? OPENCLAW_HEARTBEAT_EVERY_DISABLED
1860+
: OPENCLAW_HEARTBEAT_EVERY_ENABLED,
18571861
target: 'none',
18581862
lightContext: true,
18591863
isolatedSession: true,

src/main/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7757,6 +7757,7 @@ if (!gotTheLock) {
77577757
memoryGuardLevel?: 'strict' | 'standard' | 'relaxed';
77587758
memoryUserMemoriesMaxItems?: number;
77597759
skipMissedJobs?: boolean;
7760+
openClawHeartbeatEnabled?: boolean;
77607761
embeddingEnabled?: boolean;
77617762
embeddingProvider?: string;
77627763
embeddingModel?: string;
@@ -7797,6 +7798,9 @@ if (!gotTheLock) {
77977798
const normalizedSkipMissedJobs = typeof config.skipMissedJobs === 'boolean'
77987799
? config.skipMissedJobs
77997800
: undefined;
7801+
const normalizedOpenClawHeartbeatEnabled = typeof config.openClawHeartbeatEnabled === 'boolean'
7802+
? config.openClawHeartbeatEnabled
7803+
: undefined;
78007804
const normalizedEmbedding = normalizeEmbeddingConfig(config);
78017805
const normalizedConfig: Parameters<CoworkStore['setConfig']>[0] = {
78027806
...config,
@@ -7808,6 +7812,7 @@ if (!gotTheLock) {
78087812
memoryGuardLevel: normalizedMemoryGuardLevel,
78097813
memoryUserMemoriesMaxItems: normalizedMemoryUserMemoriesMaxItems,
78107814
skipMissedJobs: normalizedSkipMissedJobs,
7815+
openClawHeartbeatEnabled: normalizedOpenClawHeartbeatEnabled,
78117816
...normalizedEmbedding,
78127817
};
78137818
const previousConfig = getCoworkStore().getConfig();

src/main/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ contextBridge.exposeInMainWorld('electron', {
458458
memoryGuardLevel?: 'strict' | 'standard' | 'relaxed';
459459
memoryUserMemoriesMaxItems?: number;
460460
skipMissedJobs?: boolean;
461+
openClawHeartbeatEnabled?: boolean;
461462
embeddingEnabled?: boolean;
462463
embeddingProvider?: string;
463464
embeddingModel?: string;

src/renderer/components/Settings.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,6 +1654,7 @@ const Settings: React.FC<SettingsProps> = ({
16541654
const [coworkMemoryEnabled, setCoworkMemoryEnabled] = useState<boolean>(coworkConfig.memoryEnabled ?? true);
16551655
const [coworkMemoryLlmJudgeEnabled, setCoworkMemoryLlmJudgeEnabled] = useState<boolean>(coworkConfig.memoryLlmJudgeEnabled ?? false);
16561656
const [skipMissedJobs, setSkipMissedJobs] = useState<boolean>(coworkConfig.skipMissedJobs ?? true);
1657+
const [openClawHeartbeatEnabled, setOpenClawHeartbeatEnabled] = useState<boolean>(coworkConfig.openClawHeartbeatEnabled ?? true);
16571658
const [embeddingEnabled, setEmbeddingEnabled] = useState<boolean>(coworkConfig.embeddingEnabled ?? false);
16581659
const [embeddingProvider, setEmbeddingProvider] = useState<string>(coworkConfig.embeddingProvider ?? 'openai');
16591660
const [embeddingModel, setEmbeddingModel] = useState<string>(coworkConfig.embeddingModel ?? '');
@@ -1691,6 +1692,7 @@ const Settings: React.FC<SettingsProps> = ({
16911692
setCoworkMemoryEnabled(coworkConfig.memoryEnabled ?? true);
16921693
setCoworkMemoryLlmJudgeEnabled(coworkConfig.memoryLlmJudgeEnabled ?? false);
16931694
setSkipMissedJobs(coworkConfig.skipMissedJobs ?? true);
1695+
setOpenClawHeartbeatEnabled(coworkConfig.openClawHeartbeatEnabled ?? true);
16941696
setEmbeddingEnabled(coworkConfig.embeddingEnabled ?? false);
16951697
setEmbeddingProvider(coworkConfig.embeddingProvider ?? 'openai');
16961698
setEmbeddingModel(coworkConfig.embeddingModel ?? '');
@@ -1709,6 +1711,7 @@ const Settings: React.FC<SettingsProps> = ({
17091711
coworkConfig.memoryLlmJudgeEnabled,
17101712
coworkConfig.openClawSessionPolicy?.keepAlive,
17111713
coworkConfig.skipMissedJobs,
1714+
coworkConfig.openClawHeartbeatEnabled,
17121715
coworkConfig.embeddingEnabled,
17131716
coworkConfig.embeddingProvider,
17141717
coworkConfig.embeddingModel,
@@ -2635,6 +2638,7 @@ const Settings: React.FC<SettingsProps> = ({
26352638
|| coworkMemoryEnabled !== coworkConfig.memoryEnabled
26362639
|| coworkMemoryLlmJudgeEnabled !== coworkConfig.memoryLlmJudgeEnabled
26372640
|| skipMissedJobs !== (coworkConfig.skipMissedJobs ?? true)
2641+
|| openClawHeartbeatEnabled !== (coworkConfig.openClawHeartbeatEnabled ?? true)
26382642
|| openClawSessionKeepAlive !== (coworkConfig.openClawSessionPolicy?.keepAlive || OpenClawSessionKeepAliveValues.ThirtyDays)
26392643
|| embeddingEnabled !== (coworkConfig.embeddingEnabled ?? false)
26402644
|| embeddingProvider !== (coworkConfig.embeddingProvider ?? 'openai')
@@ -3154,6 +3158,7 @@ const Settings: React.FC<SettingsProps> = ({
31543158
? normalizeProvidersForSettingsSave(previousConfig.providers as ProvidersConfig)
31553159
: normalizedProviders;
31563160
const previousSkipMissedJobs = coworkConfig.skipMissedJobs ?? true;
3161+
const previousOpenClawHeartbeatEnabled = coworkConfig.openClawHeartbeatEnabled ?? true;
31573162
const previousAgentEngine = coworkConfig.agentEngine || 'openclaw';
31583163
const previousOpenClawSessionKeepAlive = coworkConfig.openClawSessionPolicy?.keepAlive
31593164
|| OpenClawSessionKeepAliveValues.ThirtyDays;
@@ -3269,6 +3274,7 @@ const Settings: React.FC<SettingsProps> = ({
32693274
memoryEnabled: coworkMemoryEnabled,
32703275
memoryLlmJudgeEnabled: coworkMemoryLlmJudgeEnabled,
32713276
skipMissedJobs,
3277+
openClawHeartbeatEnabled,
32723278
embeddingEnabled,
32733279
embeddingProvider,
32743280
embeddingModel,
@@ -3356,6 +3362,13 @@ const Settings: React.FC<SettingsProps> = ({
33563362
if (previousAgentEngine !== coworkAgentEngine) {
33573363
reportAgentEngineSettingChanged('agentEngine', coworkAgentEngine, previousAgentEngine);
33583364
}
3365+
if (previousOpenClawHeartbeatEnabled !== openClawHeartbeatEnabled) {
3366+
reportAgentEngineSettingChanged(
3367+
'openClawHeartbeatEnabled',
3368+
openClawHeartbeatEnabled,
3369+
previousOpenClawHeartbeatEnabled,
3370+
);
3371+
}
33593372
if (previousOpenClawSessionKeepAlive !== openClawSessionKeepAlive) {
33603373
reportAgentEngineSettingChanged(
33613374
'openClawSessionKeepAlive',
@@ -4637,6 +4650,23 @@ const Settings: React.FC<SettingsProps> = ({
46374650
</div>
46384651
</section>
46394652

4653+
<section className="space-y-3">
4654+
<h4 className="text-sm font-medium text-foreground">
4655+
{i18nService.t('openClawBackgroundRuntimeTitle')}
4656+
</h4>
4657+
4658+
<div className="rounded-xl border border-border bg-surface p-4">
4659+
<SettingsToggleRow
4660+
title={i18nService.t('openClawHeartbeatEnabled')}
4661+
description={i18nService.t('openClawHeartbeatEnabledDescription')}
4662+
checked={openClawHeartbeatEnabled}
4663+
onToggle={() => {
4664+
setOpenClawHeartbeatEnabled((prev) => !prev);
4665+
}}
4666+
/>
4667+
</div>
4668+
</section>
4669+
46404670
<section className="space-y-3">
46414671
<h4 className="text-sm font-medium text-foreground">
46424672
{i18nService.t('openClawMaintenanceTitle')}

src/renderer/services/i18n.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,10 @@ const translations: Record<LanguageType, Record<string, string>> = {
792792
coworkAgentEngineOpenClaw: 'OpenClaw(默认)',
793793
coworkAgentEngineOpenClawHint: '个人 AI 助理',
794794
openClawRuntimeStatusTitle: '运行状态',
795+
openClawBackgroundRuntimeTitle: '后台运行',
796+
openClawHeartbeatEnabled: '启用 OpenClaw 心跳',
797+
openClawHeartbeatEnabledDescription:
798+
'开启后 OpenClaw 会每 1 小时自动检查一次后台提醒和任务状态。关闭后可减少空闲时的 token 消耗,但提醒、任务完成通知等后台更新可能延迟或无法主动触达。',
795799
openClawGatewayAddress: '网关地址',
796800
openClawStartupProgressLabel: '启动进度',
797801
openClawStatusBadgeReady: '已就绪',
@@ -3521,6 +3525,10 @@ const translations: Record<LanguageType, Record<string, string>> = {
35213525
coworkAgentEngineOpenClaw: 'OpenClaw (Default)',
35223526
coworkAgentEngineOpenClawHint: 'Personal AI assistant',
35233527
openClawRuntimeStatusTitle: 'Runtime status',
3528+
openClawBackgroundRuntimeTitle: 'Background runtime',
3529+
openClawHeartbeatEnabled: 'Enable OpenClaw heartbeat',
3530+
openClawHeartbeatEnabledDescription:
3531+
'When enabled, OpenClaw checks background reminders and task status once every 1 hour. Turning it off can reduce idle token usage, but reminders, task completion notifications, and other background updates may be delayed or may not be delivered proactively.',
35243532
openClawGatewayAddress: 'Gateway address',
35253533
openClawStartupProgressLabel: 'Startup progress',
35263534
openClawStatusBadgeReady: 'Ready',

0 commit comments

Comments
 (0)