feat: timezone-aware context injection for agent prompts (#691)
* feat: per-group timezone architecture with context injection (#483) Implement a comprehensive timezone consistency layer so the AI agent always receives timestamps in the user's local timezone. The framework handles all UTC↔local conversion transparently — the agent never performs manual timezone math. Key changes: - Per-group timezone stored in containerConfig (no DB migration needed) - Context injection: <context timezone="..." current_time="..." /> header prepended to every agent prompt with local time and IANA timezone - Message timestamps converted from UTC to local display in formatMessages() - schedule_task translation layer: agent writes local times, framework converts to UTC using per-group timezone for cron, once, and interval types - Container TZ env var now uses per-group timezone instead of global constant - New set_timezone MCP tool for users to update their timezone dynamically - NANOCLAW_TIMEZONE passed to MCP server environment for tool confirmations Architecture: Store UTC everywhere, convert at boundaries (display to agent, parse from agent). Groups without timezone configured fall back to the server TIMEZONE constant for full backward compatibility. Closes #483 Closes #526 Co-authored-by: shawnYJ <shawny011717@users.noreply.github.com> Co-authored-by: Adrian <Lafunamor@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * style: apply prettier formatting Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * refactor: strip to minimalist context injection — global TIMEZONE only Remove per-group timezone support, set_timezone MCP tool, and all related IPC handlers. The implementation now uses the global system TIMEZONE for all groups, keeping the diff focused on the message formatting layer: mandatory timezone param in formatMessages(), <context> header injection, and formatLocalTime/formatCurrentTime helpers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: drop formatCurrentTime and simplify context header Address PR review: remove redundant formatCurrentTime() since message timestamps already carry localized times. Simplify <context> header to only include timezone name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: shawnYJ <shawny011717@users.noreply.github.com> Co-authored-by: Adrian <Lafunamor@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,13 +58,14 @@ describe('escapeXml', () => {
|
||||
// --- formatMessages ---
|
||||
|
||||
describe('formatMessages', () => {
|
||||
it('formats a single message as XML', () => {
|
||||
const result = formatMessages([makeMsg()]);
|
||||
expect(result).toBe(
|
||||
'<messages>\n' +
|
||||
'<message sender="Alice" time="2024-01-01T00:00:00.000Z">hello</message>\n' +
|
||||
'</messages>',
|
||||
);
|
||||
const TZ = 'UTC';
|
||||
|
||||
it('formats a single message as XML with context header', () => {
|
||||
const result = formatMessages([makeMsg()], TZ);
|
||||
expect(result).toContain('<context timezone="UTC" />');
|
||||
expect(result).toContain('<message sender="Alice"');
|
||||
expect(result).toContain('>hello</message>');
|
||||
expect(result).toContain('Jan 1, 2024');
|
||||
});
|
||||
|
||||
it('formats multiple messages', () => {
|
||||
@@ -73,11 +74,16 @@ describe('formatMessages', () => {
|
||||
id: '1',
|
||||
sender_name: 'Alice',
|
||||
content: 'hi',
|
||||
timestamp: 't1',
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
}),
|
||||
makeMsg({
|
||||
id: '2',
|
||||
sender_name: 'Bob',
|
||||
content: 'hey',
|
||||
timestamp: '2024-01-01T01:00:00.000Z',
|
||||
}),
|
||||
makeMsg({ id: '2', sender_name: 'Bob', content: 'hey', timestamp: 't2' }),
|
||||
];
|
||||
const result = formatMessages(msgs);
|
||||
const result = formatMessages(msgs, TZ);
|
||||
expect(result).toContain('sender="Alice"');
|
||||
expect(result).toContain('sender="Bob"');
|
||||
expect(result).toContain('>hi</message>');
|
||||
@@ -85,22 +91,35 @@ describe('formatMessages', () => {
|
||||
});
|
||||
|
||||
it('escapes special characters in sender names', () => {
|
||||
const result = formatMessages([makeMsg({ sender_name: 'A & B <Co>' })]);
|
||||
const result = formatMessages([makeMsg({ sender_name: 'A & B <Co>' })], TZ);
|
||||
expect(result).toContain('sender="A & B <Co>"');
|
||||
});
|
||||
|
||||
it('escapes special characters in content', () => {
|
||||
const result = formatMessages([
|
||||
makeMsg({ content: '<script>alert("xss")</script>' }),
|
||||
]);
|
||||
const result = formatMessages(
|
||||
[makeMsg({ content: '<script>alert("xss")</script>' })],
|
||||
TZ,
|
||||
);
|
||||
expect(result).toContain(
|
||||
'<script>alert("xss")</script>',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles empty array', () => {
|
||||
const result = formatMessages([]);
|
||||
expect(result).toBe('<messages>\n\n</messages>');
|
||||
const result = formatMessages([], TZ);
|
||||
expect(result).toContain('<context timezone="UTC" />');
|
||||
expect(result).toContain('<messages>\n\n</messages>');
|
||||
});
|
||||
|
||||
it('converts timestamps to local time for given timezone', () => {
|
||||
// 2024-01-01T18:30:00Z in America/New_York (EST) = 1:30 PM
|
||||
const result = formatMessages(
|
||||
[makeMsg({ timestamp: '2024-01-01T18:30:00.000Z' })],
|
||||
'America/New_York',
|
||||
);
|
||||
expect(result).toContain('1:30');
|
||||
expect(result).toContain('PM');
|
||||
expect(result).toContain('<context timezone="America/New_York" />');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user