diff --git a/CHANGELOG.md b/CHANGELOG.md index b00ba522bf0..341e2760a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/sessions: preserve terminal lifecycle state when final run metadata persists from a stale in-memory snapshot, preventing `main` sessions from staying stuck as running after completed or timed-out turns. - Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky. - Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc. - Plugins/Codex: allow the official npm Codex plugin to install without the unsafe-install override, keep `/codex` command ownership, and cover the real npm Docker live path through managed `.openclaw/npm` dependencies plus uninstall failure proof. diff --git a/src/agents/command/session-store.test.ts b/src/agents/command/session-store.test.ts index 75033234924..c4d0ebe7e0f 100644 --- a/src/agents/command/session-store.test.ts +++ b/src/agents/command/session-store.test.ts @@ -388,6 +388,63 @@ describe("updateSessionStoreAfterAgentRun", () => { }); }); + it("preserves terminal lifecycle state when caller has a stale running snapshot", async () => { + await withTempSessionStore(async ({ storePath }) => { + const cfg = {} as OpenClawConfig; + const sessionKey = "agent:main:explicit:test-lifecycle-preserve"; + const sessionId = "test-lifecycle-preserve-session"; + const terminalEntry: SessionEntry = { + sessionId, + updatedAt: 2_000, + status: "done", + startedAt: 1_000, + endedAt: 1_900, + runtimeMs: 900, + }; + await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: terminalEntry }, null, 2)); + + const staleInMemory: Record = { + [sessionKey]: { + sessionId, + updatedAt: 1_100, + status: "running", + startedAt: 1_000, + }, + }; + + await updateSessionStoreAfterAgentRun({ + cfg, + sessionId, + sessionKey, + storePath, + sessionStore: staleInMemory, + defaultProvider: "openai", + defaultModel: "gpt-5.4", + result: { + payloads: [], + meta: { + aborted: false, + agentMeta: { + provider: "openai", + model: "gpt-5.4", + }, + }, + } as never, + }); + + const persisted = loadSessionStore(storePath, { skipCache: true })[sessionKey]; + expect(persisted).toMatchObject({ + status: "done", + startedAt: 1_000, + endedAt: 1_900, + runtimeMs: 900, + modelProvider: "openai", + model: "gpt-5.4", + }); + expect(staleInMemory[sessionKey]?.status).toBe("done"); + }); + }); + it("persists latest systemPromptReport for downstream warning dedupe", async () => { await withTempSessionStore(async ({ storePath }) => { const sessionKey = "agent:codex:report:test-system-prompt-report"; diff --git a/src/agents/command/session-store.ts b/src/agents/command/session-store.ts index 0e5d5217efb..a5281539c95 100644 --- a/src/agents/command/session-store.ts +++ b/src/agents/command/session-store.ts @@ -36,6 +36,15 @@ function resolvePositiveInteger(value: number | undefined): number | undefined { return Math.floor(value); } +function removeLifecycleStateFromMetadataPatch(entry: SessionEntry): SessionEntry { + const next = { ...entry }; + delete next.status; + delete next.startedAt; + delete next.endedAt; + delete next.runtimeMs; + return next; +} + export async function updateSessionStoreAfterAgentRun(params: { cfg: OpenClawConfig; contextTokensOverride?: number; @@ -218,8 +227,9 @@ export async function updateSessionStoreAfterAgentRun(params: { if (compactionsThisRun > 0) { next.compactionCount = (entry.compactionCount ?? 0) + compactionsThisRun; } + const metadataPatch = removeLifecycleStateFromMetadataPatch(next); const persisted = await updateSessionStore(storePath, (store) => { - const merged = mergeSessionEntry(store[sessionKey], next); + const merged = mergeSessionEntry(store[sessionKey], metadataPatch); store[sessionKey] = merged; return merged; });