@poolzin/pool-bot 2026.2.21 → 2026.2.22
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/CHANGELOG.md +17 -0
- package/dist/agents/api-key-rotation.js +47 -0
- package/dist/agents/apply-patch-update.js +19 -9
- package/dist/agents/apply-patch.js +72 -47
- package/dist/agents/bash-tools.exec.js +141 -559
- package/dist/agents/cli-backends.js +49 -6
- package/dist/agents/cli-runner/helpers.js +69 -152
- package/dist/agents/cli-runner.js +70 -19
- package/dist/agents/identity.js +20 -1
- package/dist/agents/image-sanitization.js +9 -0
- package/dist/agents/live-auth-keys.js +123 -26
- package/dist/agents/live-model-filter.js +13 -4
- package/dist/agents/model-catalog.js +40 -9
- package/dist/agents/model-forward-compat.js +60 -23
- package/dist/agents/model-selection.js +134 -41
- package/dist/agents/pi-auth-json.js +2 -2
- package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
- package/dist/agents/pi-embedded-helpers/errors.js +140 -15
- package/dist/agents/pi-embedded-helpers/images.js +22 -12
- package/dist/agents/pi-embedded-helpers.js +2 -2
- package/dist/agents/pi-embedded-runner/abort.js +10 -3
- package/dist/agents/pi-embedded-runner/compact.js +230 -32
- package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
- package/dist/agents/pi-embedded-runner/google.js +109 -19
- package/dist/agents/pi-embedded-runner/history.js +35 -17
- package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
- package/dist/agents/pi-embedded-runner/run/images.js +81 -55
- package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
- package/dist/agents/pi-embedded-runner/run.js +193 -25
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
- package/dist/agents/pi-embedded-runner/runs.js +17 -8
- package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
- package/dist/agents/pi-embedded-runner.js +1 -1
- package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
- package/dist/agents/pi-embedded-subscribe.js +37 -0
- package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
- package/dist/agents/pi-model-discovery.js +9 -2
- package/dist/agents/pi-tool-definition-adapter.js +60 -8
- package/dist/agents/pi-tools.before-tool-call.js +1 -1
- package/dist/agents/pi-tools.js +113 -94
- package/dist/agents/pi-tools.read.js +337 -38
- package/dist/agents/poolbot-tools.js +14 -5
- package/dist/agents/sandbox/docker.js +10 -5
- package/dist/agents/sandbox/registry.js +96 -46
- package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
- package/dist/agents/sandbox-paths.js +43 -10
- package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
- package/dist/agents/session-tool-result-guard.js +39 -39
- package/dist/agents/session-transcript-repair.js +36 -33
- package/dist/agents/session-write-lock.js +62 -44
- package/dist/agents/skills/frontmatter.js +49 -88
- package/dist/agents/skills/workspace.js +335 -28
- package/dist/agents/subagent-announce.js +508 -174
- package/dist/agents/subagent-registry.js +45 -4
- package/dist/agents/subagent-spawn.js +16 -33
- package/dist/agents/system-prompt-report.js +27 -10
- package/dist/agents/system-prompt.js +26 -32
- package/dist/agents/tool-call-id.js +69 -17
- package/dist/agents/tool-display-common.js +1 -1
- package/dist/agents/tool-images.js +64 -31
- package/dist/agents/tools/canvas-tool.js +17 -11
- package/dist/agents/tools/common.js +37 -19
- package/dist/agents/tools/cron-tool.js +40 -38
- package/dist/agents/tools/gateway.js +70 -2
- package/dist/agents/tools/message-tool.js +181 -40
- package/dist/agents/tools/nodes-tool.js +128 -36
- package/dist/agents/tools/nodes-utils.js +12 -38
- package/dist/agents/tools/session-status-tool.js +24 -71
- package/dist/agents/tools/sessions-helpers.js +38 -210
- package/dist/agents/tools/sessions-spawn-tool.js +28 -198
- package/dist/agents/tools/telegram-actions.js +58 -7
- package/dist/agents/tools/web-fetch-utils.js +112 -7
- package/dist/agents/tools/web-fetch.js +279 -175
- package/dist/agents/tools/web-shared.js +71 -8
- package/dist/agents/usage.js +25 -16
- package/dist/auto-reply/commands-registry.data.js +85 -11
- package/dist/auto-reply/dispatch.js +40 -21
- package/dist/auto-reply/reply/abort.js +102 -33
- package/dist/auto-reply/reply/commands-core.js +82 -33
- package/dist/auto-reply/reply/commands-export-session.js +1 -1
- package/dist/auto-reply/reply/commands-info.js +41 -12
- package/dist/auto-reply/reply/commands-subagents.js +352 -100
- package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
- package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
- package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
- package/dist/auto-reply/reply/inbound-meta.js +12 -1
- package/dist/auto-reply/reply/mentions.js +18 -11
- package/dist/auto-reply/reply/normalize-reply.js +17 -8
- package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
- package/dist/auto-reply/reply/session.js +102 -21
- package/dist/auto-reply/reply/streaming-directives.js +16 -5
- package/dist/auto-reply/status.js +73 -50
- package/dist/browser/extension-relay.js +3 -3
- package/dist/browser/http-auth.js +1 -1
- package/dist/browser/paths.js +2 -2
- package/dist/build-info.json +3 -3
- package/dist/channels/allowlist-match.js +20 -0
- package/dist/channels/allowlists/resolve-utils.js +65 -2
- package/dist/channels/chat-type.js +8 -4
- package/dist/channels/dock.js +127 -35
- package/dist/channels/draft-stream-loop.js +6 -2
- package/dist/channels/plugins/actions/telegram.js +42 -18
- package/dist/channels/plugins/allowlist-match.js +1 -1
- package/dist/channels/plugins/group-mentions.js +51 -41
- package/dist/channels/plugins/message-action-names.js +2 -0
- package/dist/channels/plugins/message-actions.js +24 -5
- package/dist/channels/plugins/normalize/discord.js +26 -4
- package/dist/channels/plugins/normalize/signal.js +35 -22
- package/dist/channels/plugins/onboarding/helpers.js +8 -26
- package/dist/channels/plugins/outbound/imessage.js +15 -14
- package/dist/channels/registry.js +20 -7
- package/dist/cli/acp-cli.js +7 -5
- package/dist/cli/browser-cli-extension.js +25 -12
- package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
- package/dist/cli/browser-cli-state.js +101 -145
- package/dist/cli/command-options.js +28 -0
- package/dist/cli/completion-cli.js +6 -6
- package/dist/cli/cron-cli/register.cron-add.js +25 -1
- package/dist/cli/cron-cli/register.cron-edit.js +44 -0
- package/dist/cli/cron-cli/shared.js +7 -1
- package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
- package/dist/cli/daemon-cli/lifecycle.js +23 -247
- package/dist/cli/daemon-cli/register-service-commands.js +25 -4
- package/dist/cli/daemon-cli.js +1 -0
- package/dist/cli/devices-cli.js +33 -20
- package/dist/cli/gateway-cli/register.js +37 -105
- package/dist/cli/gateway-cli/run.js +49 -11
- package/dist/cli/nodes-camera.js +59 -4
- package/dist/cli/nodes-cli/register.camera.js +27 -24
- package/dist/cli/nodes-cli/rpc.js +21 -38
- package/dist/cli/qr-cli.js +2 -2
- package/dist/cli/skills-cli.format.js +2 -2
- package/dist/cli/update-cli/progress.js +2 -2
- package/dist/cli/update-cli/restart-helper.js +28 -7
- package/dist/cli/update-cli/shared.js +7 -7
- package/dist/cli/update-cli/status.js +1 -1
- package/dist/cli/update-cli/update-command.js +14 -8
- package/dist/cli/update-cli/wizard.js +2 -2
- package/dist/cli/update-cli.js +21 -1027
- package/dist/commands/auth-choice.apply.anthropic.js +10 -2
- package/dist/commands/channels/add-mutators.js +3 -35
- package/dist/commands/channels/add.js +39 -51
- package/dist/commands/config-validation.js +1 -1
- package/dist/commands/configure.gateway-auth.js +52 -15
- package/dist/commands/configure.gateway.js +84 -40
- package/dist/commands/doctor-completion.js +3 -3
- package/dist/commands/doctor-config-flow.js +536 -16
- package/dist/commands/doctor-gateway-services.js +103 -79
- package/dist/commands/doctor-memory-search.js +9 -9
- package/dist/commands/doctor-platform-notes.js +57 -30
- package/dist/commands/doctor-prompter.js +26 -15
- package/dist/commands/doctor-session-locks.js +1 -1
- package/dist/commands/doctor.js +21 -9
- package/dist/commands/model-picker.js +120 -95
- package/dist/commands/models/set.js +2 -21
- package/dist/commands/models/shared.js +65 -37
- package/dist/commands/onboard-helpers.js +81 -39
- package/dist/commands/openai-codex-oauth.js +1 -1
- package/dist/commands/sessions.js +52 -53
- package/dist/commands/status.summary.js +52 -34
- package/dist/commands/test-wizard-helpers.js +2 -2
- package/dist/config/defaults.js +79 -42
- package/dist/config/group-policy.js +50 -18
- package/dist/config/includes.js +37 -10
- package/dist/config/schema.help.js +5 -4
- package/dist/config/schema.hints.js +2 -2
- package/dist/config/schema.labels.js +1 -0
- package/dist/config/sessions/group.js +12 -11
- package/dist/config/sessions/paths.js +137 -11
- package/dist/config/sessions/store.js +185 -65
- package/dist/config/sessions/types.js +15 -1
- package/dist/config/sessions.js +1 -0
- package/dist/config/telegram-custom-commands.js +3 -2
- package/dist/config/types.js +2 -0
- package/dist/config/zod-schema.agent-defaults.js +6 -27
- package/dist/config/zod-schema.agent-runtime.js +171 -79
- package/dist/config/zod-schema.providers-core.js +138 -65
- package/dist/config/zod-schema.session.js +49 -22
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
- package/dist/cron/isolated-agent/run.js +224 -57
- package/dist/cron/normalize.js +48 -45
- package/dist/cron/run-log.js +14 -0
- package/dist/cron/service/jobs.js +190 -28
- package/dist/cron/service/normalize.js +29 -11
- package/dist/cron/service/store.js +30 -44
- package/dist/cron/service/timer.js +182 -96
- package/dist/cron/service.js +3 -0
- package/dist/cron/stagger.js +37 -0
- package/dist/daemon/inspect.js +132 -92
- package/dist/daemon/runtime-paths.js +25 -4
- package/dist/daemon/service-audit.js +47 -16
- package/dist/discord/accounts.js +23 -20
- package/dist/discord/monitor/agent-components.js +1115 -219
- package/dist/discord/monitor/allow-list.js +114 -34
- package/dist/discord/monitor/listeners.js +204 -97
- package/dist/discord/monitor/message-handler.js +21 -10
- package/dist/discord/monitor/message-handler.preflight.js +195 -101
- package/dist/discord/monitor/message-handler.process.js +384 -123
- package/dist/discord/monitor/message-utils.js +86 -23
- package/dist/discord/monitor/native-command.js +77 -57
- package/dist/discord/monitor/provider.js +122 -117
- package/dist/discord/monitor/reply-context.js +20 -16
- package/dist/discord/monitor/reply-delivery.js +40 -8
- package/dist/discord/monitor/rest-fetch.js +22 -0
- package/dist/discord/monitor/threading.js +117 -24
- package/dist/discord/send.js +2 -1
- package/dist/discord/send.outbound.js +124 -11
- package/dist/discord/send.shared.js +112 -72
- package/dist/discord/voice-message.js +3 -3
- package/dist/gateway/auth.js +119 -44
- package/dist/gateway/call.js +76 -34
- package/dist/gateway/channel-health-monitor.js +57 -50
- package/dist/gateway/client.js +63 -29
- package/dist/gateway/control-ui-contract.js +1 -1
- package/dist/gateway/gateway-config-prompts.shared.js +2 -2
- package/dist/gateway/net.js +109 -1
- package/dist/gateway/protocol/index.js +5 -8
- package/dist/gateway/protocol/schema/agent.js +19 -1
- package/dist/gateway/protocol/schema/channels.js +21 -0
- package/dist/gateway/protocol/schema/cron.js +43 -30
- package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
- package/dist/gateway/protocol/schema/sessions.js +5 -1
- package/dist/gateway/protocol/schema.js +0 -1
- package/dist/gateway/server/presence-events.js +12 -0
- package/dist/gateway/server/ws-connection/message-handler.js +203 -212
- package/dist/gateway/server/ws-connection.js +58 -21
- package/dist/gateway/server-broadcast.js +18 -13
- package/dist/gateway/server-cron.js +177 -10
- package/dist/gateway/server-methods/agent-job.js +131 -38
- package/dist/gateway/server-methods/send.js +60 -14
- package/dist/gateway/server-methods/sessions.js +160 -96
- package/dist/gateway/server-methods/system.js +5 -7
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +24 -8
- package/dist/gateway/server-node-events.js +278 -68
- package/dist/gateway/session-utils.fs.js +316 -75
- package/dist/gateway/session-utils.js +224 -70
- package/dist/gateway/sessions-patch.js +63 -20
- package/dist/gateway/test-temp-config.js +1 -1
- package/dist/gateway/tools-invoke-http.js +118 -70
- package/dist/gateway/ws-log.js +135 -107
- package/dist/hooks/frontmatter.js +36 -82
- package/dist/hooks/install.js +149 -139
- package/dist/hooks/internal-hooks.js +29 -4
- package/dist/hooks/plugin-hooks.js +2 -1
- package/dist/imessage/monitor/deliver.js +10 -4
- package/dist/imessage/monitor/monitor-provider.js +138 -375
- package/dist/imessage/monitor/runtime.js +4 -8
- package/dist/imessage/send.js +65 -19
- package/dist/infra/exec-approvals-allowlist.js +7 -0
- package/dist/infra/exec-approvals.js +35 -920
- package/dist/infra/exec-safe-bin-trust.js +64 -0
- package/dist/infra/heartbeat-runner.js +207 -134
- package/dist/infra/heartbeat-wake.js +183 -22
- package/dist/infra/install-source-utils.js +47 -0
- package/dist/infra/net/ssrf.js +170 -36
- package/dist/infra/outbound/deliver.js +224 -58
- package/dist/infra/outbound/message-action-spec.js +12 -5
- package/dist/infra/outbound/outbound-session.js +27 -25
- package/dist/infra/poolbot-root.js +32 -22
- package/dist/infra/ports.js +14 -11
- package/dist/infra/skills-remote.js +48 -37
- package/dist/infra/system-events.js +25 -11
- package/dist/infra/system-presence.js +26 -33
- package/dist/infra/tmp-poolbot-dir.js +81 -2
- package/dist/infra/wsl.js +37 -1
- package/dist/line/bot-message-context.js +163 -191
- package/dist/logging/subsystem.js +59 -22
- package/dist/markdown/ir.js +124 -50
- package/dist/media/store.js +1 -1
- package/dist/media-understanding/runner.entries.js +42 -25
- package/dist/media-understanding/runner.js +53 -488
- package/dist/memory/embeddings-gemini.js +53 -38
- package/dist/memory/manager-embedding-ops.js +48 -69
- package/dist/pairing/pairing-store.js +178 -119
- package/dist/plugin-sdk/index.js +34 -6
- package/dist/plugins/hooks.js +135 -14
- package/dist/plugins/install.js +190 -152
- package/dist/polls.js +11 -0
- package/dist/routing/resolve-route.js +190 -56
- package/dist/routing/session-key.js +38 -22
- package/dist/runtime.js +35 -9
- package/dist/security/audit-channel.js +1 -1
- package/dist/sessions/session-key-utils.js +29 -11
- package/dist/shared/frontmatter.js +5 -5
- package/dist/shared/node-list-types.js +1 -0
- package/dist/shared/string-normalization.js +15 -0
- package/dist/signal/monitor/event-handler.js +68 -36
- package/dist/signal/send.js +29 -37
- package/dist/slack/monitor/allow-list.js +10 -11
- package/dist/slack/monitor/commands.js +14 -3
- package/dist/slack/monitor/events/interactions.js +4 -4
- package/dist/slack/monitor/media.js +224 -16
- package/dist/slack/monitor/message-handler/dispatch.js +247 -13
- package/dist/slack/monitor/message-handler/prepare.js +128 -45
- package/dist/slack/monitor/slash.js +357 -144
- package/dist/slack/streaming.js +77 -0
- package/dist/telegram/accounts.js +40 -13
- package/dist/telegram/allowed-updates.js +3 -0
- package/dist/telegram/bot/delivery.js +129 -66
- package/dist/telegram/bot/helpers.js +136 -122
- package/dist/telegram/bot-handlers.js +600 -339
- package/dist/telegram/bot-message-context.js +115 -73
- package/dist/telegram/bot-message-dispatch.js +235 -104
- package/dist/telegram/bot-native-command-menu.js +3 -1
- package/dist/telegram/bot-native-commands.js +213 -193
- package/dist/telegram/bot.js +24 -132
- package/dist/telegram/draft-stream.js +84 -75
- package/dist/telegram/format.js +150 -6
- package/dist/telegram/send.js +415 -255
- package/dist/telegram/targets.js +21 -2
- package/dist/telegram/update-offset-store.js +19 -3
- package/dist/terminal/restore.js +5 -2
- package/dist/test-utils/fetch-mock.js +5 -0
- package/dist/version.js +18 -5
- package/dist/web/auto-reply/monitor/broadcast.js +7 -3
- package/dist/web/auto-reply/monitor/on-message.js +6 -3
- package/dist/web/inbound/media.js +34 -8
- package/dist/web/inbound/monitor.js +34 -17
- package/dist/web/inbound/send-api.js +18 -17
- package/dist/web/outbound.js +12 -5
- package/dist/wizard/clack-prompter.js +40 -7
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
- package/skills/apple-reminders/SKILL.md +100 -49
- package/skills/coding-agent/SKILL.md +34 -28
- package/skills/github/SKILL.md +131 -16
- package/skills/imsg/SKILL.md +112 -15
- package/skills/openhue/SKILL.md +101 -19
- package/skills/tmux/SKILL.md +111 -79
- package/skills/weather/SKILL.md +88 -25
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const DEFAULT_SAFE_BIN_TRUSTED_DIRS = [
|
|
3
|
+
"/bin",
|
|
4
|
+
"/usr/bin",
|
|
5
|
+
"/usr/local/bin",
|
|
6
|
+
"/opt/homebrew/bin",
|
|
7
|
+
"/opt/local/bin",
|
|
8
|
+
"/snap/bin",
|
|
9
|
+
"/run/current-system/sw/bin",
|
|
10
|
+
];
|
|
11
|
+
let trustedSafeBinCache = null;
|
|
12
|
+
function normalizeTrustedDir(value) {
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return path.resolve(trimmed);
|
|
18
|
+
}
|
|
19
|
+
function buildTrustedSafeBinCacheKey(pathEnv, delimiter) {
|
|
20
|
+
return `${delimiter}\u0000${pathEnv}`;
|
|
21
|
+
}
|
|
22
|
+
export function buildTrustedSafeBinDirs(params = {}) {
|
|
23
|
+
const delimiter = params.delimiter ?? path.delimiter;
|
|
24
|
+
const pathEnv = params.pathEnv ?? "";
|
|
25
|
+
const baseDirs = params.baseDirs ?? DEFAULT_SAFE_BIN_TRUSTED_DIRS;
|
|
26
|
+
const trusted = new Set();
|
|
27
|
+
for (const entry of baseDirs) {
|
|
28
|
+
const normalized = normalizeTrustedDir(entry);
|
|
29
|
+
if (normalized) {
|
|
30
|
+
trusted.add(normalized);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const pathEntries = pathEnv
|
|
34
|
+
.split(delimiter)
|
|
35
|
+
.map((entry) => normalizeTrustedDir(entry))
|
|
36
|
+
.filter((entry) => Boolean(entry));
|
|
37
|
+
for (const entry of pathEntries) {
|
|
38
|
+
trusted.add(entry);
|
|
39
|
+
}
|
|
40
|
+
return trusted;
|
|
41
|
+
}
|
|
42
|
+
export function getTrustedSafeBinDirs(params = {}) {
|
|
43
|
+
const delimiter = params.delimiter ?? path.delimiter;
|
|
44
|
+
const pathEnv = params.pathEnv ?? process.env.PATH ?? process.env.Path ?? "";
|
|
45
|
+
const key = buildTrustedSafeBinCacheKey(pathEnv, delimiter);
|
|
46
|
+
if (!params.refresh && trustedSafeBinCache?.key === key) {
|
|
47
|
+
return trustedSafeBinCache.dirs;
|
|
48
|
+
}
|
|
49
|
+
const dirs = buildTrustedSafeBinDirs({
|
|
50
|
+
pathEnv,
|
|
51
|
+
delimiter,
|
|
52
|
+
});
|
|
53
|
+
trustedSafeBinCache = { key, dirs };
|
|
54
|
+
return dirs;
|
|
55
|
+
}
|
|
56
|
+
export function isTrustedSafeBinPath(params) {
|
|
57
|
+
const trustedDirs = params.trustedDirs ??
|
|
58
|
+
getTrustedSafeBinDirs({
|
|
59
|
+
pathEnv: params.pathEnv,
|
|
60
|
+
delimiter: params.delimiter,
|
|
61
|
+
});
|
|
62
|
+
const resolvedDir = path.dirname(path.resolve(params.resolvedPath));
|
|
63
|
+
return trustedDirs.has(resolvedDir);
|
|
64
|
+
}
|
|
@@ -1,128 +1,44 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveAgentConfig, resolveAgentWorkspaceDir, resolveDefaultAgentId, } from "../agents/agent-scope.js";
|
|
4
|
-
import {
|
|
4
|
+
import { appendCronStyleCurrentTimeLine } from "../agents/current-time.js";
|
|
5
5
|
import { resolveEffectiveMessagesConfig } from "../agents/identity.js";
|
|
6
6
|
import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js";
|
|
7
|
+
import { resolveHeartbeatReplyPayload } from "../auto-reply/heartbeat-reply-payload.js";
|
|
7
8
|
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, DEFAULT_HEARTBEAT_EVERY, isHeartbeatContentEffectivelyEmpty, resolveHeartbeatPrompt as resolveHeartbeatPromptText, stripHeartbeatToken, } from "../auto-reply/heartbeat.js";
|
|
8
9
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|
9
10
|
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
|
|
10
11
|
import { getChannelPlugin } from "../channels/plugins/index.js";
|
|
11
12
|
import { parseDurationMs } from "../cli/parse-duration.js";
|
|
12
13
|
import { loadConfig } from "../config/config.js";
|
|
13
|
-
import { canonicalizeMainSessionAlias, loadSessionStore, resolveAgentIdFromSessionKey, resolveAgentMainSessionKey, resolveStorePath, saveSessionStore, updateSessionStore, } from "../config/sessions.js";
|
|
14
|
+
import { canonicalizeMainSessionAlias, loadSessionStore, resolveAgentIdFromSessionKey, resolveAgentMainSessionKey, resolveSessionFilePath, resolveStorePath, saveSessionStore, updateSessionStore, } from "../config/sessions.js";
|
|
14
15
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
15
16
|
import { getQueueSize } from "../process/command-queue.js";
|
|
16
17
|
import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
|
|
17
18
|
import { defaultRuntime } from "../runtime.js";
|
|
19
|
+
import { escapeRegExp } from "../utils.js";
|
|
18
20
|
import { formatErrorMessage } from "./errors.js";
|
|
21
|
+
import { isWithinActiveHours } from "./heartbeat-active-hours.js";
|
|
22
|
+
import { buildCronEventPrompt, isCronSystemEvent, isExecCompletionEvent, } from "./heartbeat-events-filter.js";
|
|
19
23
|
import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js";
|
|
20
24
|
import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
|
|
21
25
|
import { requestHeartbeatNow, setHeartbeatWakeHandler, } from "./heartbeat-wake.js";
|
|
22
26
|
import { deliverOutboundPayloads } from "./outbound/deliver.js";
|
|
23
27
|
import { resolveHeartbeatDeliveryTarget, resolveHeartbeatSenderContext, } from "./outbound/targets.js";
|
|
24
|
-
import {
|
|
28
|
+
import { peekSystemEventEntries } from "./system-events.js";
|
|
25
29
|
const log = createSubsystemLogger("gateway/heartbeat");
|
|
26
30
|
let heartbeatsEnabled = true;
|
|
27
31
|
export function setHeartbeatsEnabled(enabled) {
|
|
28
32
|
heartbeatsEnabled = enabled;
|
|
29
33
|
}
|
|
30
34
|
const DEFAULT_HEARTBEAT_TARGET = "last";
|
|
31
|
-
const ACTIVE_HOURS_TIME_PATTERN = /^([01]\d|2[0-3]|24):([0-5]\d)$/;
|
|
32
35
|
// Prompt used when an async exec has completed and the result should be relayed to the user.
|
|
33
36
|
// This overrides the standard heartbeat prompt to ensure the model responds with the exec result
|
|
34
37
|
// instead of just "HEARTBEAT_OK".
|
|
35
38
|
const EXEC_EVENT_PROMPT = "An async command you ran earlier has completed. The result is shown in the system messages above. " +
|
|
36
39
|
"Please relay the command output to the user in a helpful way. If the command succeeded, share the relevant output. " +
|
|
37
40
|
"If it failed, explain what went wrong.";
|
|
38
|
-
|
|
39
|
-
// This overrides the standard heartbeat prompt so the model relays the scheduled
|
|
40
|
-
// reminder instead of responding with "HEARTBEAT_OK".
|
|
41
|
-
const CRON_EVENT_PROMPT = "A scheduled reminder has been triggered. The reminder message is shown in the system messages above. " +
|
|
42
|
-
"Please relay this reminder to the user in a helpful and friendly way.";
|
|
43
|
-
function resolveActiveHoursTimezone(cfg, raw) {
|
|
44
|
-
const trimmed = raw?.trim();
|
|
45
|
-
if (!trimmed || trimmed === "user") {
|
|
46
|
-
return resolveUserTimezone(cfg.agents?.defaults?.userTimezone);
|
|
47
|
-
}
|
|
48
|
-
if (trimmed === "local") {
|
|
49
|
-
const host = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
50
|
-
return host?.trim() || "UTC";
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(new Date());
|
|
54
|
-
return trimmed;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return resolveUserTimezone(cfg.agents?.defaults?.userTimezone);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function parseActiveHoursTime(opts, raw) {
|
|
61
|
-
if (!raw || !ACTIVE_HOURS_TIME_PATTERN.test(raw)) {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
const [hourStr, minuteStr] = raw.split(":");
|
|
65
|
-
const hour = Number(hourStr);
|
|
66
|
-
const minute = Number(minuteStr);
|
|
67
|
-
if (!Number.isFinite(hour) || !Number.isFinite(minute)) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
if (hour === 24) {
|
|
71
|
-
if (!opts.allow24 || minute !== 0) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
return 24 * 60;
|
|
75
|
-
}
|
|
76
|
-
return hour * 60 + minute;
|
|
77
|
-
}
|
|
78
|
-
function resolveMinutesInTimeZone(nowMs, timeZone) {
|
|
79
|
-
try {
|
|
80
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
81
|
-
timeZone,
|
|
82
|
-
hour: "2-digit",
|
|
83
|
-
minute: "2-digit",
|
|
84
|
-
hourCycle: "h23",
|
|
85
|
-
}).formatToParts(new Date(nowMs));
|
|
86
|
-
const map = {};
|
|
87
|
-
for (const part of parts) {
|
|
88
|
-
if (part.type !== "literal") {
|
|
89
|
-
map[part.type] = part.value;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const hour = Number(map.hour);
|
|
93
|
-
const minute = Number(map.minute);
|
|
94
|
-
if (!Number.isFinite(hour) || !Number.isFinite(minute)) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
return hour * 60 + minute;
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function isWithinActiveHours(cfg, heartbeat, nowMs) {
|
|
104
|
-
const active = heartbeat?.activeHours;
|
|
105
|
-
if (!active) {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
const startMin = parseActiveHoursTime({ allow24: false }, active.start);
|
|
109
|
-
const endMin = parseActiveHoursTime({ allow24: true }, active.end);
|
|
110
|
-
if (startMin === null || endMin === null) {
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
if (startMin === endMin) {
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
const timeZone = resolveActiveHoursTimezone(cfg, active.timezone);
|
|
117
|
-
const currentMin = resolveMinutesInTimeZone(nowMs ?? Date.now(), timeZone);
|
|
118
|
-
if (currentMin === null) {
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
if (endMin > startMin) {
|
|
122
|
-
return currentMin >= startMin && currentMin < endMin;
|
|
123
|
-
}
|
|
124
|
-
return currentMin >= startMin || currentMin < endMin;
|
|
125
|
-
}
|
|
41
|
+
export { isCronSystemEvent };
|
|
126
42
|
function hasExplicitHeartbeatAgents(cfg) {
|
|
127
43
|
const list = cfg.agents?.list ?? [];
|
|
128
44
|
return list.some((entry) => Boolean(entry?.heartbeat));
|
|
@@ -228,7 +144,7 @@ function resolveHeartbeatAckMaxChars(cfg, heartbeat) {
|
|
|
228
144
|
cfg.agents?.defaults?.heartbeat?.ackMaxChars ??
|
|
229
145
|
DEFAULT_HEARTBEAT_ACK_MAX_CHARS);
|
|
230
146
|
}
|
|
231
|
-
function resolveHeartbeatSession(cfg, agentId, heartbeat) {
|
|
147
|
+
function resolveHeartbeatSession(cfg, agentId, heartbeat, forcedSessionKey) {
|
|
232
148
|
const sessionCfg = cfg.session;
|
|
233
149
|
const scope = sessionCfg?.scope ?? "per-sender";
|
|
234
150
|
const resolvedAgentId = normalizeAgentId(agentId ?? resolveDefaultAgentId(cfg));
|
|
@@ -242,6 +158,30 @@ function resolveHeartbeatSession(cfg, agentId, heartbeat) {
|
|
|
242
158
|
if (scope === "global") {
|
|
243
159
|
return { sessionKey: mainSessionKey, storePath, store, entry: mainEntry };
|
|
244
160
|
}
|
|
161
|
+
const forced = forcedSessionKey?.trim();
|
|
162
|
+
if (forced) {
|
|
163
|
+
const forcedCandidate = toAgentStoreSessionKey({
|
|
164
|
+
agentId: resolvedAgentId,
|
|
165
|
+
requestKey: forced,
|
|
166
|
+
mainKey: cfg.session?.mainKey,
|
|
167
|
+
});
|
|
168
|
+
const forcedCanonical = canonicalizeMainSessionAlias({
|
|
169
|
+
cfg,
|
|
170
|
+
agentId: resolvedAgentId,
|
|
171
|
+
sessionKey: forcedCandidate,
|
|
172
|
+
});
|
|
173
|
+
if (forcedCanonical !== "global") {
|
|
174
|
+
const sessionAgentId = resolveAgentIdFromSessionKey(forcedCanonical);
|
|
175
|
+
if (sessionAgentId === normalizeAgentId(resolvedAgentId)) {
|
|
176
|
+
return {
|
|
177
|
+
sessionKey: forcedCanonical,
|
|
178
|
+
storePath,
|
|
179
|
+
store,
|
|
180
|
+
entry: store[forcedCanonical],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
245
185
|
const trimmed = heartbeat?.session?.trim() ?? "";
|
|
246
186
|
if (!trimmed) {
|
|
247
187
|
return { sessionKey: mainSessionKey, storePath, store, entry: mainEntry };
|
|
@@ -273,24 +213,6 @@ function resolveHeartbeatSession(cfg, agentId, heartbeat) {
|
|
|
273
213
|
}
|
|
274
214
|
return { sessionKey: mainSessionKey, storePath, store, entry: mainEntry };
|
|
275
215
|
}
|
|
276
|
-
function resolveHeartbeatReplyPayload(replyResult) {
|
|
277
|
-
if (!replyResult) {
|
|
278
|
-
return undefined;
|
|
279
|
-
}
|
|
280
|
-
if (!Array.isArray(replyResult)) {
|
|
281
|
-
return replyResult;
|
|
282
|
-
}
|
|
283
|
-
for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
|
|
284
|
-
const payload = replyResult[idx];
|
|
285
|
-
if (!payload) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
if (payload.text || payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0)) {
|
|
289
|
-
return payload;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
216
|
function resolveHeartbeatReasoningPayloads(replyResult) {
|
|
295
217
|
const payloads = Array.isArray(replyResult) ? replyResult : replyResult ? [replyResult] : [];
|
|
296
218
|
return payloads.filter((payload) => {
|
|
@@ -324,8 +246,65 @@ async function restoreHeartbeatUpdatedAt(params) {
|
|
|
324
246
|
nextStore[sessionKey] = { ...nextEntry, updatedAt: resolvedUpdatedAt };
|
|
325
247
|
});
|
|
326
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Prune heartbeat transcript entries by truncating the file back to a previous size.
|
|
251
|
+
* This removes the user+assistant turns that were written during a HEARTBEAT_OK run,
|
|
252
|
+
* preventing context pollution from zero-information exchanges.
|
|
253
|
+
*/
|
|
254
|
+
async function pruneHeartbeatTranscript(params) {
|
|
255
|
+
const { transcriptPath, preHeartbeatSize } = params;
|
|
256
|
+
if (!transcriptPath || typeof preHeartbeatSize !== "number" || preHeartbeatSize < 0) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const stat = await fs.stat(transcriptPath);
|
|
261
|
+
// Only truncate if the file has grown during the heartbeat run
|
|
262
|
+
if (stat.size > preHeartbeatSize) {
|
|
263
|
+
await fs.truncate(transcriptPath, preHeartbeatSize);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// File may not exist or may have been removed - ignore errors
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the transcript file path and its current size before a heartbeat run.
|
|
272
|
+
* Returns undefined values if the session or transcript doesn't exist yet.
|
|
273
|
+
*/
|
|
274
|
+
async function captureTranscriptState(params) {
|
|
275
|
+
const { storePath, sessionKey, agentId } = params;
|
|
276
|
+
try {
|
|
277
|
+
const store = loadSessionStore(storePath);
|
|
278
|
+
const entry = store[sessionKey];
|
|
279
|
+
if (!entry?.sessionId) {
|
|
280
|
+
return {};
|
|
281
|
+
}
|
|
282
|
+
const transcriptPath = resolveSessionFilePath(entry.sessionId, entry, {
|
|
283
|
+
agentId,
|
|
284
|
+
sessionsDir: path.dirname(storePath),
|
|
285
|
+
});
|
|
286
|
+
const stat = await fs.stat(transcriptPath);
|
|
287
|
+
return { transcriptPath, preHeartbeatSize: stat.size };
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Session or transcript doesn't exist yet - nothing to prune
|
|
291
|
+
return {};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function stripLeadingHeartbeatResponsePrefix(text, responsePrefix) {
|
|
295
|
+
const normalizedPrefix = responsePrefix?.trim();
|
|
296
|
+
if (!normalizedPrefix) {
|
|
297
|
+
return text;
|
|
298
|
+
}
|
|
299
|
+
// Require a boundary after the configured prefix so short prefixes like "Hi"
|
|
300
|
+
// do not strip the beginning of normal words like "History".
|
|
301
|
+
const prefixPattern = new RegExp(`^${escapeRegExp(normalizedPrefix)}(?=$|\\s|[\\p{P}\\p{S}])\\s*`, "iu");
|
|
302
|
+
return text.replace(prefixPattern, "");
|
|
303
|
+
}
|
|
327
304
|
function normalizeHeartbeatReply(payload, responsePrefix, ackMaxChars) {
|
|
328
|
-
const
|
|
305
|
+
const rawText = typeof payload.text === "string" ? payload.text : "";
|
|
306
|
+
const textForStrip = stripLeadingHeartbeatResponsePrefix(rawText, responsePrefix);
|
|
307
|
+
const stripped = stripHeartbeatToken(textForStrip, {
|
|
329
308
|
mode: "heartbeat",
|
|
330
309
|
maxAckChars: ackMaxChars,
|
|
331
310
|
});
|
|
@@ -366,17 +345,19 @@ export async function runHeartbeatOnce(opts) {
|
|
|
366
345
|
}
|
|
367
346
|
// Skip heartbeat if HEARTBEAT.md exists but has no actionable content.
|
|
368
347
|
// This saves API calls/costs when the file is effectively empty (only comments/headers).
|
|
369
|
-
// EXCEPTION: Don't skip for exec events
|
|
370
|
-
// to process regardless of HEARTBEAT.md content.
|
|
348
|
+
// EXCEPTION: Don't skip for exec events, cron events, or explicit wake requests -
|
|
349
|
+
// they have pending system events to process regardless of HEARTBEAT.md content.
|
|
371
350
|
const isExecEventReason = opts.reason === "exec-event";
|
|
372
351
|
const isCronEventReason = Boolean(opts.reason?.startsWith("cron:"));
|
|
352
|
+
const isWakeReason = opts.reason === "wake" || Boolean(opts.reason?.startsWith("hook:"));
|
|
373
353
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
374
354
|
const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
|
|
375
355
|
try {
|
|
376
356
|
const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8");
|
|
377
357
|
if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) &&
|
|
378
358
|
!isExecEventReason &&
|
|
379
|
-
!isCronEventReason
|
|
359
|
+
!isCronEventReason &&
|
|
360
|
+
!isWakeReason) {
|
|
380
361
|
emitHeartbeatEvent({
|
|
381
362
|
status: "skipped",
|
|
382
363
|
reason: "empty-heartbeat-file",
|
|
@@ -389,7 +370,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
389
370
|
// File doesn't exist or can't be read - proceed with heartbeat.
|
|
390
371
|
// The LLM prompt says "if it exists" so this is expected behavior.
|
|
391
372
|
}
|
|
392
|
-
const { entry, sessionKey, storePath } = resolveHeartbeatSession(cfg, agentId, heartbeat);
|
|
373
|
+
const { entry, sessionKey, storePath } = resolveHeartbeatSession(cfg, agentId, heartbeat, opts.sessionKey);
|
|
393
374
|
const previousUpdatedAt = entry?.updatedAt;
|
|
394
375
|
const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat });
|
|
395
376
|
const heartbeatAccountId = heartbeat?.accountId?.trim();
|
|
@@ -422,17 +403,25 @@ export async function runHeartbeatOnce(opts) {
|
|
|
422
403
|
// If so, use a specialized prompt that instructs the model to relay the result
|
|
423
404
|
// instead of the standard heartbeat prompt with "reply HEARTBEAT_OK".
|
|
424
405
|
const isExecEvent = opts.reason === "exec-event";
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
const
|
|
428
|
-
const
|
|
406
|
+
const pendingEventEntries = peekSystemEventEntries(sessionKey);
|
|
407
|
+
const hasTaggedCronEvents = pendingEventEntries.some((event) => event.contextKey?.startsWith("cron:"));
|
|
408
|
+
const shouldInspectPendingEvents = isExecEvent || isCronEventReason || hasTaggedCronEvents;
|
|
409
|
+
const pendingEvents = shouldInspectPendingEvents
|
|
410
|
+
? pendingEventEntries.map((event) => event.text)
|
|
411
|
+
: [];
|
|
412
|
+
const cronEvents = pendingEventEntries
|
|
413
|
+
.filter((event) => (isCronEventReason || event.contextKey?.startsWith("cron:")) &&
|
|
414
|
+
isCronSystemEvent(event.text))
|
|
415
|
+
.map((event) => event.text);
|
|
416
|
+
const hasExecCompletion = pendingEvents.some(isExecCompletionEvent);
|
|
417
|
+
const hasCronEvents = cronEvents.length > 0;
|
|
429
418
|
const prompt = hasExecCompletion
|
|
430
419
|
? EXEC_EVENT_PROMPT
|
|
431
420
|
: hasCronEvents
|
|
432
|
-
?
|
|
421
|
+
? buildCronEventPrompt(cronEvents)
|
|
433
422
|
: resolveHeartbeatPrompt(cfg, heartbeat);
|
|
434
423
|
const ctx = {
|
|
435
|
-
Body: prompt,
|
|
424
|
+
Body: appendCronStyleCurrentTimeLine(prompt, cfg, startedAt),
|
|
436
425
|
From: sender,
|
|
437
426
|
To: sender,
|
|
438
427
|
Provider: hasExecCompletion ? "exec-event" : hasCronEvents ? "cron-event" : "heartbeat",
|
|
@@ -471,12 +460,24 @@ export async function runHeartbeatOnce(opts) {
|
|
|
471
460
|
to: delivery.to,
|
|
472
461
|
accountId: delivery.accountId,
|
|
473
462
|
payloads: [{ text: heartbeatOkText }],
|
|
463
|
+
agentId,
|
|
474
464
|
deps: opts.deps,
|
|
475
465
|
});
|
|
476
466
|
return true;
|
|
477
467
|
};
|
|
478
468
|
try {
|
|
479
|
-
|
|
469
|
+
// Capture transcript state before the heartbeat run so we can prune if HEARTBEAT_OK
|
|
470
|
+
const transcriptState = await captureTranscriptState({
|
|
471
|
+
storePath,
|
|
472
|
+
sessionKey,
|
|
473
|
+
agentId,
|
|
474
|
+
});
|
|
475
|
+
const heartbeatModelOverride = heartbeat?.model?.trim() || undefined;
|
|
476
|
+
const suppressToolErrorWarnings = heartbeat?.suppressToolErrorWarnings === true;
|
|
477
|
+
const replyOpts = heartbeatModelOverride
|
|
478
|
+
? { isHeartbeat: true, heartbeatModelOverride, suppressToolErrorWarnings }
|
|
479
|
+
: { isHeartbeat: true, suppressToolErrorWarnings };
|
|
480
|
+
const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);
|
|
480
481
|
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
|
|
481
482
|
const includeReasoning = heartbeat?.includeReasoning === true;
|
|
482
483
|
const reasoningPayloads = includeReasoning
|
|
@@ -489,6 +490,8 @@ export async function runHeartbeatOnce(opts) {
|
|
|
489
490
|
sessionKey,
|
|
490
491
|
updatedAt: previousUpdatedAt,
|
|
491
492
|
});
|
|
493
|
+
// Prune the transcript to remove HEARTBEAT_OK turns
|
|
494
|
+
await pruneHeartbeatTranscript(transcriptState);
|
|
492
495
|
const okSent = await maybeSendHeartbeatOk();
|
|
493
496
|
emitHeartbeatEvent({
|
|
494
497
|
status: "ok-empty",
|
|
@@ -521,6 +524,8 @@ export async function runHeartbeatOnce(opts) {
|
|
|
521
524
|
sessionKey,
|
|
522
525
|
updatedAt: previousUpdatedAt,
|
|
523
526
|
});
|
|
527
|
+
// Prune the transcript to remove HEARTBEAT_OK turns
|
|
528
|
+
await pruneHeartbeatTranscript(transcriptState);
|
|
524
529
|
const okSent = await maybeSendHeartbeatOk();
|
|
525
530
|
emitHeartbeatEvent({
|
|
526
531
|
status: "ok-token",
|
|
@@ -550,6 +555,8 @@ export async function runHeartbeatOnce(opts) {
|
|
|
550
555
|
sessionKey,
|
|
551
556
|
updatedAt: previousUpdatedAt,
|
|
552
557
|
});
|
|
558
|
+
// Prune the transcript to remove duplicate heartbeat turns
|
|
559
|
+
await pruneHeartbeatTranscript(transcriptState);
|
|
553
560
|
emitHeartbeatEvent({
|
|
554
561
|
status: "skipped",
|
|
555
562
|
reason: "duplicate",
|
|
@@ -627,6 +634,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
627
634
|
channel: delivery.channel,
|
|
628
635
|
to: delivery.to,
|
|
629
636
|
accountId: deliveryAccountId,
|
|
637
|
+
agentId,
|
|
630
638
|
payloads: [
|
|
631
639
|
...reasoningPayloads,
|
|
632
640
|
...(shouldSkipMain
|
|
@@ -699,6 +707,10 @@ export function startHeartbeatRunner(opts) {
|
|
|
699
707
|
}
|
|
700
708
|
return now + intervalMs;
|
|
701
709
|
};
|
|
710
|
+
const advanceAgentSchedule = (agent, now) => {
|
|
711
|
+
agent.lastRunMs = now;
|
|
712
|
+
agent.nextDueMs = now + agent.intervalMs;
|
|
713
|
+
};
|
|
702
714
|
const scheduleNext = () => {
|
|
703
715
|
if (state.stopped) {
|
|
704
716
|
return;
|
|
@@ -722,6 +734,7 @@ export function startHeartbeatRunner(opts) {
|
|
|
722
734
|
}
|
|
723
735
|
const delay = Math.max(0, nextDue - now);
|
|
724
736
|
state.timer = setTimeout(() => {
|
|
737
|
+
state.timer = null;
|
|
725
738
|
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
|
726
739
|
}, delay);
|
|
727
740
|
state.timer.unref?.();
|
|
@@ -774,6 +787,12 @@ export function startHeartbeatRunner(opts) {
|
|
|
774
787
|
scheduleNext();
|
|
775
788
|
};
|
|
776
789
|
const run = async (params) => {
|
|
790
|
+
if (state.stopped) {
|
|
791
|
+
return {
|
|
792
|
+
status: "skipped",
|
|
793
|
+
reason: "disabled",
|
|
794
|
+
};
|
|
795
|
+
}
|
|
777
796
|
if (!heartbeatsEnabled) {
|
|
778
797
|
return {
|
|
779
798
|
status: "skipped",
|
|
@@ -787,27 +806,73 @@ export function startHeartbeatRunner(opts) {
|
|
|
787
806
|
};
|
|
788
807
|
}
|
|
789
808
|
const reason = params?.reason;
|
|
809
|
+
const requestedAgentId = params?.agentId ? normalizeAgentId(params.agentId) : undefined;
|
|
810
|
+
const requestedSessionKey = params?.sessionKey?.trim() || undefined;
|
|
790
811
|
const isInterval = reason === "interval";
|
|
791
812
|
const startedAt = Date.now();
|
|
792
813
|
const now = startedAt;
|
|
793
814
|
let ran = false;
|
|
815
|
+
if (requestedSessionKey || requestedAgentId) {
|
|
816
|
+
const targetAgentId = requestedAgentId ?? resolveAgentIdFromSessionKey(requestedSessionKey);
|
|
817
|
+
const targetAgent = state.agents.get(targetAgentId);
|
|
818
|
+
if (!targetAgent) {
|
|
819
|
+
scheduleNext();
|
|
820
|
+
return { status: "skipped", reason: "disabled" };
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const res = await runOnce({
|
|
824
|
+
cfg: state.cfg,
|
|
825
|
+
agentId: targetAgent.agentId,
|
|
826
|
+
heartbeat: targetAgent.heartbeat,
|
|
827
|
+
reason,
|
|
828
|
+
sessionKey: requestedSessionKey,
|
|
829
|
+
deps: { runtime: state.runtime },
|
|
830
|
+
});
|
|
831
|
+
if (res.status !== "skipped" || res.reason !== "disabled") {
|
|
832
|
+
advanceAgentSchedule(targetAgent, now);
|
|
833
|
+
}
|
|
834
|
+
scheduleNext();
|
|
835
|
+
return res.status === "ran" ? { status: "ran", durationMs: Date.now() - startedAt } : res;
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
const errMsg = formatErrorMessage(err);
|
|
839
|
+
log.error(`heartbeat runner: targeted runOnce threw unexpectedly: ${errMsg}`, {
|
|
840
|
+
error: errMsg,
|
|
841
|
+
});
|
|
842
|
+
advanceAgentSchedule(targetAgent, now);
|
|
843
|
+
scheduleNext();
|
|
844
|
+
return { status: "failed", reason: errMsg };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
794
847
|
for (const agent of state.agents.values()) {
|
|
795
848
|
if (isInterval && now < agent.nextDueMs) {
|
|
796
849
|
continue;
|
|
797
850
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
851
|
+
let res;
|
|
852
|
+
try {
|
|
853
|
+
res = await runOnce({
|
|
854
|
+
cfg: state.cfg,
|
|
855
|
+
agentId: agent.agentId,
|
|
856
|
+
heartbeat: agent.heartbeat,
|
|
857
|
+
reason,
|
|
858
|
+
deps: { runtime: state.runtime },
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
catch (err) {
|
|
862
|
+
// If runOnce throws (e.g. during session compaction), we must still
|
|
863
|
+
// advance the timer and call scheduleNext so heartbeats keep firing.
|
|
864
|
+
const errMsg = formatErrorMessage(err);
|
|
865
|
+
log.error(`heartbeat runner: runOnce threw unexpectedly: ${errMsg}`, { error: errMsg });
|
|
866
|
+
advanceAgentSchedule(agent, now);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
805
869
|
if (res.status === "skipped" && res.reason === "requests-in-flight") {
|
|
870
|
+
advanceAgentSchedule(agent, now);
|
|
871
|
+
scheduleNext();
|
|
806
872
|
return res;
|
|
807
873
|
}
|
|
808
874
|
if (res.status !== "skipped" || res.reason !== "disabled") {
|
|
809
|
-
agent
|
|
810
|
-
agent.nextDueMs = now + agent.intervalMs;
|
|
875
|
+
advanceAgentSchedule(agent, now);
|
|
811
876
|
}
|
|
812
877
|
if (res.status === "ran") {
|
|
813
878
|
ran = true;
|
|
@@ -819,11 +884,19 @@ export function startHeartbeatRunner(opts) {
|
|
|
819
884
|
}
|
|
820
885
|
return { status: "skipped", reason: isInterval ? "not-due" : "disabled" };
|
|
821
886
|
};
|
|
822
|
-
|
|
887
|
+
const wakeHandler = async (params) => run({
|
|
888
|
+
reason: params.reason,
|
|
889
|
+
agentId: params.agentId,
|
|
890
|
+
sessionKey: params.sessionKey,
|
|
891
|
+
});
|
|
892
|
+
const disposeWakeHandler = setHeartbeatWakeHandler(wakeHandler);
|
|
823
893
|
updateConfig(state.cfg);
|
|
824
894
|
const cleanup = () => {
|
|
895
|
+
if (state.stopped) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
825
898
|
state.stopped = true;
|
|
826
|
-
|
|
899
|
+
disposeWakeHandler();
|
|
827
900
|
if (state.timer) {
|
|
828
901
|
clearTimeout(state.timer);
|
|
829
902
|
}
|