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:
Gabi Simons
2026-03-06 18:28:29 +02:00
committed by GitHub
parent 47ad2e654c
commit 632713b208
7 changed files with 114 additions and 41 deletions

View File

@@ -5,6 +5,7 @@ import {
ASSISTANT_NAME,
IDLE_TIMEOUT,
POLL_INTERVAL,
TIMEZONE,
TRIGGER_PATTERN,
} from './config.js';
import './channels/index.js';
@@ -29,6 +30,7 @@ import {
getAllTasks,
getMessagesSince,
getNewMessages,
getRegisteredGroup,
getRouterState,
initDatabase,
setRegisteredGroup,
@@ -170,7 +172,7 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
if (!hasTrigger) return true;
}
const prompt = formatMessages(missedMessages);
const prompt = formatMessages(missedMessages, TIMEZONE);
// Advance cursor so the piping path in startMessageLoop won't re-fetch
// these messages. Save the old cursor so we can roll back on error.
@@ -408,7 +410,7 @@ async function startMessageLoop(): Promise<void> {
);
const messagesToSend =
allPending.length > 0 ? allPending : groupMessages;
const formatted = formatMessages(messagesToSend);
const formatted = formatMessages(messagesToSend, TIMEZONE);
if (queue.sendMessage(chatJid, formatted)) {
logger.debug(