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:
@@ -74,7 +74,7 @@ describe('schedule_task authorization', () => {
|
||||
type: 'schedule_task',
|
||||
prompt: 'do something',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
targetJid: 'other@g.us',
|
||||
},
|
||||
'whatsapp_main',
|
||||
@@ -94,7 +94,7 @@ describe('schedule_task authorization', () => {
|
||||
type: 'schedule_task',
|
||||
prompt: 'self task',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
targetJid: 'other@g.us',
|
||||
},
|
||||
'other-group',
|
||||
@@ -113,7 +113,7 @@ describe('schedule_task authorization', () => {
|
||||
type: 'schedule_task',
|
||||
prompt: 'unauthorized',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
targetJid: 'main@g.us',
|
||||
},
|
||||
'other-group',
|
||||
@@ -131,7 +131,7 @@ describe('schedule_task authorization', () => {
|
||||
type: 'schedule_task',
|
||||
prompt: 'no target',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
targetJid: 'unknown@g.us',
|
||||
},
|
||||
'whatsapp_main',
|
||||
@@ -154,7 +154,7 @@ describe('pause_task authorization', () => {
|
||||
chat_jid: 'main@g.us',
|
||||
prompt: 'main task',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'isolated',
|
||||
next_run: '2025-06-01T00:00:00.000Z',
|
||||
status: 'active',
|
||||
@@ -166,7 +166,7 @@ describe('pause_task authorization', () => {
|
||||
chat_jid: 'other@g.us',
|
||||
prompt: 'other task',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'isolated',
|
||||
next_run: '2025-06-01T00:00:00.000Z',
|
||||
status: 'active',
|
||||
@@ -215,7 +215,7 @@ describe('resume_task authorization', () => {
|
||||
chat_jid: 'other@g.us',
|
||||
prompt: 'paused task',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'isolated',
|
||||
next_run: '2025-06-01T00:00:00.000Z',
|
||||
status: 'paused',
|
||||
@@ -264,7 +264,7 @@ describe('cancel_task authorization', () => {
|
||||
chat_jid: 'other@g.us',
|
||||
prompt: 'cancel me',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'isolated',
|
||||
next_run: null,
|
||||
status: 'active',
|
||||
@@ -287,7 +287,7 @@ describe('cancel_task authorization', () => {
|
||||
chat_jid: 'other@g.us',
|
||||
prompt: 'my task',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'isolated',
|
||||
next_run: null,
|
||||
status: 'active',
|
||||
@@ -310,7 +310,7 @@ describe('cancel_task authorization', () => {
|
||||
chat_jid: 'main@g.us',
|
||||
prompt: 'not yours',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'isolated',
|
||||
next_run: null,
|
||||
status: 'active',
|
||||
@@ -565,7 +565,7 @@ describe('schedule_task context_mode', () => {
|
||||
type: 'schedule_task',
|
||||
prompt: 'group context',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'group',
|
||||
targetJid: 'other@g.us',
|
||||
},
|
||||
@@ -584,7 +584,7 @@ describe('schedule_task context_mode', () => {
|
||||
type: 'schedule_task',
|
||||
prompt: 'isolated context',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'isolated',
|
||||
targetJid: 'other@g.us',
|
||||
},
|
||||
@@ -603,7 +603,7 @@ describe('schedule_task context_mode', () => {
|
||||
type: 'schedule_task',
|
||||
prompt: 'bad context',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
context_mode: 'bogus' as any,
|
||||
targetJid: 'other@g.us',
|
||||
},
|
||||
@@ -622,7 +622,7 @@ describe('schedule_task context_mode', () => {
|
||||
type: 'schedule_task',
|
||||
prompt: 'no context mode',
|
||||
schedule_type: 'once',
|
||||
schedule_value: '2025-06-01T00:00:00.000Z',
|
||||
schedule_value: '2025-06-01T00:00:00',
|
||||
targetJid: 'other@g.us',
|
||||
},
|
||||
'whatsapp_main',
|
||||
|
||||
Reference in New Issue
Block a user