0xkobold 0.7.2 → 0.8.0
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/HEARTBEAT.md +239 -0
- package/IDENTITY.md +12 -0
- package/README.md +138 -4
- package/SOUL.md +21 -0
- package/dist/package.json +10 -5
- package/dist/src/agent/bootstrap-loader.js +295 -66
- package/dist/src/agent/bootstrap-loader.js.map +1 -1
- package/dist/src/agent/context-pruning.js +10 -5
- package/dist/src/agent/context-pruning.js.map +1 -1
- package/dist/src/agent/embedded-runner.js +29 -15
- package/dist/src/agent/embedded-runner.js.map +1 -1
- package/dist/src/agent/index.js +5 -2
- package/dist/src/agent/index.js.map +1 -1
- package/dist/src/agent/system-prompt.js +167 -20
- package/dist/src/agent/system-prompt.js.map +1 -1
- package/dist/src/agent/tools/spawn-agent.js +72 -5
- package/dist/src/agent/tools/spawn-agent.js.map +1 -1
- package/dist/src/channels/slack/webhook.js +2 -2
- package/dist/src/channels/slack/webhook.js.map +1 -1
- package/dist/src/channels/telegram/bot.js +4 -4
- package/dist/src/channels/telegram/bot.js.map +1 -1
- package/dist/src/channels/whatsapp/integration.js +4 -4
- package/dist/src/channels/whatsapp/integration.js.map +1 -1
- package/dist/src/cli/commands/gateway.js +9 -10
- package/dist/src/cli/commands/gateway.js.map +1 -1
- package/dist/src/cli/commands/init.js +90 -0
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/setup.js +53 -0
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/index.js +0 -0
- package/dist/src/config/unified-config.js +5 -0
- package/dist/src/config/unified-config.js.map +1 -1
- package/dist/src/cron/index.js +2 -0
- package/dist/src/cron/index.js.map +1 -1
- package/dist/src/cron/nl-parser.js +522 -0
- package/dist/src/cron/nl-parser.js.map +1 -0
- package/dist/src/cron/runner.js +6 -31
- package/dist/src/cron/runner.js.map +1 -1
- package/dist/src/event-bus/index.js.map +1 -1
- package/dist/src/extensions/core/agent-orchestrator-extension.js +200 -148
- package/dist/src/extensions/core/agent-orchestrator-extension.js.map +1 -1
- package/dist/src/extensions/core/diagnostics-extension.js +93 -56
- package/dist/src/extensions/core/diagnostics-extension.js.map +1 -1
- package/dist/src/extensions/core/draconic-safety-extension.js +256 -3
- package/dist/src/extensions/core/draconic-safety-extension.js.map +1 -1
- package/dist/src/extensions/core/heartbeat-extension.js +416 -150
- package/dist/src/extensions/core/heartbeat-extension.js.map +1 -1
- package/dist/src/extensions/core/{generative-agents-extension.js → learning-extension.js} +90 -128
- package/dist/src/extensions/core/learning-extension.js.map +1 -0
- package/dist/src/extensions/core/perennial-memory-extension.js +884 -87
- package/dist/src/extensions/core/perennial-memory-extension.js.map +1 -1
- package/dist/src/extensions/core/routed-ollama-extension.js +345 -0
- package/dist/src/extensions/core/routed-ollama-extension.js.map +1 -0
- package/dist/src/extensions/core/task-manager-extension.js +25 -2
- package/dist/src/extensions/core/task-manager-extension.js.map +1 -1
- package/dist/src/extensions/core/websearch-enhanced-extension.js +81 -23
- package/dist/src/extensions/core/websearch-enhanced-extension.js.map +1 -1
- package/dist/src/extensions/core/workspace-footer-extension.js +40 -63
- package/dist/src/extensions/core/workspace-footer-extension.js.map +1 -1
- package/dist/src/extensions/loader.js +5 -1
- package/dist/src/extensions/loader.js.map +1 -1
- package/dist/src/gateway/gateway-server.js +74 -3
- package/dist/src/gateway/gateway-server.js.map +1 -1
- package/dist/src/gateway/index.js +2 -2
- package/dist/src/gateway/index.js.map +1 -1
- package/dist/src/gateway/methods/agent.js +1 -1
- package/dist/src/gateway/methods/agent.js.map +1 -1
- package/dist/src/gateway/methods/index.js +4 -0
- package/dist/src/gateway/methods/index.js.map +1 -1
- package/dist/src/gateway/methods/node.js +261 -0
- package/dist/src/gateway/methods/node.js.map +1 -0
- package/dist/src/gateway/queue-modes.js +356 -0
- package/dist/src/gateway/queue-modes.js.map +1 -0
- package/dist/src/index.js +47 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/llm/community-analytics.js +569 -0
- package/dist/src/llm/community-analytics.js.map +1 -0
- package/dist/src/llm/index.js +28 -4
- package/dist/src/llm/index.js.map +1 -1
- package/dist/src/llm/model-discovery.js +335 -0
- package/dist/src/llm/model-discovery.js.map +1 -0
- package/dist/src/llm/model-popularity.js +566 -0
- package/dist/src/llm/model-popularity.js.map +1 -0
- package/dist/src/llm/model-scoring-db.js +553 -0
- package/dist/src/llm/model-scoring-db.js.map +1 -0
- package/dist/src/llm/multi-provider.js +258 -0
- package/dist/src/llm/multi-provider.js.map +1 -0
- package/dist/src/llm/ollama.js +133 -187
- package/dist/src/llm/ollama.js.map +1 -1
- package/dist/src/llm/router-commands.js +773 -0
- package/dist/src/llm/router-commands.js.map +1 -0
- package/dist/src/llm/router-core.js +600 -0
- package/dist/src/llm/router-core.js.map +1 -0
- package/dist/src/memory/checkpoint-manager.js +278 -0
- package/dist/src/memory/checkpoint-manager.js.map +1 -0
- package/dist/src/memory/conflict-detector.js +279 -0
- package/dist/src/memory/conflict-detector.js.map +1 -0
- package/dist/src/memory/context-graph.js +360 -0
- package/dist/src/memory/context-graph.js.map +1 -0
- package/dist/src/memory/dialectic/benchmark-cli.js +200 -0
- package/dist/src/memory/dialectic/benchmark-cli.js.map +1 -0
- package/dist/src/memory/dialectic/debug-reasoning.js +37 -0
- package/dist/src/memory/dialectic/debug-reasoning.js.map +1 -0
- package/dist/src/memory/dialectic/index.js +135 -0
- package/dist/src/memory/dialectic/index.js.map +1 -0
- package/dist/src/memory/dialectic/nudges.js +380 -0
- package/dist/src/memory/dialectic/nudges.js.map +1 -0
- package/dist/src/memory/dialectic/reasoning-engine.js +607 -0
- package/dist/src/memory/dialectic/reasoning-engine.js.map +1 -0
- package/dist/src/memory/dialectic/reasoning.js +390 -0
- package/dist/src/memory/dialectic/reasoning.js.map +1 -0
- package/dist/src/memory/dialectic/skill-creation.js +481 -0
- package/dist/src/memory/dialectic/skill-creation.js.map +1 -0
- package/dist/src/memory/dialectic/store.js +663 -0
- package/dist/src/memory/dialectic/store.js.map +1 -0
- package/dist/src/memory/dialectic/types.js +11 -0
- package/dist/src/memory/dialectic/types.js.map +1 -0
- package/dist/src/memory/index.js +24 -2
- package/dist/src/memory/index.js.map +1 -1
- package/dist/src/memory/memory-decay.js +350 -0
- package/dist/src/memory/memory-decay.js.map +1 -0
- package/dist/src/memory/memory-integration.js +5 -5
- package/dist/src/memory/memory-integration.js.map +1 -1
- package/dist/src/memory/migrate-memory-stream.js +97 -0
- package/dist/src/memory/migrate-memory-stream.js.map +1 -0
- package/dist/src/memory/session-memory-bridge.js +49 -5
- package/dist/src/memory/session-memory-bridge.js.map +1 -1
- package/dist/src/memory/session-store.js +123 -0
- package/dist/src/memory/session-store.js.map +1 -1
- package/dist/src/memory/smart-write-rules.js +164 -0
- package/dist/src/memory/smart-write-rules.js.map +1 -0
- package/dist/src/memory/tiered-memory.js +436 -0
- package/dist/src/memory/tiered-memory.js.map +1 -0
- package/dist/src/memory/types.js +6 -0
- package/dist/src/memory/types.js.map +1 -0
- package/dist/src/memory/verify-migration.js +99 -0
- package/dist/src/memory/verify-migration.js.map +1 -0
- package/dist/src/pi-config.js +11 -9
- package/dist/src/pi-config.js.map +1 -1
- package/dist/src/skills/conditional-skills.js +464 -0
- package/dist/src/skills/conditional-skills.js.map +1 -0
- package/dist/src/skills/index.js +5 -0
- package/dist/src/skills/index.js.map +1 -1
- package/dist/src/skills/loader.js +56 -0
- package/dist/src/skills/loader.js.map +1 -1
- package/dist/src/skills/skill-manage.js +417 -0
- package/dist/src/skills/skill-manage.js.map +1 -0
- package/dist/src/tui/commands/orchestration-commands.js +62 -0
- package/dist/src/tui/commands/orchestration-commands.js.map +1 -1
- package/package.json +10 -5
- package/skills/model-router-test.ts +65 -0
- package/dist/src/extensions/core/auto-security-scan-extension.js +0 -41
- package/dist/src/extensions/core/auto-security-scan-extension.js.map +0 -1
- package/dist/src/extensions/core/cloudflare-browser-extension.js +0 -389
- package/dist/src/extensions/core/cloudflare-browser-extension.js.map +0 -1
- package/dist/src/extensions/core/compaction-safeguard.js +0 -223
- package/dist/src/extensions/core/compaction-safeguard.js.map +0 -1
- package/dist/src/extensions/core/generative-agents-extension.js.map +0 -1
- package/dist/src/extensions/core/obsidian-bridge-extension.js +0 -488
- package/dist/src/extensions/core/obsidian-bridge-extension.js.map +0 -1
- package/dist/src/llm/router.js +0 -145
- package/dist/src/llm/router.js.map +0 -1
- package/skills/kobold-scan-skill/node_modules/.package-lock.json +0 -172
- package/skills/kobold-scan-skill/node_modules/ansi-styles/index.d.ts +0 -345
- package/skills/kobold-scan-skill/node_modules/ansi-styles/index.js +0 -163
- package/skills/kobold-scan-skill/node_modules/ansi-styles/license +0 -9
- package/skills/kobold-scan-skill/node_modules/ansi-styles/package.json +0 -56
- package/skills/kobold-scan-skill/node_modules/ansi-styles/readme.md +0 -152
- package/skills/kobold-scan-skill/node_modules/balanced-match/.github/FUNDING.yml +0 -2
- package/skills/kobold-scan-skill/node_modules/balanced-match/LICENSE.md +0 -21
- package/skills/kobold-scan-skill/node_modules/balanced-match/README.md +0 -97
- package/skills/kobold-scan-skill/node_modules/balanced-match/index.js +0 -62
- package/skills/kobold-scan-skill/node_modules/balanced-match/package.json +0 -48
- package/skills/kobold-scan-skill/node_modules/brace-expansion/.github/FUNDING.yml +0 -2
- package/skills/kobold-scan-skill/node_modules/brace-expansion/LICENSE +0 -21
- package/skills/kobold-scan-skill/node_modules/brace-expansion/README.md +0 -135
- package/skills/kobold-scan-skill/node_modules/brace-expansion/index.js +0 -203
- package/skills/kobold-scan-skill/node_modules/brace-expansion/package.json +0 -49
- package/skills/kobold-scan-skill/node_modules/chalk/index.d.ts +0 -415
- package/skills/kobold-scan-skill/node_modules/chalk/license +0 -9
- package/skills/kobold-scan-skill/node_modules/chalk/package.json +0 -68
- package/skills/kobold-scan-skill/node_modules/chalk/readme.md +0 -341
- package/skills/kobold-scan-skill/node_modules/chalk/source/index.js +0 -229
- package/skills/kobold-scan-skill/node_modules/chalk/source/templates.js +0 -134
- package/skills/kobold-scan-skill/node_modules/chalk/source/util.js +0 -39
- package/skills/kobold-scan-skill/node_modules/color-convert/CHANGELOG.md +0 -54
- package/skills/kobold-scan-skill/node_modules/color-convert/LICENSE +0 -21
- package/skills/kobold-scan-skill/node_modules/color-convert/README.md +0 -68
- package/skills/kobold-scan-skill/node_modules/color-convert/conversions.js +0 -839
- package/skills/kobold-scan-skill/node_modules/color-convert/index.js +0 -81
- package/skills/kobold-scan-skill/node_modules/color-convert/package.json +0 -48
- package/skills/kobold-scan-skill/node_modules/color-convert/route.js +0 -97
- package/skills/kobold-scan-skill/node_modules/color-name/LICENSE +0 -8
- package/skills/kobold-scan-skill/node_modules/color-name/README.md +0 -11
- package/skills/kobold-scan-skill/node_modules/color-name/index.js +0 -152
- package/skills/kobold-scan-skill/node_modules/color-name/package.json +0 -28
- package/skills/kobold-scan-skill/node_modules/commander/LICENSE +0 -22
- package/skills/kobold-scan-skill/node_modules/commander/Readme.md +0 -1129
- package/skills/kobold-scan-skill/node_modules/commander/esm.mjs +0 -16
- package/skills/kobold-scan-skill/node_modules/commander/index.js +0 -27
- package/skills/kobold-scan-skill/node_modules/commander/lib/argument.js +0 -147
- package/skills/kobold-scan-skill/node_modules/commander/lib/command.js +0 -2179
- package/skills/kobold-scan-skill/node_modules/commander/lib/error.js +0 -45
- package/skills/kobold-scan-skill/node_modules/commander/lib/help.js +0 -461
- package/skills/kobold-scan-skill/node_modules/commander/lib/option.js +0 -326
- package/skills/kobold-scan-skill/node_modules/commander/lib/suggestSimilar.js +0 -100
- package/skills/kobold-scan-skill/node_modules/commander/package-support.json +0 -16
- package/skills/kobold-scan-skill/node_modules/commander/package.json +0 -80
- package/skills/kobold-scan-skill/node_modules/commander/typings/index.d.ts +0 -891
- package/skills/kobold-scan-skill/node_modules/fs.realpath/LICENSE +0 -43
- package/skills/kobold-scan-skill/node_modules/fs.realpath/README.md +0 -33
- package/skills/kobold-scan-skill/node_modules/fs.realpath/index.js +0 -66
- package/skills/kobold-scan-skill/node_modules/fs.realpath/old.js +0 -303
- package/skills/kobold-scan-skill/node_modules/fs.realpath/package.json +0 -26
- package/skills/kobold-scan-skill/node_modules/glob/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/glob/README.md +0 -399
- package/skills/kobold-scan-skill/node_modules/glob/common.js +0 -244
- package/skills/kobold-scan-skill/node_modules/glob/glob.js +0 -790
- package/skills/kobold-scan-skill/node_modules/glob/package.json +0 -55
- package/skills/kobold-scan-skill/node_modules/glob/sync.js +0 -486
- package/skills/kobold-scan-skill/node_modules/has-flag/index.d.ts +0 -39
- package/skills/kobold-scan-skill/node_modules/has-flag/index.js +0 -8
- package/skills/kobold-scan-skill/node_modules/has-flag/license +0 -9
- package/skills/kobold-scan-skill/node_modules/has-flag/package.json +0 -46
- package/skills/kobold-scan-skill/node_modules/has-flag/readme.md +0 -89
- package/skills/kobold-scan-skill/node_modules/inflight/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/inflight/README.md +0 -37
- package/skills/kobold-scan-skill/node_modules/inflight/inflight.js +0 -54
- package/skills/kobold-scan-skill/node_modules/inflight/package.json +0 -29
- package/skills/kobold-scan-skill/node_modules/inherits/LICENSE +0 -16
- package/skills/kobold-scan-skill/node_modules/inherits/README.md +0 -42
- package/skills/kobold-scan-skill/node_modules/inherits/inherits.js +0 -9
- package/skills/kobold-scan-skill/node_modules/inherits/inherits_browser.js +0 -27
- package/skills/kobold-scan-skill/node_modules/inherits/package.json +0 -29
- package/skills/kobold-scan-skill/node_modules/minimatch/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/minimatch/README.md +0 -259
- package/skills/kobold-scan-skill/node_modules/minimatch/lib/path.js +0 -4
- package/skills/kobold-scan-skill/node_modules/minimatch/minimatch.js +0 -944
- package/skills/kobold-scan-skill/node_modules/minimatch/package.json +0 -35
- package/skills/kobold-scan-skill/node_modules/once/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/once/README.md +0 -79
- package/skills/kobold-scan-skill/node_modules/once/once.js +0 -42
- package/skills/kobold-scan-skill/node_modules/once/package.json +0 -33
- package/skills/kobold-scan-skill/node_modules/supports-color/browser.js +0 -5
- package/skills/kobold-scan-skill/node_modules/supports-color/index.js +0 -135
- package/skills/kobold-scan-skill/node_modules/supports-color/license +0 -9
- package/skills/kobold-scan-skill/node_modules/supports-color/package.json +0 -53
- package/skills/kobold-scan-skill/node_modules/supports-color/readme.md +0 -76
- package/skills/kobold-scan-skill/node_modules/wrappy/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/wrappy/README.md +0 -36
- package/skills/kobold-scan-skill/node_modules/wrappy/package.json +0 -29
- package/skills/kobold-scan-skill/node_modules/wrappy/wrappy.js +0 -33
|
@@ -1,54 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Heartbeat Extension -
|
|
2
|
+
* Heartbeat Extension - OpenClaw-Compatible Periodic Agent Check-ins
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Runs periodic agent turns in the main session so the model can surface
|
|
5
|
+
* anything that needs attention without spamming the user.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* OpenClaw-Compatible Configuration:
|
|
8
|
+
* - Per-agent or global configuration
|
|
9
|
+
* - Session isolation and light context options
|
|
10
|
+
* - Delivery targets (last, none, specific channel)
|
|
11
|
+
* - Active hours with timezone support
|
|
12
|
+
* - Response contract (HEARTBEAT_OK token handling)
|
|
11
13
|
*
|
|
12
|
-
* Configuration in kobold.json:
|
|
14
|
+
* Configuration in kobold.json or openclaw.json:
|
|
13
15
|
* {
|
|
14
16
|
* "agents": {
|
|
15
17
|
* "defaults": {
|
|
16
18
|
* "heartbeat": {
|
|
17
|
-
* "enabled": true,
|
|
18
19
|
* "every": "30m",
|
|
19
20
|
* "prompt": "Read HEARTBEAT.md...",
|
|
20
21
|
* "ackMaxChars": 300,
|
|
21
|
-
* "
|
|
22
|
+
* "target": "none",
|
|
23
|
+
* "activeHours": { "start": "09:00", "end": "22:00", "timezone": "America/New_York" },
|
|
24
|
+
* "isolatedSession": false,
|
|
25
|
+
* "lightContext": false,
|
|
26
|
+
* "model": null,
|
|
27
|
+
* "includeReasoning": false,
|
|
28
|
+
* "suppressToolErrorWarnings": false
|
|
22
29
|
* }
|
|
23
|
-
* }
|
|
30
|
+
* },
|
|
31
|
+
* "list": [
|
|
32
|
+
* {
|
|
33
|
+
* "id": "ops",
|
|
34
|
+
* "heartbeat": {
|
|
35
|
+
* "every": "1h",
|
|
36
|
+
* "target": "telegram",
|
|
37
|
+
* "to": "+15551234567"
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
* ]
|
|
24
41
|
* }
|
|
25
42
|
* }
|
|
26
43
|
*
|
|
44
|
+
* Response Contract:
|
|
45
|
+
* - If nothing needs attention, reply with "HEARTBEAT_OK"
|
|
46
|
+
* - For alerts, do NOT include HEARTBEAT_OK
|
|
47
|
+
* - HEARTBEAT_OK at start or end is stripped; reply dropped if remaining ≤ ackMaxChars
|
|
48
|
+
*
|
|
27
49
|
* For users: Edit HEARTBEAT.md in your workspace to customize checks.
|
|
28
50
|
*/
|
|
29
51
|
import * as fs from "node:fs/promises";
|
|
30
52
|
import * as path from "node:path";
|
|
53
|
+
import { fileURLToPath } from "node:url";
|
|
31
54
|
import { loadConfig } from "../../config/loader.js";
|
|
32
|
-
//
|
|
55
|
+
// Get the directory of this module for template resolution
|
|
56
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
57
|
+
const TEMPLATE_PATH = path.join(__dirname, "heartbeat-template.md");
|
|
58
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
59
|
+
// CONSTANTS
|
|
60
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
33
61
|
const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
|
|
34
62
|
const DEFAULT_EVERY = "30m";
|
|
35
63
|
const DEFAULT_ACK_MAX_CHARS = 300;
|
|
36
64
|
const HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
|
65
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
// UTILITY FUNCTIONS
|
|
67
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
37
68
|
function getDefaultConfig() {
|
|
38
69
|
return {
|
|
39
70
|
enabled: true,
|
|
40
71
|
every: DEFAULT_EVERY,
|
|
41
|
-
prompt: `Read HEARTBEAT.md if it exists. Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply ${HEARTBEAT_TOKEN}.`,
|
|
72
|
+
prompt: `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply ${HEARTBEAT_TOKEN}.`,
|
|
42
73
|
ackMaxChars: DEFAULT_ACK_MAX_CHARS,
|
|
74
|
+
target: "none",
|
|
43
75
|
activeHours: null,
|
|
76
|
+
isolatedSession: false,
|
|
77
|
+
lightContext: false,
|
|
78
|
+
includeReasoning: false,
|
|
79
|
+
suppressToolErrorWarnings: false,
|
|
80
|
+
directPolicy: "allow",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function getDefaultState() {
|
|
84
|
+
return {
|
|
85
|
+
lastHeartbeat: null,
|
|
86
|
+
lastDelivery: null,
|
|
87
|
+
lastChannel: null,
|
|
88
|
+
skippedCount: 0,
|
|
89
|
+
ackCount: 0,
|
|
90
|
+
alertCount: 0,
|
|
44
91
|
};
|
|
45
92
|
}
|
|
46
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Parse duration string to milliseconds
|
|
95
|
+
* Supports: "30m", "1h", "2h", "1h30m", "90s"
|
|
96
|
+
*/
|
|
47
97
|
function parseDurationMs(duration) {
|
|
48
98
|
const regex = /^(?:(\d+)h)?\s*(?:(\d+)m)?\s?(?:(\d+)s?)?$/i;
|
|
49
99
|
const match = duration.trim().match(regex);
|
|
50
100
|
if (!match) {
|
|
51
|
-
console.warn(`[Heartbeat] Invalid duration "${duration}", using default`);
|
|
101
|
+
console.warn(`[Heartbeat] Invalid duration "${duration}", using default 30m`);
|
|
52
102
|
return 30 * 60 * 1000;
|
|
53
103
|
}
|
|
54
104
|
const hours = parseInt(match[1] || "0", 10);
|
|
@@ -56,56 +106,71 @@ function parseDurationMs(duration) {
|
|
|
56
106
|
const seconds = parseInt(match[3] || "0", 10);
|
|
57
107
|
return ((hours * 60 + minutes) * 60 + seconds) * 1000;
|
|
58
108
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// Check if content is effectively empty
|
|
71
|
-
function isEffectivelyEmpty(content) {
|
|
72
|
-
const lines = content.split("\n");
|
|
73
|
-
for (const line of lines) {
|
|
74
|
-
const trimmed = line.trim();
|
|
75
|
-
if (!trimmed)
|
|
76
|
-
continue;
|
|
77
|
-
if (/^#+\s/.test(trimmed))
|
|
78
|
-
continue;
|
|
79
|
-
if (/^[-*+]\s*(\[[\sXx]\]\s*)?$/.test(trimmed))
|
|
80
|
-
continue;
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
return true;
|
|
109
|
+
/**
|
|
110
|
+
* Parse time string "HH:MM" to minutes since midnight
|
|
111
|
+
*/
|
|
112
|
+
function parseTimeToMinutes(timeStr) {
|
|
113
|
+
const [hours, minutes] = timeStr.split(":").map(Number);
|
|
114
|
+
return hours * 60 + minutes;
|
|
84
115
|
}
|
|
85
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Check if current time is within active hours
|
|
118
|
+
*/
|
|
86
119
|
function isWithinActiveHours(config) {
|
|
87
120
|
if (!config.activeHours)
|
|
88
121
|
return true;
|
|
89
122
|
const now = new Date();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
// Get timezone
|
|
124
|
+
let tz;
|
|
125
|
+
if (config.activeHours.timezone === "local") {
|
|
126
|
+
tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
127
|
+
}
|
|
128
|
+
else if (config.activeHours.timezone) {
|
|
129
|
+
tz = config.activeHours.timezone;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Try user timezone from config, fallback to local
|
|
133
|
+
tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
137
|
+
timeZone: tz,
|
|
138
|
+
hour: "2-digit",
|
|
139
|
+
minute: "2-digit",
|
|
140
|
+
hour12: false,
|
|
141
|
+
});
|
|
142
|
+
const timeStr = formatter.format(now);
|
|
143
|
+
const [currentHour, currentMin] = timeStr.split(":").map(Number);
|
|
144
|
+
const currentMinutes = currentHour * 60 + currentMin;
|
|
145
|
+
const startMinutes = parseTimeToMinutes(config.activeHours.start);
|
|
146
|
+
const endMinutes = parseTimeToMinutes(config.activeHours.end);
|
|
147
|
+
// Handle wrap-around (e.g., 22:00 to 06:00)
|
|
148
|
+
if (startMinutes === endMinutes) {
|
|
149
|
+
// Zero-width window, always outside
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (startMinutes > endMinutes) {
|
|
153
|
+
// Wrap around midnight
|
|
154
|
+
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
|
155
|
+
}
|
|
156
|
+
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Invalid timezone, use local
|
|
160
|
+
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
161
|
+
const startMinutes = parseTimeToMinutes(config.activeHours.start);
|
|
162
|
+
const endMinutes = parseTimeToMinutes(config.activeHours.end);
|
|
163
|
+
if (startMinutes === endMinutes)
|
|
164
|
+
return false;
|
|
165
|
+
if (startMinutes > endMinutes) {
|
|
166
|
+
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
|
167
|
+
}
|
|
168
|
+
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
|
105
169
|
}
|
|
106
|
-
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
|
107
170
|
}
|
|
108
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Find workspace directory containing HEARTBEAT.md
|
|
173
|
+
*/
|
|
109
174
|
async function findWorkspaceDir() {
|
|
110
175
|
try {
|
|
111
176
|
await fs.access(HEARTBEAT_FILENAME);
|
|
@@ -130,52 +195,126 @@ async function findWorkspaceDir() {
|
|
|
130
195
|
}
|
|
131
196
|
return process.cwd();
|
|
132
197
|
}
|
|
133
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Read HEARTBEAT.md from workspace
|
|
200
|
+
*/
|
|
201
|
+
async function readHeartbeatFile(workspacePath) {
|
|
134
202
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
203
|
+
const filePath = path.join(workspacePath, HEARTBEAT_FILENAME);
|
|
204
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
205
|
+
return content;
|
|
137
206
|
}
|
|
138
207
|
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Check if content is effectively empty (no actionable items)
|
|
213
|
+
*/
|
|
214
|
+
function isEffectivelyEmpty(content) {
|
|
215
|
+
const lines = content.split("\n");
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
const trimmed = line.trim();
|
|
218
|
+
if (!trimmed)
|
|
219
|
+
continue;
|
|
220
|
+
if (/^#+\s/.test(trimmed))
|
|
221
|
+
continue; // Headers
|
|
222
|
+
if (/^[-*+]\s*(\[[\sXx]\]\s*)?$/.test(trimmed))
|
|
223
|
+
continue; // Empty list items
|
|
224
|
+
if (/^<!--.*-->$/.test(trimmed))
|
|
225
|
+
continue; // HTML comments
|
|
226
|
+
if (/^\*\*.*\*\*$/.test(trimmed))
|
|
227
|
+
continue; // Bold standalone
|
|
139
228
|
return false;
|
|
140
229
|
}
|
|
230
|
+
return true;
|
|
141
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Process HEARTBEAT_OK response contract
|
|
234
|
+
* Returns { isAck, strippedContent }
|
|
235
|
+
*/
|
|
236
|
+
function processHeartbeatResponse(content, ackMaxChars) {
|
|
237
|
+
const trimmed = content.trim();
|
|
238
|
+
// Check for HEARTBEAT_OK at start or end
|
|
239
|
+
const startsWithToken = trimmed.startsWith(HEARTBEAT_TOKEN);
|
|
240
|
+
const endsWithToken = trimmed.endsWith(HEARTBEAT_TOKEN);
|
|
241
|
+
if (!startsWithToken && !endsWithToken) {
|
|
242
|
+
// Not an ack - this is an alert
|
|
243
|
+
return { isAck: false, strippedContent: content, shouldDeliver: true };
|
|
244
|
+
}
|
|
245
|
+
// Strip the token
|
|
246
|
+
let stripped = trimmed;
|
|
247
|
+
if (startsWithToken) {
|
|
248
|
+
stripped = stripped.slice(HEARTBEAT_TOKEN.length).trim();
|
|
249
|
+
}
|
|
250
|
+
if (endsWithToken && !startsWithToken) {
|
|
251
|
+
stripped = stripped.slice(0, -HEARTBEAT_TOKEN.length).trim();
|
|
252
|
+
}
|
|
253
|
+
// Check remaining length
|
|
254
|
+
const shouldDeliver = stripped.length > ackMaxChars;
|
|
255
|
+
return {
|
|
256
|
+
isAck: true,
|
|
257
|
+
strippedContent: stripped,
|
|
258
|
+
shouldDeliver,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Load heartbeat configuration from kobold.json
|
|
263
|
+
*/
|
|
264
|
+
async function loadHeartbeatConfig() {
|
|
265
|
+
const defaultConfig = getDefaultConfig();
|
|
266
|
+
try {
|
|
267
|
+
const snapshot = await loadConfig();
|
|
268
|
+
const hbConfig = snapshot.config.agents?.defaults?.heartbeat;
|
|
269
|
+
if (hbConfig) {
|
|
270
|
+
return {
|
|
271
|
+
enabled: hbConfig.enabled ?? defaultConfig.enabled,
|
|
272
|
+
every: hbConfig.every ?? defaultConfig.every,
|
|
273
|
+
prompt: hbConfig.prompt ?? defaultConfig.prompt,
|
|
274
|
+
ackMaxChars: hbConfig.ackMaxChars ?? defaultConfig.ackMaxChars,
|
|
275
|
+
target: hbConfig.target ?? defaultConfig.target,
|
|
276
|
+
to: hbConfig.to,
|
|
277
|
+
activeHours: hbConfig.activeHours ?? defaultConfig.activeHours,
|
|
278
|
+
isolatedSession: hbConfig.isolatedSession ?? defaultConfig.isolatedSession,
|
|
279
|
+
lightContext: hbConfig.lightContext ?? defaultConfig.lightContext,
|
|
280
|
+
model: hbConfig.model,
|
|
281
|
+
includeReasoning: hbConfig.includeReasoning ?? defaultConfig.includeReasoning,
|
|
282
|
+
suppressToolErrorWarnings: hbConfig.suppressToolErrorWarnings ?? defaultConfig.suppressToolErrorWarnings,
|
|
283
|
+
directPolicy: hbConfig.directPolicy ?? defaultConfig.directPolicy,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
console.warn("[Heartbeat] Failed to load config, using defaults:", err);
|
|
289
|
+
}
|
|
290
|
+
return defaultConfig;
|
|
291
|
+
}
|
|
292
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
293
|
+
// EXTENSION
|
|
294
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
142
295
|
export default function heartbeatExtension(pi) {
|
|
143
296
|
const startTime = Date.now();
|
|
144
297
|
let checkInterval = null;
|
|
145
|
-
let lastHeartbeat = 0;
|
|
146
|
-
// Load config from kobold.json
|
|
147
298
|
let config = getDefaultConfig();
|
|
299
|
+
let state = getDefaultState();
|
|
148
300
|
let configLoaded = false;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
enabled: hbConfig.enabled ?? config.enabled,
|
|
156
|
-
every: hbConfig.every ?? config.every,
|
|
157
|
-
prompt: hbConfig.prompt ?? config.prompt,
|
|
158
|
-
ackMaxChars: hbConfig.ackMaxChars ?? config.ackMaxChars,
|
|
159
|
-
activeHours: hbConfig.activeHours ?? config.activeHours,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
configLoaded = true;
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
console.warn('[Heartbeat] Failed to load config, using defaults:', err);
|
|
166
|
-
}
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════
|
|
302
|
+
// CONFIG LOADING
|
|
303
|
+
// ═══════════════════════════════════════════════════════════════
|
|
304
|
+
async function reloadConfig() {
|
|
305
|
+
config = await loadHeartbeatConfig();
|
|
306
|
+
configLoaded = true;
|
|
167
307
|
}
|
|
168
308
|
// Load config on startup
|
|
169
|
-
|
|
309
|
+
reloadConfig().then(() => {
|
|
170
310
|
console.log(`[Heartbeat] Config loaded (enabled: ${config.enabled}, every: ${config.every})`);
|
|
171
311
|
});
|
|
172
|
-
// Reload config on environment changes
|
|
173
312
|
pi.on("session_start", async () => {
|
|
174
|
-
await
|
|
313
|
+
await reloadConfig();
|
|
175
314
|
console.log(`[Heartbeat] Extension loaded (enabled: ${config.enabled}, every: ${config.every})`);
|
|
176
315
|
});
|
|
177
316
|
// ═══════════════════════════════════════════════════════════════
|
|
178
|
-
//
|
|
317
|
+
// HEARTBEAT CHECK TOOL
|
|
179
318
|
// ═══════════════════════════════════════════════════════════════
|
|
180
319
|
pi.registerTool({
|
|
181
320
|
name: "heartbeat_check",
|
|
@@ -189,68 +328,107 @@ export default function heartbeatExtension(pi) {
|
|
|
189
328
|
type: "boolean",
|
|
190
329
|
description: "Force a check even if recently performed",
|
|
191
330
|
},
|
|
331
|
+
agent: {
|
|
332
|
+
type: "string",
|
|
333
|
+
description: "Agent ID to check (for per-agent configs)",
|
|
334
|
+
},
|
|
192
335
|
},
|
|
193
336
|
},
|
|
194
337
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
195
|
-
const { force } = params;
|
|
338
|
+
const { force, agent } = params;
|
|
196
339
|
const now = Date.now();
|
|
197
340
|
const everyMs = parseDurationMs(config.every);
|
|
198
341
|
// Check active hours
|
|
199
342
|
if (!isWithinActiveHours(config)) {
|
|
200
343
|
return {
|
|
201
|
-
content: [
|
|
344
|
+
content: [
|
|
345
|
+
{
|
|
202
346
|
type: "text",
|
|
203
|
-
text:
|
|
204
|
-
}
|
|
205
|
-
|
|
347
|
+
text: `Heartbeat skipped - outside active hours (${config.activeHours?.start} - ${config.activeHours?.end}). Reply ${HEARTBEAT_TOKEN} to acknowledge.`,
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
details: { skipped: true, reason: "outside_active_hours", activeHours: config.activeHours },
|
|
206
351
|
};
|
|
207
352
|
}
|
|
208
|
-
|
|
353
|
+
// Check if too soon (unless forced)
|
|
354
|
+
if (!force && state.lastHeartbeat && now - new Date(state.lastHeartbeat).getTime() < everyMs) {
|
|
355
|
+
const nextCheck = new Date(new Date(state.lastHeartbeat).getTime() + everyMs);
|
|
209
356
|
return {
|
|
210
|
-
content: [
|
|
357
|
+
content: [
|
|
358
|
+
{
|
|
211
359
|
type: "text",
|
|
212
|
-
text:
|
|
213
|
-
}
|
|
214
|
-
|
|
360
|
+
text: `Heartbeat skipped - next check at ${nextCheck.toLocaleTimeString()}. Reply ${HEARTBEAT_TOKEN} to acknowledge.`,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
details: { skipped: true, reason: "too_soon", nextCheck: nextCheck.toISOString() },
|
|
215
364
|
};
|
|
216
365
|
}
|
|
366
|
+
// Find and read HEARTBEAT.md
|
|
217
367
|
const workspaceDir = await findWorkspaceDir();
|
|
218
368
|
const heartbeatContent = await readHeartbeatFile(workspaceDir);
|
|
219
369
|
if (heartbeatContent === null) {
|
|
220
370
|
return {
|
|
221
|
-
content: [
|
|
371
|
+
content: [
|
|
372
|
+
{
|
|
222
373
|
type: "text",
|
|
223
374
|
text: `No HEARTBEAT.md found in workspace. Reply ${HEARTBEAT_TOKEN} to acknowledge, or create the file to enable heartbeat checks.`,
|
|
224
|
-
}
|
|
225
|
-
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
details: { fileExists: false, workspaceDir },
|
|
226
378
|
};
|
|
227
379
|
}
|
|
228
380
|
if (isEffectivelyEmpty(heartbeatContent)) {
|
|
229
381
|
return {
|
|
230
|
-
content: [
|
|
382
|
+
content: [
|
|
383
|
+
{
|
|
231
384
|
type: "text",
|
|
232
|
-
text:
|
|
233
|
-
}
|
|
385
|
+
text: `HEARTBEAT.md is effectively empty (no actionable items). Reply ${HEARTBEAT_TOKEN} to acknowledge.`,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
234
388
|
details: { fileExists: true, isEmpty: true },
|
|
235
389
|
};
|
|
236
390
|
}
|
|
237
|
-
|
|
391
|
+
// Update state
|
|
392
|
+
state.lastHeartbeat = new Date().toISOString();
|
|
393
|
+
// Build response with config context
|
|
394
|
+
const responseLines = [
|
|
395
|
+
`HEARTBEAT.md content:`,
|
|
396
|
+
"",
|
|
397
|
+
heartbeatContent,
|
|
398
|
+
"",
|
|
399
|
+
"---",
|
|
400
|
+
`Check interval: ${config.every}`,
|
|
401
|
+
config.activeHours ? `Active hours: ${config.activeHours.start} - ${config.activeHours.end}` : null,
|
|
402
|
+
`Delivery target: ${config.target}`,
|
|
403
|
+
"",
|
|
404
|
+
`Review the checklist above. If nothing needs attention, reply with "${HEARTBEAT_TOKEN}". Otherwise, describe what needs attention.`,
|
|
405
|
+
].filter(Boolean);
|
|
238
406
|
return {
|
|
239
|
-
content: [
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
240
409
|
type: "text",
|
|
241
|
-
text:
|
|
242
|
-
}
|
|
243
|
-
|
|
410
|
+
text: responseLines.join("\n"),
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
details: {
|
|
414
|
+
fileExists: true,
|
|
415
|
+
isEmpty: false,
|
|
416
|
+
config: {
|
|
417
|
+
every: config.every,
|
|
418
|
+
target: config.target,
|
|
419
|
+
activeHours: config.activeHours,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
244
422
|
};
|
|
245
423
|
},
|
|
246
424
|
});
|
|
247
425
|
// ═══════════════════════════════════════════════════════════════
|
|
248
|
-
//
|
|
426
|
+
// HEARTBEAT STATUS COMMAND
|
|
249
427
|
// ═══════════════════════════════════════════════════════════════
|
|
250
428
|
pi.registerCommand("heartbeat", {
|
|
251
429
|
description: "Show heartbeat status or trigger manual check",
|
|
252
430
|
handler: async (args, ctx) => {
|
|
253
|
-
await
|
|
431
|
+
await reloadConfig();
|
|
254
432
|
if (args.trim() === "now" || args.trim() === "check") {
|
|
255
433
|
if (!isWithinActiveHours(config)) {
|
|
256
434
|
ctx.ui.notify("❤️ Heartbeat skipped - outside active hours.", "info");
|
|
@@ -270,61 +448,91 @@ export default function heartbeatExtension(pi) {
|
|
|
270
448
|
}
|
|
271
449
|
return;
|
|
272
450
|
}
|
|
451
|
+
// Show status
|
|
273
452
|
const lines = [
|
|
274
453
|
"❤️ Heartbeat Configuration",
|
|
275
454
|
`Enabled: ${config.enabled ? "✅" : "❌"}`,
|
|
276
455
|
`Interval: ${config.every}`,
|
|
277
456
|
`Ack Max Chars: ${config.ackMaxChars}`,
|
|
457
|
+
`Target: ${config.target}`,
|
|
458
|
+
`Isolated Session: ${config.isolatedSession}`,
|
|
459
|
+
`Light Context: ${config.lightContext}`,
|
|
278
460
|
];
|
|
279
461
|
if (config.activeHours) {
|
|
280
462
|
lines.push(`Active Hours: ${config.activeHours.start} - ${config.activeHours.end}`);
|
|
463
|
+
if (config.activeHours.timezone) {
|
|
464
|
+
lines.push(`Timezone: ${config.activeHours.timezone}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (config.model) {
|
|
468
|
+
lines.push(`Model Override: ${config.model}`);
|
|
281
469
|
}
|
|
282
470
|
const workspaceDir = await findWorkspaceDir();
|
|
283
|
-
const hbExists = await
|
|
471
|
+
const hbExists = await fs
|
|
472
|
+
.access(path.join(workspaceDir, HEARTBEAT_FILENAME))
|
|
473
|
+
.then(() => true)
|
|
474
|
+
.catch(() => false);
|
|
284
475
|
lines.push(`HEARTBEAT.md: ${hbExists ? "✅ Found" : "⚠️ Not found"}`);
|
|
476
|
+
// State
|
|
477
|
+
lines.push("");
|
|
478
|
+
lines.push("📊 State:");
|
|
479
|
+
lines.push(`Last Check: ${state.lastHeartbeat || "Never"}`);
|
|
480
|
+
lines.push(`Acknowledged: ${state.ackCount}`);
|
|
481
|
+
lines.push(`Alerts: ${state.alertCount}`);
|
|
482
|
+
lines.push(`Skipped: ${state.skippedCount}`);
|
|
285
483
|
lines.push("", "Usage:", " /heartbeat now - Run check immediately", " /heartbeat-init - Create HEARTBEAT.md");
|
|
286
484
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
287
485
|
},
|
|
288
486
|
});
|
|
487
|
+
// ═══════════════════════════════════════════════════════════════
|
|
488
|
+
// HEARTBEAT INIT COMMAND
|
|
489
|
+
// ═══════════════════════════════════════════════════════════════
|
|
289
490
|
pi.registerCommand("heartbeat-init", {
|
|
290
491
|
description: "Create HEARTBEAT.md template in workspace",
|
|
291
492
|
handler: async (_args, ctx) => {
|
|
292
493
|
const workspaceDir = await findWorkspaceDir();
|
|
293
494
|
const filePath = path.join(workspaceDir, HEARTBEAT_FILENAME);
|
|
294
|
-
|
|
495
|
+
const exists = await fs
|
|
496
|
+
.access(filePath)
|
|
497
|
+
.then(() => true)
|
|
498
|
+
.catch(() => false);
|
|
499
|
+
if (exists) {
|
|
295
500
|
ctx.ui.notify(`HEARTBEAT.md already exists at ${filePath}`, "warning");
|
|
296
501
|
return;
|
|
297
502
|
}
|
|
298
|
-
|
|
503
|
+
// Try to read the template file, fallback to embedded template
|
|
504
|
+
let template;
|
|
505
|
+
try {
|
|
506
|
+
template = await fs.readFile(TEMPLATE_PATH, "utf-8");
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// Fallback to minimal template
|
|
510
|
+
template = `# Heartbeat Checklist
|
|
511
|
+
|
|
512
|
+
## Response Protocol
|
|
299
513
|
|
|
300
|
-
|
|
301
|
-
This file controls what the agent checks during periodic heartbeats.
|
|
302
|
-
Keep it short and actionable. If this file is empty or only contains headers,
|
|
303
|
-
heartbeats will be skipped to save tokens.
|
|
514
|
+
Reply with HEARTBEAT_OK if nothing needs attention.
|
|
304
515
|
|
|
305
|
-
|
|
306
|
-
If nothing needs attention, it replies with HEARTBEAT_OK.
|
|
307
|
-
If something needs attention, it describes the issue without the token.
|
|
308
|
-
-->
|
|
516
|
+
---
|
|
309
517
|
|
|
310
518
|
## Regular Checks
|
|
311
519
|
|
|
312
|
-
- [ ]
|
|
313
|
-
- [ ]
|
|
314
|
-
- [ ]
|
|
520
|
+
- [ ] Pending tasks needing human input
|
|
521
|
+
- [ ] Blocked items
|
|
522
|
+
- [ ] System status alerts
|
|
315
523
|
|
|
316
|
-
## Context-Aware
|
|
524
|
+
## Context-Aware Checks
|
|
317
525
|
|
|
318
|
-
Only
|
|
319
|
-
- [ ]
|
|
320
|
-
- [ ]
|
|
321
|
-
- [ ]
|
|
526
|
+
Only when relevant:
|
|
527
|
+
- [ ] Scheduled tasks/reminders
|
|
528
|
+
- [ ] Social platform notifications
|
|
529
|
+
- [ ] Monitoring alerts
|
|
322
530
|
|
|
323
|
-
|
|
531
|
+
---
|
|
324
532
|
|
|
325
|
-
|
|
326
|
-
If something needs attention → brief alert message (no HEARTBEAT_OK token)
|
|
533
|
+
HEARTBEAT_OK
|
|
327
534
|
`;
|
|
535
|
+
}
|
|
328
536
|
try {
|
|
329
537
|
await fs.writeFile(filePath, template, "utf-8");
|
|
330
538
|
ctx.ui.notify(`✅ Created ${filePath}`, "info");
|
|
@@ -336,6 +544,55 @@ If something needs attention → brief alert message (no HEARTBEAT_OK token)
|
|
|
336
544
|
},
|
|
337
545
|
});
|
|
338
546
|
// ═══════════════════════════════════════════════════════════════
|
|
547
|
+
// PER-AGENT HEARTBEAT CONFIG
|
|
548
|
+
// ═══════════════════════════════════════════════════════════════
|
|
549
|
+
pi.registerTool({
|
|
550
|
+
name: "heartbeat_config",
|
|
551
|
+
label: "Heartbeat Config",
|
|
552
|
+
description: "Get or update heartbeat configuration. Use this to check current settings or modify behavior per agent.",
|
|
553
|
+
// @ts-ignore TSchema mismatch
|
|
554
|
+
parameters: {
|
|
555
|
+
type: "object",
|
|
556
|
+
properties: {
|
|
557
|
+
action: {
|
|
558
|
+
type: "string",
|
|
559
|
+
enum: ["get", "set"],
|
|
560
|
+
description: "Get current config or update settings",
|
|
561
|
+
},
|
|
562
|
+
setting: {
|
|
563
|
+
type: "object",
|
|
564
|
+
description: "Settings to update (for set action)",
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
async execute(_toolCallId, params) {
|
|
569
|
+
const { action, setting } = params;
|
|
570
|
+
if (action === "set" && setting) {
|
|
571
|
+
// Merge new settings
|
|
572
|
+
config = { ...config, ...setting };
|
|
573
|
+
return {
|
|
574
|
+
content: [
|
|
575
|
+
{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: `Heartbeat config updated:\n${JSON.stringify(config, null, 2)}`,
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
details: { config },
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
// Get current config
|
|
584
|
+
return {
|
|
585
|
+
content: [
|
|
586
|
+
{
|
|
587
|
+
type: "text",
|
|
588
|
+
text: `Current heartbeat configuration:\n${JSON.stringify(config, null, 2)}`,
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
details: { config, state },
|
|
592
|
+
};
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
// ═══════════════════════════════════════════════════════════════
|
|
339
596
|
// HEALTH CHECK COMMAND (for VPS deployment)
|
|
340
597
|
// ═══════════════════════════════════════════════════════════════
|
|
341
598
|
pi.registerCommand("healthz", {
|
|
@@ -349,32 +606,35 @@ If something needs attention → brief alert message (no HEARTBEAT_OK token)
|
|
|
349
606
|
timestamp: now,
|
|
350
607
|
uptime: `${Math.floor(uptime / 86400)}d ${Math.floor((uptime % 86400) / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${Math.floor(uptime % 60)}s`,
|
|
351
608
|
uptimeSeconds: Math.floor(uptime),
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
609
|
+
heartbeat: {
|
|
610
|
+
enabled: config.enabled,
|
|
611
|
+
interval: config.every,
|
|
612
|
+
lastCheck: state.lastHeartbeat,
|
|
613
|
+
activeHours: config.activeHours,
|
|
614
|
+
currentlyActive: isWithinActiveHours(config),
|
|
359
615
|
},
|
|
360
616
|
memory: {
|
|
361
|
-
heapUsed: Math.round(memory.heapUsed / 1024 / 1024)
|
|
362
|
-
heapTotal: Math.round(memory.heapTotal / 1024 / 1024)
|
|
363
|
-
rss: Math.round(memory.rss / 1024 / 1024)
|
|
364
|
-
}
|
|
617
|
+
heapUsed: `${Math.round(memory.heapUsed / 1024 / 1024)} MB`,
|
|
618
|
+
heapTotal: `${Math.round(memory.heapTotal / 1024 / 1024)} MB`,
|
|
619
|
+
rss: `${Math.round(memory.rss / 1024 / 1024)} MB`,
|
|
620
|
+
},
|
|
621
|
+
stats: {
|
|
622
|
+
acknowledged: state.ackCount,
|
|
623
|
+
alerts: state.alertCount,
|
|
624
|
+
skipped: state.skippedCount,
|
|
625
|
+
},
|
|
365
626
|
};
|
|
366
|
-
// Return JSON for programmatic access
|
|
367
627
|
ctx.ui.notify(JSON.stringify(status, null, 2), "info");
|
|
368
628
|
},
|
|
369
629
|
});
|
|
370
|
-
// Also register /health as alias
|
|
371
630
|
pi.registerCommand("health", {
|
|
372
631
|
description: "Health check (alias for /healthz)",
|
|
373
632
|
handler: async (_args, ctx) => {
|
|
374
633
|
// Forward to healthz
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
634
|
+
const cmds = pi.getCommands?.() || [];
|
|
635
|
+
const healthz = cmds.find((c) => c.name === "healthz");
|
|
636
|
+
if (healthz) {
|
|
637
|
+
await healthz.handler("", ctx);
|
|
378
638
|
}
|
|
379
639
|
},
|
|
380
640
|
});
|
|
@@ -382,26 +642,32 @@ If something needs attention → brief alert message (no HEARTBEAT_OK token)
|
|
|
382
642
|
// AUTO-HEARTBEAT SCHEDULER
|
|
383
643
|
// ═══════════════════════════════════════════════════════════════
|
|
384
644
|
pi.on("session_start", async () => {
|
|
385
|
-
await
|
|
645
|
+
await reloadConfig();
|
|
386
646
|
if (!config.enabled) {
|
|
387
647
|
console.log("[Heartbeat] Disabled in config, not starting scheduler");
|
|
388
648
|
return;
|
|
389
649
|
}
|
|
390
650
|
const everyMs = parseDurationMs(config.every);
|
|
651
|
+
// Clear any existing interval
|
|
652
|
+
if (checkInterval) {
|
|
653
|
+
clearInterval(checkInterval);
|
|
654
|
+
}
|
|
391
655
|
// Schedule periodic checks
|
|
392
656
|
checkInterval = setInterval(async () => {
|
|
393
657
|
if (!isWithinActiveHours(config)) {
|
|
394
658
|
console.log("[Heartbeat] Skipped - outside active hours");
|
|
659
|
+
state.skippedCount++;
|
|
395
660
|
return;
|
|
396
661
|
}
|
|
397
662
|
const workspaceDir = await findWorkspaceDir();
|
|
398
663
|
const content = await readHeartbeatFile(workspaceDir);
|
|
399
664
|
// Skip if no file or empty file
|
|
400
665
|
if (content === null || isEffectivelyEmpty(content)) {
|
|
666
|
+
console.log("[Heartbeat] Skipped - no or empty HEARTBEAT.md");
|
|
401
667
|
return;
|
|
402
668
|
}
|
|
403
|
-
//
|
|
404
|
-
pi.sendUserMessage(
|
|
669
|
+
// Trigger heartbeat via user message
|
|
670
|
+
pi.sendUserMessage(config.prompt, { deliverAs: "followUp" });
|
|
405
671
|
}, everyMs);
|
|
406
672
|
console.log(`[Heartbeat] Scheduler started (every ${config.every})`);
|
|
407
673
|
});
|
|
@@ -411,6 +677,6 @@ If something needs attention → brief alert message (no HEARTBEAT_OK token)
|
|
|
411
677
|
console.log("[Heartbeat] Scheduler stopped");
|
|
412
678
|
}
|
|
413
679
|
});
|
|
414
|
-
console.log("[Heartbeat] Extension loaded");
|
|
680
|
+
console.log("[Heartbeat] Extension loaded (OpenClaw-compatible)");
|
|
415
681
|
}
|
|
416
682
|
//# sourceMappingURL=heartbeat-extension.js.map
|