mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:30:44 +00:00
fix(startup): prioritize bootstrap on fresh sessions
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user