fix: guard against undefined event.content in cron agentTurn payload (#66302)

* fix: remove documentation fences from HEARTBEAT.md template

The HEARTBEAT.md template wrapped its content in markdown code fences
and a doc heading for display purposes. Since loadTemplate() only strips
YAML front matter, these artifacts leaked into generated workspace files,
causing isHeartbeatContentEffectivelyEmpty() to consider them non-empty
and triggering unnecessary API calls.

Remove the markdown fences and doc heading so the template produces
clean content after front-matter stripping.

Closes #66284

* fix: guard against undefined event.content in cron agentTurn payload

When a cron job fires with agentTurn payload, event.content is undefined.
parseFaceTags(undefined) returned undefined, which propagated to
userContent.startsWith("/") causing a TypeError crash.

- Fix parseFaceTags and filterInternalMarkers to return "" for falsy input
  instead of returning the falsy value itself
- Add null coalescing fallback at the gateway call site
- Add unit tests for undefined/null/empty string inputs

Closes #66283

* fix: address review — remove redundant guards, casts, and unrelated HEARTBEAT.md change

* fix: guard against undefined event.content in cron agentTurn payload (#66302) (thanks @xinmotlanthua)

---------

Co-authored-by: khanhkhanhlele <namkhanh2172@gmail.com>
Co-authored-by: sliverp <870080352@qq.com>
This commit is contained in:
xinmotlanthua
2026-04-15 10:47:21 +07:00
committed by GitHub
parent fb92ca1a4d
commit 90c06c04c8
3 changed files with 17 additions and 4 deletions

View File

@@ -2,6 +2,18 @@ import { describe, expect, it, vi } from "vitest";
import { parseFaceTags } from "./text-parsing.js";
describe("parseFaceTags", () => {
it("returns empty string when input is undefined", () => {
expect(parseFaceTags(undefined)).toBe("");
});
it("returns empty string when input is null", () => {
expect(parseFaceTags(null)).toBe("");
});
it("returns empty string when input is empty string", () => {
expect(parseFaceTags("")).toBe("");
});
it("skips oversized base64 ext payloads before decoding", () => {
const oversizedBase64 = "A".repeat(100_000);
const tag = `<faceType=1,faceId="1",ext="${oversizedBase64}">`;

View File

@@ -5,9 +5,9 @@ import type { RefAttachmentSummary } from "../ref-index-store.js";
const MAX_FACE_EXT_BYTES = 64 * 1024;
/** Replace QQ face tags with readable text labels. */
export function parseFaceTags(text: string): string {
export function parseFaceTags(text: string | undefined | null): string {
if (!text) {
return text;
return "";
}
return text.replace(/<faceType=\d+,faceId="[^"]*",ext="([^"]*)">/g, (_match, ext: string) => {
@@ -26,9 +26,9 @@ export function parseFaceTags(text: string): string {
}
/** Remove internal framework markers before sending text outward. */
export function filterInternalMarkers(text: string): string {
export function filterInternalMarkers(text: string | undefined | null): string {
if (!text) {
return text;
return "";
}
let result = text.replace(/\[\[[a-z_]+:\s*[^\]]*\]\]/gi, "");