Skip to content

Commit 6451550

Browse files
authored
fix(heartbeat): refresh stale Current time line on every helper call (#44993) (#75025)
Rebase onto current upstream/main (head 4780546). Resolves the conflict from upstream's two-line Current time + Reference UTC helper output: appendCronStyleCurrentTimeLine now refreshes/collapses any prior helper-injected block via CURRENT_TIME_LINE_RE instead of returning early on a stale base.includes('Current time:') match. Preserves upstream-added doc comments. 16/16 current-time.test.ts pass; tsgo core clean.
1 parent 10a4c7c commit 6451550

2 files changed

Lines changed: 109 additions & 4 deletions

File tree

src/agents/current-time.test.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Covers cron-style current-time formatting and invalid Date fallbacks.
22
import { afterEach, describe, expect, it, vi } from "vitest";
3-
import { resolveCronStyleNow } from "./current-time.js";
3+
import { appendCronStyleCurrentTimeLine, resolveCronStyleNow } from "./current-time.js";
44

55
describe("resolveCronStyleNow", () => {
66
afterEach(() => {
@@ -33,3 +33,75 @@ describe("resolveCronStyleNow", () => {
3333
expect(result.timeLine).toContain("Reference UTC: 1970-01-01 00:00 UTC");
3434
});
3535
});
36+
37+
const CFG = {
38+
agents: {
39+
defaults: {
40+
userTimezone: "UTC",
41+
},
42+
},
43+
};
44+
45+
describe("appendCronStyleCurrentTimeLine", () => {
46+
it("returns the empty input unchanged", () => {
47+
expect(appendCronStyleCurrentTimeLine("", CFG, Date.now())).toBe("");
48+
});
49+
50+
it("appends a Current time line when none is present", () => {
51+
const out = appendCronStyleCurrentTimeLine(
52+
"Heartbeat tick",
53+
CFG,
54+
Date.parse("2026-04-30T10:00:00Z"),
55+
);
56+
expect(out).toContain("Heartbeat tick");
57+
expect(out).toMatch(/Reference UTC: 2026-04-30 10:00 UTC/);
58+
});
59+
60+
it("refreshes an existing Current time line on subsequent calls (#44993)", () => {
61+
const oldNow = Date.parse("2026-04-30T08:00:00Z");
62+
const newNow = Date.parse("2026-04-30T10:00:00Z");
63+
const firstPass = appendCronStyleCurrentTimeLine("Heartbeat tick", CFG, oldNow);
64+
expect(firstPass).toMatch(/Reference UTC: 2026-04-30 08:00 UTC/);
65+
66+
const secondPass = appendCronStyleCurrentTimeLine(firstPass, CFG, newNow);
67+
expect(secondPass).toContain("Heartbeat tick");
68+
expect(secondPass).toMatch(/Reference UTC: 2026-04-30 10:00 UTC/);
69+
expect(secondPass).not.toMatch(/Reference UTC: 2026-04-30 08:00 UTC/);
70+
expect(secondPass.match(/Current time:/g)?.length).toBe(1);
71+
});
72+
73+
it("collapses multiple Current time blocks into a single fresh entry", () => {
74+
const stale = [
75+
"Heartbeat tick",
76+
"Current time: Wednesday, January 1st, 2025 - 12:00 AM (UTC)\nReference UTC: 2025-01-01 00:00 UTC",
77+
"Current time: Thursday, January 2nd, 2025 - 12:00 AM (UTC)\nReference UTC: 2025-01-02 00:00 UTC",
78+
].join("\n");
79+
const newNow = Date.parse("2026-04-30T10:00:00Z");
80+
const out = appendCronStyleCurrentTimeLine(stale, CFG, newNow);
81+
expect(out).toContain("Heartbeat tick");
82+
expect(out).toMatch(/Reference UTC: 2026-04-30 10:00 UTC/);
83+
expect(out).not.toMatch(/Reference UTC: 2025-01-01 00:00 UTC/);
84+
expect(out).not.toMatch(/Reference UTC: 2025-01-02 00:00 UTC/);
85+
expect(out.match(/Current time:/g)?.length).toBe(1);
86+
});
87+
88+
it("matches helper blocks with natural-language formattedTime (#44993 codex P1)", () => {
89+
const helperShape =
90+
"Heartbeat tick\nCurrent time: Thursday, April 30th, 2026 - 10:00 AM (Asia/Seoul)\nReference UTC: 2026-04-30 01:00 UTC";
91+
const newNow = Date.parse("2026-04-30T10:00:00Z");
92+
const out = appendCronStyleCurrentTimeLine(helperShape, CFG, newNow);
93+
expect(out).not.toMatch(/Asia\/Seoul/);
94+
expect(out.match(/Current time:/g)?.length).toBe(1);
95+
expect(out).toMatch(/Reference UTC: 2026-04-30 10:00 UTC/);
96+
});
97+
98+
it("preserves user-authored content that starts with 'Current time:'", () => {
99+
const userContent = "Reminder from cron:\nCurrent time: please check the dashboard before EOD";
100+
const newNow = Date.parse("2026-04-30T10:00:00Z");
101+
const out = appendCronStyleCurrentTimeLine(userContent, CFG, newNow);
102+
expect(out).toContain("Reminder from cron:");
103+
expect(out).toContain("Current time: please check the dashboard before EOD");
104+
expect(out).toMatch(/Current time: .+? \(UTC\)\nReference UTC: 2026-04-30 10:00 UTC/);
105+
expect(out.match(/Current time:/g)?.length).toBe(2);
106+
});
107+
});

src/agents/current-time.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,45 @@ export function resolveCronStyleNow(cfg: TimeConfigLike, nowMs: number): CronSty
3636
return { userTimezone, formattedTime, timeLine };
3737
}
3838

39-
/** Append a current-time block unless the text already contains one. */
39+
/**
40+
* Append a fresh current-time block, or refresh a previously helper-injected one,
41+
* so heartbeat/cron prompts flowing through this helper repeatedly never leak a
42+
* stale `Current time:` value (issue #44993).
43+
*/
44+
// Matches the helper's own injected two-line `Current time: ...\nReference UTC: ...` block.
45+
// Upstream #42654 split the helper output across two lines:
46+
// Line 1: `Current time: <formattedTime> (<userTimezone>)`
47+
// Line 2: `Reference UTC: YYYY-MM-DD HH:MM UTC`
48+
// The natural-language `formattedTime` portion is locale/format-dependent (e.g.
49+
// `Thursday, April 30th, 2026 - 10:00 AM` from `formatUserTime`, or an ISO fallback),
50+
// so we anchor on the helper-only deterministic shape: `(<TZ>)` on line 1 immediately
51+
// followed by `Reference UTC: <ISO UTC>` on line 2. The `(TZ)` group rejects parens (so
52+
// timezone IDs like `Asia/Seoul` are accepted), and the strict `Reference UTC:` prefix
53+
// plus ISO+UTC tail rejects user-authored reminder lines that happen to start with
54+
// `Current time:` but lack the helper's exact two-line tail format.
55+
const CURRENT_TIME_LINE_RE =
56+
/^Current time: .+? \([^)]+\)\nReference UTC: \d{4}-\d{2}-\d{2} \d{2}:\d{2} UTC$/gm;
57+
4058
export function appendCronStyleCurrentTimeLine(text: string, cfg: TimeConfigLike, nowMs: number) {
4159
const base = text.trimEnd();
42-
if (!base || base.includes("Current time:")) {
60+
if (!base) {
4361
return base;
4462
}
4563
const { timeLine } = resolveCronStyleNow(cfg, nowMs);
46-
return `${base}\n${timeLine}`;
64+
if (!CURRENT_TIME_LINE_RE.test(base)) {
65+
return `${base}\n${timeLine}`;
66+
}
67+
CURRENT_TIME_LINE_RE.lastIndex = 0;
68+
let replaced = false;
69+
const refreshed = base.replace(CURRENT_TIME_LINE_RE, () => {
70+
if (replaced) {
71+
return "";
72+
}
73+
replaced = true;
74+
return timeLine;
75+
});
76+
return refreshed
77+
.replace(/\n{3,}/g, "\n\n")
78+
.replace(/\n\n+(?=Current time:)/g, "\n")
79+
.trimEnd();
4780
}

0 commit comments

Comments
 (0)