feat(ai-chat): inject current date & per-message timestamps into agent context (#22632)

## Summary

Gives the AI chat agent temporal awareness by injecting the current date
into
the system prompt and a per-message "sent at" timestamp into each user
message,
formatted in the member's timezone. Also hardens all timezone formatting
against
the `"system"` sentinel value, which was crashing the stream job.

## What changed

**Message timestamps (new)**
- Added `injectMessageTimestamps` util: prepends a
`<message_timestamp>Sent: …</message_timestamp>`
text part to each user message before it's sent to the model, so the
agent can
  reason about "yesterday", "last week", etc.
- `loadMessagesFromDB` now stores the message time in the canonical
`metadata.createdAt` slot (ISO string, JSON-serializable for the BullMQ
job
payload) instead of a non-typed top-level `createdAt` field that nothing
read.
- Migrated the AI chat message pipeline from the generic `UIMessage` to
the
typed `ExtendedUIMessage` (`chat-execution.service`,
`extract-code-interpreter-files`,
`replace-unsupported-file-parts`, and related types), since
`metadata.createdAt`
  is declared on `ExtendedUIMessage`.

**Current date in context**
- System prompt now includes `Current date: …` formatted in the member's
timezone
  (`system-prompt-builder.service`).
- Settings › AI prompt preview mirrors the same `Current date` line.

**Timezone safety (bug fix)**
- Workspace members default `timeZone` to the `"system"` sentinel, which
is only
  resolvable client-side. Passing it (or any invalid IANA zone) to
`Intl.DateTimeFormat` throws `RangeError: Invalid time zone specified:
system`,
  which was failing the stream job.
- Added `getValidTimeZoneOrUndefined`, which returns a valid IANA zone
or
`undefined` (letting the runtime fall back to its default). Used in both
`injectMessageTimestamps` and `formatCurrentDate`. This mirrors the
existing
  `isValidTimeZone` convention in the calendar module.

## Notes / follow-ups

- For members who never changed `timeZone` from `"system"`, timestamps
fall back
to the server's default zone (UTC). To honor their real local time, the
frontend would need to send the browser-detected zone with the chat
request
(the same way calendar/charts already pass a resolved zone). Not
included here.

## Test plan

- [x] `inject-message-timestamps.util.spec.ts` — covers timestamp
injection,
assistant messages untouched, invalid `createdAt`, and the `"system"`
      timezone no longer throwing.
- [ ] Send a chat message and confirm the agent sees the correct
date/time.
- [ ] Verify a member with `timeZone = "system"` no longer crashes the
stream job.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22632?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Etienne
2026-07-09 09:02:56 +02:00
committed by GitHub
parent 3a5545c753
commit cc7b41db0e
14 changed files with 277 additions and 26 deletions
@@ -0,0 +1,21 @@
import { getValidTimeZoneOrUndefined } from '@/utils/date/getValidTimeZoneOrUndefined';
describe('getValidTimeZoneOrUndefined', () => {
it('should return undefined for null, undefined, empty string, and system', () => {
expect(getValidTimeZoneOrUndefined(null)).toBeUndefined();
expect(getValidTimeZoneOrUndefined(undefined)).toBeUndefined();
expect(getValidTimeZoneOrUndefined('')).toBeUndefined();
expect(getValidTimeZoneOrUndefined('system')).toBeUndefined();
});
it('should return undefined for invalid IANA time zones', () => {
expect(getValidTimeZoneOrUndefined('Not/A_Timezone')).toBeUndefined();
});
it('should return the time zone for valid IANA time zones', () => {
expect(getValidTimeZoneOrUndefined('America/New_York')).toBe(
'America/New_York',
);
expect(getValidTimeZoneOrUndefined('UTC')).toBe('UTC');
});
});
@@ -0,0 +1,17 @@
import { isNonEmptyString } from '@sniptt/guards';
export const getValidTimeZoneOrUndefined = (
timeZone: string | null | undefined,
): string | undefined => {
if (!isNonEmptyString(timeZone) || timeZone === 'system') {
return undefined;
}
try {
new Intl.DateTimeFormat('en-US', { timeZone });
return timeZone;
} catch {
return undefined;
}
};
@@ -34,6 +34,7 @@ export {
ACCEPTED_DATE_FORMATS,
ACCEPTED_DATE_TIME_FORMATS,
} from './date/dateInputFormats';
export { getValidTimeZoneOrUndefined } from './date/getValidTimeZoneOrUndefined';
export { isDateWithoutTime } from './date/isDateWithoutTime';
export { isPlainDateAfter } from './date/isPlainDateAfter';
export { isPlainDateBefore } from './date/isPlainDateBefore';