@poolzin/pool-bot 2026.2.0 → 2026.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/bash-tools.exec.js +76 -25
- package/dist/agents/cli-runner/helpers.js +9 -11
- package/dist/agents/identity.js +47 -7
- package/dist/agents/memory-search.js +25 -8
- package/dist/agents/model-selection.js +21 -0
- package/dist/agents/pi-embedded-block-chunker.js +117 -42
- package/dist/agents/pi-embedded-helpers/errors.js +183 -78
- package/dist/agents/pi-embedded-helpers.js +1 -1
- package/dist/agents/pi-embedded-runner/compact.js +1 -0
- package/dist/agents/pi-embedded-runner/model.js +61 -2
- package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
- package/dist/agents/pi-embedded-runner/run.js +199 -46
- package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
- package/dist/agents/pi-embedded-subscribe.js +118 -29
- package/dist/agents/pi-tools.js +10 -5
- package/dist/agents/poolbot-tools.js +15 -10
- package/dist/agents/sandbox-paths.js +31 -0
- package/dist/agents/session-tool-result-guard.js +94 -15
- package/dist/agents/shell-utils.js +51 -0
- package/dist/agents/skills/bundled-context.js +23 -0
- package/dist/agents/skills/bundled-dir.js +41 -7
- package/dist/agents/skills-install.js +60 -23
- package/dist/agents/subagent-announce.js +79 -34
- package/dist/agents/tool-policy.conformance.js +14 -0
- package/dist/agents/tool-policy.js +24 -0
- package/dist/agents/tools/cron-tool.js +166 -19
- package/dist/agents/tools/discord-actions-presence.js +78 -0
- package/dist/agents/tools/message-tool.js +56 -2
- package/dist/agents/tools/sessions-history-tool.js +69 -1
- package/dist/agents/tools/web-search.js +211 -42
- package/dist/agents/usage.js +23 -1
- package/dist/agents/workspace-run.js +67 -0
- package/dist/agents/workspace-templates.js +44 -0
- package/dist/auto-reply/command-auth.js +121 -6
- package/dist/auto-reply/envelope.js +50 -72
- package/dist/auto-reply/reply/commands-compact.js +1 -0
- package/dist/auto-reply/reply/commands-context-report.js +1 -0
- package/dist/auto-reply/reply/commands-context.js +1 -0
- package/dist/auto-reply/reply/commands-models.js +107 -60
- package/dist/auto-reply/reply/commands-ptt.js +171 -0
- package/dist/auto-reply/reply/get-reply-run.js +2 -1
- package/dist/auto-reply/reply/inbound-context.js +5 -1
- package/dist/auto-reply/reply/model-selection.js +3 -3
- package/dist/auto-reply/thinking.js +88 -43
- package/dist/browser/bridge-server.js +13 -0
- package/dist/browser/cdp.helpers.js +38 -24
- package/dist/browser/client-fetch.js +50 -7
- package/dist/browser/config.js +1 -10
- package/dist/browser/extension-relay.js +101 -40
- package/dist/browser/pw-ai.js +1 -1
- package/dist/browser/pw-session.js +143 -8
- package/dist/browser/pw-tools-core.interactions.js +125 -27
- package/dist/browser/pw-tools-core.responses.js +1 -1
- package/dist/browser/pw-tools-core.state.js +1 -1
- package/dist/browser/routes/agent.act.js +86 -41
- package/dist/browser/routes/dispatcher.js +4 -4
- package/dist/browser/screenshot.js +1 -1
- package/dist/browser/server.js +13 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/reply-prefix.js +8 -1
- package/dist/cli/cron-cli/register.cron-add.js +61 -40
- package/dist/cli/cron-cli/register.cron-edit.js +60 -34
- package/dist/cli/cron-cli/shared.js +56 -41
- package/dist/cli/dns-cli.js +26 -14
- package/dist/cli/gateway-cli/register.js +37 -19
- package/dist/cli/memory-cli.js +5 -5
- package/dist/cli/parse-bytes.js +37 -0
- package/dist/cli/update-cli.js +173 -52
- package/dist/commands/agent.js +1 -0
- package/dist/commands/doctor-config-flow.js +61 -5
- package/dist/commands/doctor-state-migrations.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/model-allowlist.js +29 -0
- package/dist/commands/model-picker.js +2 -1
- package/dist/commands/models/list.status-command.js +43 -23
- package/dist/commands/models/shared.js +15 -0
- package/dist/commands/onboard-custom.js +384 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
- package/dist/commands/onboard-skills.js +63 -38
- package/dist/commands/openai-model-default.js +41 -0
- package/dist/config/defaults.js +3 -2
- package/dist/config/paths.js +136 -35
- package/dist/config/plugin-auto-enable.js +21 -5
- package/dist/config/redact-snapshot.js +153 -0
- package/dist/config/schema.field-metadata.js +590 -0
- package/dist/config/schema.js +2 -2
- package/dist/config/sessions/store.js +291 -23
- package/dist/config/zod-schema.agent-defaults.js +3 -0
- package/dist/config/zod-schema.agent-runtime.js +13 -2
- package/dist/config/zod-schema.providers-core.js +142 -0
- package/dist/config/zod-schema.session.js +3 -0
- package/dist/cron/delivery.js +57 -0
- package/dist/cron/isolated-agent/delivery-target.js +18 -3
- package/dist/cron/isolated-agent/helpers.js +22 -5
- package/dist/cron/isolated-agent/run.js +171 -63
- package/dist/cron/isolated-agent/session.js +2 -0
- package/dist/cron/normalize.js +356 -28
- package/dist/cron/parse.js +10 -5
- package/dist/cron/run-log.js +35 -10
- package/dist/cron/schedule.js +41 -6
- package/dist/cron/service/jobs.js +208 -35
- package/dist/cron/service/ops.js +72 -16
- package/dist/cron/service/state.js +2 -0
- package/dist/cron/service/store.js +386 -14
- package/dist/cron/service/timer.js +390 -147
- package/dist/cron/session-reaper.js +86 -0
- package/dist/cron/store.js +23 -8
- package/dist/cron/validate-timestamp.js +43 -0
- package/dist/discord/monitor/agent-components.js +438 -0
- package/dist/discord/monitor/allow-list.js +28 -5
- package/dist/discord/monitor/gateway-registry.js +29 -0
- package/dist/discord/monitor/native-command.js +44 -23
- package/dist/discord/monitor/sender-identity.js +45 -0
- package/dist/discord/pluralkit.js +27 -0
- package/dist/discord/send.outbound.js +92 -5
- package/dist/discord/send.shared.js +60 -23
- package/dist/discord/targets.js +84 -1
- package/dist/entry.js +15 -9
- package/dist/extensionAPI.js +8 -0
- package/dist/gateway/control-ui.js +8 -1
- package/dist/gateway/hooks-mapping.js +3 -0
- package/dist/gateway/hooks.js +65 -0
- package/dist/gateway/net.js +96 -31
- package/dist/gateway/node-command-policy.js +50 -15
- package/dist/gateway/origin-check.js +56 -0
- package/dist/gateway/protocol/client-info.js +9 -0
- package/dist/gateway/protocol/index.js +9 -2
- package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
- package/dist/gateway/protocol/schema/cron.js +22 -10
- package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
- package/dist/gateway/protocol/schema/sessions.js +12 -0
- package/dist/gateway/server/hooks.js +1 -1
- package/dist/gateway/server-broadcast.js +26 -9
- package/dist/gateway/server-chat.js +112 -23
- package/dist/gateway/server-discovery-runtime.js +10 -2
- package/dist/gateway/server-http.js +109 -11
- package/dist/gateway/server-methods/agent-timestamp.js +60 -0
- package/dist/gateway/server-methods/agents.js +321 -2
- package/dist/gateway/server-methods/usage.js +559 -16
- package/dist/gateway/server-runtime-state.js +22 -8
- package/dist/gateway/server-startup-memory.js +16 -0
- package/dist/gateway/server.impl.js +5 -1
- package/dist/gateway/session-utils.fs.js +23 -25
- package/dist/gateway/session-utils.js +20 -10
- package/dist/gateway/sessions-patch.js +7 -22
- package/dist/gateway/test-helpers.server.js +35 -2
- package/dist/imessage/constants.js +2 -0
- package/dist/imessage/monitor/deliver.js +4 -1
- package/dist/imessage/monitor/monitor-provider.js +51 -1
- package/dist/infra/bonjour-discovery.js +131 -70
- package/dist/infra/control-ui-assets.js +134 -12
- package/dist/infra/errors.js +12 -0
- package/dist/infra/exec-approvals.js +266 -57
- package/dist/infra/format-time/format-datetime.js +79 -0
- package/dist/infra/format-time/format-duration.js +81 -0
- package/dist/infra/format-time/format-relative.js +80 -0
- package/dist/infra/heartbeat-runner.js +140 -49
- package/dist/infra/home-dir.js +54 -0
- package/dist/infra/net/fetch-guard.js +122 -0
- package/dist/infra/net/ssrf.js +65 -29
- package/dist/infra/outbound/abort.js +14 -0
- package/dist/infra/outbound/message-action-runner.js +77 -13
- package/dist/infra/outbound/outbound-session.js +143 -37
- package/dist/infra/poolbot-root.js +43 -1
- package/dist/infra/session-cost-usage.js +631 -41
- package/dist/infra/state-migrations.js +317 -47
- package/dist/infra/update-global.js +35 -0
- package/dist/infra/update-runner.js +149 -43
- package/dist/infra/warning-filter.js +65 -0
- package/dist/infra/widearea-dns.js +30 -9
- package/dist/logging/redact-identifier.js +12 -0
- package/dist/media/fetch.js +81 -58
- package/dist/media-understanding/apply.js +403 -3
- package/dist/media-understanding/attachments.js +38 -27
- package/dist/media-understanding/defaults.js +16 -0
- package/dist/media-understanding/providers/deepgram/audio.js +22 -14
- package/dist/media-understanding/providers/google/audio.js +24 -17
- package/dist/media-understanding/providers/google/video.js +24 -17
- package/dist/media-understanding/providers/image.js +2 -2
- package/dist/media-understanding/providers/index.js +4 -1
- package/dist/media-understanding/providers/openai/audio.js +22 -14
- package/dist/media-understanding/providers/shared.js +16 -11
- package/dist/media-understanding/providers/zai/index.js +6 -0
- package/dist/media-understanding/runner.js +158 -90
- package/dist/memory/batch-voyage.js +277 -0
- package/dist/memory/embeddings-voyage.js +75 -0
- package/dist/memory/embeddings.js +28 -16
- package/dist/memory/internal.js +101 -18
- package/dist/memory/manager.js +154 -48
- package/dist/memory/search-manager.js +173 -0
- package/dist/memory/session-files.js +9 -3
- package/dist/node-host/runner.js +34 -24
- package/dist/node-host/with-timeout.js +27 -0
- package/dist/plugins/commands.js +5 -1
- package/dist/plugins/config-state.js +86 -7
- package/dist/plugins/source-display.js +51 -0
- package/dist/process/exec.js +20 -2
- package/dist/routing/resolve-route.js +12 -0
- package/dist/routing/session-key.js +15 -0
- package/dist/runtime.js +2 -0
- package/dist/security/audit-extra.async.js +601 -0
- package/dist/security/audit-extra.js +2 -830
- package/dist/security/audit-extra.sync.js +505 -0
- package/dist/security/channel-metadata.js +34 -0
- package/dist/security/external-content.js +88 -6
- package/dist/security/skill-scanner.js +330 -0
- package/dist/sessions/session-key-utils.js +7 -0
- package/dist/signal/monitor/event-handler.js +80 -1
- package/dist/slack/monitor/media.js +85 -15
- package/dist/tailscale/detect.js +1 -2
- package/dist/telegram/bot/helpers.js +109 -28
- package/dist/telegram/bot-handlers.js +144 -3
- package/dist/telegram/bot-message-context.js +37 -10
- package/dist/telegram/bot-message-dispatch.js +48 -15
- package/dist/telegram/bot-native-commands.js +86 -29
- package/dist/telegram/bot.js +30 -29
- package/dist/telegram/model-buttons.js +163 -0
- package/dist/telegram/monitor.js +110 -85
- package/dist/telegram/send.js +129 -47
- package/dist/terminal/restore.js +45 -0
- package/dist/test-helpers/state-dir-env.js +16 -0
- package/dist/tts/tts.js +12 -6
- package/dist/tui/tui-session-actions.js +166 -54
- package/dist/utils/fetch-timeout.js +20 -0
- package/dist/utils/normalize-secret-input.js +19 -0
- package/dist/utils/transcript-tools.js +58 -0
- package/dist/utils.js +45 -14
- package/dist/version.js +42 -5
- package/package.json +1 -1
|
@@ -5,23 +5,23 @@ import { resolveUserTimezone } from "../agents/date-time.js";
|
|
|
5
5
|
import { resolveEffectiveMessagesConfig } from "../agents/identity.js";
|
|
6
6
|
import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js";
|
|
7
7
|
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, DEFAULT_HEARTBEAT_EVERY, isHeartbeatContentEffectivelyEmpty, resolveHeartbeatPrompt as resolveHeartbeatPromptText, stripHeartbeatToken, } from "../auto-reply/heartbeat.js";
|
|
8
|
-
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
|
|
9
8
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|
9
|
+
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
|
|
10
10
|
import { getChannelPlugin } from "../channels/plugins/index.js";
|
|
11
11
|
import { parseDurationMs } from "../cli/parse-duration.js";
|
|
12
12
|
import { loadConfig } from "../config/config.js";
|
|
13
13
|
import { canonicalizeMainSessionAlias, loadSessionStore, resolveAgentIdFromSessionKey, resolveAgentMainSessionKey, resolveStorePath, saveSessionStore, updateSessionStore, } from "../config/sessions.js";
|
|
14
|
-
import { formatErrorMessage } from "../infra/errors.js";
|
|
15
|
-
import { peekSystemEvents } from "../infra/system-events.js";
|
|
16
14
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
17
15
|
import { getQueueSize } from "../process/command-queue.js";
|
|
18
|
-
import { defaultRuntime } from "../runtime.js";
|
|
19
16
|
import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
|
|
17
|
+
import { defaultRuntime } from "../runtime.js";
|
|
18
|
+
import { formatErrorMessage } from "./errors.js";
|
|
20
19
|
import { emitHeartbeatEvent, resolveIndicatorType } from "./heartbeat-events.js";
|
|
21
20
|
import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
|
|
22
21
|
import { requestHeartbeatNow, setHeartbeatWakeHandler, } from "./heartbeat-wake.js";
|
|
23
22
|
import { deliverOutboundPayloads } from "./outbound/deliver.js";
|
|
24
23
|
import { resolveHeartbeatDeliveryTarget, resolveHeartbeatSenderContext, } from "./outbound/targets.js";
|
|
24
|
+
import { peekSystemEvents } from "./system-events.js";
|
|
25
25
|
const log = createSubsystemLogger("gateway/heartbeat");
|
|
26
26
|
let heartbeatsEnabled = true;
|
|
27
27
|
export function setHeartbeatsEnabled(enabled) {
|
|
@@ -35,6 +35,11 @@ const ACTIVE_HOURS_TIME_PATTERN = /^([01]\d|2[0-3]|24):([0-5]\d)$/;
|
|
|
35
35
|
const EXEC_EVENT_PROMPT = "An async command you ran earlier has completed. The result is shown in the system messages above. " +
|
|
36
36
|
"Please relay the command output to the user in a helpful way. If the command succeeded, share the relevant output. " +
|
|
37
37
|
"If it failed, explain what went wrong.";
|
|
38
|
+
// Prompt used when a scheduled cron job has fired and injected a system event.
|
|
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.";
|
|
38
43
|
function resolveActiveHoursTimezone(cfg, raw) {
|
|
39
44
|
const trimmed = raw?.trim();
|
|
40
45
|
if (!trimmed || trimmed === "user") {
|
|
@@ -53,16 +58,19 @@ function resolveActiveHoursTimezone(cfg, raw) {
|
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
function parseActiveHoursTime(opts, raw) {
|
|
56
|
-
if (!raw || !ACTIVE_HOURS_TIME_PATTERN.test(raw))
|
|
61
|
+
if (!raw || !ACTIVE_HOURS_TIME_PATTERN.test(raw)) {
|
|
57
62
|
return null;
|
|
63
|
+
}
|
|
58
64
|
const [hourStr, minuteStr] = raw.split(":");
|
|
59
65
|
const hour = Number(hourStr);
|
|
60
66
|
const minute = Number(minuteStr);
|
|
61
|
-
if (!Number.isFinite(hour) || !Number.isFinite(minute))
|
|
67
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) {
|
|
62
68
|
return null;
|
|
69
|
+
}
|
|
63
70
|
if (hour === 24) {
|
|
64
|
-
if (!opts.allow24 || minute !== 0)
|
|
71
|
+
if (!opts.allow24 || minute !== 0) {
|
|
65
72
|
return null;
|
|
73
|
+
}
|
|
66
74
|
return 24 * 60;
|
|
67
75
|
}
|
|
68
76
|
return hour * 60 + minute;
|
|
@@ -77,13 +85,15 @@ function resolveMinutesInTimeZone(nowMs, timeZone) {
|
|
|
77
85
|
}).formatToParts(new Date(nowMs));
|
|
78
86
|
const map = {};
|
|
79
87
|
for (const part of parts) {
|
|
80
|
-
if (part.type !== "literal")
|
|
88
|
+
if (part.type !== "literal") {
|
|
81
89
|
map[part.type] = part.value;
|
|
90
|
+
}
|
|
82
91
|
}
|
|
83
92
|
const hour = Number(map.hour);
|
|
84
93
|
const minute = Number(map.minute);
|
|
85
|
-
if (!Number.isFinite(hour) || !Number.isFinite(minute))
|
|
94
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) {
|
|
86
95
|
return null;
|
|
96
|
+
}
|
|
87
97
|
return hour * 60 + minute;
|
|
88
98
|
}
|
|
89
99
|
catch {
|
|
@@ -92,18 +102,22 @@ function resolveMinutesInTimeZone(nowMs, timeZone) {
|
|
|
92
102
|
}
|
|
93
103
|
function isWithinActiveHours(cfg, heartbeat, nowMs) {
|
|
94
104
|
const active = heartbeat?.activeHours;
|
|
95
|
-
if (!active)
|
|
105
|
+
if (!active) {
|
|
96
106
|
return true;
|
|
107
|
+
}
|
|
97
108
|
const startMin = parseActiveHoursTime({ allow24: false }, active.start);
|
|
98
109
|
const endMin = parseActiveHoursTime({ allow24: true }, active.end);
|
|
99
|
-
if (startMin === null || endMin === null)
|
|
110
|
+
if (startMin === null || endMin === null) {
|
|
100
111
|
return true;
|
|
101
|
-
|
|
112
|
+
}
|
|
113
|
+
if (startMin === endMin) {
|
|
102
114
|
return true;
|
|
115
|
+
}
|
|
103
116
|
const timeZone = resolveActiveHoursTimezone(cfg, active.timezone);
|
|
104
117
|
const currentMin = resolveMinutesInTimeZone(nowMs ?? Date.now(), timeZone);
|
|
105
|
-
if (currentMin === null)
|
|
118
|
+
if (currentMin === null) {
|
|
106
119
|
return true;
|
|
120
|
+
}
|
|
107
121
|
if (endMin > startMin) {
|
|
108
122
|
return currentMin >= startMin && currentMin < endMin;
|
|
109
123
|
}
|
|
@@ -124,11 +138,13 @@ export function isHeartbeatEnabledForAgent(cfg, agentId) {
|
|
|
124
138
|
}
|
|
125
139
|
function resolveHeartbeatConfig(cfg, agentId) {
|
|
126
140
|
const defaults = cfg.agents?.defaults?.heartbeat;
|
|
127
|
-
if (!agentId)
|
|
141
|
+
if (!agentId) {
|
|
128
142
|
return defaults;
|
|
143
|
+
}
|
|
129
144
|
const overrides = resolveAgentConfig(cfg, agentId)?.heartbeat;
|
|
130
|
-
if (!defaults && !overrides)
|
|
145
|
+
if (!defaults && !overrides) {
|
|
131
146
|
return overrides;
|
|
147
|
+
}
|
|
132
148
|
return { ...defaults, ...overrides };
|
|
133
149
|
}
|
|
134
150
|
export function resolveHeartbeatSummaryForAgent(cfg, agentId) {
|
|
@@ -185,11 +201,13 @@ export function resolveHeartbeatIntervalMs(cfg, overrideEvery, heartbeat) {
|
|
|
185
201
|
heartbeat?.every ??
|
|
186
202
|
cfg.agents?.defaults?.heartbeat?.every ??
|
|
187
203
|
DEFAULT_HEARTBEAT_EVERY;
|
|
188
|
-
if (!raw)
|
|
204
|
+
if (!raw) {
|
|
189
205
|
return null;
|
|
206
|
+
}
|
|
190
207
|
const trimmed = String(raw).trim();
|
|
191
|
-
if (!trimmed)
|
|
208
|
+
if (!trimmed) {
|
|
192
209
|
return null;
|
|
210
|
+
}
|
|
193
211
|
let ms;
|
|
194
212
|
try {
|
|
195
213
|
ms = parseDurationMs(trimmed, { defaultUnit: "m" });
|
|
@@ -197,8 +215,9 @@ export function resolveHeartbeatIntervalMs(cfg, overrideEvery, heartbeat) {
|
|
|
197
215
|
catch {
|
|
198
216
|
return null;
|
|
199
217
|
}
|
|
200
|
-
if (ms <= 0)
|
|
218
|
+
if (ms <= 0) {
|
|
201
219
|
return null;
|
|
220
|
+
}
|
|
202
221
|
return ms;
|
|
203
222
|
}
|
|
204
223
|
export function resolveHeartbeatPrompt(cfg, heartbeat) {
|
|
@@ -215,7 +234,9 @@ function resolveHeartbeatSession(cfg, agentId, heartbeat) {
|
|
|
215
234
|
const resolvedAgentId = normalizeAgentId(agentId ?? resolveDefaultAgentId(cfg));
|
|
216
235
|
const mainSessionKey = scope === "global" ? "global" : resolveAgentMainSessionKey({ cfg, agentId: resolvedAgentId });
|
|
217
236
|
const storeAgentId = scope === "global" ? resolveDefaultAgentId(cfg) : resolvedAgentId;
|
|
218
|
-
const storePath = resolveStorePath(sessionCfg?.store, {
|
|
237
|
+
const storePath = resolveStorePath(sessionCfg?.store, {
|
|
238
|
+
agentId: storeAgentId,
|
|
239
|
+
});
|
|
219
240
|
const store = loadSessionStore(storePath);
|
|
220
241
|
const mainEntry = store[mainSessionKey];
|
|
221
242
|
if (scope === "global") {
|
|
@@ -242,20 +263,28 @@ function resolveHeartbeatSession(cfg, agentId, heartbeat) {
|
|
|
242
263
|
if (canonical !== "global") {
|
|
243
264
|
const sessionAgentId = resolveAgentIdFromSessionKey(canonical);
|
|
244
265
|
if (sessionAgentId === normalizeAgentId(resolvedAgentId)) {
|
|
245
|
-
return {
|
|
266
|
+
return {
|
|
267
|
+
sessionKey: canonical,
|
|
268
|
+
storePath,
|
|
269
|
+
store,
|
|
270
|
+
entry: store[canonical],
|
|
271
|
+
};
|
|
246
272
|
}
|
|
247
273
|
}
|
|
248
274
|
return { sessionKey: mainSessionKey, storePath, store, entry: mainEntry };
|
|
249
275
|
}
|
|
250
276
|
function resolveHeartbeatReplyPayload(replyResult) {
|
|
251
|
-
if (!replyResult)
|
|
277
|
+
if (!replyResult) {
|
|
252
278
|
return undefined;
|
|
253
|
-
|
|
279
|
+
}
|
|
280
|
+
if (!Array.isArray(replyResult)) {
|
|
254
281
|
return replyResult;
|
|
282
|
+
}
|
|
255
283
|
for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
|
|
256
284
|
const payload = replyResult[idx];
|
|
257
|
-
if (!payload)
|
|
285
|
+
if (!payload) {
|
|
258
286
|
continue;
|
|
287
|
+
}
|
|
259
288
|
if (payload.text || payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0)) {
|
|
260
289
|
return payload;
|
|
261
290
|
}
|
|
@@ -271,22 +300,27 @@ function resolveHeartbeatReasoningPayloads(replyResult) {
|
|
|
271
300
|
}
|
|
272
301
|
async function restoreHeartbeatUpdatedAt(params) {
|
|
273
302
|
const { storePath, sessionKey, updatedAt } = params;
|
|
274
|
-
if (typeof updatedAt !== "number")
|
|
303
|
+
if (typeof updatedAt !== "number") {
|
|
275
304
|
return;
|
|
305
|
+
}
|
|
276
306
|
const store = loadSessionStore(storePath);
|
|
277
307
|
const entry = store[sessionKey];
|
|
278
|
-
if (!entry)
|
|
308
|
+
if (!entry) {
|
|
279
309
|
return;
|
|
310
|
+
}
|
|
280
311
|
const nextUpdatedAt = Math.max(entry.updatedAt ?? 0, updatedAt);
|
|
281
|
-
if (entry.updatedAt === nextUpdatedAt)
|
|
312
|
+
if (entry.updatedAt === nextUpdatedAt) {
|
|
282
313
|
return;
|
|
314
|
+
}
|
|
283
315
|
await updateSessionStore(storePath, (nextStore) => {
|
|
284
316
|
const nextEntry = nextStore[sessionKey] ?? entry;
|
|
285
|
-
if (!nextEntry)
|
|
317
|
+
if (!nextEntry) {
|
|
286
318
|
return;
|
|
319
|
+
}
|
|
287
320
|
const resolvedUpdatedAt = Math.max(nextEntry.updatedAt ?? 0, updatedAt);
|
|
288
|
-
if (nextEntry.updatedAt === resolvedUpdatedAt)
|
|
321
|
+
if (nextEntry.updatedAt === resolvedUpdatedAt) {
|
|
289
322
|
return;
|
|
323
|
+
}
|
|
290
324
|
nextStore[sessionKey] = { ...nextEntry, updatedAt: resolvedUpdatedAt };
|
|
291
325
|
});
|
|
292
326
|
}
|
|
@@ -332,13 +366,17 @@ export async function runHeartbeatOnce(opts) {
|
|
|
332
366
|
}
|
|
333
367
|
// Skip heartbeat if HEARTBEAT.md exists but has no actionable content.
|
|
334
368
|
// This saves API calls/costs when the file is effectively empty (only comments/headers).
|
|
335
|
-
// EXCEPTION: Don't skip for exec events - they have pending system events
|
|
369
|
+
// EXCEPTION: Don't skip for exec events or cron events - they have pending system events
|
|
370
|
+
// to process regardless of HEARTBEAT.md content.
|
|
336
371
|
const isExecEventReason = opts.reason === "exec-event";
|
|
372
|
+
const isCronEventReason = Boolean(opts.reason?.startsWith("cron:"));
|
|
337
373
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
338
374
|
const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
|
|
339
375
|
try {
|
|
340
376
|
const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8");
|
|
341
|
-
if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) &&
|
|
377
|
+
if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) &&
|
|
378
|
+
!isExecEventReason &&
|
|
379
|
+
!isCronEventReason) {
|
|
342
380
|
emitHeartbeatEvent({
|
|
343
381
|
status: "skipped",
|
|
344
382
|
reason: "empty-heartbeat-file",
|
|
@@ -354,6 +392,20 @@ export async function runHeartbeatOnce(opts) {
|
|
|
354
392
|
const { entry, sessionKey, storePath } = resolveHeartbeatSession(cfg, agentId, heartbeat);
|
|
355
393
|
const previousUpdatedAt = entry?.updatedAt;
|
|
356
394
|
const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat });
|
|
395
|
+
const heartbeatAccountId = heartbeat?.accountId?.trim();
|
|
396
|
+
if (delivery.reason === "unknown-account") {
|
|
397
|
+
log.warn("heartbeat: unknown accountId", {
|
|
398
|
+
accountId: delivery.accountId ?? heartbeatAccountId ?? null,
|
|
399
|
+
target: heartbeat?.target ?? "last",
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
else if (heartbeatAccountId) {
|
|
403
|
+
log.info("heartbeat: using explicit accountId", {
|
|
404
|
+
accountId: delivery.accountId ?? heartbeatAccountId,
|
|
405
|
+
target: heartbeat?.target ?? "last",
|
|
406
|
+
channel: delivery.channel,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
357
409
|
const visibility = delivery.channel !== "none"
|
|
358
410
|
? resolveHeartbeatVisibility({
|
|
359
411
|
cfg,
|
|
@@ -362,19 +414,28 @@ export async function runHeartbeatOnce(opts) {
|
|
|
362
414
|
})
|
|
363
415
|
: { showOk: false, showAlerts: true, useIndicator: true };
|
|
364
416
|
const { sender } = resolveHeartbeatSenderContext({ cfg, entry, delivery });
|
|
365
|
-
const responsePrefix = resolveEffectiveMessagesConfig(cfg, agentId
|
|
366
|
-
|
|
417
|
+
const responsePrefix = resolveEffectiveMessagesConfig(cfg, agentId, {
|
|
418
|
+
channel: delivery.channel !== "none" ? delivery.channel : undefined,
|
|
419
|
+
accountId: delivery.accountId,
|
|
420
|
+
}).responsePrefix;
|
|
421
|
+
// Check if this is an exec event or cron event with pending system events.
|
|
367
422
|
// If so, use a specialized prompt that instructs the model to relay the result
|
|
368
423
|
// instead of the standard heartbeat prompt with "reply HEARTBEAT_OK".
|
|
369
424
|
const isExecEvent = opts.reason === "exec-event";
|
|
370
|
-
const
|
|
425
|
+
const isCronEvent = Boolean(opts.reason?.startsWith("cron:"));
|
|
426
|
+
const pendingEvents = isExecEvent || isCronEvent ? peekSystemEvents(sessionKey) : [];
|
|
371
427
|
const hasExecCompletion = pendingEvents.some((evt) => evt.includes("Exec finished"));
|
|
372
|
-
const
|
|
428
|
+
const hasCronEvents = isCronEvent && pendingEvents.length > 0;
|
|
429
|
+
const prompt = hasExecCompletion
|
|
430
|
+
? EXEC_EVENT_PROMPT
|
|
431
|
+
: hasCronEvents
|
|
432
|
+
? CRON_EVENT_PROMPT
|
|
433
|
+
: resolveHeartbeatPrompt(cfg, heartbeat);
|
|
373
434
|
const ctx = {
|
|
374
435
|
Body: prompt,
|
|
375
436
|
From: sender,
|
|
376
437
|
To: sender,
|
|
377
|
-
Provider: hasExecCompletion ? "exec-event" : "heartbeat",
|
|
438
|
+
Provider: hasExecCompletion ? "exec-event" : hasCronEvents ? "cron-event" : "heartbeat",
|
|
378
439
|
SessionKey: sessionKey,
|
|
379
440
|
};
|
|
380
441
|
if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) {
|
|
@@ -383,14 +444,16 @@ export async function runHeartbeatOnce(opts) {
|
|
|
383
444
|
reason: "alerts-disabled",
|
|
384
445
|
durationMs: Date.now() - startedAt,
|
|
385
446
|
channel: delivery.channel !== "none" ? delivery.channel : undefined,
|
|
447
|
+
accountId: delivery.accountId,
|
|
386
448
|
});
|
|
387
449
|
return { status: "skipped", reason: "alerts-disabled" };
|
|
388
450
|
}
|
|
389
451
|
const heartbeatOkText = responsePrefix ? `${responsePrefix} ${HEARTBEAT_TOKEN}` : HEARTBEAT_TOKEN;
|
|
390
452
|
const canAttemptHeartbeatOk = Boolean(visibility.showOk && delivery.channel !== "none" && delivery.to);
|
|
391
453
|
const maybeSendHeartbeatOk = async () => {
|
|
392
|
-
if (!canAttemptHeartbeatOk || delivery.channel === "none" || !delivery.to)
|
|
454
|
+
if (!canAttemptHeartbeatOk || delivery.channel === "none" || !delivery.to) {
|
|
393
455
|
return false;
|
|
456
|
+
}
|
|
394
457
|
const heartbeatPlugin = getChannelPlugin(delivery.channel);
|
|
395
458
|
if (heartbeatPlugin?.heartbeat?.checkReady) {
|
|
396
459
|
const readiness = await heartbeatPlugin.heartbeat.checkReady({
|
|
@@ -398,8 +461,9 @@ export async function runHeartbeatOnce(opts) {
|
|
|
398
461
|
accountId: delivery.accountId,
|
|
399
462
|
deps: opts.deps,
|
|
400
463
|
});
|
|
401
|
-
if (!readiness.ok)
|
|
464
|
+
if (!readiness.ok) {
|
|
402
465
|
return false;
|
|
466
|
+
}
|
|
403
467
|
}
|
|
404
468
|
await deliverOutboundPayloads({
|
|
405
469
|
cfg,
|
|
@@ -431,6 +495,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
431
495
|
reason: opts.reason,
|
|
432
496
|
durationMs: Date.now() - startedAt,
|
|
433
497
|
channel: delivery.channel !== "none" ? delivery.channel : undefined,
|
|
498
|
+
accountId: delivery.accountId,
|
|
434
499
|
silent: !okSent,
|
|
435
500
|
indicatorType: visibility.useIndicator ? resolveIndicatorType("ok-empty") : undefined,
|
|
436
501
|
});
|
|
@@ -462,6 +527,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
462
527
|
reason: opts.reason,
|
|
463
528
|
durationMs: Date.now() - startedAt,
|
|
464
529
|
channel: delivery.channel !== "none" ? delivery.channel : undefined,
|
|
530
|
+
accountId: delivery.accountId,
|
|
465
531
|
silent: !okSent,
|
|
466
532
|
indicatorType: visibility.useIndicator ? resolveIndicatorType("ok-token") : undefined,
|
|
467
533
|
});
|
|
@@ -491,6 +557,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
491
557
|
durationMs: Date.now() - startedAt,
|
|
492
558
|
hasMedia: false,
|
|
493
559
|
channel: delivery.channel !== "none" ? delivery.channel : undefined,
|
|
560
|
+
accountId: delivery.accountId,
|
|
494
561
|
});
|
|
495
562
|
return { status: "ran", durationMs: Date.now() - startedAt };
|
|
496
563
|
}
|
|
@@ -508,11 +575,16 @@ export async function runHeartbeatOnce(opts) {
|
|
|
508
575
|
preview: previewText?.slice(0, 200),
|
|
509
576
|
durationMs: Date.now() - startedAt,
|
|
510
577
|
hasMedia: mediaUrls.length > 0,
|
|
578
|
+
accountId: delivery.accountId,
|
|
511
579
|
});
|
|
512
580
|
return { status: "ran", durationMs: Date.now() - startedAt };
|
|
513
581
|
}
|
|
514
582
|
if (!visibility.showAlerts) {
|
|
515
|
-
await restoreHeartbeatUpdatedAt({
|
|
583
|
+
await restoreHeartbeatUpdatedAt({
|
|
584
|
+
storePath,
|
|
585
|
+
sessionKey,
|
|
586
|
+
updatedAt: previousUpdatedAt,
|
|
587
|
+
});
|
|
516
588
|
emitHeartbeatEvent({
|
|
517
589
|
status: "skipped",
|
|
518
590
|
reason: "alerts-disabled",
|
|
@@ -520,6 +592,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
520
592
|
durationMs: Date.now() - startedAt,
|
|
521
593
|
channel: delivery.channel,
|
|
522
594
|
hasMedia: mediaUrls.length > 0,
|
|
595
|
+
accountId: delivery.accountId,
|
|
523
596
|
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
|
|
524
597
|
});
|
|
525
598
|
return { status: "ran", durationMs: Date.now() - startedAt };
|
|
@@ -540,6 +613,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
540
613
|
durationMs: Date.now() - startedAt,
|
|
541
614
|
hasMedia: mediaUrls.length > 0,
|
|
542
615
|
channel: delivery.channel,
|
|
616
|
+
accountId: delivery.accountId,
|
|
543
617
|
});
|
|
544
618
|
log.info("heartbeat: channel not ready", {
|
|
545
619
|
channel: delivery.channel,
|
|
@@ -586,6 +660,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
586
660
|
durationMs: Date.now() - startedAt,
|
|
587
661
|
hasMedia: mediaUrls.length > 0,
|
|
588
662
|
channel: delivery.channel,
|
|
663
|
+
accountId: delivery.accountId,
|
|
589
664
|
indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
|
|
590
665
|
});
|
|
591
666
|
return { status: "ran", durationMs: Date.now() - startedAt };
|
|
@@ -597,6 +672,7 @@ export async function runHeartbeatOnce(opts) {
|
|
|
597
672
|
reason,
|
|
598
673
|
durationMs: Date.now() - startedAt,
|
|
599
674
|
channel: delivery.channel !== "none" ? delivery.channel : undefined,
|
|
675
|
+
accountId: delivery.accountId,
|
|
600
676
|
indicatorType: visibility.useIndicator ? resolveIndicatorType("failed") : undefined,
|
|
601
677
|
});
|
|
602
678
|
log.error(`heartbeat failed: ${reason}`, { error: reason });
|
|
@@ -624,22 +700,26 @@ export function startHeartbeatRunner(opts) {
|
|
|
624
700
|
return now + intervalMs;
|
|
625
701
|
};
|
|
626
702
|
const scheduleNext = () => {
|
|
627
|
-
if (state.stopped)
|
|
703
|
+
if (state.stopped) {
|
|
628
704
|
return;
|
|
705
|
+
}
|
|
629
706
|
if (state.timer) {
|
|
630
707
|
clearTimeout(state.timer);
|
|
631
708
|
state.timer = null;
|
|
632
709
|
}
|
|
633
|
-
if (state.agents.size === 0)
|
|
710
|
+
if (state.agents.size === 0) {
|
|
634
711
|
return;
|
|
712
|
+
}
|
|
635
713
|
const now = Date.now();
|
|
636
714
|
let nextDue = Number.POSITIVE_INFINITY;
|
|
637
715
|
for (const agent of state.agents.values()) {
|
|
638
|
-
if (agent.nextDueMs < nextDue)
|
|
716
|
+
if (agent.nextDueMs < nextDue) {
|
|
639
717
|
nextDue = agent.nextDueMs;
|
|
718
|
+
}
|
|
640
719
|
}
|
|
641
|
-
if (!Number.isFinite(nextDue))
|
|
720
|
+
if (!Number.isFinite(nextDue)) {
|
|
642
721
|
return;
|
|
722
|
+
}
|
|
643
723
|
const delay = Math.max(0, nextDue - now);
|
|
644
724
|
state.timer = setTimeout(() => {
|
|
645
725
|
requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
|
@@ -647,8 +727,9 @@ export function startHeartbeatRunner(opts) {
|
|
|
647
727
|
state.timer.unref?.();
|
|
648
728
|
};
|
|
649
729
|
const updateConfig = (cfg) => {
|
|
650
|
-
if (state.stopped)
|
|
730
|
+
if (state.stopped) {
|
|
651
731
|
return;
|
|
732
|
+
}
|
|
652
733
|
const now = Date.now();
|
|
653
734
|
const prevAgents = state.agents;
|
|
654
735
|
const prevEnabled = prevAgents.size > 0;
|
|
@@ -656,8 +737,9 @@ export function startHeartbeatRunner(opts) {
|
|
|
656
737
|
const intervals = [];
|
|
657
738
|
for (const agent of resolveHeartbeatAgents(cfg)) {
|
|
658
739
|
const intervalMs = resolveHeartbeatIntervalMs(cfg, undefined, agent.heartbeat);
|
|
659
|
-
if (!intervalMs)
|
|
740
|
+
if (!intervalMs) {
|
|
660
741
|
continue;
|
|
742
|
+
}
|
|
661
743
|
intervals.push(intervalMs);
|
|
662
744
|
const prevState = prevAgents.get(agent.agentId);
|
|
663
745
|
const nextDueMs = resolveNextDue(now, intervalMs, prevState);
|
|
@@ -693,10 +775,16 @@ export function startHeartbeatRunner(opts) {
|
|
|
693
775
|
};
|
|
694
776
|
const run = async (params) => {
|
|
695
777
|
if (!heartbeatsEnabled) {
|
|
696
|
-
return {
|
|
778
|
+
return {
|
|
779
|
+
status: "skipped",
|
|
780
|
+
reason: "disabled",
|
|
781
|
+
};
|
|
697
782
|
}
|
|
698
783
|
if (state.agents.size === 0) {
|
|
699
|
-
return {
|
|
784
|
+
return {
|
|
785
|
+
status: "skipped",
|
|
786
|
+
reason: "disabled",
|
|
787
|
+
};
|
|
700
788
|
}
|
|
701
789
|
const reason = params?.reason;
|
|
702
790
|
const isInterval = reason === "interval";
|
|
@@ -721,12 +809,14 @@ export function startHeartbeatRunner(opts) {
|
|
|
721
809
|
agent.lastRunMs = now;
|
|
722
810
|
agent.nextDueMs = now + agent.intervalMs;
|
|
723
811
|
}
|
|
724
|
-
if (res.status === "ran")
|
|
812
|
+
if (res.status === "ran") {
|
|
725
813
|
ran = true;
|
|
814
|
+
}
|
|
726
815
|
}
|
|
727
816
|
scheduleNext();
|
|
728
|
-
if (ran)
|
|
817
|
+
if (ran) {
|
|
729
818
|
return { status: "ran", durationMs: Date.now() - startedAt };
|
|
819
|
+
}
|
|
730
820
|
return { status: "skipped", reason: isInterval ? "not-due" : "disabled" };
|
|
731
821
|
};
|
|
732
822
|
setHeartbeatWakeHandler(async (params) => run({ reason: params.reason }));
|
|
@@ -734,8 +824,9 @@ export function startHeartbeatRunner(opts) {
|
|
|
734
824
|
const cleanup = () => {
|
|
735
825
|
state.stopped = true;
|
|
736
826
|
setHeartbeatWakeHandler(null);
|
|
737
|
-
if (state.timer)
|
|
827
|
+
if (state.timer) {
|
|
738
828
|
clearTimeout(state.timer);
|
|
829
|
+
}
|
|
739
830
|
state.timer = null;
|
|
740
831
|
};
|
|
741
832
|
opts.abortSignal?.addEventListener("abort", cleanup, { once: true });
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function normalize(value) {
|
|
4
|
+
const trimmed = value?.trim();
|
|
5
|
+
return trimmed ? trimmed : undefined;
|
|
6
|
+
}
|
|
7
|
+
export function resolveEffectiveHomeDir(env = process.env, homedir = os.homedir) {
|
|
8
|
+
const raw = resolveRawHomeDir(env, homedir);
|
|
9
|
+
return raw ? path.resolve(raw) : undefined;
|
|
10
|
+
}
|
|
11
|
+
function resolveRawHomeDir(env, homedir) {
|
|
12
|
+
const explicitHome = normalize(env.CLAWDBOT_HOME);
|
|
13
|
+
if (explicitHome) {
|
|
14
|
+
if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) {
|
|
15
|
+
const fallbackHome = normalize(env.HOME) ?? normalize(env.USERPROFILE) ?? normalizeSafe(homedir);
|
|
16
|
+
if (fallbackHome) {
|
|
17
|
+
return explicitHome.replace(/^~(?=$|[\\/])/, fallbackHome);
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return explicitHome;
|
|
22
|
+
}
|
|
23
|
+
const envHome = normalize(env.HOME);
|
|
24
|
+
if (envHome) {
|
|
25
|
+
return envHome;
|
|
26
|
+
}
|
|
27
|
+
const userProfile = normalize(env.USERPROFILE);
|
|
28
|
+
if (userProfile) {
|
|
29
|
+
return userProfile;
|
|
30
|
+
}
|
|
31
|
+
return normalizeSafe(homedir);
|
|
32
|
+
}
|
|
33
|
+
function normalizeSafe(homedir) {
|
|
34
|
+
try {
|
|
35
|
+
return normalize(homedir());
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function resolveRequiredHomeDir(env = process.env, homedir = os.homedir) {
|
|
42
|
+
return resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd());
|
|
43
|
+
}
|
|
44
|
+
export function expandHomePrefix(input, opts) {
|
|
45
|
+
if (!input.startsWith("~")) {
|
|
46
|
+
return input;
|
|
47
|
+
}
|
|
48
|
+
const home = normalize(opts?.home) ??
|
|
49
|
+
resolveEffectiveHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir);
|
|
50
|
+
if (!home) {
|
|
51
|
+
return input;
|
|
52
|
+
}
|
|
53
|
+
return input.replace(/^~(?=$|[\\/])/, home);
|
|
54
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { closeDispatcher, createPinnedDispatcher, resolvePinnedHostname, resolvePinnedHostnameWithPolicy, } from "./ssrf.js";
|
|
2
|
+
const DEFAULT_MAX_REDIRECTS = 3;
|
|
3
|
+
function isRedirectStatus(status) {
|
|
4
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
5
|
+
}
|
|
6
|
+
function buildAbortSignal(params) {
|
|
7
|
+
const { timeoutMs, signal } = params;
|
|
8
|
+
if (!timeoutMs && !signal) {
|
|
9
|
+
return { signal: undefined, cleanup: () => { } };
|
|
10
|
+
}
|
|
11
|
+
if (!timeoutMs) {
|
|
12
|
+
return { signal, cleanup: () => { } };
|
|
13
|
+
}
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
+
const onAbort = () => controller.abort();
|
|
17
|
+
if (signal) {
|
|
18
|
+
if (signal.aborted) {
|
|
19
|
+
controller.abort();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const cleanup = () => {
|
|
26
|
+
clearTimeout(timeoutId);
|
|
27
|
+
if (signal) {
|
|
28
|
+
signal.removeEventListener("abort", onAbort);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return { signal: controller.signal, cleanup };
|
|
32
|
+
}
|
|
33
|
+
export async function fetchWithSsrFGuard(params) {
|
|
34
|
+
const fetcher = params.fetchImpl ?? globalThis.fetch;
|
|
35
|
+
if (!fetcher) {
|
|
36
|
+
throw new Error("fetch is not available");
|
|
37
|
+
}
|
|
38
|
+
const maxRedirects = typeof params.maxRedirects === "number" && Number.isFinite(params.maxRedirects)
|
|
39
|
+
? Math.max(0, Math.floor(params.maxRedirects))
|
|
40
|
+
: DEFAULT_MAX_REDIRECTS;
|
|
41
|
+
const { signal, cleanup } = buildAbortSignal({
|
|
42
|
+
timeoutMs: params.timeoutMs,
|
|
43
|
+
signal: params.signal,
|
|
44
|
+
});
|
|
45
|
+
let released = false;
|
|
46
|
+
const release = async (dispatcher) => {
|
|
47
|
+
if (released) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
released = true;
|
|
51
|
+
cleanup();
|
|
52
|
+
await closeDispatcher(dispatcher ?? undefined);
|
|
53
|
+
};
|
|
54
|
+
const visited = new Set();
|
|
55
|
+
let currentUrl = params.url;
|
|
56
|
+
let redirectCount = 0;
|
|
57
|
+
while (true) {
|
|
58
|
+
let parsedUrl;
|
|
59
|
+
try {
|
|
60
|
+
parsedUrl = new URL(currentUrl);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
await release();
|
|
64
|
+
throw new Error("Invalid URL: must be http or https");
|
|
65
|
+
}
|
|
66
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
67
|
+
await release();
|
|
68
|
+
throw new Error("Invalid URL: must be http or https");
|
|
69
|
+
}
|
|
70
|
+
let dispatcher = null;
|
|
71
|
+
try {
|
|
72
|
+
const usePolicy = Boolean(params.policy?.allowPrivateNetwork || params.policy?.allowedHostnames?.length);
|
|
73
|
+
const pinned = usePolicy
|
|
74
|
+
? await resolvePinnedHostnameWithPolicy(parsedUrl.hostname, {
|
|
75
|
+
lookupFn: params.lookupFn,
|
|
76
|
+
policy: params.policy,
|
|
77
|
+
})
|
|
78
|
+
: await resolvePinnedHostname(parsedUrl.hostname, params.lookupFn);
|
|
79
|
+
if (params.pinDns !== false) {
|
|
80
|
+
dispatcher = createPinnedDispatcher(pinned);
|
|
81
|
+
}
|
|
82
|
+
const init = {
|
|
83
|
+
...(params.init ? { ...params.init } : {}),
|
|
84
|
+
redirect: "manual",
|
|
85
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
86
|
+
...(signal ? { signal } : {}),
|
|
87
|
+
};
|
|
88
|
+
const response = await fetcher(parsedUrl.toString(), init);
|
|
89
|
+
if (isRedirectStatus(response.status)) {
|
|
90
|
+
const location = response.headers.get("location");
|
|
91
|
+
if (!location) {
|
|
92
|
+
await release(dispatcher);
|
|
93
|
+
throw new Error(`Redirect missing location header (${response.status})`);
|
|
94
|
+
}
|
|
95
|
+
redirectCount += 1;
|
|
96
|
+
if (redirectCount > maxRedirects) {
|
|
97
|
+
await release(dispatcher);
|
|
98
|
+
throw new Error(`Too many redirects (limit: ${maxRedirects})`);
|
|
99
|
+
}
|
|
100
|
+
const nextUrl = new URL(location, parsedUrl).toString();
|
|
101
|
+
if (visited.has(nextUrl)) {
|
|
102
|
+
await release(dispatcher);
|
|
103
|
+
throw new Error("Redirect loop detected");
|
|
104
|
+
}
|
|
105
|
+
visited.add(nextUrl);
|
|
106
|
+
void response.body?.cancel();
|
|
107
|
+
await closeDispatcher(dispatcher);
|
|
108
|
+
currentUrl = nextUrl;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
response,
|
|
113
|
+
finalUrl: currentUrl,
|
|
114
|
+
release: async () => release(dispatcher),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
await release(dispatcher);
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|