evOo-local/packages/adapters/src/current-time.ts
Claude 4875753d9c
feat(memory): remember appends dated notes instead of replacing memory
The remember tool rewrote the bot's whole MEMORY.md with each call, so every
new fact erased the earlier ones unless the model re-sent the full document.

- MemoryCommitRequest gains mode "append" (the store joins the note onto the
  current content inside the same serializable transaction and still writes a
  revision row) and every commit is capped at 16 KB per document.
- remember appends a dated bullet ("- YYYY-MM-DD: ...", the user's local date)
  by default and rewrites only with mode "replace"; a full document returns an
  error that tells the bot to condense it. Retries of the same tool call stay
  single appends through the existing effect record.
- Scripted runtime memory entries can append too; the scripted "remember"
  script now appends instead of overwriting the bot's memory.
- When memory does not fit the run's 32 KB budget, the prompt keeps a
  document's newest lines instead of its oldest.
- memory.update maps an oversized save to BAD_REQUEST.
- docs/PRODUCT.md records that UserFact is the cross-space user model and the
  space MEMORY.md stays space notes (no migration).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:10:25 +00:00

88 lines
3.3 KiB
TypeScript

import { knownTimeZone } from "@rakazo/core";
import type { PrismaClient } from "@rakazo/db";
/**
* Models have no clock. Without an explicit anchor they infer "now" from training data
* or from timestamps that happen to appear in the conversation, and then reason about
* deadlines, recency and scheduling from a stale date. Every run states the real present
* moment in its system instructions so that never has to be guessed: in the user's time
* zone when one is known (with the offset, so it stays unambiguous), otherwise in UTC.
*/
export function formatCurrentTimeInstruction(
timeZone: string | null | undefined,
now: Date = new Date(),
): string {
const zone = knownTimeZone(timeZone);
const clock = zonedClock(now, zone ?? "UTC");
const moment = zone
? `${clock.weekday}, ${clock.local}${zone === "UTC" ? "Z" : clock.offset} (${zone}, the user's time zone)`
: `${clock.weekday}, ${clock.local}Z (UTC)`;
return [
`Current date and time: ${moment}.`,
"Treat this as the present moment for everything you say and do.",
zone
? "Use it to judge what is past, upcoming, or overdue, and write absolute dates rather than relative ones."
: "Use it to judge what is past, upcoming, or overdue, convert it to the user's time zone when one is known, and write absolute dates rather than relative ones.",
"Never infer today's date from your training data or from timestamps quoted in the conversation.",
].join(" ");
}
/** Today's date as YYYY-MM-DD in the user's time zone, or in UTC when none is known. */
export function formatLocalDate(
timeZone: string | null | undefined,
now: Date = new Date(),
): string {
return zonedClock(now, knownTimeZone(timeZone) ?? "UTC").local.slice(0, 10);
}
/** The user's time zone from their profile when this runtime knows it. */
export async function loadUserTimeZone(
prisma: Pick<PrismaClient, "userProfile">,
userId: string,
): Promise<string | undefined> {
const profile = await prisma.userProfile.findUnique({
where: { userId },
select: { timeZone: true },
});
return knownTimeZone(profile?.timeZone);
}
function zonedClock(now: Date, timeZone: string) {
const parts = new Map(
new Intl.DateTimeFormat("en-US", {
timeZone,
weekday: "long",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
})
.formatToParts(now)
.map((part) => [part.type, part.value]),
);
const part = (type: Intl.DateTimeFormatPartTypes) => parts.get(type) ?? "";
const [year, month, day, hour, minute, second] = (
["year", "month", "day", "hour", "minute", "second"] as const
).map(part);
// The wall clock read as if it were UTC, minus the real instant, is the zone's offset.
const wallClock = Date.UTC(
Number(year),
Number(month) - 1,
Number(day),
Number(hour),
Number(minute),
Number(second),
);
const offsetMinutes = Math.round((wallClock - Math.floor(now.getTime() / 1000) * 1000) / 60_000);
const absolute = Math.abs(offsetMinutes);
const pad = (value: number) => String(value).padStart(2, "0");
return {
weekday: part("weekday"),
local: `${year}-${month}-${day}T${hour}:${minute}:${second}`,
offset: `${offsetMinutes < 0 ? "-" : "+"}${pad(Math.floor(absolute / 60))}:${pad(absolute % 60)}`,
};
}