fix(startup): prioritize bootstrap on fresh sessions

This commit is contained in:
Tak Hoffman
2026-04-16 17:47:49 -05:00
parent b21540fabc
commit 81818df1b4
7 changed files with 74 additions and 10 deletions

View File

@@ -489,7 +489,10 @@ export async function runEmbeddedAttempt(
const workspaceNotes = hookAdjustedBootstrapFiles.some(
(file) => file.name === DEFAULT_BOOTSTRAP_FILENAME && !file.missing,
)
? ["Reminder: commit your changes in this workspace after edits."]
? [
"If BOOTSTRAP.md is present in Project Context, it overrides the normal first greeting. Read it and follow its instructions first, then update or delete it when complete.",
"Reminder: commit your changes in this workspace after edits.",
]
: undefined;
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();

View File

@@ -409,6 +409,19 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("Reminder: commit your changes in this workspace after edits.");
});
it("includes BOOTSTRAP override guidance in workspace notes when provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/openclaw",
workspaceNotes: [
"If BOOTSTRAP.md is present in Project Context, it overrides the normal first greeting. Read it and follow its instructions first, then update or delete it when complete.",
],
});
expect(prompt).toContain("BOOTSTRAP.md is present in Project Context");
expect(prompt).toContain("it overrides the normal first greeting");
expect(prompt).toContain("Read it and follow its instructions first");
});
it("shows timezone section for 12h, 24h, and timezone-only modes", () => {
const cases = [
{

View File

@@ -446,7 +446,8 @@ export async function runGreetingPromptForBareNewOrReset(params: {
expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce();
const prompt = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? "";
expect(prompt).toContain("A new session was started via /new or /reset");
expect(prompt).toContain("If runtime-provided startup context is included for this first turn");
expect(prompt).toContain("Execute your Session Startup sequence now");
expect(prompt).toContain("read the required files before responding to the user");
}
export function installTriggerHandlingE2eTestHooks() {

View File

@@ -3,11 +3,15 @@ import type { OpenClawConfig } from "../../config/config.js";
import { buildBareSessionResetPrompt } from "./session-reset-prompt.js";
describe("buildBareSessionResetPrompt", () => {
it("includes the runtime-owned startup instruction without falsely claiming context exists", () => {
it("includes the explicit Session Startup instruction for bare /new and /reset", () => {
const prompt = buildBareSessionResetPrompt();
expect(prompt).toContain("If runtime-provided startup context is included for this first turn");
expect(prompt).not.toContain("read the required files before responding to the user");
expect(prompt).not.toContain("Startup context has already been assembled by runtime");
expect(prompt).toContain("Execute your Session Startup sequence now");
expect(prompt).toContain("read the required files before responding to the user");
expect(prompt).toContain("If BOOTSTRAP.md exists in the provided Project Context");
expect(prompt).toContain("read it and follow its instructions first");
expect(prompt).not.toContain(
"If runtime-provided startup context is included for this first turn",
);
});
it("appends current time line so agents know the date", () => {

View File

@@ -2,11 +2,11 @@ import { appendCronStyleCurrentTimeLine } from "../../agents/current-time.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
const BARE_SESSION_RESET_PROMPT_BASE =
"A new session was started via /new or /reset. If runtime-provided startup context is included for this first turn, use it before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.";
"A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. If BOOTSTRAP.md exists in the provided Project Context, read it and follow its instructions first. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.";
/**
* Build the bare session reset prompt, appending the current date/time so agents
* know which daily memory files the runtime resolved for startup context.
* know which daily memory files to read during their Session Startup sequence.
* Without this, agents on /new or /reset guess the date from their training cutoff.
*/
export function buildBareSessionResetPrompt(cfg?: OpenClawConfig, nowMs?: number): string {

View File

@@ -18,9 +18,13 @@ export async function resolveAndPersistSessionFile(params: {
const { sessionId, sessionKey, sessionStore, storePath } = params;
const baseEntry = params.sessionEntry ??
sessionStore[sessionKey] ?? { sessionId, updatedAt: Date.now() };
const shouldReusePersistedSessionFile = baseEntry.sessionId === sessionId;
const fallbackSessionFile = params.fallbackSessionFile?.trim();
const entryForResolve =
!baseEntry.sessionFile && fallbackSessionFile
const entryForResolve = !shouldReusePersistedSessionFile
? fallbackSessionFile
? { ...baseEntry, sessionFile: fallbackSessionFile }
: { ...baseEntry, sessionFile: undefined }
: !baseEntry.sessionFile && fallbackSessionFile
? { ...baseEntry, sessionFile: fallbackSessionFile }
: baseEntry;
const sessionFile = resolveSessionFilePath(sessionId, entryForResolve, {

View File

@@ -396,4 +396,43 @@ describe("resolveAndPersistSessionFile", () => {
const saved = loadSessionStore(fixture.storePath(), { skipCache: true });
expect(saved[sessionKey]?.sessionFile).toBe(fallbackSessionFile);
});
it("rotates to a new transcript path when sessionId changes on the same session key", async () => {
const previousSessionId = "old-session-id";
const nextSessionId = "new-session-id";
const sessionKey = "agent:main:telegram:group:123";
const previousSessionFile = resolveSessionTranscriptPathInDir(
previousSessionId,
fixture.sessionsDir(),
);
const expectedNextSessionFile = resolveSessionTranscriptPathInDir(
nextSessionId,
fixture.sessionsDir(),
);
const store = {
[sessionKey]: {
sessionId: previousSessionId,
updatedAt: Date.now(),
sessionFile: previousSessionFile,
},
};
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
const sessionStore = loadSessionStore(fixture.storePath(), { skipCache: true });
const result = await resolveAndPersistSessionFile({
sessionId: nextSessionId,
sessionKey,
sessionStore,
storePath: fixture.storePath(),
sessionEntry: sessionStore[sessionKey],
sessionsDir: fixture.sessionsDir(),
});
expect(result.sessionFile).toBe(expectedNextSessionFile);
expect(result.sessionFile).not.toBe(previousSessionFile);
expect(result.sessionEntry.sessionFile).toBe(expectedNextSessionFile);
const saved = loadSessionStore(fixture.storePath(), { skipCache: true });
expect(saved[sessionKey]?.sessionFile).toBe(expectedNextSessionFile);
});
});