File size: 3,819 Bytes
78d0c37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// Agent timestamp injection adds compact local-time context to direct gateway
// agent messages without double-stamping channel envelopes or cron prompts.
import { resolveUserTimezone } from "../../agents/date-time.js";
import type { OpenClawConfig } from "../../config/types.js";
import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.ts";

/**
 * Cron jobs inject "Current time: ..." into their messages.
 * Skip injection for those.
 */
const CRON_TIME_MARKER = "Current time: ";

/**
 * Matches a leading `[... YYYY-MM-DD HH:MM ...]` envelope — either from
 * channel plugins or from a previous injection. Uses the same YYYY-MM-DD
 * HH:MM format as {@link formatZonedTimestamp}, so detection stays in sync
 * with the formatting.
 */
const TIMESTAMP_ENVELOPE_PATTERN = /^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/;

interface TimestampInjectionOptions {
  timezone?: string;
  now?: Date;
  includeTimestamp?: boolean;
}

/**
 * Build a `[DOW YYYY-MM-DD HH:MM TZ] ` prefix string from an explicit date.
 *
 * Returns undefined if formatting fails (malformed timezone etc.).
 * Does NOT guard against TIMESTAMP_ENVELOPE_PATTERN or CRON_TIME_MARKER —
 * callers that need those guards should use {@link injectTimestamp} instead.
 *
 * This is the primitive used by the persistence path to stamp each stored
 * message with ITS OWN arrival timestamp (not the current wall-clock time),
 * so historical messages carry a stable, immutable prefix.
 */
export function buildTimestampPrefix(
  date: Date,
  opts?: Pick<TimestampInjectionOptions, "timezone">,
): string | undefined {
  // The weekday keeps date reasoning reliable without another Intl formatter.
  const formatted = formatZonedTimestamp(date, {
    timeZone: opts?.timezone ?? "UTC",
    displayWeekday: true,
  });
  return formatted ? `[${formatted}] ` : undefined;
}

/**
 * Injects a compact timestamp prefix into a message if one isn't already
 * present. Uses the same `YYYY-MM-DD HH:MM TZ` format as channel envelope
 * timestamps ({@link formatZonedTimestamp}), keeping token cost low (~7
 * tokens) and format consistent across all agent contexts.
 *
 * NOTE: The standard user-turn path no longer calls this. Per-message stamps
 * are now applied once at the LLM boundary (normalizeMessagesForLlmBoundary)
 * from each message's own timestamp, so storage stays bare and the current and
 * historical sends are byte-identical — eliminating the prompt-cache bust
 * described in issue #3658. This helper is retained only for any remaining
 * non-user-turn callers and as the shared prefix primitive's wrapper.
 *
 * Channel messages (Discord, Telegram, etc.) already have timestamps via
 * envelope formatting and take a separate code path — they never reach
 * these handlers, so there is no double-stamping risk. The detection
 * pattern is a safety net for edge cases.
 *
 * @see https://github.com/openclaw/openclaw/issues/3658
 */
export function injectTimestamp(message: string, opts?: TimestampInjectionOptions): string {
  if (opts?.includeTimestamp === false) {
    return message;
  }
  if (!message.trim()) {
    return message;
  }

  // Already has an envelope or injected timestamp
  if (TIMESTAMP_ENVELOPE_PATTERN.test(message)) {
    return message;
  }

  // Already has a cron-injected timestamp
  if (message.includes(CRON_TIME_MARKER)) {
    return message;
  }

  const now = opts?.now ?? new Date();
  const prefix = buildTimestampPrefix(now, opts);
  if (!prefix) {
    return message;
  }

  return `${prefix}${message}`;
}

/**
 * Build TimestampInjectionOptions from an OpenClawConfig.
 */
export function timestampOptsFromConfig(cfg: OpenClawConfig): TimestampInjectionOptions {
  return {
    timezone: resolveUserTimezone(cfg.agents?.defaults?.userTimezone),
    includeTimestamp: true,
  };
}