feat(date-time): standardize time context and tool timestamps
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
- Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
|
||||
- Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter.
|
||||
- Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.
|
||||
- Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).
|
||||
- Tools: normalize Slack/Discord message timestamps with `timestampMs`/`timestampUtc` while keeping raw provider fields.
|
||||
- Docs: add Date & Time guide and update prompt/timezone configuration docs.
|
||||
- Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4.
|
||||
- Memory: make `node-llama-cpp` an optional dependency (avoid Node 25 install failures) and improve local-embeddings fallback/errors.
|
||||
- Browser: add `snapshot refs=aria` (Playwright aria-ref ids) for self-resolving refs across `snapshot` → `act`.
|
||||
|
||||
@@ -363,6 +363,7 @@ The agent can call `discord` with actions like:
|
||||
- `react` / `reactions` (add or list reactions)
|
||||
- `sticker`, `poll`, `permissions`
|
||||
- `readMessages`, `sendMessage`, `editMessage`, `deleteMessage`
|
||||
- Read/search/pin tool payloads include normalized `timestampMs` (UTC epoch ms) and `timestampUtc` alongside raw Discord `timestamp`.
|
||||
- `threadCreate`, `threadList`, `threadReply`
|
||||
- `pinMessage`, `unpinMessage`, `listPins`
|
||||
- `searchMessages`, `memberInfo`, `roleInfo`, `roleAdd`, `roleRemove`, `emojiList`
|
||||
|
||||
@@ -327,4 +327,5 @@ Slack tool actions can be gated with `channels.slack.actions.*`:
|
||||
- Bot-authored messages are ignored by default; enable via `channels.slack.allowBots` or `channels.slack.channels.<id>.allowBots`.
|
||||
- Warning: If you allow replies to other bots (`channels.slack.allowBots=true` or `channels.slack.channels.<id>.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.slack.channels.<id>.users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
|
||||
- For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions).
|
||||
- Read/pin tool payloads include normalized `timestampMs` (UTC epoch ms) and `timestampUtc` alongside raw Slack `ts`.
|
||||
- Attachments are downloaded to the media store when permitted and under the size limit.
|
||||
|
||||
@@ -20,7 +20,7 @@ The prompt is intentionally compact and uses fixed sections:
|
||||
- **Workspace**: working directory (`agents.defaults.workspace`).
|
||||
- **Workspace Files (injected)**: indicates bootstrap files are included below.
|
||||
- **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available.
|
||||
- **Time**: UTC default + the user’s local time (already converted).
|
||||
- **Current Date & Time**: user-local time, timezone, and time format.
|
||||
- **Reply Tags**: optional reply tag syntax for supported providers.
|
||||
- **Heartbeats**: heartbeat prompt and ack behavior.
|
||||
- **Runtime**: host, OS, node, model, thinking level (one line).
|
||||
@@ -46,12 +46,19 @@ To inspect how much each injected file contributes (raw vs injected, truncation,
|
||||
|
||||
## Time handling
|
||||
|
||||
The Time line is compact and explicit:
|
||||
The system prompt includes a dedicated **Current Date & Time** section when user
|
||||
time or timezone is known. It is explicit about:
|
||||
|
||||
- Assume timestamps are **UTC** unless stated.
|
||||
- The listed **user time** is already converted to `agents.defaults.userTimezone` (if set).
|
||||
- The user’s **local time** (already converted).
|
||||
- The **time zone** used for the conversion.
|
||||
- The **time format** (12-hour / 24-hour).
|
||||
|
||||
Use `agents.defaults.userTimezone` in `~/.clawdbot/clawdbot.json` to change the user time zone.
|
||||
Configure with:
|
||||
|
||||
- `agents.defaults.userTimezone`
|
||||
- `agents.defaults.timeFormat` (`auto` | `12` | `24`)
|
||||
|
||||
See [Date & Time](/date-time) for full behavior details.
|
||||
|
||||
## Skills
|
||||
|
||||
|
||||
@@ -19,10 +19,15 @@ Inbound messages are wrapped in an envelope like:
|
||||
|
||||
The timestamp in the envelope is **always UTC**, with minutes precision.
|
||||
|
||||
## Tool payloads (raw provider data)
|
||||
## Tool payloads (raw provider data + normalized fields)
|
||||
|
||||
Tool calls (`channels.discord.readMessages`, `channels.slack.readMessages`, etc.) return **raw provider timestamps**.
|
||||
These are typically UTC ISO strings (Discord) or UTC epoch strings (Slack). We do not rewrite them.
|
||||
We also attach normalized fields for consistency:
|
||||
|
||||
- `timestampMs` (UTC epoch milliseconds)
|
||||
- `timestampUtc` (ISO 8601 UTC string)
|
||||
|
||||
Raw provider fields are preserved.
|
||||
|
||||
## User timezone for the system prompt
|
||||
|
||||
@@ -31,10 +36,14 @@ unset, Clawdbot resolves the **host timezone at runtime** (no config write).
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: { userTimezone: "America/Chicago" }
|
||||
agents: { defaults: { userTimezone: "America/Chicago" } }
|
||||
}
|
||||
```
|
||||
|
||||
The system prompt includes:
|
||||
- `User timezone: America/Chicago`
|
||||
- `Current user time: 2026-01-05 15:26`
|
||||
- `Current Date & Time` section with local time and timezone
|
||||
- `Time format: 12-hour` or `24-hour`
|
||||
|
||||
You can control the prompt format with `agents.defaults.timeFormat` (`auto` | `12` | `24`).
|
||||
|
||||
See [Date & Time](/date-time) for the full behavior and examples.
|
||||
|
||||
85
docs/date-time.md
Normal file
85
docs/date-time.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
summary: "Date and time handling across envelopes, prompts, tools, and connectors"
|
||||
read_when:
|
||||
- You are changing how timestamps are shown to the model or users
|
||||
- You are debugging time formatting in messages or system prompt output
|
||||
---
|
||||
|
||||
# Date & Time
|
||||
|
||||
Clawdbot uses **UTC for transport timestamps** and **user-local time only in the system prompt**.
|
||||
We avoid rewriting provider timestamps so tools keep their native semantics.
|
||||
|
||||
## Message envelopes (UTC)
|
||||
|
||||
Inbound messages are wrapped with a UTC timestamp (minute precision):
|
||||
|
||||
```
|
||||
[Provider ... 2026-01-05T21:26Z] message text
|
||||
```
|
||||
|
||||
This envelope timestamp is **always UTC**, regardless of the host timezone.
|
||||
|
||||
## System prompt: Current Date & Time
|
||||
|
||||
If the user timezone or local time is known, the system prompt includes a dedicated
|
||||
**Current Date & Time** section:
|
||||
|
||||
```
|
||||
Thursday, January 15th, 2026 — 3:07 PM (America/Chicago)
|
||||
Time format: 12-hour
|
||||
```
|
||||
|
||||
If only the timezone is known, we still include the section and instruct the model
|
||||
to assume UTC for unknown time references.
|
||||
|
||||
## System event lines (UTC)
|
||||
|
||||
Queued system events inserted into agent context are prefixed with a UTC timestamp:
|
||||
|
||||
```
|
||||
System: [2026-01-12T20:19:17Z] Model switched.
|
||||
```
|
||||
|
||||
### Configure user timezone + format
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
userTimezone: "America/Chicago",
|
||||
timeFormat: "auto" // auto | 12 | 24
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `userTimezone` sets the **user-local timezone** for prompt context.
|
||||
- `timeFormat` controls **12h/24h display** in the prompt. `auto` follows OS prefs.
|
||||
|
||||
## Time format detection (auto)
|
||||
|
||||
When `timeFormat: "auto"`, Clawdbot inspects the OS preference (macOS/Windows)
|
||||
and falls back to locale formatting. The detected value is **cached per process**
|
||||
to avoid repeated system calls.
|
||||
|
||||
## Tool payloads + connectors (raw provider time + normalized fields)
|
||||
|
||||
Channel tools return **provider-native timestamps** and add normalized fields for consistency:
|
||||
|
||||
- `timestampMs`: epoch milliseconds (UTC)
|
||||
- `timestampUtc`: ISO 8601 UTC string
|
||||
|
||||
Raw provider fields are preserved so nothing is lost.
|
||||
|
||||
- Slack: epoch-like strings from the API
|
||||
- Discord: UTC ISO timestamps
|
||||
- Telegram/WhatsApp: provider-specific numeric/ISO timestamps
|
||||
|
||||
If you need local time, convert it downstream using the known timezone.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [System Prompt](/concepts/system-prompt)
|
||||
- [Timezones](/concepts/timezone)
|
||||
- [Messages](/concepts/messages)
|
||||
@@ -1211,6 +1211,17 @@ message envelopes). If unset, Clawdbot uses the host timezone at runtime.
|
||||
}
|
||||
```
|
||||
|
||||
### `agents.defaults.timeFormat`
|
||||
|
||||
Controls the **time format** shown in the system prompt’s Current Date & Time section.
|
||||
Default: `auto` (OS preference).
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { timeFormat: "auto" } } // auto | 12 | 24
|
||||
}
|
||||
```
|
||||
|
||||
### `messages`
|
||||
|
||||
Controls inbound/outbound prefixes and optional ack reactions.
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { CliBackendConfig } from "../../config/types.js";
|
||||
import { runExec } from "../../process/exec.js";
|
||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
||||
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
||||
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
||||
|
||||
@@ -69,44 +70,6 @@ export type CliOutput = {
|
||||
usage?: CliUsage;
|
||||
};
|
||||
|
||||
function resolveUserTimezone(configured?: string): string {
|
||||
const trimmed = configured?.trim();
|
||||
if (trimmed) {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(new Date());
|
||||
return trimmed;
|
||||
} catch {
|
||||
// ignore invalid timezone
|
||||
}
|
||||
}
|
||||
const host = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
return host?.trim() || "UTC";
|
||||
}
|
||||
|
||||
function formatUserTime(date: Date, timeZone: string): string | undefined {
|
||||
try {
|
||||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone,
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
}).formatToParts(date);
|
||||
const map: Record<string, string> = {};
|
||||
for (const part of parts) {
|
||||
if (part.type !== "literal") map[part.type] = part.value;
|
||||
}
|
||||
if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute)
|
||||
return undefined;
|
||||
return `${map.weekday} ${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
||||
const models = cfg?.agents?.defaults?.models ?? {};
|
||||
const entries: Array<{ alias: string; model: string }> = [];
|
||||
@@ -134,7 +97,8 @@ export function buildSystemPrompt(params: {
|
||||
modelDisplay: string;
|
||||
}) {
|
||||
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
|
||||
const userTime = formatUserTime(new Date(), userTimezone);
|
||||
const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat);
|
||||
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
|
||||
return buildAgentSystemPrompt({
|
||||
workspaceDir: params.workspaceDir,
|
||||
defaultThinkLevel: params.defaultThinkLevel,
|
||||
@@ -153,6 +117,7 @@ export function buildSystemPrompt(params: {
|
||||
modelAliasLines: buildModelAliasLines(params.config),
|
||||
userTimezone,
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles: params.contextFiles,
|
||||
});
|
||||
}
|
||||
|
||||
164
src/agents/date-time.ts
Normal file
164
src/agents/date-time.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export type TimeFormatPreference = "auto" | "12" | "24";
|
||||
export type ResolvedTimeFormat = "12" | "24";
|
||||
|
||||
let cachedTimeFormat: ResolvedTimeFormat | undefined;
|
||||
|
||||
export function resolveUserTimezone(configured?: string): string {
|
||||
const trimmed = configured?.trim();
|
||||
if (trimmed) {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(new Date());
|
||||
return trimmed;
|
||||
} catch {
|
||||
// ignore invalid timezone
|
||||
}
|
||||
}
|
||||
const host = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
return host?.trim() || "UTC";
|
||||
}
|
||||
|
||||
export function resolveUserTimeFormat(preference?: TimeFormatPreference): ResolvedTimeFormat {
|
||||
if (preference === "12" || preference === "24") return preference;
|
||||
if (cachedTimeFormat) return cachedTimeFormat;
|
||||
cachedTimeFormat = detectSystemTimeFormat() ? "24" : "12";
|
||||
return cachedTimeFormat;
|
||||
}
|
||||
|
||||
export function normalizeTimestamp(
|
||||
raw: unknown,
|
||||
): { timestampMs: number; timestampUtc: string } | undefined {
|
||||
if (raw == null) return undefined;
|
||||
let timestampMs: number | undefined;
|
||||
|
||||
if (raw instanceof Date) {
|
||||
timestampMs = raw.getTime();
|
||||
} else if (typeof raw === "number" && Number.isFinite(raw)) {
|
||||
timestampMs = raw < 1_000_000_000_000 ? Math.round(raw * 1000) : Math.round(raw);
|
||||
} else if (typeof raw === "string") {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
||||
const num = Number(trimmed);
|
||||
if (Number.isFinite(num)) {
|
||||
if (trimmed.includes(".")) {
|
||||
timestampMs = Math.round(num * 1000);
|
||||
} else if (trimmed.length >= 13) {
|
||||
timestampMs = Math.round(num);
|
||||
} else {
|
||||
timestampMs = Math.round(num * 1000);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const parsed = Date.parse(trimmed);
|
||||
if (!Number.isNaN(parsed)) timestampMs = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (timestampMs === undefined || !Number.isFinite(timestampMs)) return undefined;
|
||||
return { timestampMs, timestampUtc: new Date(timestampMs).toISOString() };
|
||||
}
|
||||
|
||||
export function withNormalizedTimestamp<T extends Record<string, unknown>>(
|
||||
value: T,
|
||||
rawTimestamp: unknown,
|
||||
): T & { timestampMs?: number; timestampUtc?: string } {
|
||||
const normalized = normalizeTimestamp(rawTimestamp);
|
||||
if (!normalized) return value;
|
||||
return {
|
||||
...value,
|
||||
timestampMs:
|
||||
typeof value.timestampMs === "number" && Number.isFinite(value.timestampMs)
|
||||
? value.timestampMs
|
||||
: normalized.timestampMs,
|
||||
timestampUtc:
|
||||
typeof value.timestampUtc === "string" && value.timestampUtc.trim()
|
||||
? value.timestampUtc
|
||||
: normalized.timestampUtc,
|
||||
};
|
||||
}
|
||||
|
||||
function detectSystemTimeFormat(): boolean {
|
||||
if (process.platform === "darwin") {
|
||||
try {
|
||||
const result = execSync("defaults read -g AppleICUForce24HourTime 2>/dev/null", {
|
||||
encoding: "utf8",
|
||||
timeout: 500,
|
||||
}).trim();
|
||||
if (result === "1") return true;
|
||||
if (result === "0") return false;
|
||||
} catch {
|
||||
// Not set, fall through
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
const result = execSync(
|
||||
'powershell -Command "(Get-Culture).DateTimeFormat.ShortTimePattern"',
|
||||
{ encoding: "utf8", timeout: 1000 },
|
||||
).trim();
|
||||
if (result.startsWith("H")) return true;
|
||||
if (result.startsWith("h")) return false;
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const sample = new Date(2000, 0, 1, 13, 0);
|
||||
const formatted = new Intl.DateTimeFormat(undefined, { hour: "numeric" }).format(sample);
|
||||
return formatted.includes("13");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function ordinalSuffix(day: number): string {
|
||||
if (day >= 11 && day <= 13) return "th";
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
return "st";
|
||||
case 2:
|
||||
return "nd";
|
||||
case 3:
|
||||
return "rd";
|
||||
default:
|
||||
return "th";
|
||||
}
|
||||
}
|
||||
|
||||
export function formatUserTime(
|
||||
date: Date,
|
||||
timeZone: string,
|
||||
format: ResolvedTimeFormat,
|
||||
): string | undefined {
|
||||
const use24Hour = format === "24";
|
||||
try {
|
||||
const parts = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: use24Hour ? "2-digit" : "numeric",
|
||||
minute: "2-digit",
|
||||
hourCycle: use24Hour ? "h23" : "h12",
|
||||
}).formatToParts(date);
|
||||
const map: Record<string, string> = {};
|
||||
for (const part of parts) {
|
||||
if (part.type !== "literal") map[part.type] = part.value;
|
||||
}
|
||||
if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute)
|
||||
return undefined;
|
||||
const dayNum = parseInt(map.day, 10);
|
||||
const suffix = ordinalSuffix(dayNum);
|
||||
const timePart = use24Hour
|
||||
? `${map.hour}:${map.minute}`
|
||||
: `${map.hour}:${map.minute} ${map.dayPeriod ?? ""}`.trim();
|
||||
return `${map.weekday}, ${map.month} ${dayNum}${suffix}, ${map.year} — ${timePart}`;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -53,12 +53,11 @@ import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager
|
||||
import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "./system-prompt.js";
|
||||
import { splitSdkTools } from "./tool-split.js";
|
||||
import type { EmbeddedPiCompactResult } from "./types.js";
|
||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
||||
import {
|
||||
describeUnknownError,
|
||||
formatUserTime,
|
||||
mapThinkingLevel,
|
||||
resolveExecToolDefaults,
|
||||
resolveUserTimezone,
|
||||
} from "./utils.js";
|
||||
|
||||
export async function compactEmbeddedPiSession(params: {
|
||||
@@ -228,7 +227,10 @@ export async function compactEmbeddedPiSession(params: {
|
||||
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated);
|
||||
const reasoningTagHint = isReasoningTagProvider(provider);
|
||||
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
|
||||
const userTime = formatUserTime(new Date(), userTimezone);
|
||||
const userTimeFormat = resolveUserTimeFormat(
|
||||
params.config?.agents?.defaults?.timeFormat,
|
||||
);
|
||||
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
|
||||
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
||||
sessionKey: params.sessionKey,
|
||||
config: params.config,
|
||||
@@ -251,6 +253,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
modelAliasLines: buildModelAliasLines(params.config),
|
||||
userTimezone,
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
});
|
||||
const systemPrompt = createSystemPromptOverride(appendPrompt);
|
||||
|
||||
@@ -58,12 +58,8 @@ import { prewarmSessionFile, trackSessionManagerAccess } from "../session-manage
|
||||
import { prepareSessionManagerForRun } from "../session-manager-init.js";
|
||||
import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "../system-prompt.js";
|
||||
import { splitSdkTools } from "../tool-split.js";
|
||||
import {
|
||||
formatUserTime,
|
||||
mapThinkingLevel,
|
||||
resolveExecToolDefaults,
|
||||
resolveUserTimezone,
|
||||
} from "../utils.js";
|
||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../../date-time.js";
|
||||
import { mapThinkingLevel, resolveExecToolDefaults } from "../utils.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js";
|
||||
|
||||
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
|
||||
@@ -186,7 +182,8 @@ export async function runEmbeddedAttempt(
|
||||
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated);
|
||||
const reasoningTagHint = isReasoningTagProvider(params.provider);
|
||||
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
|
||||
const userTime = formatUserTime(new Date(), userTimezone);
|
||||
const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat);
|
||||
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
|
||||
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
||||
sessionKey: params.sessionKey,
|
||||
config: params.config,
|
||||
@@ -211,6 +208,7 @@ export async function runEmbeddedAttempt(
|
||||
modelAliasLines: buildModelAliasLines(params.config),
|
||||
userTimezone,
|
||||
userTime,
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
});
|
||||
const systemPromptReport = buildSystemPromptReport({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { ResolvedTimeFormat } from "../date-time.js";
|
||||
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
||||
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
||||
import { buildToolSummaryMap } from "../tool-summaries.js";
|
||||
@@ -33,6 +34,7 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
modelAliasLines: string[];
|
||||
userTimezone: string;
|
||||
userTime?: string;
|
||||
userTimeFormat?: ResolvedTimeFormat;
|
||||
contextFiles?: EmbeddedContextFile[];
|
||||
}): string {
|
||||
return buildAgentSystemPrompt({
|
||||
@@ -52,6 +54,7 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
modelAliasLines: params.modelAliasLines,
|
||||
userTimezone: params.userTimezone,
|
||||
userTime: params.userTime,
|
||||
userTimeFormat: params.userTimeFormat,
|
||||
contextFiles: params.contextFiles,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,45 +17,6 @@ export function resolveExecToolDefaults(config?: ClawdbotConfig): ExecToolDefaul
|
||||
return { ...tools.bash, ...tools.exec };
|
||||
}
|
||||
|
||||
export function resolveUserTimezone(configured?: string): string {
|
||||
const trimmed = configured?.trim();
|
||||
if (trimmed) {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(new Date());
|
||||
return trimmed;
|
||||
} catch {
|
||||
// ignore invalid timezone
|
||||
}
|
||||
}
|
||||
const host = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
return host?.trim() || "UTC";
|
||||
}
|
||||
|
||||
export function formatUserTime(date: Date, timeZone: string): string | undefined {
|
||||
try {
|
||||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone,
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
}).formatToParts(date);
|
||||
const map: Record<string, string> = {};
|
||||
for (const part of parts) {
|
||||
if (part.type !== "literal") map[part.type] = part.value;
|
||||
}
|
||||
if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute) {
|
||||
return undefined;
|
||||
}
|
||||
return `${map.weekday} ${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function describeUnknownError(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof error === "string") return error;
|
||||
|
||||
@@ -71,15 +71,42 @@ describe("buildAgentSystemPrompt", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("includes user time when provided", () => {
|
||||
it("includes user time when provided (12-hour)", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/clawd",
|
||||
userTimezone: "America/Chicago",
|
||||
userTime: "Monday 2026-01-05 15:26",
|
||||
userTime: "Monday, January 5th, 2026 — 3:26 PM",
|
||||
userTimeFormat: "12",
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Current Date & Time");
|
||||
expect(prompt).toContain("Monday, January 5th, 2026 — 3:26 PM (America/Chicago)");
|
||||
expect(prompt).toContain("Time format: 12-hour");
|
||||
});
|
||||
|
||||
it("includes user time when provided (24-hour)", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/clawd",
|
||||
userTimezone: "America/Chicago",
|
||||
userTime: "Monday, January 5th, 2026 — 15:26",
|
||||
userTimeFormat: "24",
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Current Date & Time");
|
||||
expect(prompt).toContain("Monday, January 5th, 2026 — 15:26 (America/Chicago)");
|
||||
expect(prompt).toContain("Time format: 24-hour");
|
||||
});
|
||||
|
||||
it("shows UTC fallback when only timezone is provided", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/clawd",
|
||||
userTimezone: "America/Chicago",
|
||||
userTimeFormat: "24",
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Current Date & Time");
|
||||
expect(prompt).toContain(
|
||||
"Time: assume UTC unless stated. User time zone: America/Chicago. Current user time (local, 24-hour): Monday 2026-01-05 15:26 (America/Chicago).",
|
||||
"Time zone: America/Chicago. Current time unknown; assume UTC for date/time references.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
|
||||
import type { ResolvedTimeFormat } from "./date-time.js";
|
||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||
|
||||
export function buildAgentSystemPrompt(params: {
|
||||
@@ -15,6 +16,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
modelAliasLines?: string[];
|
||||
userTimezone?: string;
|
||||
userTime?: string;
|
||||
userTimeFormat?: ResolvedTimeFormat;
|
||||
contextFiles?: EmbeddedContextFile[];
|
||||
skillsPrompt?: string;
|
||||
heartbeatPrompt?: string;
|
||||
@@ -312,17 +314,21 @@ export function buildAgentSystemPrompt(params: {
|
||||
ownerLine ? "## User Identity" : "",
|
||||
ownerLine ?? "",
|
||||
ownerLine ? "" : "",
|
||||
...(userTimezone || userTime
|
||||
? [
|
||||
"## Current Date & Time",
|
||||
userTime
|
||||
? `${userTime} (${userTimezone ?? "unknown"})`
|
||||
: `Time zone: ${userTimezone}. Current time unknown; assume UTC for date/time references.`,
|
||||
params.userTimeFormat
|
||||
? `Time format: ${params.userTimeFormat === "24" ? "24-hour" : "12-hour"}`
|
||||
: "",
|
||||
"",
|
||||
]
|
||||
: []),
|
||||
"## Workspace Files (injected)",
|
||||
"These user-editable files are loaded by Clawdbot and included below in Project Context.",
|
||||
"",
|
||||
userTimezone || userTime
|
||||
? `Time: assume UTC unless stated. User time zone: ${
|
||||
userTimezone ?? "unknown"
|
||||
}. Current user time (local, 24-hour): ${userTime ?? "unknown"} (${
|
||||
userTimezone ?? "unknown"
|
||||
}).`
|
||||
: "",
|
||||
userTimezone || userTime ? "" : "",
|
||||
"## Reply Tags",
|
||||
"To request a native reply/quote on supported surfaces, include one tag in your reply:",
|
||||
"- [[reply_to_current]] replies to the triggering message.",
|
||||
|
||||
@@ -27,32 +27,7 @@ import {
|
||||
readStringArrayParam,
|
||||
readStringParam,
|
||||
} from "./common.js";
|
||||
|
||||
function formatDiscordTimestamp(ts?: string | null): string | undefined {
|
||||
if (!ts) return undefined;
|
||||
const date = new Date(ts);
|
||||
if (Number.isNaN(date.getTime())) return undefined;
|
||||
|
||||
const yyyy = String(date.getFullYear()).padStart(4, "0");
|
||||
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getDate()).padStart(2, "0");
|
||||
const hh = String(date.getHours()).padStart(2, "0");
|
||||
const min = String(date.getMinutes()).padStart(2, "0");
|
||||
|
||||
// getTimezoneOffset() is minutes *behind* UTC. Flip sign to get ISO offset.
|
||||
const offsetMinutes = -date.getTimezoneOffset();
|
||||
const sign = offsetMinutes >= 0 ? "+" : "-";
|
||||
const absOffsetMinutes = Math.abs(offsetMinutes);
|
||||
const offsetH = String(Math.floor(absOffsetMinutes / 60)).padStart(2, "0");
|
||||
const offsetM = String(absOffsetMinutes % 60).padStart(2, "0");
|
||||
|
||||
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const tzSuffix = tz ? `{${tz}}` : "";
|
||||
|
||||
// Compact ISO-like *local* timestamp with minutes precision.
|
||||
// Example: 2025-01-02T03:04-08:00{America/Los_Angeles}
|
||||
return `${yyyy}-${mm}-${dd}T${hh}:${min}${sign}${offsetH}:${offsetM}${tzSuffix}`;
|
||||
}
|
||||
import { withNormalizedTimestamp } from "../date-time.js";
|
||||
|
||||
function parseDiscordMessageLink(link: string) {
|
||||
const normalized = link.trim();
|
||||
@@ -76,6 +51,13 @@ export async function handleDiscordMessagingAction(
|
||||
params: Record<string, unknown>,
|
||||
isActionEnabled: ActionGate<DiscordActionConfig>,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const normalizeMessage = (message: unknown) => {
|
||||
if (!message || typeof message !== "object") return message;
|
||||
return withNormalizedTimestamp(
|
||||
message as Record<string, unknown>,
|
||||
(message as { timestamp?: unknown }).timestamp,
|
||||
);
|
||||
};
|
||||
switch (action) {
|
||||
case "react": {
|
||||
if (!isActionEnabled("reactions")) {
|
||||
@@ -189,7 +171,13 @@ export async function handleDiscordMessagingAction(
|
||||
);
|
||||
}
|
||||
const message = await fetchMessageDiscord(channelId, messageId);
|
||||
return jsonResult({ ok: true, message, guildId, channelId, messageId });
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
message: normalizeMessage(message),
|
||||
guildId,
|
||||
channelId,
|
||||
messageId,
|
||||
});
|
||||
}
|
||||
case "readMessages": {
|
||||
if (!isActionEnabled("messages")) {
|
||||
@@ -207,11 +195,10 @@ export async function handleDiscordMessagingAction(
|
||||
after: readStringParam(params, "after"),
|
||||
around: readStringParam(params, "around"),
|
||||
});
|
||||
const formattedMessages = messages.map((message) => ({
|
||||
...message,
|
||||
timestamp: formatDiscordTimestamp(message.timestamp) ?? message.timestamp,
|
||||
}));
|
||||
return jsonResult({ ok: true, messages: formattedMessages });
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
messages: messages.map((message) => normalizeMessage(message)),
|
||||
});
|
||||
}
|
||||
case "sendMessage": {
|
||||
if (!isActionEnabled("messages")) {
|
||||
@@ -357,7 +344,7 @@ export async function handleDiscordMessagingAction(
|
||||
required: true,
|
||||
});
|
||||
const pins = await listPinsDiscord(channelId);
|
||||
return jsonResult({ ok: true, pins });
|
||||
return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) });
|
||||
}
|
||||
case "searchMessages": {
|
||||
if (!isActionEnabled("search")) {
|
||||
@@ -386,7 +373,23 @@ export async function handleDiscordMessagingAction(
|
||||
authorIds: authorIdList.length ? authorIdList : undefined,
|
||||
limit,
|
||||
});
|
||||
return jsonResult({ ok: true, results });
|
||||
if (!results || typeof results !== "object") {
|
||||
return jsonResult({ ok: true, results });
|
||||
}
|
||||
const resultsRecord = results as Record<string, unknown>;
|
||||
const messages = resultsRecord.messages;
|
||||
const normalizedMessages = Array.isArray(messages)
|
||||
? messages.map((group) =>
|
||||
Array.isArray(group) ? group.map((msg) => normalizeMessage(msg)) : group,
|
||||
)
|
||||
: messages;
|
||||
return jsonResult({
|
||||
ok: true,
|
||||
results: {
|
||||
...resultsRecord,
|
||||
messages: normalizedMessages,
|
||||
},
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
|
||||
@@ -17,6 +17,7 @@ const editChannelDiscord = vi.fn(async () => ({
|
||||
name: "edited",
|
||||
}));
|
||||
const editMessageDiscord = vi.fn(async () => ({}));
|
||||
const fetchMessageDiscord = vi.fn(async () => ({}));
|
||||
const fetchChannelPermissionsDiscord = vi.fn(async () => ({}));
|
||||
const fetchReactionsDiscord = vi.fn(async () => ({}));
|
||||
const listPinsDiscord = vi.fn(async () => ({}));
|
||||
@@ -42,6 +43,7 @@ vi.mock("../../discord/send.js", () => ({
|
||||
deleteMessageDiscord: (...args: unknown[]) => deleteMessageDiscord(...args),
|
||||
editChannelDiscord: (...args: unknown[]) => editChannelDiscord(...args),
|
||||
editMessageDiscord: (...args: unknown[]) => editMessageDiscord(...args),
|
||||
fetchMessageDiscord: (...args: unknown[]) => fetchMessageDiscord(...args),
|
||||
fetchChannelPermissionsDiscord: (...args: unknown[]) => fetchChannelPermissionsDiscord(...args),
|
||||
fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args),
|
||||
listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args),
|
||||
@@ -134,6 +136,80 @@ describe("handleDiscordMessagingAction", () => {
|
||||
),
|
||||
).rejects.toThrow(/Discord reactions are disabled/);
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to readMessages payloads", async () => {
|
||||
readMessagesDiscord.mockResolvedValueOnce([
|
||||
{ id: "1", timestamp: "2026-01-15T10:00:00.000Z" },
|
||||
]);
|
||||
|
||||
const result = await handleDiscordMessagingAction(
|
||||
"readMessages",
|
||||
{ channelId: "C1" },
|
||||
enableAllActions,
|
||||
);
|
||||
const payload = result.details as { messages: Array<{ timestampMs?: number; timestampUtc?: string }> };
|
||||
|
||||
const expectedMs = Date.parse("2026-01-15T10:00:00.000Z");
|
||||
expect(payload.messages[0].timestampMs).toBe(expectedMs);
|
||||
expect(payload.messages[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to fetchMessage payloads", async () => {
|
||||
fetchMessageDiscord.mockResolvedValueOnce({
|
||||
id: "1",
|
||||
timestamp: "2026-01-15T11:00:00.000Z",
|
||||
});
|
||||
|
||||
const result = await handleDiscordMessagingAction(
|
||||
"fetchMessage",
|
||||
{ guildId: "G1", channelId: "C1", messageId: "M1" },
|
||||
enableAllActions,
|
||||
);
|
||||
const payload = result.details as { message?: { timestampMs?: number; timestampUtc?: string } };
|
||||
|
||||
const expectedMs = Date.parse("2026-01-15T11:00:00.000Z");
|
||||
expect(payload.message?.timestampMs).toBe(expectedMs);
|
||||
expect(payload.message?.timestampUtc).toBe(new Date(expectedMs).toISOString());
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to listPins payloads", async () => {
|
||||
listPinsDiscord.mockResolvedValueOnce([
|
||||
{ id: "1", timestamp: "2026-01-15T12:00:00.000Z" },
|
||||
]);
|
||||
|
||||
const result = await handleDiscordMessagingAction(
|
||||
"listPins",
|
||||
{ channelId: "C1" },
|
||||
enableAllActions,
|
||||
);
|
||||
const payload = result.details as { pins: Array<{ timestampMs?: number; timestampUtc?: string }> };
|
||||
|
||||
const expectedMs = Date.parse("2026-01-15T12:00:00.000Z");
|
||||
expect(payload.pins[0].timestampMs).toBe(expectedMs);
|
||||
expect(payload.pins[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to searchMessages payloads", async () => {
|
||||
searchMessagesDiscord.mockResolvedValueOnce({
|
||||
total_results: 1,
|
||||
messages: [[{ id: "1", timestamp: "2026-01-15T13:00:00.000Z" }]],
|
||||
});
|
||||
|
||||
const result = await handleDiscordMessagingAction(
|
||||
"searchMessages",
|
||||
{ guildId: "G1", content: "hi" },
|
||||
enableAllActions,
|
||||
);
|
||||
const payload = result.details as {
|
||||
results?: { messages?: Array<Array<{ timestampMs?: number; timestampUtc?: string }>> };
|
||||
};
|
||||
|
||||
const expectedMs = Date.parse("2026-01-15T13:00:00.000Z");
|
||||
expect(payload.results?.messages?.[0]?.[0]?.timestampMs).toBe(expectedMs);
|
||||
expect(payload.results?.messages?.[0]?.[0]?.timestampUtc).toBe(
|
||||
new Date(expectedMs).toISOString(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const channelsEnabled = (key: keyof DiscordActionConfig) => key === "channels";
|
||||
|
||||
@@ -325,4 +325,38 @@ describe("handleSlackAction", () => {
|
||||
threadTs: "1111111111.111111",
|
||||
});
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to readMessages payloads", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
readSlackMessages.mockResolvedValueOnce({
|
||||
messages: [{ ts: "1735689600.456", text: "hi" }],
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
const result = await handleSlackAction({ action: "readMessages", channelId: "C1" }, cfg);
|
||||
const payload = result.details as { messages: Array<{ timestampMs?: number; timestampUtc?: string }> };
|
||||
|
||||
const expectedMs = Math.round(1735689600.456 * 1000);
|
||||
expect(payload.messages[0].timestampMs).toBe(expectedMs);
|
||||
expect(payload.messages[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
|
||||
});
|
||||
|
||||
it("adds normalized timestamps to pin payloads", async () => {
|
||||
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
|
||||
listSlackPins.mockResolvedValueOnce([
|
||||
{
|
||||
type: "message",
|
||||
message: { ts: "1735689600.789", text: "pinned" },
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await handleSlackAction({ action: "listPins", channelId: "C1" }, cfg);
|
||||
const payload = result.details as {
|
||||
pins: Array<{ message?: { timestampMs?: number; timestampUtc?: string } }>;
|
||||
};
|
||||
|
||||
const expectedMs = Math.round(1735689600.789 * 1000);
|
||||
expect(payload.pins[0].message?.timestampMs).toBe(expectedMs);
|
||||
expect(payload.pins[0].message?.timestampUtc).toBe(new Date(expectedMs).toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
sendSlackMessage,
|
||||
unpinSlackMessage,
|
||||
} from "../../slack/actions.js";
|
||||
import { withNormalizedTimestamp } from "../date-time.js";
|
||||
import { createActionGate, jsonResult, readReactionParams, readStringParam } from "./common.js";
|
||||
|
||||
const messagingActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]);
|
||||
@@ -197,7 +198,13 @@ export async function handleSlackAction(
|
||||
before: before ?? undefined,
|
||||
after: after ?? undefined,
|
||||
});
|
||||
return jsonResult({ ok: true, ...result });
|
||||
const messages = result.messages.map((message) =>
|
||||
withNormalizedTimestamp(
|
||||
message as Record<string, unknown>,
|
||||
(message as { ts?: unknown }).ts,
|
||||
),
|
||||
);
|
||||
return jsonResult({ ok: true, messages, hasMore: result.hasMore });
|
||||
}
|
||||
default:
|
||||
break;
|
||||
@@ -234,7 +241,16 @@ export async function handleSlackAction(
|
||||
const pins = accountOpts
|
||||
? await listSlackPins(channelId, accountOpts)
|
||||
: await listSlackPins(channelId);
|
||||
return jsonResult({ ok: true, pins });
|
||||
const normalizedPins = pins.map((pin) => {
|
||||
const message = pin.message
|
||||
? withNormalizedTimestamp(
|
||||
pin.message as Record<string, unknown>,
|
||||
(pin.message as { ts?: unknown }).ts,
|
||||
)
|
||||
: pin.message;
|
||||
return message ? { ...pin, message } : pin;
|
||||
});
|
||||
return jsonResult({ ok: true, pins: normalizedPins });
|
||||
}
|
||||
|
||||
if (action === "memberInfo") {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { enqueueSystemEvent, resetSystemEventsForTest } from "../../infra/system
|
||||
import { prependSystemEvents } from "./session-updates.js";
|
||||
|
||||
describe("prependSystemEvents", () => {
|
||||
it("adds a local timestamp to queued system events", async () => {
|
||||
it("adds a UTC timestamp to queued system events", async () => {
|
||||
vi.useFakeTimers();
|
||||
const timestamp = new Date("2026-01-12T20:19:17");
|
||||
vi.setSystemTime(timestamp);
|
||||
@@ -20,15 +20,7 @@ describe("prependSystemEvents", () => {
|
||||
prefixedBodyBase: "User: hi",
|
||||
});
|
||||
|
||||
const expectedTimestamp = timestamp.toLocaleString("en-US", {
|
||||
hour12: false,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
const expectedTimestamp = "2026-01-12T20:19:17Z";
|
||||
|
||||
expect(result).toContain(`System: [${expectedTimestamp}] Model switched.`);
|
||||
|
||||
|
||||
@@ -25,16 +25,17 @@ export async function prependSystemEvents(params: {
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const formatSystemEventTimestamp = (ts: number) =>
|
||||
new Date(ts).toLocaleString("en-US", {
|
||||
hour12: false,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
const formatSystemEventTimestamp = (ts: number) => {
|
||||
const date = new Date(ts);
|
||||
if (Number.isNaN(date.getTime())) return "unknown-time";
|
||||
const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getUTCDate()).padStart(2, "0");
|
||||
const hh = String(date.getUTCHours()).padStart(2, "0");
|
||||
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
const sec = String(date.getUTCSeconds()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd}T${hh}:${min}:${sec}Z`;
|
||||
};
|
||||
|
||||
const systemLines: string[] = [];
|
||||
const queued = drainSystemEventEntries(params.sessionKey);
|
||||
|
||||
@@ -103,6 +103,8 @@ export type AgentDefaultsConfig = {
|
||||
bootstrapMaxChars?: number;
|
||||
/** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */
|
||||
userTimezone?: string;
|
||||
/** Time format in system prompt: auto (OS preference), 12-hour, or 24-hour. */
|
||||
timeFormat?: "auto" | "12" | "24";
|
||||
/** Optional display-only context window override (used for % in status UIs). */
|
||||
contextTokens?: number;
|
||||
/** Optional CLI backends for text-only fallback (claude-cli, etc.). */
|
||||
|
||||
@@ -41,6 +41,7 @@ export const AgentDefaultsSchema = z
|
||||
skipBootstrap: z.boolean().optional(),
|
||||
bootstrapMaxChars: z.number().int().positive().optional(),
|
||||
userTimezone: z.string().optional(),
|
||||
timeFormat: z.union([z.literal("auto"), z.literal("12"), z.literal("24")]).optional(),
|
||||
contextTokens: z.number().int().positive().optional(),
|
||||
cliBackends: z.record(z.string(), CliBackendSchema).optional(),
|
||||
memorySearch: MemorySearchSchema,
|
||||
|
||||
Reference in New Issue
Block a user