mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
refactor: modularize slack/config/cron/daemon internals
This commit is contained in:
59
src/cron/heartbeat-policy.test.ts
Normal file
59
src/cron/heartbeat-policy.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
shouldEnqueueCronMainSummary,
|
||||
shouldSkipHeartbeatOnlyDelivery,
|
||||
} from "./heartbeat-policy.js";
|
||||
|
||||
describe("shouldSkipHeartbeatOnlyDelivery", () => {
|
||||
it("suppresses empty payloads", () => {
|
||||
expect(shouldSkipHeartbeatOnlyDelivery([], 300)).toBe(true);
|
||||
});
|
||||
|
||||
it("suppresses when any payload is a heartbeat ack and no media is present", () => {
|
||||
expect(
|
||||
shouldSkipHeartbeatOnlyDelivery(
|
||||
[{ text: "Checked inbox and calendar." }, { text: "HEARTBEAT_OK" }],
|
||||
300,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not suppress when media is present", () => {
|
||||
expect(
|
||||
shouldSkipHeartbeatOnlyDelivery(
|
||||
[{ text: "HEARTBEAT_OK", mediaUrl: "https://example.com/image.png" }],
|
||||
300,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldEnqueueCronMainSummary", () => {
|
||||
const isSystemEvent = (text: string) => text.includes("HEARTBEAT_OK");
|
||||
|
||||
it("enqueues only when delivery was requested but did not run", () => {
|
||||
expect(
|
||||
shouldEnqueueCronMainSummary({
|
||||
summaryText: "HEARTBEAT_OK",
|
||||
deliveryRequested: true,
|
||||
delivered: false,
|
||||
deliveryAttempted: false,
|
||||
suppressMainSummary: false,
|
||||
isCronSystemEvent: isSystemEvent,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not enqueue after attempted outbound delivery", () => {
|
||||
expect(
|
||||
shouldEnqueueCronMainSummary({
|
||||
summaryText: "HEARTBEAT_OK",
|
||||
deliveryRequested: true,
|
||||
delivered: false,
|
||||
deliveryAttempted: true,
|
||||
suppressMainSummary: false,
|
||||
isCronSystemEvent: isSystemEvent,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
48
src/cron/heartbeat-policy.ts
Normal file
48
src/cron/heartbeat-policy.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { stripHeartbeatToken } from "../auto-reply/heartbeat.js";
|
||||
|
||||
export type HeartbeatDeliveryPayload = {
|
||||
text?: string;
|
||||
mediaUrl?: string;
|
||||
mediaUrls?: string[];
|
||||
};
|
||||
|
||||
export function shouldSkipHeartbeatOnlyDelivery(
|
||||
payloads: HeartbeatDeliveryPayload[],
|
||||
ackMaxChars: number,
|
||||
): boolean {
|
||||
if (payloads.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const hasAnyMedia = payloads.some(
|
||||
(payload) => (payload.mediaUrls?.length ?? 0) > 0 || Boolean(payload.mediaUrl),
|
||||
);
|
||||
if (hasAnyMedia) {
|
||||
return false;
|
||||
}
|
||||
return payloads.some((payload) => {
|
||||
const result = stripHeartbeatToken(payload.text, {
|
||||
mode: "heartbeat",
|
||||
maxAckChars: ackMaxChars,
|
||||
});
|
||||
return result.shouldSkip;
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldEnqueueCronMainSummary(params: {
|
||||
summaryText: string | undefined;
|
||||
deliveryRequested: boolean;
|
||||
delivered: boolean | undefined;
|
||||
deliveryAttempted: boolean | undefined;
|
||||
suppressMainSummary: boolean;
|
||||
isCronSystemEvent: (text: string) => boolean;
|
||||
}): boolean {
|
||||
const summaryText = params.summaryText?.trim();
|
||||
return Boolean(
|
||||
summaryText &&
|
||||
params.isCronSystemEvent(summaryText) &&
|
||||
params.deliveryRequested &&
|
||||
!params.delivered &&
|
||||
params.deliveryAttempted !== true &&
|
||||
!params.suppressMainSummary,
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||
stripHeartbeatToken,
|
||||
} from "../../auto-reply/heartbeat.js";
|
||||
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS } from "../../auto-reply/heartbeat.js";
|
||||
import { truncateUtf16Safe } from "../../utils.js";
|
||||
import { shouldSkipHeartbeatOnlyDelivery } from "../heartbeat-policy.js";
|
||||
|
||||
type DeliveryPayload = {
|
||||
text?: string;
|
||||
@@ -91,27 +89,7 @@ export function pickLastDeliverablePayload(payloads: DeliveryPayload[]) {
|
||||
* Returns true when any payload is a heartbeat ack token and no payload contains media.
|
||||
*/
|
||||
export function isHeartbeatOnlyResponse(payloads: DeliveryPayload[], ackMaxChars: number) {
|
||||
if (payloads.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// If any payload has media, deliver regardless — there's real content.
|
||||
const hasAnyMedia = payloads.some(
|
||||
(payload) => (payload.mediaUrls?.length ?? 0) > 0 || Boolean(payload.mediaUrl),
|
||||
);
|
||||
if (hasAnyMedia) {
|
||||
return false;
|
||||
}
|
||||
// An agent may emit multiple text payloads (narration, tool summaries)
|
||||
// before a final HEARTBEAT_OK. If *any* payload is a heartbeat ack token,
|
||||
// the agent is signaling "nothing needs attention" — the preceding text
|
||||
// payloads are just internal narration and should not be delivered.
|
||||
return payloads.some((payload) => {
|
||||
const result = stripHeartbeatToken(payload.text, {
|
||||
mode: "heartbeat",
|
||||
maxAckChars: ackMaxChars,
|
||||
});
|
||||
return result.shouldSkip;
|
||||
});
|
||||
return shouldSkipHeartbeatOnlyDelivery(payloads, ackMaxChars);
|
||||
}
|
||||
|
||||
export function resolveHeartbeatAckMaxChars(agentCfg?: { heartbeat?: { ackMaxChars?: number } }) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isCronSystemEvent } from "../../infra/heartbeat-events-filter.js";
|
||||
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
|
||||
import { DEFAULT_AGENT_ID } from "../../routing/session-key.js";
|
||||
import { resolveCronDeliveryPlan } from "../delivery.js";
|
||||
import { shouldEnqueueCronMainSummary } from "../heartbeat-policy.js";
|
||||
import { sweepCronRunSessions } from "../session-reaper.js";
|
||||
import type {
|
||||
CronDeliveryStatus,
|
||||
@@ -995,12 +996,14 @@ export async function executeJobCore(
|
||||
const suppressMainSummary =
|
||||
res.status === "error" && res.errorKind === "delivery-target" && deliveryPlan.requested;
|
||||
if (
|
||||
summaryText &&
|
||||
isCronSystemEvent(summaryText) &&
|
||||
deliveryPlan.requested &&
|
||||
!res.delivered &&
|
||||
res.deliveryAttempted !== true &&
|
||||
!suppressMainSummary
|
||||
shouldEnqueueCronMainSummary({
|
||||
summaryText,
|
||||
deliveryRequested: deliveryPlan.requested,
|
||||
delivered: res.delivered,
|
||||
deliveryAttempted: res.deliveryAttempted,
|
||||
suppressMainSummary,
|
||||
isCronSystemEvent,
|
||||
})
|
||||
) {
|
||||
const prefix = "Cron";
|
||||
const label =
|
||||
|
||||
Reference in New Issue
Block a user