fix(harness): use zone-aware timestamp in memory flush header (#3088) - #3111
fix(harness): use zone-aware timestamp in memory flush header (#3088)#3111Coder-xiaosuo wants to merge 2 commits into
Conversation
…header 时间戳改为时区感知, -Duser.timezone / TZ 现在会生效
oss-maintainer
left a comment
There was a problem hiding this comment.
Summary
Zone-aware flush header + injectable Clock is a correct fix for the Instant.now()/LocalDate.now() mismatch in #3088, and the multi-zone test matrix is welcome. Not approving yet for two reasons: (1) no CI signal — the only status on this head is license/cla, so the suite has not actually run; (2) the change covers only half of the ledger write path and changes the on-disk timestamp format, which deserves a closer look.
Findings
- [Warning]
MemoryFlushManager.java:118— the newClockoverload is never used by production construction sites (HarnessAgent.java:1078,CompactionMiddleware.java:107,MemoryFlushMiddleware.java:269), so the zone is still only whatever the JVM default happens to be. - [Warning]
MemoryFlushManager.java:136—ISO_OFFSET_DATE_TIMEoutput is notInstant.parse-compatible and has variable fractional-second width; this is a user-visible storage format change. - [Warning]
MemoryFlushManager.java:259—MemorySaveToolwrites into the samememory/YYYY-MM-DD.mdledger and still has the originalLocalDate.now()+Instant.now()mismatch, so one file will end up with two timestamp conventions. - [Warning]
MemoryFlushManagerTest.java:109— mutating the JVM-global default time zone in a unit test is a flake risk under concurrent execution; please confirm the suite is green locally. - [Info]
MemoryFlushManagerTest.java:96— no regression test pins the 2-arg production constructor honoring the configured zone.
Suggestions
- Use an explicit pattern such as
uuuu-MM-ddTHH:mm:ss.SSSXXXif a stable width is preferable, or document the header format change. - Consider a shared
MemoryTimestamps-style helper soMemoryFlushManagerandMemorySaveToolstay consistent. @ResourceLock("user.timezone")instead of an unprotectedTimeZone.setDefault.
Automated review by github-manager-bot
| * {@link Clock#fixed} to pin an instant. | ||
| */ | ||
| public MemoryFlushManager( | ||
| WorkspaceManager workspaceManager, Model model, String flushPrompt, Clock clock) { |
There was a problem hiding this comment.
[Warning] The new Clock overload is not reachable from production wiring: the three construction sites still use the 2-/3-arg constructors — HarnessAgent.java:1078, CompactionMiddleware.java:107, MemoryFlushMiddleware.java:269. So in a real run the zone can only ever be the JVM default, and Clock.systemDefaultZone() is effectively fixed at construction time.
Fixing the header rendering is the right first step, but for #3088 to be configurable (rather than "whatever -Duser.timezone happens to be") it would help to plumb a ZoneId/Clock through MemoryFlushMiddleware/HarnessAgent.Builder and pass it down here. Happy to see that as a follow-up issue rather than in this PR — just want to make sure the injectable clock does not stay test-only.
| } | ||
|
|
||
| private static String formatTimestamp(ZonedDateTime now) { | ||
| return now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); |
There was a problem hiding this comment.
[Warning] Two things worth pinning down before this format ships in the on-disk ledger:
- The rendered value is no longer
Instant.parse-compatible.Instant.parse("2026-09-10T16:05:32.3096574+08:00")throws, so any consumer (Studio/export tooling/user scripts) that parses the## Memory Flush —header as anInstantwill break on the new output. Nothing in this repo parses the header today, so the risk is downstream only, but it is a user-visible storage-format change. ISO_OFFSET_DATE_TIMEdrops trailing zeros in the fractional part, so the timestamp width is variable (.3096574,.3, or none at all for a whole second). Anything doing fixed-width regex/lexicographic sorting on the header will be surprised.
If stable output matters more than formatter brevity, an explicit pattern such as DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSXXX") gives fixed 3-digit millis and a preserved offset. Otherwise please add a line to the changelog / migration notes calling out the header format change.
| String.format("\n## Memory Flush — %s\n%s\n", formatTimestamp(now), content); | ||
|
|
||
| String dailyRelPath = WorkspaceConstants.MEMORY_DIR + "/" + today + ".md"; | ||
| String dailyRelPath = WorkspaceConstants.MEMORY_DIR + "/" + now.toLocalDate() + ".md"; |
There was a problem hiding this comment.
[Warning] This is only half of the daily-ledger write path. MemorySaveTool.memorySave (agentscope-harness/.../tool/MemorySaveTool.java:76-79) appends into the same memory/YYYY-MM-DD.md file and still does:
String today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
...
"\n## Memory Save — %s\n%s\n", Instant.now().toString(), ...That path still has exactly the bug #3088 describes (file name from the default-zone date, header from a UTC Instant — they disagree across a local midnight), and after this PR a single ledger file will contain two different timestamp conventions side by side.
Consider extracting the zone-aware helper (e.g. a small MemoryTimestamps.now(clock) used by both) or filing a follow-up so the two writers stay consistent.
| void formatTimestamp_honorsSystemDefaultZone() { | ||
| TimeZone original = TimeZone.getDefault(); | ||
| try { | ||
| TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); |
There was a problem hiding this comment.
[Warning] Mutating the JVM-global default time zone from a unit test is a flake source: TimeZone.setDefault(...) leaks to every other test in the same fork, and Surefire/JUnit concurrent execution can run them while it is set. The finally restore only helps if nothing else runs concurrently.
Suggested options, cheapest first:
- annotate the method with
@ResourceLock("user.timezone")(and@Execution(SAME_THREAD)), or - replace the mutation with an assertion that does not need it, e.g.
assertEquals(ZoneId.systemDefault(), Clock.systemDefaultZone().getZone())plus a fixed-instant check throughZonedDateTime.now(Clock.system(ZoneId.of("Asia/Shanghai"))).
Also note the CI signal here is still missing — the only check reported on this PR is license/cla, so the test suite has not actually run yet (workflows for first contributions normally need maintainer approval). Could you confirm mvn -pl agentscope-harness test -Dtest=MemoryFlushManager*Test is green locally under the different TZ values you listed?
| "America/New_York, 2026-09-10T08:05:32.309657400Z, 2026-09-10T04:05:32.3096574-04:00", | ||
| "Asia/Shanghai, 2026-09-10T20:00:00Z, 2026-09-11T04:00:00+08:00" | ||
| }) | ||
| void formatTimestamp_rendersClockZoneOffset(String zoneId, String instantStr, String expected) { |
There was a problem hiding this comment.
[Info] Nice coverage on the helper and the end-to-end file-name/header pairing. One gap: every new assertion goes through the package-private formatTimestamp(Clock) or the explicit 4-arg constructor. There is no regression test that pins the reported symptom — the 2-arg production constructor honouring the configured zone. That is the path a future refactor is most likely to silently break. Something like "default constructor + fixed default zone ⇒ header offset matches ZoneId.systemDefault()" would close it (the honorsSystemDefaultZone case above covers the helper, not the constructor wiring).
AgentScope-Java Version
2.0.3-SNAPSHOT
Description
Background
Fixes #3088.
MemoryFlushManagerappends a timestamped section to today's daily memory ledger(
memory/YYYY-MM-DD.md) on every flush. The section header was rendered withjava.time.Instant.now().toString(), and the file name date withLocalDate.now().Root Cause
Instantis an absolute UTC point in time, soInstant.toString()always emits a...Zsuffix. As a result-Duser.timezone=Asia/Shanghai(orTZ=...) had no effecton the header: a UTC+8 user expected
2026-09-10T16:05:32+08:00but saw2026-09-10T08:05:32Z. Additionally, the file name date usedLocalDate.now()(thedefault zone) while the header used
Instant, so the two could disagree across a dayboundary.
Changes
MemoryFlushManagerjava.time.Clock(new 4-arg constructor). Production default isClock.systemDefaultZone(), so-Duser.timezone/TZare honored; the existing2-arg and 3-arg constructors are preserved and delegate to it.
ZonedDateTime.now(clock).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), whichpreserves the local UTC offset.
flushMemoriescaptures a singleZonedDateTime nowand passes it down towriteMemoryFiles, so the daily file name date and the header timestamp always comefrom the same instant (no cross-day mismatch).
formatTimestamp(Clock)helper so the behavior isdirectly unit-testable.
MemoryFlushManagerTest) — existing tests kept untouched; added:formatTimestamp_rendersClockZoneOffset— parameterized over UTC / Asia/Shanghai /America/New_York / a cross-day case, asserting the rendered string and that
OffsetDateTime.parse(actual).toInstant()equals the originalInstant.formatTimestamp_honorsSystemDefaultZone— verifies the production default(
Clock.systemDefaultZone()) picks up the JVM default zone.flushMemories_writesZoneAwareHeaderAndMatchingFileName— end-to-end: with a fixedclock at
2026-09-10T20:00:00ZinAsia/Shanghai, asserts the ledger file is namedmemory/2026-09-11.mdand the header is## Memory Flush — 2026-09-11T04:00:00+08:00.docs/v2/en/docs/harness/memory.mdanddocs/v2/zh/docs/harness/memory.mdnow document that the header timestamp is a zone-aware ISO-8601 offset date-time
(previously always
...Z) and that it still round-trips viaOffsetDateTime.parse.Note on the exact format
DateTimeFormatter.ISO_OFFSET_DATE_TIMEstrips trailing zeros from the fractionalsecond, so
.309657400is rendered as.3096574. The offset and the instant it parsesback to are unaffected. We kept this standard formatter (rather than a custom 9-digit
pattern) so the value remains a plain ISO-8601 offset date-time.
Compatibility
...Zto a local offset (...+08:00/...-04:00).OffsetDateTime.parse(actual).toInstant()(or any ISO-8601offset parser) are unaffected — the instant is identical, no data ambiguity.
How to test
A/B verification matrix:
2026-09-10T08:05:32.309657400Z2026-09-10T08:05:32.309657400Z2026-09-10T08:05:32.3096574Z2026-09-10T16:05:32.3096574+08:002026-09-10T04:05:32.3096574-04:002026-09-10T20:00:00Z2026-09-11T04:00:00+08:00Fixes #3088Checklist
Please check the following items before code is ready to be reviewed.
mvn spotless:applymvn test)