From 5f562e65f68408b976575a623450e872026f0ade Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 10 Aug 2026 23:10:44 -0700 Subject: [PATCH] refactor: burn down export name collision baseline (#121870) * refactor(sessions): distinguish persistence owners * refactor(config): consolidate legacy record helper * refactor(sessions): name sqlite number coercion * chore(scripts): ratchet export collision baseline --- .../lib/export-name-collision-baseline.json | 22 ------------------- .../agent-command.live-model-switch.test.ts | 2 +- src/agents/agent-command.ts | 7 +++--- .../assistant-transcript-repair.test.ts | 15 +++++++------ .../command/assistant-transcript-repair.ts | 6 ++--- .../command/attempt-execution.shared.test.ts | 18 +++++++-------- .../command/attempt-execution.shared.ts | 2 +- src/agents/command/model-selection.ts | 6 ++--- src/agents/command/post-run.ts | 5 +++-- src/agents/command/run-embedded-attempt.ts | 5 +++-- src/agents/command/session-helpers.ts | 17 -------------- src/agents/command/session-preparation.ts | 6 ++--- src/agents/pending-final-delivery-marker.ts | 4 ++-- src/auto-reply/reply/commands-dock.ts | 4 ++-- .../reply/commands-session-store.test.ts | 20 ++++++++--------- .../reply/commands-session-store.ts | 2 +- .../reply/commands-session.activation.test.ts | 2 +- src/auto-reply/reply/commands-session.ts | 14 ++++++------ src/auto-reply/reply/commands-tts.ts | 6 ++--- .../shared/legacy-config-record-shared.ts | 11 ---------- .../doctor/shared/legacy-web-tools-migrate.ts | 3 +-- .../sessions/session-accessor.sqlite-delta.ts | 10 ++++----- .../sessions/session-accessor.sqlite-entry.ts | 6 ++--- ...session-accessor.sqlite-lifecycle-state.ts | 6 ++--- .../session-accessor.sqlite-normalize.ts | 2 +- .../sessions/session-accessor.sqlite-read.ts | 12 +++++----- ...ession-accessor.sqlite-transcript-state.ts | 4 ++-- 27 files changed, 85 insertions(+), 132 deletions(-) diff --git a/scripts/lib/export-name-collision-baseline.json b/scripts/lib/export-name-collision-baseline.json index c8a40461ecf7..21dca44ebf0e 100644 --- a/scripts/lib/export-name-collision-baseline.json +++ b/scripts/lib/export-name-collision-baseline.json @@ -262,13 +262,6 @@ ], "sdk": true }, - { - "name": "ensureRecord", - "files": [ - "src/commands/doctor/shared/legacy-config-record-shared.ts", - "src/config/legacy.shared.ts" - ] - }, { "name": "extractAssistantVisibleText", "files": [ @@ -681,13 +674,6 @@ "src/cron/run-diagnostics.ts" ] }, - { - "name": "normalizeSqliteNumber", - "files": [ - "src/config/sessions/session-accessor.sqlite-normalize.ts", - "src/infra/sqlite-number.ts" - ] - }, { "name": "normalizeStringRecord", "files": [ @@ -793,14 +779,6 @@ "src/gateway/server-methods/__mocks__/tools-effective.runtime.ts" ] }, - { - "name": "persistSessionEntry", - "files": [ - "src/agents/command/attempt-execution.shared.ts", - "src/agents/command/session-helpers.ts", - "src/auto-reply/reply/commands-session-store.ts" - ] - }, { "name": "planPluginUninstall", "files": [ diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index 9f110605a6c3..70964035e03f 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -162,7 +162,7 @@ vi.mock("./command/attempt-execution.shared.js", async () => { ); return { ...actual, - persistSessionEntry: (...args: unknown[]) => state.persistSessionEntryMock(...args), + persistAgentSession: (...args: unknown[]) => state.persistSessionEntryMock(...args), }; }); diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index 538644aa299b..ead294e62691 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -37,13 +37,14 @@ import { } from "./agent-command-restart-recovery.js"; import { runAcpAgentCommand } from "./command/acp-execution.js"; import { repairPendingAssistantTranscriptTurns } from "./command/assistant-transcript-repair.js"; +import { persistAgentSession } from "./command/attempt-execution.shared.js"; import { emitIngressModelUsageDiagnostic } from "./command/ingress-diagnostics.js"; import { resolveEmbeddedModelSelection } from "./command/model-selection.js"; import { finalizeEmbeddedAgentCommand } from "./command/post-run.js"; import { prepareAgentCommandExecution } from "./command/prepare.js"; import { runEmbeddedAgentAttempt } from "./command/run-embedded-attempt.js"; import { loadSessionStoreRuntime, resolveAgentCommandDeps } from "./command/runtime-loaders.js"; -import { persistSessionEntry, prepareCurrentRunDelivery } from "./command/session-helpers.js"; +import { prepareCurrentRunDelivery } from "./command/session-helpers.js"; import { prepareEmbeddedSessionState } from "./command/session-preparation.js"; import { clearRotatedSessionMetadata } from "./command/session.js"; import type { @@ -354,7 +355,7 @@ async function agentCommandInternal( suppressTextDelivery: opts.internalDeliverySuppressText, }), }; - const persisted = await persistSessionEntry({ + const persisted = await persistAgentSession({ sessionStore, sessionKey, storePath, @@ -566,7 +567,7 @@ async function agentCommandInternal( }), updatedAt: Date.now(), }; - const persisted = await persistSessionEntry({ + const persisted = await persistAgentSession({ sessionStore, sessionKey, storePath, diff --git a/src/agents/command/assistant-transcript-repair.test.ts b/src/agents/command/assistant-transcript-repair.test.ts index b902642bce2c..baeb2c736ad0 100644 --- a/src/agents/command/assistant-transcript-repair.test.ts +++ b/src/agents/command/assistant-transcript-repair.test.ts @@ -15,14 +15,14 @@ import type { EmbeddedAgentRunResult } from "../embedded-agent.js"; import type { loadManifestModelCatalog } from "../model-catalog.js"; import type { persistCliTurnTranscript } from "./attempt-execution.js"; import type { runAgentAttempt } from "./attempt-execution.runtime.js"; -import type { persistSessionEntry } from "./session-helpers.js"; +import type { persistAgentSession } from "./attempt-execution.shared.js"; type ProviderModelNormalizationParams = { provider: string; context: { modelId: string } }; type LoadManifestModelCatalogParams = Parameters[0]; type RunAgentAttempt = typeof runAgentAttempt; type PersistCliTurnTranscript = typeof persistCliTurnTranscript; type AppendExactAssistantMessage = typeof appendExactAssistantMessageToSessionTranscript; -type PersistSessionEntry = typeof persistSessionEntry; +type PersistSessionEntry = typeof persistAgentSession; type CliCompactionParams = { sessionEntry?: SessionEntry; sessionKey: string; @@ -205,13 +205,14 @@ vi.mock("../../config/sessions/transcript.runtime.js", async () => { }; }); -vi.mock("./session-helpers.js", async () => { - const actual = - await vi.importActual("./session-helpers.js"); +vi.mock("./attempt-execution.shared.js", async () => { + const actual = await vi.importActual( + "./attempt-execution.shared.js", + ); return { ...actual, - persistSessionEntry: (...args: Parameters) => { - state.persistSessionEntryReal = actual.persistSessionEntry; + persistAgentSession: (...args: Parameters) => { + state.persistSessionEntryReal = actual.persistAgentSession; return state.persistSessionEntryMock(...args); }, }; diff --git a/src/agents/command/assistant-transcript-repair.ts b/src/agents/command/assistant-transcript-repair.ts index 42df8a89407d..7b05961fa779 100644 --- a/src/agents/command/assistant-transcript-repair.ts +++ b/src/agents/command/assistant-transcript-repair.ts @@ -4,8 +4,8 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { runAgentHarnessBeforeMessageWriteHook } from "../harness/hook-helpers.js"; +import { persistAgentSession } from "./attempt-execution.shared.js"; import { loadTranscriptAppendRuntime } from "./runtime-loaders.js"; -import { persistSessionEntry } from "./session-helpers.js"; const log = createSubsystemLogger("agents/assistant-transcript-repair"); @@ -58,7 +58,7 @@ export async function persistAssistantTranscriptRepairRecord(params: { createdAt: now, }; try { - await persistSessionEntry({ + await persistAgentSession({ sessionStore: context.sessionStore, sessionKey: context.sessionKey, storePath: context.storePath, @@ -148,7 +148,7 @@ export async function repairPendingAssistantTranscriptTurns(params: { return; } try { - await persistSessionEntry({ + await persistAgentSession({ sessionStore: context.sessionStore, sessionKey: context.sessionKey, storePath: context.storePath, diff --git a/src/agents/command/attempt-execution.shared.test.ts b/src/agents/command/attempt-execution.shared.test.ts index 03026923bb34..8566f26a91f1 100644 --- a/src/agents/command/attempt-execution.shared.test.ts +++ b/src/agents/command/attempt-execution.shared.test.ts @@ -11,7 +11,7 @@ import { INTERNAL_RUNTIME_CONTEXT_END, } from "../internal-runtime-context.js"; import { - persistSessionEntry, + persistAgentSession, resolveAcpPromptBody, resolveInternalEventTranscriptBody, } from "./attempt-execution.shared.js"; @@ -90,7 +90,7 @@ describe("attempt execution prompt materialization", () => { }); }); -describe("persistSessionEntry", () => { +describe("persistAgentSession", () => { const sessionKey = "agent:main:main"; it("clears stale local entries when guarded persistence sees no persisted entry", async () => { @@ -106,7 +106,7 @@ describe("persistSessionEntry", () => { // A guarded write can decline persistence after rereading disk; local // memory must be cleared too so later turns do not reuse stale entries. - const persisted = await persistSessionEntry({ + const persisted = await persistAgentSession({ sessionStore, sessionKey, storePath, @@ -160,7 +160,7 @@ describe("persistSessionEntry", () => { await replaceSessionEntry({ sessionKey, storePath }, currentEntry); const sessionStore = { [sessionKey]: staleEntry }; - const persisted = await persistSessionEntry({ + const persisted = await persistAgentSession({ sessionStore, sessionKey, storePath, @@ -206,7 +206,7 @@ describe("persistSessionEntry", () => { await replaceSessionEntry({ sessionKey, storePath }, currentEntry); const sessionStore = { [sessionKey]: initialEntry }; - const persisted = await persistSessionEntry({ + const persisted = await persistAgentSession({ sessionStore, sessionKey, storePath, @@ -244,7 +244,7 @@ describe("persistSessionEntry", () => { }; const sessionStore = { [sessionKey]: staleEntry }; - const persisted = await persistSessionEntry({ + const persisted = await persistAgentSession({ sessionStore, sessionKey, storePath, @@ -275,14 +275,14 @@ describe("persistSessionEntry", () => { }; const sessionStore = { [sessionKey]: staleEntry }; - const first = await persistSessionEntry({ + const first = await persistAgentSession({ sessionStore, sessionKey, storePath, initialEntry: staleEntry, entry: staleEntry, }); - const second = await persistSessionEntry({ + const second = await persistAgentSession({ sessionStore, sessionKey, storePath, @@ -314,7 +314,7 @@ describe("persistSessionEntry", () => { updatedAt: 1, }; - const persisted = await persistSessionEntry({ + const persisted = await persistAgentSession({ sessionStore, sessionKey, storePath, diff --git a/src/agents/command/attempt-execution.shared.ts b/src/agents/command/attempt-execution.shared.ts index ca8b887cd579..3f8c686d569a 100644 --- a/src/agents/command/attempt-execution.shared.ts +++ b/src/agents/command/attempt-execution.shared.ts @@ -26,7 +26,7 @@ type PersistSessionEntryParams = { }; /** Persists one session entry while keeping the caller's in-memory store aligned. */ -export async function persistSessionEntry( +export async function persistAgentSession( params: PersistSessionEntryParams, ): Promise { let rejectedMissingEntry = false; diff --git a/src/agents/command/model-selection.ts b/src/agents/command/model-selection.ts index 8ca4827e8ceb..b2dff2947786 100644 --- a/src/agents/command/model-selection.ts +++ b/src/agents/command/model-selection.ts @@ -59,6 +59,7 @@ import { normalizeThinkingCatalogProviders, resolveEffectiveAgentRuntime, } from "../thinking-runtime.js"; +import { persistAgentSession } from "./attempt-execution.shared.js"; import { normalizeAgentCommandDefaultModelRef, normalizeAgentCommandModelRef, @@ -67,7 +68,6 @@ import { import { normalizeExplicitOverrideInput } from "./prepare.js"; import type { resolveAgentRunContext } from "./run-context.js"; import { loadTranscriptResolveRuntime } from "./runtime-loaders.js"; -import { persistSessionEntry } from "./session-helpers.js"; import type { AgentCommandOpts } from "./types.js"; type AgentRunContext = ReturnType; @@ -219,7 +219,7 @@ export async function resolveEmbeddedModelSelection(params: { } } if (entryUpdated) { - sessionEntry = await persistSessionEntry({ + sessionEntry = await persistAgentSession({ sessionStore: params.sessionStore, sessionKey: params.sessionKey, storePath: params.storePath, @@ -596,7 +596,7 @@ export async function resolveEmbeddedModelSelection(params: { thinkingLevel: params.thinkOverride, }; sessionEntry = - (await persistSessionEntry({ + (await persistAgentSession({ sessionStore: params.sessionStore, sessionKey: params.sessionKey, storePath: params.storePath, diff --git a/src/agents/command/post-run.ts b/src/agents/command/post-run.ts index 11cedf6a5fe3..8be0cb41186b 100644 --- a/src/agents/command/post-run.ts +++ b/src/agents/command/post-run.ts @@ -19,6 +19,7 @@ import { persistPendingFinalDeliveryMarker } from "../pending-final-delivery-mar import type { AgentRunSessionTarget } from "../run-session-target.js"; import { throwAgentRunRestartAbortReason } from "../run-termination.js"; import { persistAssistantTranscriptRepairRecord } from "./assistant-transcript-repair.js"; +import { persistAgentSession } from "./attempt-execution.shared.js"; import type { PreparedAgentCommandExecution } from "./prepare.js"; import type { EmbeddedAgentAttempt } from "./run-embedded-attempt.js"; import { @@ -26,7 +27,7 @@ import { loadDeliveryRuntime, loadSessionStoreRuntime, } from "./runtime-loaders.js"; -import { clearPendingFinalDelivery, persistSessionEntry } from "./session-helpers.js"; +import { clearPendingFinalDelivery } from "./session-helpers.js"; import type { EmbeddedSessionState } from "./session-preparation.js"; import type { AgentCommandOpts } from "./types.js"; @@ -383,7 +384,7 @@ export async function finalizeEmbeddedAgentCommand(params: { !pendingFinalDeliveryMarker.hasSendableFinalPayload && entry.pendingFinalDelivery?.kind === "transport-only"; if (deliveryResult?.deliverySucceeded === true || clearStaleTransportOnly) { - sessionEntry = await persistSessionEntry({ + sessionEntry = await persistAgentSession({ sessionStore, sessionKey, storePath, diff --git a/src/agents/command/run-embedded-attempt.ts b/src/agents/command/run-embedded-attempt.ts index 016608eb2b2f..1b0082c289c9 100644 --- a/src/agents/command/run-embedded-attempt.ts +++ b/src/agents/command/run-embedded-attempt.ts @@ -51,12 +51,13 @@ import { createAgentAttemptLifecycleCallbacks, type AgentAttemptLifecycleState, } from "./attempt-callbacks.js"; +import { persistAgentSession } from "./attempt-execution.shared.js"; import { createAgentCommandLifecycle } from "./lifecycle.js"; import { normalizeAgentCommandModelRef } from "./model-ref.js"; import type { EmbeddedModelSelection } from "./model-selection.js"; import type { PreparedAgentCommandExecution } from "./prepare.js"; import { loadAttemptExecutionRuntime, type AgentAttemptResult } from "./runtime-loaders.js"; -import { persistSessionEntry, resolveInternalSessionEffectsSource } from "./session-helpers.js"; +import { resolveInternalSessionEffectsSource } from "./session-helpers.js"; import type { EmbeddedSessionState } from "./session-preparation.js"; import type { AgentCommandOpts } from "./types.js"; @@ -313,7 +314,7 @@ export async function runEmbeddedAgentAttempt(params: { } const nextSessionEntry = { ...sessionEntry }; clearAutoFallbackPrimaryProbeSelection(nextSessionEntry); - sessionEntry = await persistSessionEntry({ + sessionEntry = await persistAgentSession({ sessionStore, sessionKey, storePath, diff --git a/src/agents/command/session-helpers.ts b/src/agents/command/session-helpers.ts index f5d66281edc7..4320ea94d091 100644 --- a/src/agents/command/session-helpers.ts +++ b/src/agents/command/session-helpers.ts @@ -19,25 +19,8 @@ import { isDeliverableMessageChannel, } from "../../utils/message-channel.js"; import type { AgentRunSessionTarget } from "../run-session-target.js"; -import { persistSessionEntry as persistSessionEntryBase } from "./attempt-execution.shared.js"; import type { AgentCommandOpts } from "./types.js"; -type PersistSessionEntryParams = { - sessionStore: Record; - sessionKey: string; - storePath: string; - initialEntry: SessionEntry; - entry: SessionEntry; -}; - -export async function persistSessionEntry( - params: PersistSessionEntryParams & { - shouldPersist?: (entry: SessionEntry | undefined) => boolean; - }, -): Promise { - return await persistSessionEntryBase(params); -} - export function clearPendingFinalDelivery(entry: SessionEntry, updatedAt: number): SessionEntry { return { ...entry, diff --git a/src/agents/command/session-preparation.ts b/src/agents/command/session-preparation.ts index 72b68e00a56a..6ff401cd89e7 100644 --- a/src/agents/command/session-preparation.ts +++ b/src/agents/command/session-preparation.ts @@ -6,9 +6,9 @@ import { registerAgentRunContext } from "../../infra/agent-run-registry.js"; import { applyVerboseOverride } from "../../sessions/level-overrides.js"; import { recordSessionHumanDirectMessage } from "../../sessions/session-state-events.js"; import { resolveEffectiveAgentSkillFilter } from "../../skills/discovery/agent-filter.js"; +import { persistAgentSession } from "./attempt-execution.shared.js"; import { resolveAgentRunContext } from "./run-context.js"; import { loadExecDefaultsRuntime, loadSkillsRuntime } from "./runtime-loaders.js"; -import { persistSessionEntry } from "./session-helpers.js"; import type { AgentCommandOpts } from "./types.js"; export async function prepareEmbeddedSessionState(params: { @@ -100,7 +100,7 @@ export async function prepareEmbeddedSessionState(params: { sessionStartedAt: current.sessionStartedAt ?? now, skillsSnapshot, }; - sessionEntry = await persistSessionEntry({ + sessionEntry = await persistAgentSession({ sessionStore: params.sessionStore, sessionKey: params.sessionKey, storePath: params.storePath, @@ -131,7 +131,7 @@ export async function prepareEmbeddedSessionState(params: { agentStatus: undefined, }; applyVerboseOverride(next, params.verboseOverride); - sessionEntry = await persistSessionEntry({ + sessionEntry = await persistAgentSession({ sessionStore: params.sessionStore, sessionKey: params.sessionKey, storePath: params.storePath, diff --git a/src/agents/pending-final-delivery-marker.ts b/src/agents/pending-final-delivery-marker.ts index d473992a0c36..ccca9655f174 100644 --- a/src/agents/pending-final-delivery-marker.ts +++ b/src/agents/pending-final-delivery-marker.ts @@ -8,7 +8,7 @@ import { import type { SessionEntry } from "../config/sessions/types.js"; import { isSubagentSessionKey } from "../routing/session-key.js"; import type { DeliveryContext } from "../utils/delivery-context.shared.js"; -import { persistSessionEntry } from "./command/attempt-execution.shared.js"; +import { persistAgentSession } from "./command/attempt-execution.shared.js"; type PersistPendingFinalDeliveryMarkerParams = { deliver: boolean; @@ -65,7 +65,7 @@ export async function persistPendingFinalDeliveryMarker( } const now = Date.now(); - const persisted = await persistSessionEntry({ + const persisted = await persistAgentSession({ sessionStore: params.sessionStore, sessionKey: params.sessionKey, storePath: params.storePath, diff --git a/src/auto-reply/reply/commands-dock.ts b/src/auto-reply/reply/commands-dock.ts index 69730b6590ba..a11b6639183d 100644 --- a/src/auto-reply/reply/commands-dock.ts +++ b/src/auto-reply/reply/commands-dock.ts @@ -12,7 +12,7 @@ import { import { resolveTextCommand } from "../commands-registry.js"; import { resolveCommandSurfaceChannel } from "./channel-context.js"; import { commandReply, defineAuthorizedTextCommand } from "./command-gates.js"; -import { persistSessionEntry } from "./commands-session-store.js"; +import { persistCommandSession } from "./commands-session-store.js"; import type { CommandHandler, HandleCommandsParams } from "./commands-types.js"; const DOCK_KEY_PREFIX = "dock:"; @@ -165,7 +165,7 @@ export const handleDockCommand: CommandHandler = defineAuthorizedTextCommand( origin: sessionDeliveryOrigin(sessionEntry), }); params.sessionEntry = sessionEntry; - const persisted = await persistSessionEntry({ + const persisted = await persistCommandSession({ ...params, touchedFields: ["delivery"], }); diff --git a/src/auto-reply/reply/commands-session-store.test.ts b/src/auto-reply/reply/commands-session-store.test.ts index 9b86482902ab..aaf55cbe46f3 100644 --- a/src/auto-reply/reply/commands-session-store.test.ts +++ b/src/auto-reply/reply/commands-session-store.test.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { loadSessionEntry, replaceSessionEntry } from "../../config/sessions/session-accessor.js"; import type { SessionEntry } from "../../config/sessions/types.js"; -import { persistAbortTargetEntry, persistSessionEntry } from "./commands-session-store.js"; +import { persistAbortTargetEntry, persistCommandSession } from "./commands-session-store.js"; async function withTempStore(run: (storePath: string) => Promise): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-command-session-store-")); @@ -27,7 +27,7 @@ describe("commands session store persistence", () => { const sessionStore: Record = { [sessionKey]: entry }; await expect( - persistSessionEntry({ + persistCommandSession({ allowCreateSessionEntry: true, sessionEntry: entry, sessionStore, @@ -60,7 +60,7 @@ describe("commands session store persistence", () => { const sessionStore: Record = { [sessionKey]: entry }; await expect( - persistSessionEntry({ + persistCommandSession({ sessionEntry: entry, sessionStore, sessionKey, @@ -88,7 +88,7 @@ describe("commands session store persistence", () => { delivery: { kind: "none" }, }; const seedEntry = { ...entry }; - await persistSessionEntry({ + await persistCommandSession({ allowCreateSessionEntry: true, sessionEntry: seedEntry, sessionStore: { [sessionKey]: seedEntry }, @@ -99,7 +99,7 @@ describe("commands session store persistence", () => { const sessionStore: Record = { [sessionKey]: entry }; await expect( - persistSessionEntry({ + persistCommandSession({ sessionEntry: entry, sessionStore, sessionKey, @@ -147,7 +147,7 @@ describe("commands session store persistence", () => { label: "After rename", pinnedAt: undefined, }; - await persistSessionEntry({ + await persistCommandSession({ allowCreateSessionEntry: true, sessionEntry: concurrentEntry, sessionStore: { [sessionKey]: concurrentEntry }, @@ -160,7 +160,7 @@ describe("commands session store persistence", () => { try { await expect( - persistSessionEntry({ + persistCommandSession({ sessionEntry: entry, sessionStore, sessionKey, @@ -214,7 +214,7 @@ describe("commands session store persistence", () => { const sessionStore = { [sessionKey]: sessionEntry }; await expect( - persistSessionEntry({ + persistCommandSession({ initialSessionEntry: initialEntry, sessionEntry, sessionStore, @@ -246,7 +246,7 @@ describe("commands session store persistence", () => { const sessionStore = { [sessionKey]: sessionEntry }; await expect( - persistSessionEntry({ + persistCommandSession({ initialSessionEntry: initialEntry, sessionEntry, sessionStore, @@ -286,7 +286,7 @@ describe("commands session store persistence", () => { const sessionStore = { [sessionKey]: sessionEntry }; await expect( - persistSessionEntry({ + persistCommandSession({ initialSessionEntry: initialEntry, sessionEntry, sessionStore, diff --git a/src/auto-reply/reply/commands-session-store.ts b/src/auto-reply/reply/commands-session-store.ts index d45a8f2ee2c5..075166d69d07 100644 --- a/src/auto-reply/reply/commands-session-store.ts +++ b/src/auto-reply/reply/commands-session-store.ts @@ -35,7 +35,7 @@ export function resolveCommandSessionEntryForKey( }; } -export async function persistSessionEntry(params: PersistSessionEntryParams): Promise { +export async function persistCommandSession(params: PersistSessionEntryParams): Promise { if (!params.sessionEntry || !params.sessionStore || !params.sessionKey) { return false; } diff --git a/src/auto-reply/reply/commands-session.activation.test.ts b/src/auto-reply/reply/commands-session.activation.test.ts index 9b8a3e328275..786d103ee73c 100644 --- a/src/auto-reply/reply/commands-session.activation.test.ts +++ b/src/auto-reply/reply/commands-session.activation.test.ts @@ -9,7 +9,7 @@ const persistenceConflictReply = vi.hoisted(() => ({ })); vi.mock("./commands-session-store.js", () => ({ - persistSessionEntry: persistSessionEntryMock, + persistCommandSession: persistSessionEntryMock, sessionEntryPersistenceConflictReply: () => persistenceConflictReply, })); diff --git a/src/auto-reply/reply/commands-session.ts b/src/auto-reply/reply/commands-session.ts index 140f3bf61d05..8dea53d44d98 100644 --- a/src/auto-reply/reply/commands-session.ts +++ b/src/auto-reply/reply/commands-session.ts @@ -58,7 +58,7 @@ import { } from "./command-gates.js"; import { handleAbortTrigger, handleStopCommand } from "./commands-session-abort.js"; import { - persistSessionEntry, + persistCommandSession, sessionEntryPersistenceConflictReply, } from "./commands-session-store.js"; import type { CommandHandler, HandleCommandsParams } from "./commands-types.js"; @@ -237,7 +237,7 @@ export const handleActivationCommand: CommandHandler = async (params, allowTextC params.sessionEntry.groupActivation = activationCommand.mode; params.sessionEntry.groupActivationNeedsSystemIntro = true; if ( - !(await persistSessionEntry({ + !(await persistCommandSession({ ...params, touchedFields: ["groupActivation", "groupActivationNeedsSystemIntro"], })) @@ -267,7 +267,7 @@ export const handleSendPolicyCommand: CommandHandler = defineAuthorizedTextComma } else { params.sessionEntry.sendPolicy = sendPolicyCommand.mode; } - if (!(await persistSessionEntry({ ...params, touchedFields: ["sendPolicy"] }))) { + if (!(await persistCommandSession({ ...params, touchedFields: ["sendPolicy"] }))) { return sessionEntryPersistenceConflictReply(); } } @@ -366,7 +366,7 @@ export const handleUsageCommand: CommandHandler = defineAuthorizedTextCommand( delete targetSessionEntry.responseUsage; params.sessionStore[params.sessionKey] = targetSessionEntry; if ( - !(await persistSessionEntry({ + !(await persistCommandSession({ ...params, sessionEntry: targetSessionEntry, touchedFields: ["responseUsage"], @@ -392,7 +392,7 @@ export const handleUsageCommand: CommandHandler = defineAuthorizedTextCommand( targetSessionEntry.responseUsage = next; params.sessionStore[params.sessionKey] = targetSessionEntry; if ( - !(await persistSessionEntry({ + !(await persistCommandSession({ ...params, sessionEntry: targetSessionEntry, touchedFields: ["responseUsage"], @@ -440,7 +440,7 @@ export const handleFastCommand: CommandHandler = defineAuthorizedTextCommand( if (targetSessionEntry && params.sessionStore && params.sessionKey) { delete targetSessionEntry.fastMode; if ( - !(await persistSessionEntry({ + !(await persistCommandSession({ ...params, sessionEntry: targetSessionEntry, touchedFields: ["fastMode"], @@ -457,7 +457,7 @@ export const handleFastCommand: CommandHandler = defineAuthorizedTextCommand( if (targetSessionEntry && params.sessionStore && params.sessionKey) { targetSessionEntry.fastMode = nextMode; if ( - !(await persistSessionEntry({ + !(await persistCommandSession({ ...params, sessionEntry: targetSessionEntry, touchedFields: ["fastMode"], diff --git a/src/auto-reply/reply/commands-tts.ts b/src/auto-reply/reply/commands-tts.ts index d3ff417cac8f..9f687ba1e889 100644 --- a/src/auto-reply/reply/commands-tts.ts +++ b/src/auto-reply/reply/commands-tts.ts @@ -43,7 +43,7 @@ import { matchCommandPrefix, } from "./command-gates.js"; import { - persistSessionEntry, + persistCommandSession, sessionEntryPersistenceConflictReply, } from "./commands-session-store.js"; import type { CommandHandler, CommandHandlerResult } from "./commands-types.js"; @@ -205,7 +205,7 @@ async function handleTtsChatAction( return { shouldContinue: false, reply: ttsUsage() }; } - if (!(await persistSessionEntry({ ...params, touchedFields: ["ttsAuto"] }))) { + if (!(await persistCommandSession({ ...params, touchedFields: ["ttsAuto"] }))) { return sessionEntryPersistenceConflictReply(); } return stopWithText(replyText); @@ -257,7 +257,7 @@ async function handleTtsLatestAction( params.sessionEntry.lastTtsReadLatestHash = hash; params.sessionEntry.lastTtsReadLatestAt = Date.now(); if ( - !(await persistSessionEntry({ + !(await persistCommandSession({ ...params, touchedFields: ["lastTtsReadLatestHash", "lastTtsReadLatestAt"], })) diff --git a/src/commands/doctor/shared/legacy-config-record-shared.ts b/src/commands/doctor/shared/legacy-config-record-shared.ts index a316b3a0896e..18e060d1edc8 100644 --- a/src/commands/doctor/shared/legacy-config-record-shared.ts +++ b/src/commands/doctor/shared/legacy-config-record-shared.ts @@ -11,17 +11,6 @@ export function cloneRecord(value: T | undefined): T { return { ...value } as T; } -/** Ensure a nested config value is a mutable record and return it. */ -export function ensureRecord(target: JsonRecord, key: string): JsonRecord { - const current = target[key]; - if (isRecord(current)) { - return current; - } - const next: JsonRecord = {}; - target[key] = next; - return next; -} - /** Own-property guard used by migrations that must preserve falsy values. */ export function hasOwnKey(target: JsonRecord, key: string): boolean { return Object.hasOwn(target, key); diff --git a/src/commands/doctor/shared/legacy-web-tools-migrate.ts b/src/commands/doctor/shared/legacy-web-tools-migrate.ts index 3af2aa71d923..f5812071e749 100644 --- a/src/commands/doctor/shared/legacy-web-tools-migrate.ts +++ b/src/commands/doctor/shared/legacy-web-tools-migrate.ts @@ -1,8 +1,7 @@ // Legacy web tool config migrations into plugin-owned provider config. -import { mergeMissing } from "../../../config/legacy.shared.js"; +import { ensureRecord, mergeMissing } from "../../../config/legacy.shared.js"; import { cloneRecord, - ensureRecord, hasOwnKey, isRecord, type JsonRecord, diff --git a/src/config/sessions/session-accessor.sqlite-delta.ts b/src/config/sessions/session-accessor.sqlite-delta.ts index 8aa3ca110a2e..d5f2b70547a4 100644 --- a/src/config/sessions/session-accessor.sqlite-delta.ts +++ b/src/config/sessions/session-accessor.sqlite-delta.ts @@ -11,7 +11,7 @@ import type { SessionTranscriptReadScope, TranscriptEvent, } from "./session-accessor.sqlite-contract.js"; -import { normalizeSqliteNumber } from "./session-accessor.sqlite-normalize.js"; +import { coerceSqliteNumber } from "./session-accessor.sqlite-normalize.js"; import { getSessionKysely, resolveSqliteTranscriptReadScope, @@ -183,7 +183,7 @@ function readRawDeltaInTransaction( .limit(1), ); const maxSeq = Math.min( - frontier ? normalizeSqliteNumber(frontier.seq) : -1, + frontier ? coerceSqliteNumber(frontier.seq) : -1, beforeEventSeq === undefined ? Number.POSITIVE_INFINITY : beforeEventSeq - 1, ); if (cursor.lastSeq > maxSeq) { @@ -210,8 +210,8 @@ function readRawDeltaInTransaction( .orderBy("seq", "asc") .limit(maxEvents + 1), ).rows.map((row) => ({ - seq: normalizeSqliteNumber(row.seq), - serializedBytes: normalizeSqliteNumber(row.serialized_bytes), + seq: coerceSqliteNumber(row.seq), + serializedBytes: coerceSqliteNumber(row.serialized_bytes), })); let serializedBytes = 0; @@ -239,7 +239,7 @@ function readRawDeltaInTransaction( .orderBy("seq", "asc"), ).rows.map((row) => ({ event: JSON.parse(row.event_json) as TranscriptEvent, - seq: normalizeSqliteNumber(row.seq), + seq: coerceSqliteNumber(row.seq), })); const nextCursor = encodeRawTranscriptCursor({ ...cursor, lastSeq }); const requiredBytes = diff --git a/src/config/sessions/session-accessor.sqlite-entry.ts b/src/config/sessions/session-accessor.sqlite-entry.ts index 4786fe7c4b66..26d476396356 100644 --- a/src/config/sessions/session-accessor.sqlite-entry.ts +++ b/src/config/sessions/session-accessor.sqlite-entry.ts @@ -51,7 +51,7 @@ import { } from "./session-accessor.sqlite-maintenance.js"; import { createFallbackSessionEntry, - normalizeSqliteNumber, + coerceSqliteNumber, } from "./session-accessor.sqlite-normalize.js"; import { cloneSessionEntry, @@ -297,7 +297,7 @@ export function countSqliteSessionEntryRowsReadOnly(scope: SessionEntryListScope .selectFrom("session_nodes") .select((expression) => expression.fn.countAll().as("count")), ); - return row ? normalizeSqliteNumber(row.count) : 0; + return row ? coerceSqliteNumber(row.count) : 0; }, toDatabaseOptions(resolved)); return result.found ? result.value : 0; } @@ -411,7 +411,7 @@ export function readSqliteSessionUpdatedAt(scope: SessionAccessScope): number | const resolved = resolveSqliteScope(scope); const database = openOpenClawAgentDatabase(toDatabaseOptions(resolved)); const row = readSessionEntryRow(database, resolved.sessionKey)?.row; - return row ? normalizeSqliteNumber(row.updated_at) : undefined; + return row ? coerceSqliteNumber(row.updated_at) : undefined; } /** Applies a partial entry update to the additive SQLite session store. */ diff --git a/src/config/sessions/session-accessor.sqlite-lifecycle-state.ts b/src/config/sessions/session-accessor.sqlite-lifecycle-state.ts index 26690e1004ee..2eec63bf03f8 100644 --- a/src/config/sessions/session-accessor.sqlite-lifecycle-state.ts +++ b/src/config/sessions/session-accessor.sqlite-lifecycle-state.ts @@ -31,7 +31,7 @@ import type { SqliteProjectedLifecycleMutation, SqliteSessionEntryRemovalPlan, } from "./session-accessor.sqlite-lifecycle-types.js"; -import { normalizeSqliteNumber } from "./session-accessor.sqlite-normalize.js"; +import { coerceSqliteNumber } from "./session-accessor.sqlite-normalize.js"; import { loadSqliteTranscriptEventsFromDatabase } from "./session-accessor.sqlite-read.js"; import { collectSqliteSessionStateIdsForEntry } from "./session-accessor.sqlite-references.js"; import { cloneSessionEntry, getSessionKysely } from "./session-accessor.sqlite-scope.js"; @@ -103,7 +103,7 @@ function readSessionTranscriptUpdatedAt( if (row?.updated_at === null || row?.updated_at === undefined) { return undefined; } - return normalizeSqliteNumber(row.updated_at); + return coerceSqliteNumber(row.updated_at); } function sqliteTranscriptStateIsReclaimable(params: { @@ -611,7 +611,7 @@ export function planSqliteSessionLifecycleArtifactCleanup( !sqliteTranscriptStateIsReclaimable({ database, // Admission updates the node even when a run has no event yet or reuses old events. - sessionUpdatedAt: normalizeSqliteNumber(row.updated_at), + sessionUpdatedAt: coerceSqliteNumber(row.updated_at), sessionId: row.current_session_id, nowMs: params.nowMs, orphanTranscriptMinAgeMs: params.orphanTranscriptMinAgeMs, diff --git a/src/config/sessions/session-accessor.sqlite-normalize.ts b/src/config/sessions/session-accessor.sqlite-normalize.ts index c6d49ca47009..e047edf62819 100644 --- a/src/config/sessions/session-accessor.sqlite-normalize.ts +++ b/src/config/sessions/session-accessor.sqlite-normalize.ts @@ -21,6 +21,6 @@ export function normalizeSqliteChatType(value: unknown): "direct" | "group" | "c return null; } -export function normalizeSqliteNumber(value: number | bigint): number { +export function coerceSqliteNumber(value: number | bigint): number { return typeof value === "bigint" ? Number(value) : value; } diff --git a/src/config/sessions/session-accessor.sqlite-read.ts b/src/config/sessions/session-accessor.sqlite-read.ts index 95c4583a3096..e7e0513d15a8 100644 --- a/src/config/sessions/session-accessor.sqlite-read.ts +++ b/src/config/sessions/session-accessor.sqlite-read.ts @@ -19,7 +19,7 @@ import type { SessionTranscriptStats, TranscriptEvent, } from "./session-accessor.sqlite-contract.js"; -import { normalizeSqliteNumber } from "./session-accessor.sqlite-normalize.js"; +import { coerceSqliteNumber } from "./session-accessor.sqlite-normalize.js"; import { getSessionKysely, resolveSqliteTranscriptReadScope, @@ -127,7 +127,7 @@ export function loadSqliteTranscriptEventRowsAfterSeqSync( } return executeSqliteQuerySync(database.db, query.orderBy("seq", "asc")).rows.map((row) => ({ event: JSON.parse(row.event_json) as TranscriptEvent, - seq: normalizeSqliteNumber(row.seq), + seq: coerceSqliteNumber(row.seq), })); } @@ -150,7 +150,7 @@ export function readSqliteTranscriptEventAtSeqSync( return row ? { event: JSON.parse(row.event_json) as TranscriptEvent, - seq: normalizeSqliteNumber(row.seq), + seq: coerceSqliteNumber(row.seq), } : undefined; } @@ -200,7 +200,7 @@ export function readSqliteTranscriptEventRows( ).rows; return rows.map((row) => ({ eventJson: row.event_json, - seq: normalizeSqliteNumber(row.seq), + seq: coerceSqliteNumber(row.seq), })); } @@ -219,9 +219,9 @@ export function readSqliteTranscriptStorageRows( .orderBy("seq", "asc"), ).rows; return rows.map((row) => ({ - createdAt: normalizeSqliteNumber(row.created_at), + createdAt: coerceSqliteNumber(row.created_at), eventJson: row.event_json, - seq: normalizeSqliteNumber(row.seq), + seq: coerceSqliteNumber(row.seq), })); } diff --git a/src/config/sessions/session-accessor.sqlite-transcript-state.ts b/src/config/sessions/session-accessor.sqlite-transcript-state.ts index 089c8fbfd9e5..b9e7aea90e62 100644 --- a/src/config/sessions/session-accessor.sqlite-transcript-state.ts +++ b/src/config/sessions/session-accessor.sqlite-transcript-state.ts @@ -6,7 +6,7 @@ import { } from "../../infra/kysely-sync.js"; import type { OpenClawAgentDatabase } from "../../state/openclaw-agent-db.js"; import { publishSqliteSessionEntryCacheInvalidation } from "./session-accessor.sqlite-entry-cache.js"; -import { normalizeSqliteNumber } from "./session-accessor.sqlite-normalize.js"; +import { coerceSqliteNumber } from "./session-accessor.sqlite-normalize.js"; import { getSessionKysely, type ResolvedTranscriptScope } from "./session-accessor.sqlite-scope.js"; import { parseSqliteSessionEntryJson } from "./session-accessor.sqlite-status.js"; import { @@ -203,7 +203,7 @@ export function readNextTranscriptSeq(database: OpenClawAgentDatabase, sessionId .where("session_id", "=", sessionId), ); const maxSeq = - row?.max_seq === null || row?.max_seq === undefined ? -1 : normalizeSqliteNumber(row.max_seq); + row?.max_seq === null || row?.max_seq === undefined ? -1 : coerceSqliteNumber(row.max_seq); return maxSeq + 1; }