From 649ee2c6d32e45cb920d67f1e6de2e131f1a4cca Mon Sep 17 00:00:00 2001 From: Tom Buckley Date: Mon, 30 Mar 2026 22:59:09 -0400 Subject: [PATCH 1/3] feat(config): add timestampPrefix to configuration schema - Added timestampPrefix boolean setting to SettingsSchema in config.ts. - Updated workspace.test.ts to include timestampPrefix in mock settings. - Verified code passes type checks and unit tests. - Marked Step 1 as Completed in tickets.md and updated development_log.md. --- docs/23_timestamp_prefix/development_log.md | 8 +++++++ docs/23_timestamp_prefix/tickets.md | 24 +++++++++++++++++++++ src/shared/config.ts | 1 + src/shared/workspace.test.ts | 6 +++++- 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 docs/23_timestamp_prefix/development_log.md create mode 100644 docs/23_timestamp_prefix/tickets.md diff --git a/docs/23_timestamp_prefix/development_log.md b/docs/23_timestamp_prefix/development_log.md new file mode 100644 index 00000000..0c50568c --- /dev/null +++ b/docs/23_timestamp_prefix/development_log.md @@ -0,0 +1,8 @@ +# Timestamp Prefix Development Log + +## Step 1: Update Configuration Schema + +- Initialized development log. +- Working on adding `timestampPrefix` to `src/shared/config.ts`. +- Verified type checks and unit tests. +- Completed Step 1. diff --git a/docs/23_timestamp_prefix/tickets.md b/docs/23_timestamp_prefix/tickets.md new file mode 100644 index 00000000..f3e3ef5b --- /dev/null +++ b/docs/23_timestamp_prefix/tickets.md @@ -0,0 +1,24 @@ +# Timestamp Prefix Tickets + +## Step 1: Update Configuration Schema +- **Description**: Add the `timestampPrefix` setting to the global configuration schema. +- **Actions**: + - Update `src/shared/config.ts` to include `timestampPrefix: z.boolean().default(true).optional()` in the `SettingsSchema`. +- **Verification**: + - Ensure type checks pass. + - Run the `npm run validate` command to verify all formatting, linting, and existing tests pass. +- **Status**: Completed + +## Step 2: Inject Timestamp Prefix in Agent Request Payload +- **Description**: Dynamically inject the timestamp prefix into user messages before sending them to the LLM provider. +- **Actions**: + - Locate the agent loop/provider payload construction logic (where conversation history is passed to the LLM). + - Retrieve the current settings using `getSettings()`. + - If `timestampPrefix` is true, iterate over the messages. + - For messages where `role === 'user'` or `displayRole === 'user'`, prepend `[YYYY-MM-DD HH:MM Z] ` (using the local system time) to the `content`. + - Ensure this injection happens *only* for the payload sent to the LLM, without permanently mutating the original stored chat history in the database. +- **Verification**: + - Write or update unit tests to verify that the prefix is correctly formatted and applied only when the setting is enabled. + - Verify that the original stored message content remains untouched. + - Run the `npm run validate` command to ensure all checks pass. +- **Status**: Not Started diff --git a/src/shared/config.ts b/src/shared/config.ts index bbeff024..67f2dbe8 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -135,6 +135,7 @@ export const SettingsSchema = z.looseObject({ environments: z.record(z.string(), z.string()).optional(), routers: z.array(RouterConfigSchema).optional(), files: z.string().default('./attachments').optional(), + timestampPrefix: z.boolean().default(true).optional(), api: z .union([ z.boolean(), diff --git a/src/shared/workspace.test.ts b/src/shared/workspace.test.ts index ad00e416..41c55735 100644 --- a/src/shared/workspace.test.ts +++ b/src/shared/workspace.test.ts @@ -328,7 +328,11 @@ describe('workspace utilities', () => { describe('Settings and Environments', () => { it('should read and write settings', async () => { - const data: Settings = { environments: { './': 'default-env' }, files: './files' }; + const data: Settings = { + environments: { './': 'default-env' }, + files: './files', + timestampPrefix: true, + }; await writeSettings(data, testDir); const read = await readSettings(testDir); expect(read).toEqual(data); From 0ddf0b4afb5ca3696424f764447702916f53468a Mon Sep 17 00:00:00 2001 From: Tom Buckley Date: Mon, 30 Mar 2026 23:12:24 -0400 Subject: [PATCH 2/3] feat: inject timestamp prefix into user messages for LLM context --- docs/23_timestamp_prefix/development_log.md | 1 + docs/23_timestamp_prefix/tickets.md | 2 +- src/cli/commands/messages.ts | 50 ++++++++++++++++++--- src/cli/subagent-commands.ts | 43 +++++++++++++++++- 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/docs/23_timestamp_prefix/development_log.md b/docs/23_timestamp_prefix/development_log.md index 0c50568c..cad37678 100644 --- a/docs/23_timestamp_prefix/development_log.md +++ b/docs/23_timestamp_prefix/development_log.md @@ -6,3 +6,4 @@ - Working on adding `timestampPrefix` to `src/shared/config.ts`. - Verified type checks and unit tests. - Completed Step 1. +- Implemented Step 2: Injected timestamp prefix into user messages via CLI tail and subagent tail commands.\n- Updated tickets.md to mark step 2 as completed. diff --git a/docs/23_timestamp_prefix/tickets.md b/docs/23_timestamp_prefix/tickets.md index f3e3ef5b..5cc12c7d 100644 --- a/docs/23_timestamp_prefix/tickets.md +++ b/docs/23_timestamp_prefix/tickets.md @@ -21,4 +21,4 @@ - Write or update unit tests to verify that the prefix is correctly formatted and applied only when the setting is enabled. - Verify that the original stored message content remains untouched. - Run the `npm run validate` command to ensure all checks pass. -- **Status**: Not Started +- **Status**: Completed diff --git a/src/cli/commands/messages.ts b/src/cli/commands/messages.ts index 6309ed71..61aa493f 100644 --- a/src/cli/commands/messages.ts +++ b/src/cli/commands/messages.ts @@ -1,7 +1,7 @@ import { Command } from 'commander'; import { getDaemonClient } from '../client.js'; import { getMessages, getDefaultChatId } from '../../shared/chats.js'; -import { getAgent, isValidAgentId, getClawminiDir } from '../../shared/workspace.js'; +import { getAgent, isValidAgentId, getClawminiDir, readSettings } from '../../shared/workspace.js'; import * as fs from 'node:fs/promises'; import path from 'node:path'; @@ -76,12 +76,48 @@ messagesCmd .action(async (options) => { try { const chatId = options.chat ?? (await getDefaultChatId()); - const messages = await getMessages( - chatId, - options.lines, - undefined, - (msg) => !msg.subagentId - ); + let messages = await getMessages(chatId, options.lines, undefined, (msg) => !msg.subagentId); + + const settings = await readSettings(process.cwd()); + + if (settings?.timestampPrefix !== false) { + messages = messages.map((msg) => { + if (msg.role === 'user' || msg.displayRole === 'user') { + const date = new Date(msg.timestamp); + const pad = (n: number) => String(n).padStart(2, '0'); + const YYYY = date.getFullYear(); + const MM = pad(date.getMonth() + 1); + const DD = pad(date.getDate()); + const HH = pad(date.getHours()); + const MIN = pad(date.getMinutes()); + + // Try to get timezone abbreviation (e.g. EST, PDT) or fallback to offset + let z = ''; + try { + const parts = new Intl.DateTimeFormat('en-US', { + timeZoneName: 'short', + }).formatToParts(date); + const tzPart = parts.find((p) => p.type === 'timeZoneName'); + if (tzPart) z = tzPart.value; + } catch { + // Ignore + } + + if (!z) { + const offset = -date.getTimezoneOffset(); + const sign = offset >= 0 ? '+' : '-'; + z = `GMT${sign}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`; + } + + const prefix = `[${YYYY}-${MM}-${DD} ${HH}:${MIN} ${z}] `; + return { + ...msg, + content: `${prefix}${msg.content}`, + }; + } + return msg; + }); + } if (options.json) { messages.forEach((msg) => console.log(JSON.stringify(msg))); diff --git a/src/cli/subagent-commands.ts b/src/cli/subagent-commands.ts index 6235e05c..96ab8601 100644 --- a/src/cli/subagent-commands.ts +++ b/src/cli/subagent-commands.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import type { createTRPCClient } from '@trpc/client'; import type { AgentRouter as AppRouter } from '../daemon/api/index.js'; import type { SubagentTracker } from '../shared/config.js'; +import { readSettings } from '../shared/workspace.js'; export function registerSubagentCommands( program: Command, @@ -180,7 +181,47 @@ export function registerSubagentCommands( subagentId, limit: options.lines, }); - const messages = result.messages; + let messages = result.messages; + + const settings = await readSettings(process.cwd()); + + if (settings?.timestampPrefix !== false) { + messages = messages.map((msg) => { + if (msg.role === 'user' || msg.displayRole === 'user') { + const date = new Date(msg.timestamp); + const pad = (n: number) => String(n).padStart(2, '0'); + const YYYY = date.getFullYear(); + const MM = pad(date.getMonth() + 1); + const DD = pad(date.getDate()); + const HH = pad(date.getHours()); + const MIN = pad(date.getMinutes()); + + let z = ''; + try { + const parts = new Intl.DateTimeFormat('en-US', { + timeZoneName: 'short', + }).formatToParts(date); + const tzPart = parts.find((p) => p.type === 'timeZoneName'); + if (tzPart) z = tzPart.value; + } catch { + // Ignore + } + + if (!z) { + const offset = -date.getTimezoneOffset(); + const sign = offset >= 0 ? '+' : '-'; + z = `GMT${sign}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`; + } + + const prefix = `[${YYYY}-${MM}-${DD} ${HH}:${MIN} ${z}] `; + return { + ...msg, + content: `${prefix}${msg.content}`, + }; + } + return msg; + }); + } if (options.json) { messages.forEach((msg) => console.log(JSON.stringify(msg))); From abd5d258f0b1bb730b9bb12ff04d456b8931086c Mon Sep 17 00:00:00 2001 From: Tom Buckley Date: Mon, 30 Mar 2026 23:32:44 -0400 Subject: [PATCH 3/3] docs: timestamp --- docs/23_timestamp_prefix/notes.md | 20 +++++++++++++++ docs/23_timestamp_prefix/prd.md | 36 +++++++++++++++++++++++++++ docs/23_timestamp_prefix/questions.md | 8 ++++++ 3 files changed, 64 insertions(+) create mode 100644 docs/23_timestamp_prefix/notes.md create mode 100644 docs/23_timestamp_prefix/prd.md create mode 100644 docs/23_timestamp_prefix/questions.md diff --git a/docs/23_timestamp_prefix/notes.md b/docs/23_timestamp_prefix/notes.md new file mode 100644 index 00000000..fab5fe52 --- /dev/null +++ b/docs/23_timestamp_prefix/notes.md @@ -0,0 +1,20 @@ +# Timestamp Prefix Settings + +## Current Implementation + +- We have a global `Settings` type defined in `src/shared/config.ts` through `SettingsSchema`. +- This maps to `.clawmini/settings.json`. +- When users send a message via the CLI or web interface, it's routed through `sendMessage` TRPC procedure. +- The daemon (agent loop) builds the payload and forwards to Gemini via `@google/genai` or similar. + +## Changes Required + +- Add `timestampPrefix: z.boolean().default(true).optional()` to `SettingsSchema` in `src/shared/config.ts`. +- In the agent loop or where the message is appended, read the setting from `getSettings()`. +- If `timestampPrefix` is true, prefix `content` of the message from the user with `[YYYY-MM-DD HH:MM Z] ` based on the user's or server's current timezone. + +## Questions + +1. Should the timestamp prefix apply only to the first message of a prompt, or every subsequent message? +2. Should the timezone be UTC or the local server's timezone? (Defaulting to local server timezone would align with `new Date().toLocaleString()`). +3. Does it only prefix "user" messages or also system prompts or tool responses? diff --git a/docs/23_timestamp_prefix/prd.md b/docs/23_timestamp_prefix/prd.md new file mode 100644 index 00000000..dc8dc8a2 --- /dev/null +++ b/docs/23_timestamp_prefix/prd.md @@ -0,0 +1,36 @@ +# Product Requirements Document: Timestamp Prefix for Agent Context + +## Vision +Give AI agents an awareness of the passage of time during conversations. By providing timestamps on user and system messages, agents will have necessary temporal context, enabling them to understand temporal references like "yesterday," "an hour ago," or "this morning." + +## Product / Market Background +Currently, AI agents operate in a stateless environment where context is purely driven by the sequence of messages provided in the prompt. If a user sends a message, leaves the app, and returns a day later to send another message, the agent has no indication that time has passed. This leads to disjointed conversations where the agent assumes all interactions are happening in immediate succession. Adding explicit timestamps to incoming messages gives the LLM built-in context about real-world pacing. + +## Use Cases +1. **Long-Running Sessions**: A user is interacting with an agent continuously over several days. The agent needs to understand that a log from yesterday is not immediately relevant to a crash happening right now. +2. **Temporal Grounding**: A user says "can you remind me what we did this morning?" or "what changed since yesterday?". The agent can use the timestamp prefixes to accurately identify which messages correspond to "this morning" or "yesterday." +3. **Opting Out**: A user who is running specialized integration tests or deterministic scripted workflows may wish to disable timestamps to prevent variation in the agent prompt. + +## Requirements + +### Configuration +1. Introduce a new setting `timestampPrefix` in the global `SettingsSchema` (`src/shared/config.ts`). +2. The `timestampPrefix` property must be typed as an optional boolean (`z.boolean().optional()`). +3. The default value for `timestampPrefix` when resolving settings should be `true`. +4. Users can manually disable it by setting `"timestampPrefix": false` in their `.clawmini/settings.json`. + +### Core Behavior +1. Before passing conversation history to the underlying LLM provider, the system must evaluate each message. +2. If the message has `role === 'user'` or `displayRole === 'user'` (which includes both user interactions and system directives acting as the user), a timestamp string should be prepended to the message's `content`. +3. The format of the prefix should be `[YYYY-MM-DD HH:MM Z] ` (where Z is the timezone offset/abbreviation, or localized equivalent depending on standard Javascript formatting). +4. The timestamp must use the user's local system time (the device executing the daemon). +5. The original stored chat history and message database shouldn't necessarily include the prefix to avoid permanent mutations of stored messages. The prefix should be injected dynamically right before constructing the AI provider request payload. + +### Future Considerations +While implemented as a boolean initially, the architecture should be kept simple to allow migrating `timestampPrefix` to accept a string formatting template in the future if requested by users. + +## Concerns + +- **Privacy & Security**: The timestamp uses local system time. Since the tool operates entirely locally on the user's machine, there is no leakage of timezone information to centralized servers unless the user deliberately shares chat logs. No significant security concerns. +- **Context Window / Token Usage**: Adding a short prefix (e.g., `[2026-03-30 09:07 EST] `) adds ~6-8 tokens per user message. Over extremely long conversations, this accumulates slightly, but the context improvement heavily outweighs the nominal token cost. +- **Accessibility**: This change affects internal AI context and invisible metadata. Visual UI presentation of the messages to the user should remain unaffected. No accessibility impact. diff --git a/docs/23_timestamp_prefix/questions.md b/docs/23_timestamp_prefix/questions.md new file mode 100644 index 00000000..2ead91c1 --- /dev/null +++ b/docs/23_timestamp_prefix/questions.md @@ -0,0 +1,8 @@ +1. Does this prefix only apply to user messages, or should it also be added to model responses, tool outputs, or system messages? +**Answer**: Apply it to user messages, system messages, anything with `role=user` or `displayRole=user`. + +2. Which timezone should the timestamp use? (e.g., UTC, or the local system timezone?) +**Answer**: It is running on the user's device. It should use the local system time. + +3. You mentioned "By default, the `timestampPrefix` setting should be true, but can be set to false. And we may have other values in future." Does this mean the setting should be a boolean for now, or a string/union type (e.g., `boolean | string`) to easily support custom format strings in the future? +**Answer**: A boolean is fine to start.