Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/23_timestamp_prefix/development_log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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.
- 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.
20 changes: 20 additions & 0 deletions docs/23_timestamp_prefix/notes.md
Original file line number Diff line number Diff line change
@@ -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?
36 changes: 36 additions & 0 deletions docs/23_timestamp_prefix/prd.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions docs/23_timestamp_prefix/questions.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 24 additions & 0 deletions docs/23_timestamp_prefix/tickets.md
Original file line number Diff line number Diff line change
@@ -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**: Completed
50 changes: 43 additions & 7 deletions src/cli/commands/messages.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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)));
Expand Down
43 changes: 42 additions & 1 deletion src/cli/subagent-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)));
Expand Down
1 change: 1 addition & 0 deletions src/shared/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
6 changes: 5 additions & 1 deletion src/shared/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading