openclaw/src/plugin-sdk/config-runtime.ts
Thomas M 0a01386756
fix: canonicalize session keys at write time (#30654) (thanks @thomasxm)
* fix: canonicalize session keys at write time to prevent orphaned sessions (#29683)

resolveSessionKey() uses hardcoded DEFAULT_AGENT_ID="main", but all read
paths canonicalize via cfg. When the configured default agent differs
(e.g. "ops" with mainKey "work"), writes produce "agent:main:main" while
reads look up "agent:ops:work", orphaning transcripts on every restart.

Fix all three write-path call sites by wrapping with
canonicalizeMainSessionAlias:
- initSessionState (auto-reply/reply/session.ts)
- runWebHeartbeatOnce (web/auto-reply/heartbeat-runner.ts)
- resolveCronAgentSessionKey (cron/isolated-agent/session-key.ts)

Add startup migration (migrateOrphanedSessionKeys) to rename existing
orphaned keys to canonical form, merging by most-recent updatedAt.

* fix: address review — track agent IDs in migration map, align snapshot key

P1: migrateOrphanedSessionKeys now tracks agentId alongside each store
path in a Map instead of inferring from the filesystem path. This
correctly handles custom session.store templates outside the default
agents/<id>/ layout.

P2: Pass the already-canonicalized sessionKey to getSessionSnapshot so
the heartbeat snapshot reads/restores use the same key as the write path.

* fix: log migration results at all early return points

migrateOrphanedSessionKeys runs before detectLegacyStateMigrations, so
it can canonicalize legacy keys (e.g. "main" → "agent:main:main") before
the legacy detector sees them. This caused the early return path to skip
logging, breaking doctor-state-migrations tests that assert log.info was
called.

Extract logMigrationResults helper and call it at every return point.

* fix: handle shared stores and ~ expansion in migration

P1: When session.store has no {agentId}, all agents resolve to the same
file. Track all agentIds per store path (Map<path, Set<id>>) and run
canonicalization once per agent. Skip cross-agent "agent:main:*"
remapping when "main" is a legitimate configured agent sharing the store,
to avoid merging its data into another agent's namespace.

P2: Use expandHomePrefix (environment-aware ~ resolution) instead of
os.homedir() in resolveStorePathFromTemplate, matching the runtime
resolveStorePath behavior for OPENCLAW_HOME/HOME overrides.

* fix: narrow cross-agent remap to provable orphan aliases only

Only remap agent:main:* keys where the suffix is a main session alias
("main" or the configured mainKey). Other agent:main:* keys — hooks,
subagents, cron sessions, per-sender keys — may be intentional
cross-agent references and must not be silently moved into another
agent's namespace.

* fix: run orphan-key session migration at gateway startup (#29683)

* fix: canonicalize cross-agent legacy main aliases in session keys (#29683)

* fix: guard shared-store migration against cross-agent legacy alias remap (#29683)

* refactor: split session-key migration out of pr 30654

---------

Co-authored-by: Your Name <your_email@example.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-03-29 18:59:25 +05:30

113 lines
3.6 KiB
TypeScript

// Shared config/runtime boundary for plugins that need config loading,
// config writes, or session-store helpers without importing src internals.
export { resolveDefaultAgentId } from "../agents/agent-scope.js";
export {
getRuntimeConfigSnapshot,
loadConfig,
readConfigFileSnapshotForWrite,
writeConfigFile,
} from "../config/io.js";
export { logConfigUpdated } from "../config/logging.js";
export { updateConfig } from "../commands/models/shared.js";
export { resolveChannelModelOverride } from "../channels/model-overrides.js";
export { resolveMarkdownTableMode } from "../config/markdown-tables.js";
export {
resolveChannelGroupPolicy,
resolveChannelGroupRequireMention,
type ChannelGroupPolicy,
} from "../config/group-policy.js";
export {
GROUP_POLICY_BLOCKED_LABEL,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
resolveOpenProviderRuntimeGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "../config/runtime-group-policy.js";
export {
isNativeCommandsExplicitlyDisabled,
resolveNativeCommandsEnabled,
resolveNativeSkillsEnabled,
} from "../config/commands.js";
export {
TELEGRAM_COMMAND_NAME_PATTERN,
normalizeTelegramCommandName,
resolveTelegramCustomCommands,
} from "../config/telegram-custom-commands.js";
export {
mapStreamingModeToSlackLegacyDraftStreamMode,
resolveDiscordPreviewStreamMode,
resolveSlackNativeStreaming,
resolveSlackStreamingMode,
resolveTelegramPreviewStreamMode,
type SlackLegacyDraftStreamMode,
type StreamingMode,
} from "../config/discord-preview-streaming.js";
export { resolveActiveTalkProviderConfig } from "../config/talk.js";
export { resolveAgentMaxConcurrent } from "../config/agent-limits.js";
export { loadCronStore, resolveCronStorePath, saveCronStore } from "../cron/store.js";
export { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js";
export { coerceSecretRef } from "../config/types.secrets.js";
export {
resolveConfiguredSecretInputString,
resolveConfiguredSecretInputWithFallback,
resolveRequiredConfiguredSecretRefInputString,
} from "../gateway/resolve-configured-secret-input-string.js";
export type {
DiscordAccountConfig,
DiscordActionConfig,
DiscordAutoPresenceConfig,
DiscordExecApprovalConfig,
DiscordGuildChannelConfig,
DiscordGuildEntry,
DiscordIntentsConfig,
DiscordSlashCommandConfig,
DmPolicy,
GroupPolicy,
MarkdownTableMode,
OpenClawConfig,
ReplyToMode,
SignalReactionNotificationMode,
SlackAccountConfig,
SlackChannelConfig,
SlackReactionNotificationMode,
SlackSlashCommandConfig,
TelegramAccountConfig,
TelegramActionConfig,
TelegramDirectConfig,
TelegramExecApprovalConfig,
TelegramGroupConfig,
TelegramInlineButtonsScope,
TelegramNetworkConfig,
TelegramTopicConfig,
TtsAutoMode,
TtsConfig,
TtsMode,
TtsModelOverrideConfig,
TtsProvider,
} from "../config/types.js";
export {
loadSessionStore,
readSessionUpdatedAt,
recordSessionMetaFromInbound,
resolveSessionKey,
resolveStorePath,
updateLastRoute,
updateSessionStore,
type SessionResetMode,
type SessionScope,
} from "../config/sessions.js";
export { resolveGroupSessionKey } from "../config/sessions/group.js";
export { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js";
export {
evaluateSessionFreshness,
resolveChannelResetConfig,
resolveSessionResetPolicy,
resolveSessionResetType,
resolveThreadFlag,
} from "../config/sessions/reset.js";
export { resolveSessionStoreEntry } from "../config/sessions/store.js";
export {
isDangerousNameMatchingEnabled,
resolveDangerousNameMatchingEnabled,
} from "../config/dangerous-name-matching.js";