diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 3c07668d5b6..150b8a54c52 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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(); diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 7790fa40820..c2cc13868f1 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -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 = [ { diff --git a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts b/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts index 62f1e63158e..a688c57f1e0 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.test-harness.ts @@ -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() { diff --git a/src/auto-reply/reply/session-reset-prompt.test.ts b/src/auto-reply/reply/session-reset-prompt.test.ts index 4d5c98df984..fe4d757bc2e 100644 --- a/src/auto-reply/reply/session-reset-prompt.test.ts +++ b/src/auto-reply/reply/session-reset-prompt.test.ts @@ -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", () => { diff --git a/src/auto-reply/reply/session-reset-prompt.ts b/src/auto-reply/reply/session-reset-prompt.ts index e4f983edb93..a0c89c4afce 100644 --- a/src/auto-reply/reply/session-reset-prompt.ts +++ b/src/auto-reply/reply/session-reset-prompt.ts @@ -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 { diff --git a/src/config/sessions/session-file.ts b/src/config/sessions/session-file.ts index e668db26503..a37dcb08847 100644 --- a/src/config/sessions/session-file.ts +++ b/src/config/sessions/session-file.ts @@ -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, { diff --git a/src/config/sessions/sessions.test.ts b/src/config/sessions/sessions.test.ts index a1fc1fa4ae0..9ec166aa692 100644 --- a/src/config/sessions/sessions.test.ts +++ b/src/config/sessions/sessions.test.ts @@ -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); + }); });