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
88 lines
3.3 KiB
TypeScript
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)}`,
|
|
};
|
|
}
|