refactor: delete dead infra and config exports (#106019)

* refactor: delete dead infra and config exports

* refactor: preserve live infra and config contracts

* refactor(config): remove obsolete file-store lifecycle APIs

* refactor(infra): finish current-main dead export cleanup
This commit is contained in:
Peter Steinberger
2026-07-13 12:00:47 -07:00
committed by GitHub
parent 8dd57279cd
commit d1684f48a3
444 changed files with 3159 additions and 14812 deletions
+1 -1
View File
@@ -1961,7 +1961,7 @@
"test:unit:fast:audit": "node scripts/test-unit-fast-audit.mjs",
"test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs",
"test:watch": "node scripts/test-projects.mjs --watch",
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/process/exec.windows.test.ts src/process/windows-command.test.ts src/infra/backup-create.windows.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
"test:windows:ci": "node scripts/test-projects.mjs src/shared/runtime-import.test.ts src/process/exec.windows.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
"ts-topology": "node --import tsx scripts/ts-topology.ts",
-547
View File
@@ -1311,214 +1311,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
"src/commitments/store.ts: saveCommitmentStore",
"src/compat/legacy-names.ts: MACOS_APP_SOURCES_DIR",
"src/compat/legacy-names.ts: PROJECT_NAME",
"src/config/backup-rotation.ts: cleanOrphanBackups",
"src/config/backup-rotation.ts: hardenBackupPermissions",
"src/config/backup-rotation.ts: rotateConfigBackups",
"src/config/channel-config-metadata.ts: ChannelDmPolicyMetadata",
"src/config/channel-config-metadata.ts: ChannelSchemaMetadataWithOwnership",
"src/config/commands.ts: isCommandFlagEnabled",
"src/config/commands.ts: isRestartEnabled",
"src/config/cron-limits.ts: DEFAULT_CRON_TRIGGER_MIN_INTERVAL_MS",
"src/config/env-vars.ts: collectDurableServiceEnvVars",
"src/config/env-vars.ts: readStateDirDotEnvVars",
"src/config/includes.ts: deepMerge",
"src/config/includes.ts: MAX_INCLUDE_FILE_BYTES",
"src/config/includes.ts: MAX_INCLUDE_PATH_LENGTH",
"src/config/io.audit.ts: redactConfigAuditArgv",
"src/config/io.health-state.ts: ConfigHealthStateDeps",
"src/config/io.invalid-config.ts: formatInvalidConfigLogMessage",
"src/config/io.invalid-config.ts: logInvalidConfigOnce",
"src/config/io.observe-recovery.ts: ObserveRecoveryDeps",
"src/config/io.observe-recovery.ts: resolveLastKnownGoodConfigPath",
"src/config/io.observe-suspicious.ts: ConfigObserveSuspiciousBaseline",
"src/config/io.owner-display-secret.ts: OwnerDisplaySecretRuntimeState",
"src/config/io.write-prepare.ts: unsetPathForWrite",
"src/config/issue-format.ts: normalizeConfigIssue",
"src/config/issue-format.ts: normalizeConfigIssuePath",
"src/config/markdown-tables.ts: ResolveMarkdownTableMode",
"src/config/markdown-tables.ts: ResolveMarkdownTableModeParams",
"src/config/markdown-tables.types.ts: ResolveMarkdownTableMode",
"src/config/media-audio-field-metadata.ts: MEDIA_AUDIO_FIELD_KEYS",
"src/config/mutate.ts: ConfigMutationBase",
"src/config/patch-replace-paths.ts: normalizeConfigPatchReplacePath",
"src/config/plugin-auto-enable.shared.ts: PluginAutoEnableCandidate",
"src/config/plugin-auto-enable.shared.ts: PluginAutoEnableResult",
"src/config/runtime-group-policy.ts: resetMissingProviderGroupPolicyFallbackWarningsForTesting",
"src/config/runtime-group-policy.ts: resolveRuntimeGroupPolicy",
"src/config/schema-base.ts: BaseConfigSchemaResponse",
"src/config/schema.hints.ts: __test__",
"src/config/schema.hints.ts: isPluginOwnedChannelHintPath",
"src/config/schema.hints.ts: isSensitiveConfigPath",
"src/config/schema.tags.ts: CONFIG_TAGS",
"src/config/schema.tags.ts: ConfigTag",
"src/config/schema.tags.ts: deriveTagsForPath",
"src/config/schema.ts: ConfigSchema",
"src/config/schema.ts: ConfigSchemaLookupChild",
"src/config/schema.ts: ConfigSchemaLookupResult",
"src/config/schema.ts: ConfigSchemaReloadKind",
"src/config/schema.ts: ConfigSchemaReloadMetadata",
"src/config/schema.ts: ConfigSchemaReloadMetadataResolver",
"src/config/schema.ts: ConfigUiHint",
"src/config/schema.ts: ConfigUiHints",
"src/config/sessions.ts: CanonicalizeSessionEntryAliasesResult",
"src/config/sessions.ts: DeleteSessionEntryLifecycleParams",
"src/config/sessions.ts: DeleteSessionEntryLifecycleResult",
"src/config/sessions.ts: ResetSessionEntryLifecycleParams",
"src/config/sessions.ts: ResetSessionEntryLifecycleResult",
"src/config/sessions.ts: ResolvedSessionEntryCandidateTarget",
"src/config/sessions.ts: SessionEntryCandidateAccessScope",
"src/config/sessions.ts: SessionLifecycleArchivedTranscript",
"src/config/sessions.ts: SessionLifecycleStoreTarget",
"src/config/sessions/artifacts.ts: isTrajectoryPointerArtifactName",
"src/config/sessions/artifacts.ts: isTrajectoryRuntimeArtifactName",
"src/config/sessions/artifacts.ts: parseCompactionCheckpointTranscriptFileName",
"src/config/sessions/cleanup-service.ts: SessionCleanupAction",
"src/config/sessions/cleanup-service.ts: SessionsCleanupRunResult",
"src/config/sessions/disk-budget.ts: SessionDiskBudgetConfig",
"src/config/sessions/disk-budget.ts: SessionDiskBudgetLogger",
"src/config/sessions/entry-freshness.ts: ResolvedSessionEntryResetFreshness",
"src/config/sessions/entry-freshness.ts: ResolveSessionEntryResetFreshnessParams",
"src/config/sessions/entry-freshness.ts: SessionEntryLifecycleTimestamps",
"src/config/sessions/goals.ts: SessionGoalSnapshot",
"src/config/sessions/lifecycle.ts: SESSION_WORK_START_INVALIDATED_ERROR_CODE",
"src/config/sessions/paths.ts: SAFE_SESSION_ID_RE",
"src/config/sessions/plugin-host-cleanup.ts: PluginHostSessionCleanupMode",
"src/config/sessions/reset-policy.ts: DEFAULT_RESET_AT_HOUR",
"src/config/sessions/reset-policy.ts: DEFAULT_RESET_MODE",
"src/config/sessions/reset-policy.ts: resolveDailyResetAtMs",
"src/config/sessions/reset.ts: DEFAULT_RESET_AT_HOUR",
"src/config/sessions/reset.ts: DEFAULT_RESET_MODE",
"src/config/sessions/reset.ts: isThreadSessionKey",
"src/config/sessions/reset.ts: resolveDailyResetAtMs",
"src/config/sessions/runtime-types.ts: ReadSessionUpdatedAt",
"src/config/sessions/runtime-types.ts: RecordSessionMetaFromInbound",
"src/config/sessions/runtime-types.ts: UpdateLastRoute",
"src/config/sessions/session-accessor.sqlite.ts: appendSqliteTranscriptEvents",
"src/config/sessions/session-accessor.sqlite.ts: deleteSqliteTranscript",
"src/config/sessions/session-accessor.sqlite.ts: sqliteTranscriptExists",
"src/config/sessions/session-registry-maintenance.ts: SessionRegistryMaintenanceStoreOptions",
"src/config/sessions/session-registry-maintenance.ts: SessionRegistryMaintenanceStoreSummary",
"src/config/sessions/session-transcript-index.ts: extractTranscriptIndexEntry",
"src/config/sessions/session-transcript-search.ts: resetSessionTranscriptSearchForTest",
"src/config/sessions/session-transcript-search.ts: waitForSessionTranscriptReconcileForTest",
"src/config/sessions/skill-prompt-blobs.ts: getSessionSkillPromptRefCacheStatsForTest",
"src/config/sessions/skill-prompt-blobs.ts: getValidSessionSkillPromptBlobCacheStatsForTest",
"src/config/sessions/store-cache.ts: DeepReadonly",
"src/config/sessions/store-cache.ts: getSerializedSessionStoreCacheStatsForTest",
"src/config/sessions/store-cache.ts: getSessionStoreSnapshotCacheStatsForTest",
"src/config/sessions/store-cache.ts: getSessionStoreStringInternStatsForTest",
"src/config/sessions/store-load.ts: LoadSessionStoreOptions",
"src/config/sessions/store-load.ts: ReadSessionEntryOptions",
"src/config/sessions/store-load.ts: readSessionStoreSnapshot",
"src/config/sessions/store-maintenance-operations.ts: FileBackedSessionStoreMaintenanceParams",
"src/config/sessions/store-maintenance-operations.ts: FileBackedSessionStoreMaintenanceResult",
"src/config/sessions/store-maintenance-preserve.ts: SessionMaintenancePreserveKeysProvider",
"src/config/sessions/store-maintenance.ts: isGatewayModelRunSessionKey",
"src/config/sessions/store-maintenance.ts: isProtectedSessionMaintenanceEntry",
"src/config/sessions/store-maintenance.ts: QuotaSuspensionEntryMaintenanceResult",
"src/config/sessions/store-maintenance.ts: resolveSessionEntryMaintenanceHighWater",
"src/config/sessions/store-writer-state.ts: getSessionStoreWriterQueueSizeForTest",
"src/config/sessions/store-writer-state.ts: SessionStoreWriterQueue",
"src/config/sessions/store-writer.ts: RunExclusiveSessionStoreWriteOptions",
"src/config/sessions/store.ts: applySessionEntryLifecycleMutation",
"src/config/sessions/store.ts: archiveRemovedSessionTranscripts",
"src/config/sessions/store.ts: capEntryCount",
"src/config/sessions/store.ts: cleanupSessionLifecycleArtifacts",
"src/config/sessions/store.ts: drainSessionStoreWriterQueuesForTest",
"src/config/sessions/store.ts: getActiveSessionMaintenanceWarning",
"src/config/sessions/store.ts: getSessionStoreWriterQueueSizeForTest",
"src/config/sessions/store.ts: listSessionEntries",
"src/config/sessions/store.ts: normalizeStoreSessionKey",
"src/config/sessions/store.ts: patchSessionEntry",
"src/config/sessions/store.ts: pruneStaleModelRunEntries",
"src/config/sessions/store.ts: purgeDeletedAgentSessionEntries",
"src/config/sessions/store.ts: readSessionEntries",
"src/config/sessions/store.ts: readSessionStoreSnapshot",
"src/config/sessions/store.ts: recordSessionMetaFromInbound",
"src/config/sessions/store.ts: ResolvedSessionMaintenanceConfig",
"src/config/sessions/store.ts: resolveMaintenanceConfig",
"src/config/sessions/store.ts: rollbackAgentHarnessSessionEntryLifecycle",
"src/config/sessions/store.ts: rollbackPluginOwnedSessionEntryLifecycle",
"src/config/sessions/store.ts: SessionMaintenanceApplyReport",
"src/config/sessions/store.ts: SessionMaintenanceWarning",
"src/config/sessions/store.ts: SessionStoreSnapshot",
"src/config/sessions/store.ts: SessionStoreSnapshotEntries",
"src/config/sessions/store.ts: SessionStoreSnapshotEntry",
"src/config/sessions/store.ts: updateLastRoute",
"src/config/sessions/store.ts: upsertSessionEntry",
"src/config/sessions/transcript-jsonl.ts: appendJsonlEntry",
"src/config/sessions/transcript-jsonl.ts: appendSerializedJsonlEntry",
"src/config/sessions/transcript-jsonl.ts: writeJsonlEntry",
"src/config/sessions/transcript-replay.ts: DEFAULT_REPLAY_MAX_MESSAGES",
"src/config/sessions/transcript-stream.ts: TranscriptReverseStreamOptions",
"src/config/sessions/transcript-stream.ts: TranscriptStreamOptions",
"src/config/sessions/transcript-tree.ts: SessionTranscriptTreeEntry",
"src/config/sessions/transcript-tree.ts: SessionTranscriptTreeNode",
"src/config/sessions/transcript-write-context.ts: resolveOwnedSessionTranscriptWriteLockRunner",
"src/config/sessions/transcript-write-context.ts: runWithOwnedSessionTranscriptWritePublication",
"src/config/sessions/transcript.ts: ReadRecentSessionConversationTextOptions",
"src/config/sessions/transcript.ts: ReadRecentSessionConversationTextParams",
"src/config/sessions/transcript.ts: readRecentUserAssistantTextFromSessionTranscript",
"src/config/sessions/transcript.ts: TailAssistantTranscriptText",
"src/config/sessions/types.ts: LaneExecutionState",
"src/config/sessions/types.ts: mergeSessionEntryWithPolicy",
"src/config/sessions/types.ts: SessionAcpIdentitySource",
"src/config/sessions/types.ts: SessionAcpIdentityState",
"src/config/sessions/types.ts: SessionChannelId",
"src/config/sessions/types.ts: SessionCompactionTranscriptReference",
"src/config/sessions/types.ts: SessionContextBudgetStatusRoute",
"src/config/sessions/types.ts: SessionEntryMergePolicy",
"src/config/sessions/types.ts: SessionPluginDebugEntry",
"src/config/sessions/types.ts: SessionPluginJsonValue",
"src/config/sessions/types.ts: SessionPluginNextTurnInjection",
"src/config/sessions/types.ts: SubagentRecoveryState",
"src/config/state-dir-dotenv.ts: DurableServiceEnvVarSources",
"src/config/state-dir-dotenv.ts: readStateDirDotEnvVars",
"src/config/validation.ts: __testing",
"src/config/validation.ts: testing",
"src/config/version.ts: isSameOpenClawStableFamily",
"src/config/version.ts: parseOpenClawVersion",
"src/config/zod-schema.agent-runtime.ts: AgentRuntimePolicySchema",
"src/config/zod-schema.agents.ts: BroadcastStrategySchema",
"src/config/zod-schema.core.ts: _ModelCompatSchemaAssignableToType",
"src/config/zod-schema.core.ts: _ModelCompatTypeAssignableToSchema",
"src/config/zod-schema.core.ts: _ToolsMediaAsyncCompletionSchemaAssignableToType",
"src/config/zod-schema.core.ts: _ToolsMediaAsyncCompletionTypeAssignableToSchema",
"src/config/zod-schema.core.ts: MarkdownTableModeSchema",
"src/config/zod-schema.core.ts: MentionPatternsModeSchema",
"src/config/zod-schema.core.ts: normalizeAllowFrom",
"src/config/zod-schema.providers-core.ts: DiscordAccountSchema",
"src/config/zod-schema.providers-core.ts: DiscordDmSchema",
"src/config/zod-schema.providers-core.ts: DiscordGuildChannelSchema",
"src/config/zod-schema.providers-core.ts: DiscordGuildSchema",
"src/config/zod-schema.providers-core.ts: DiscordThreadSchema",
"src/config/zod-schema.providers-core.ts: IMessageAccountSchemaBase",
"src/config/zod-schema.providers-core.ts: IrcAccountSchema",
"src/config/zod-schema.providers-core.ts: IrcAccountSchemaBase",
"src/config/zod-schema.providers-core.ts: IrcConfigSchema",
"src/config/zod-schema.providers-core.ts: IrcGroupSchema",
"src/config/zod-schema.providers-core.ts: IrcNickServSchema",
"src/config/zod-schema.providers-core.ts: MSTeamsChannelSchema",
"src/config/zod-schema.providers-core.ts: MSTeamsTeamSchema",
"src/config/zod-schema.providers-core.ts: SignalAccountSchemaBase",
"src/config/zod-schema.providers-core.ts: SlackAccountSchema",
"src/config/zod-schema.providers-core.ts: SlackChannelSchema",
"src/config/zod-schema.providers-core.ts: SlackDmSchema",
"src/config/zod-schema.providers-core.ts: SlackRelaySchema",
"src/config/zod-schema.providers-core.ts: SlackSocketModeSchema",
"src/config/zod-schema.providers-core.ts: SlackThreadSchema",
"src/config/zod-schema.providers-core.ts: TelegramAccountSchema",
"src/config/zod-schema.providers-core.ts: TelegramAccountSchemaBase",
"src/config/zod-schema.providers-core.ts: TelegramDirectSchema",
"src/config/zod-schema.providers-core.ts: TelegramGroupSchema",
"src/config/zod-schema.providers-core.ts: TelegramTopicSchema",
"src/config/zod-schema.providers-googlechat.ts: GoogleChatAccountSchema",
"src/config/zod-schema.providers-googlechat.ts: GoogleChatDmSchema",
"src/config/zod-schema.providers-googlechat.ts: GoogleChatGroupSchema",
"src/config/zod-schema.providers-whatsapp.ts: WhatsAppAccountSchema",
"src/config/zod-schema.proxy.ts: ProxyLoopbackModeSchema",
"src/config/zod-schema.ts: installZodDefaultLocale",
"src/config/zod-schema.ts: McpServerSchema",
"src/context-engine/registry.ts: clearContextEngineRuntimeQuarantine",
"src/context-engine/registry.ts: ContextEngineRegistrationResult",
"src/context-engine/registry.ts: getContextEngineFactory",
@@ -1606,345 +1398,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
"src/image-generation/runtime-types.ts: ListRuntimeImageGenerationProvidersParams",
"src/image-generation/runtime-types.ts: RuntimeImageGenerationProvider",
"src/image-generation/runtime.ts: ImageGenerationRuntimeDeps",
"src/infra/active-node-context.ts: ActiveNodeContext",
"src/infra/active-node-context.ts: resetActiveNodeContextForTests",
"src/infra/agent-events.ts: AgentApprovalEventKind",
"src/infra/agent-events.ts: AgentApprovalEventPhase",
"src/infra/agent-events.ts: AgentApprovalEventStatus",
"src/infra/agent-events.ts: AgentItemEventKind",
"src/infra/agent-events.ts: AgentItemEventPhase",
"src/infra/agent-events.ts: AgentItemEventStatus",
"src/infra/agent-events.ts: AgentRunContext",
"src/infra/agent-events.ts: resetAgentRunContextForTest",
"src/infra/approval-native-route-coordinator.ts: clearApprovalNativeRouteStateForTest",
"src/infra/approval-resolution-ref.ts: APPROVAL_RESOLUTION_REF_LENGTH",
"src/infra/approval-view-model.ts: isTypedApprovalActionView",
"src/infra/archive.ts: ArchiveSecurityError",
"src/infra/backup-create.ts: __test",
"src/infra/backup-create.ts: buildExtensionsNodeModulesFilter",
"src/infra/backup-create.ts: testApi",
"src/infra/clawhub-install-trust.ts: ClawHubTrustAcceptedResult",
"src/infra/clawhub-install-trust.ts: ClawHubTrustFailure",
"src/infra/clawhub-install-trust.ts: ClawHubTrustInstallRecordFields",
"src/infra/clawhub.ts: ClawHubArtifactModerationState",
"src/infra/clawhub.ts: ClawHubArtifactScanState",
"src/infra/clawhub.ts: ClawHubPackageEnvironmentSummary",
"src/infra/clawhub.ts: ClawHubPackageHostTarget",
"src/infra/clawhub.ts: ClawHubPackageListItem",
"src/infra/clawhub.ts: ClawHubPromotionModel",
"src/infra/clawhub.ts: ClawHubPromotionsFeed",
"src/infra/clawhub.ts: ClawHubPromotionsFeedFetchResult",
"src/infra/clawhub.ts: ClawHubSkillSecurityVerdictRequestItem",
"src/infra/clawhub.ts: ClawHubSkillSecurityVerdictsResponse",
"src/infra/clawhub.ts: ClawHubSkillVerificationDecision",
"src/infra/clawhub.ts: parseClawHubPromotion",
"src/infra/clawhub.ts: resolveClawHubAuthToken",
"src/infra/command-analysis/explain.ts: explainCommandForDisplay",
"src/infra/command-analysis/explain.ts: summarizeCommandExplanation",
"src/infra/command-analysis/explain.ts: summarizeCommandSegmentsForDisplay",
"src/infra/command-analysis/policy.ts: CommandPolicyAnalysis",
"src/infra/command-analysis/risks.ts: CarriedShellBuiltinHit",
"src/infra/command-analysis/risks.ts: COMMAND_CARRIER_EXECUTABLES",
"src/infra/command-analysis/risks.ts: CommandCarrierHit",
"src/infra/command-analysis/risks.ts: detectEnvSplitStringFlag",
"src/infra/command-analysis/risks.ts: resolveCarrierCommandArgv",
"src/infra/command-carriers.ts: ParsedEnvInvocationPrelude",
"src/infra/command-explainer/tree-sitter-runtime.ts: getBashParserForCommandExplanation",
"src/infra/command-explainer/tree-sitter-runtime.ts: resolvePackageFileForCommandExplanation",
"src/infra/command-explainer/tree-sitter-runtime.ts: setBashParserLoaderForCommandExplanationForTest",
"src/infra/control-ui-assets.ts: resolveControlUiDistIndexPath",
"src/infra/control-ui-assets.ts: resolveControlUiRepoRoot",
"src/infra/delivery-queue-sqlite.ts: DeliveryQueueEntryState",
"src/infra/delivery-queue-sqlite.ts: FailedDeliveryQueueCount",
"src/infra/delivery-queue-sqlite.ts: FailPendingDeliveryQueueEntryResult",
"src/infra/delivery-recovery.shared.ts: RECOVERY_REPLAY_SPACING_MS",
"src/infra/dev-install-branch.ts: detectDevInstallGitBranch",
"src/infra/device-bootstrap.ts: DEVICE_BOOTSTRAP_TOKEN_TTL_MS",
"src/infra/device-bootstrap.ts: DeviceBootstrapTokenRecord",
"src/infra/device-pairing-migration.ts: LegacyDevicePairingMigrationResult",
"src/infra/device-pairing-store.ts: DevicePairingStoreState",
"src/infra/device-pairing-store.ts: DevicePairingStoreTarget",
"src/infra/device-pairing-store.ts: resolveDevicePairingStateDbOptions",
"src/infra/device-pairing.ts: ApproveDevicePairingResult",
"src/infra/device-pairing.ts: DevicePairingAccessMetadata",
"src/infra/device-pairing.ts: DevicePairingForbiddenReason",
"src/infra/device-pairing.ts: DevicePairingForbiddenResult",
"src/infra/device-pairing.ts: DevicePairingList",
"src/infra/device-pairing.ts: DevicePairingPendingRecord",
"src/infra/device-pairing.ts: DevicePairingSupersededRequest",
"src/infra/device-pairing.ts: PairedDeviceApprovalKind",
"src/infra/device-pairing.ts: PairedDeviceMetadataPatch",
"src/infra/device-pairing.ts: PairedDeviceNodeSurface",
"src/infra/device-pairing.ts: RequestDevicePairingResult",
"src/infra/device-pairing.ts: RevokeDeviceTokenResult",
"src/infra/device-pairing.ts: RotateDeviceTokenResult",
"src/infra/diagnostic-trace-context.ts: resetDiagnosticTraceContextForTest",
"src/infra/dispatch-wrapper-resolution.ts: unwrapEnvInvocation",
"src/infra/ed25519-signature.ts: base64UrlEncode",
"src/infra/event-session-routing.ts: parseDirectAgentSessionTarget",
"src/infra/exec-approval-forwarder.ts: buildExecApprovalRequestMessage",
"src/infra/exec-approval-policy-snapshot.ts: ExecApprovalPolicyRule",
"src/infra/exec-approvals-effective.ts: ExecPolicyHostDefaults",
"src/infra/exec-approvals.ts: DurableExecApprovalRequirement",
"src/infra/exec-approvals.ts: persistAllowAlwaysDecisionLocked",
"src/infra/exec-approvals.ts: recordAllowlistMatchesUseLocked",
"src/infra/exec-authorization-plan.ts: ExecAuthorizationDialect",
"src/infra/exec-authorization-plan.ts: ExecAuthorizationGroup",
"src/infra/exec-authorization-plan.ts: ExecAuthorizationTransport",
"src/infra/exec-authorization-plan.ts: ExecAuthorizationTrustMode",
"src/infra/exec-authorization-render.ts: AuthorizedShellRenderMode",
"src/infra/exec-authorization-render.ts: AuthorizedShellRenderResult",
"src/infra/exec-auto-review.ts: ExecAutoReviewHost",
"src/infra/exec-control-command-guard.ts: UnsafeExecControlShellCommandKind",
"src/infra/exec-policy.ts: ExecPolicyLayer",
"src/infra/exec-safe-bin-policy-profiles.ts: renderDefaultSafeBinsDocText",
"src/infra/exec-safe-bin-policy-profiles.ts: renderSafeBinDeniedFlagsDocBullets",
"src/infra/exec-safe-bin-policy-profiles.ts: SAFE_BIN_PROFILE_FIXTURES",
"src/infra/exec-safe-bin-policy.ts: buildLongFlagPrefixMap",
"src/infra/exec-safe-bin-policy.ts: collectKnownLongFlags",
"src/infra/exec-safe-bin-policy.ts: renderDefaultSafeBinsDocText",
"src/infra/exec-safe-bin-policy.ts: renderSafeBinDeniedFlagsDocBullets",
"src/infra/exec-safe-bin-policy.ts: SAFE_BIN_PROFILE_FIXTURES",
"src/infra/exec-wrapper-resolution.ts: basenameLower",
"src/infra/exec-wrapper-resolution.ts: extractShellWrapperInlineCommand",
"src/infra/exec-wrapper-resolution.ts: isDispatchWrapperExecutable",
"src/infra/exec-wrapper-resolution.ts: resolveDispatchWrapperTrustPlan",
"src/infra/exec-wrapper-resolution.ts: unwrapEnvInvocation",
"src/infra/exec-wrapper-tokens.ts: basenameLower",
"src/infra/executable-path.ts: isExecutableFile",
"src/infra/file-store.ts: FileStore",
"src/infra/fs-safe-advanced.ts: assertNoHardlinkedFinalPath",
"src/infra/fs-safe-advanced.ts: FileIdentityStat",
"src/infra/fs-safe-advanced.ts: writeSiblingTempFile",
"src/infra/gateway-active-work.ts: GatewayActiveWorkCounts",
"src/infra/gateway-boot-lifecycle.ts: GATEWAY_BOOT_LIFECYCLE_RETENTION_MS",
"src/infra/gateway-boot-lifecycle.ts: GATEWAY_BOOT_LOOP_UNCLEAN_THRESHOLD",
"src/infra/gateway-boot-lifecycle.ts: GATEWAY_BOOT_LOOP_WINDOW_MS",
"src/infra/gateway-boot-lifecycle.ts: GatewayBootLifecycleOutcome",
"src/infra/gateway-suspend-coordinator.ts: GATEWAY_SUSPEND_RETRY_AFTER_MS",
"src/infra/gateway-suspend-coordinator.ts: GATEWAY_SUSPEND_TTL_MS",
"src/infra/gateway-suspend-coordinator.ts: GatewaySuspendPrepareResult",
"src/infra/gateway-suspend-coordinator.ts: GatewaySuspendResumeResult",
"src/infra/gateway-suspend-coordinator.ts: GatewaySuspendStatusResult",
"src/infra/gateway-suspend-coordinator.ts: resetGatewaySuspendCoordinatorForTest",
"src/infra/git-commit.ts: CommitMetadataReaders",
"src/infra/git-commit.ts: testing",
"src/infra/heartbeat-cooldown.ts: DEFAULT_FLOOD_THRESHOLD",
"src/infra/heartbeat-cooldown.ts: DEFAULT_MIN_WAKE_SPACING_MS",
"src/infra/heartbeat-cooldown.ts: ShouldDeferInput",
"src/infra/heartbeat-wake.ts: hasHeartbeatWakeHandler",
"src/infra/heartbeat-wake.ts: hasPendingHeartbeatWake",
"src/infra/heartbeat-wake.ts: HeartbeatWakeOverride",
"src/infra/heartbeat-wake.ts: resetHeartbeatWakeStateForTests",
"src/infra/heartbeat-wake.ts: RetryableHeartbeatBusySkipReason",
"src/infra/inline-option-token.ts: InlineOptionToken",
"src/infra/install-source-utils.ts: NpmResolutionFields",
"src/infra/jsonl-socket.ts: testApi",
"src/infra/net/http-connect-tunnel.ts: HttpConnectTunnelParams",
"src/infra/net/proxy/active-proxy-state.ts: resetActiveManagedProxyStateForTests",
"src/infra/net/proxy/proxy-validation.ts: DEFAULT_PROXY_VALIDATION_ALLOWED_URLS",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationApnsCheck",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationApnsCheckParams",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationApnsCheckResult",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationCheck",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationCheckKind",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationConfigSource",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationFetchCheck",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationFetchCheckParams",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationFetchCheckResult",
"src/infra/net/proxy/proxy-validation.ts: ProxyValidationResolvedConfig",
"src/infra/net/proxy/proxy-validation.ts: resolveProxyValidationConfig",
"src/infra/net/proxy/proxy-validation.ts: ResolveProxyValidationConfigOptions",
"src/infra/net/proxy/proxy-validation.ts: RunProxyValidationOptions",
"src/infra/net/undici-runtime.ts: TEST_UNDICI_RUNTIME_DEPS_KEY",
"src/infra/node-pairing-migration.ts: LegacyNodePairingMigrationResult",
"src/infra/node-pairing.ts: NodePairingPendingSnapshot",
"src/infra/npm-integrity.ts: resolveNpmIntegrityDrift",
"src/infra/npm-managed-root.ts: MissingRequiredPlatformPackage",
"src/infra/npm-pack-install.ts: installFromNpmSpecArchive",
"src/infra/npm-registry-spec.ts: isOpenClawStableCorrectionVersion",
"src/infra/openclaw-exec-env.ts: OPENCLAW_CLI_ENV_VALUE",
"src/infra/openclaw-root.ts: __testing",
"src/infra/openclaw-root.ts: testing",
"src/infra/outbound/channel-resolution.ts: resetOutboundChannelResolutionStateForTest",
"src/infra/outbound/channel-selection.ts: testing",
"src/infra/outbound/deliver-types.ts: PLATFORM_MESSAGE_NOT_DISPATCHED_ERROR_CODE",
"src/infra/outbound/deliver.ts: normalizeOutboundPayloads",
"src/infra/outbound/delivery-queue-recovery.ts: computeBackoffMs",
"src/infra/outbound/delivery-queue-recovery.ts: isEntryEligibleForRecoveryRetry",
"src/infra/outbound/delivery-queue-recovery.ts: isPermanentDeliveryError",
"src/infra/outbound/delivery-queue-recovery.ts: MAX_RETRIES",
"src/infra/outbound/delivery-queue.ts: computeBackoffMs",
"src/infra/outbound/delivery-queue.ts: failPendingDelivery",
"src/infra/outbound/delivery-queue.ts: isEntryEligibleForRecoveryRetry",
"src/infra/outbound/delivery-queue.ts: isPermanentDeliveryError",
"src/infra/outbound/delivery-queue.ts: loadPendingDeliveries",
"src/infra/outbound/delivery-queue.ts: loadPendingDelivery",
"src/infra/outbound/delivery-queue.ts: MAX_RETRIES",
"src/infra/outbound/delivery-queue.ts: moveToFailed",
"src/infra/outbound/directory-cache.ts: DirectoryCacheKey",
"src/infra/outbound/payloads.ts: normalizeOutboundPayloads",
"src/infra/outbound/target-errors.ts: ambiguousTargetMessage",
"src/infra/outbound/target-errors.ts: missingTargetMessage",
"src/infra/outbound/target-errors.ts: unknownTargetMessage",
"src/infra/outbound/target-normalization.ts: testing",
"src/infra/outbound/target-resolver.ts: resolveMessagingTarget",
"src/infra/package-dist-inventory.ts: assertNoLegacyPluginDependencyStagingDebris",
"src/infra/package-dist-inventory.ts: collectLegacyPluginDependencyStagingDebrisPaths",
"src/infra/package-dist-inventory.ts: isLegacyPluginDependencyInstallStagePath",
"src/infra/package-dist-inventory.ts: LOCAL_BUILD_METADATA_DIST_PATHS",
"src/infra/package-dist-inventory.ts: writePackageDistInventory",
"src/infra/package-json.ts: readPackageJson",
"src/infra/package-manager-exec-wrapper.ts: NPM_EXEC_OPTIONS_WITH_VALUE",
"src/infra/package-manager-exec-wrapper.ts: PackageManagerExecInvocation",
"src/infra/package-update-steps.ts: PackageUpdateStepResult",
"src/infra/pairing-files.ts: writeJson",
"src/infra/pairing-token.ts: PAIRING_TOKEN_BYTES",
"src/infra/path-guards.ts: hasNodeErrorCode",
"src/infra/path-guards.ts: isNodeError",
"src/infra/path-guards.ts: isPathInsideWithRealpath",
"src/infra/path-guards.ts: isSymlinkOpenError",
"src/infra/path-guards.ts: isWithinDir",
"src/infra/path-guards.ts: resolveSafeBaseDir",
"src/infra/path-guards.ts: resolveSafeRelativePath",
"src/infra/path-guards.ts: safeRealpathSync",
"src/infra/path-guards.ts: splitSafeRelativePath",
"src/infra/path-safety.ts: hasNodeErrorCode",
"src/infra/path-safety.ts: isNodeError",
"src/infra/path-safety.ts: isNotFoundPathError",
"src/infra/path-safety.ts: isSymlinkOpenError",
"src/infra/path-safety.ts: normalizeWindowsPathForComparison",
"src/infra/path-safety.ts: resolveSafeBaseDir",
"src/infra/path-safety.ts: resolveSafeRelativePath",
"src/infra/path-safety.ts: splitSafeRelativePath",
"src/infra/permissions.ts: formatWindowsAclSummary",
"src/infra/permissions.ts: inspectWindowsAcl",
"src/infra/permissions.ts: parseIcaclsOutput",
"src/infra/permissions.ts: resolveWindowsUserPrincipal",
"src/infra/permissions.ts: summarizeWindowsAcl",
"src/infra/permissions.ts: WindowsAclEntry",
"src/infra/permissions.ts: WindowsAclSummary",
"src/infra/ports-format.ts: formatPortListener",
"src/infra/ports-format.ts: isSingleExpectedGatewayListener",
"src/infra/ports-netstat.ts: WindowsNetstatListener",
"src/infra/ports.ts: buildPortHints",
"src/infra/ports.ts: isSingleExpectedGatewayListener",
"src/infra/ports.ts: PortConnections",
"src/infra/ports.ts: PortListenerKind",
"src/infra/private-file-store.ts: PrivateFileStoreSync",
"src/infra/promotions-feed.ts: PromotionClaimRecord",
"src/infra/promotions-feed.ts: PromotionsFeedState",
"src/infra/promotions-feed.ts: readPromotionsFeedState",
"src/infra/push-apns-http2.ts: ApnsResponseBodyCapture",
"src/infra/push-apns-http2.ts: ConnectApnsHttp2SessionParams",
"src/infra/push-apns-http2.ts: ProbeApnsHttp2ReachabilityViaProxyParams",
"src/infra/push-apns-http2.ts: ProbeApnsHttp2ReachabilityViaProxyResult",
"src/infra/push-apns.relay.ts: DEFAULT_APNS_RELAY_BASE_URL",
"src/infra/push-apns.relay.ts: DEFAULT_APNS_SANDBOX_RELAY_BASE_URL",
"src/infra/push-apns.ts: ApnsPushResult",
"src/infra/push-apns.ts: shouldInvalidateApnsRegistration",
"src/infra/push-web.ts: listWebPushSubscriptions",
"src/infra/regular-file.ts: RegularFileStatResult",
"src/infra/regular-file.ts: resolveRegularFileAppendFlags",
"src/infra/regular-file.ts: statRegularFile",
"src/infra/regular-file.ts: statRegularFileSync",
"src/infra/resolve-system-bin.ts: getTrustedDirsForTest",
"src/infra/resolve-system-bin.ts: resetResolveSystemBin",
"src/infra/restart-coordinator.ts: SafeGatewayRestartBlocker",
"src/infra/restart-coordinator.ts: SafeGatewayRestartCounts",
"src/infra/restart-coordinator.ts: SafeGatewayRestartPreflight",
"src/infra/restart-handoff.ts: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND",
"src/infra/restart-handoff.ts: GatewayRestartHandoffRestartKind",
"src/infra/restart-handoff.ts: GatewayRestartHandoffSource",
"src/infra/restart-handoff.ts: GatewayRestartHandoffSupervisorMode",
"src/infra/restart-sentinel.ts: RestartSentinel",
"src/infra/restart-sentinel.ts: RestartSentinelLog",
"src/infra/restart-sentinel.ts: RestartSentinelStats",
"src/infra/restart-sentinel.ts: RestartSentinelStep",
"src/infra/restart-stale-pids.ts: __testing",
"src/infra/restart-stale-pids.ts: testing",
"src/infra/restart.ts: __testing",
"src/infra/restart.ts: DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS",
"src/infra/restart.ts: emitGatewayRestart",
"src/infra/restart.ts: findGatewayPidsOnPortSync",
"src/infra/restart.ts: GatewayRestartEmitResult",
"src/infra/restart.ts: RestartAttempt",
"src/infra/restart.ts: RestartAuditInfo",
"src/infra/restart.ts: RestartDeferralHooks",
"src/infra/restart.ts: RestartEmitHooks",
"src/infra/restart.ts: testing",
"src/infra/root-paths.ts: ensureDirectoryWithinRoot",
"src/infra/runtime-guard.ts: detectRuntime",
"src/infra/runtime-guard.ts: parseMinimumNodeEngine",
"src/infra/runtime-guard.ts: RuntimeDetails",
"src/infra/runtime-guard.ts: runtimeSatisfies",
"src/infra/session-cost-usage-cache.sqlite.ts: sessionCostUsageCacheTestApi",
"src/infra/session-cost-usage.ts: loadSessionCostSummaryFromCache",
"src/infra/session-cost-usage.ts: refreshCostUsageCache",
"src/infra/session-cost-usage.ts: requestCostUsageCacheRefresh",
"src/infra/session-delivery-queue-recovery.ts: isSessionDeliveryEligibleForRetry",
"src/infra/session-delivery-queue.ts: ackSessionDelivery",
"src/infra/session-delivery-queue.ts: failSessionDelivery",
"src/infra/session-delivery-queue.ts: isSessionDeliveryEligibleForRetry",
"src/infra/session-delivery-queue.ts: loadPendingSessionDeliveries",
"src/infra/session-maintenance-warning.ts: __testing",
"src/infra/session-maintenance-warning.ts: testing",
"src/infra/shell-env.ts: resetShellPathCacheForTests",
"src/infra/shell-inline-command.ts: POWERSHELL_INLINE_COMMAND_FLAGS",
"src/infra/sqlite-integrity.ts: SqliteIntegrityChecks",
"src/infra/sqlite-snapshot.ts: CreateVerifiedSqliteSnapshotOptions",
"src/infra/sqlite-snapshot.ts: PublishedSqliteFileGuard",
"src/infra/sqlite-snapshot.ts: PublishVerifiedSqliteFileOptions",
"src/infra/sqlite-snapshot.ts: SqliteFileContent",
"src/infra/sqlite-snapshot.ts: VerifiedSqliteSnapshot",
"src/infra/sqlite-wal.ts: DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES",
"src/infra/sqlite-wal.ts: DEFAULT_SQLITE_WAL_CHECKPOINT_INTERVAL_MS",
"src/infra/sqlite-wal.ts: enableIncrementalAutoVacuumForFreshDatabase",
"src/infra/stale-lock-file.ts: LockFileOwnerPayload",
"src/infra/state-migrations.cron-run-logs.ts: CRON_RUN_LOG_TASK_IMPORT_MIGRATION_ID",
"src/infra/state-migrations.cron-run-logs.ts: CronRunLogTaskImportResult",
"src/infra/state-migrations.debug-proxy.ts: LegacyDebugProxyCaptureDetection",
"src/infra/state-migrations.fs.ts: isLegacyWhatsAppAuthFile",
"src/infra/state-migrations.session-store.ts: sessionStoreTextMayNeedCanonicalization",
"src/infra/state-migrations.ts: sessionStoreTextMayNeedCanonicalization",
"src/infra/supervisor-markers.ts: DetectRespawnSupervisorOptions",
"src/infra/system-run-approval-binding.ts: matchSystemRunApprovalEnvHash",
"src/infra/system-run-command.ts: validateSystemRunCommandConsistency",
"src/infra/tailnet.ts: listTailnetAddresses",
"src/infra/tailscale.ts: getTestTailscaleBinaryOverride",
"src/infra/tailscale.ts: tailscaleFunnelStatusCoversPort",
"src/infra/update-channels.ts: isPrereleaseTag",
"src/infra/update-channels.ts: UpdateChannelSource",
"src/infra/update-check.ts: checkDepsStatus",
"src/infra/update-check.ts: DepsStatus",
"src/infra/update-check.ts: ExtendedStableResolutionResult",
"src/infra/update-check.ts: fetchNpmLatestVersion",
"src/infra/update-check.ts: fetchNpmRegistryVersionForChannel",
"src/infra/update-check.ts: GitUpdateStatus",
"src/infra/update-check.ts: NpmPackageTargetStatus",
"src/infra/update-check.ts: NpmTagStatus",
"src/infra/update-check.ts: PackageManager",
"src/infra/update-check.ts: RegistryStatus",
"src/infra/update-global.ts: isExplicitPackageInstallSpec",
"src/infra/update-global.ts: isMainPackageTarget",
"src/infra/update-global.ts: OPENCLAW_MAIN_PACKAGE_SPEC",
"src/infra/update-global.ts: resolveGlobalInstallCommand",
"src/infra/update-global.ts: resolveGlobalRoot",
"src/infra/update-managed-service-handoff.ts: ManagedServiceUpdateHandoffResult",
"src/infra/update-managed-service-handoff.ts: stripSupervisorHintEnv",
"src/infra/update-package-manager.ts: PackageManagerCommandRunner",
"src/infra/update-post-core-finalize.ts: PostCoreFinalizeOutcome",
"src/infra/update-post-core-finalize.ts: PostCoreFinalizeSpawner",
"src/infra/update-runner.ts: UpdateInstallSurface",
"src/infra/update-runner.ts: UpdateStepCompletion",
"src/infra/voicewake-routing.ts: normalizeVoiceWakeTriggerWord",
"src/infra/windows-encoding.ts: parseWindowsCodePage",
"src/infra/windows-install-roots.ts: DEFAULT_WINDOWS_SYSTEM_ROOT",
"src/infra/windows-install-roots.ts: normalizeWindowsInstallRoot",
"src/infra/windows-install-roots.ts: privateTestApi",
"src/infra/windows-install-roots.ts: resetWindowsInstallRootsForTests",
"src/infra/windows-shell-command.ts: RebuiltShellCommandResult",
"src/infra/windows-shell-command.ts: ShellSegmentRenderResult",
"src/llm/providers/stream-wrappers/anthropic-family-cache-semantics.ts: isAnthropicBedrockModel",
"src/llm/utils/node-http-proxy.ts: resolveHttpProxyUrlForTarget",
"src/llm/utils/node-http-proxy.ts: UNSUPPORTED_PROXY_PROTOCOL_MESSAGE",
+125
View File
@@ -0,0 +1,125 @@
import fs from "node:fs/promises";
import path from "node:path";
import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { writeJson } from "../../src/infra/json-files.ts";
import {
collectPackageDistInventory,
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
} from "../../src/infra/package-dist-inventory.ts";
export { LOCAL_BUILD_METADATA_DIST_PATHS } from "./local-build-metadata-paths.mjs";
export { PACKAGE_DIST_INVENTORY_RELATIVE_PATH };
const INSTALL_STAGE_DEBRIS_DIR_PATTERN = /^\.openclaw-install-stage(?:-[^/]+)?$/iu;
function normalizeRelativePath(value: string): string {
return value.replace(/\\/g, "/");
}
function isInstallStageDirName(value: string): boolean {
return INSTALL_STAGE_DEBRIS_DIR_PATTERN.test(value);
}
export function isLegacyPluginDependencyInstallStagePath(relativePath: string): boolean {
const parts = normalizeRelativePath(relativePath).split("/");
return (
parts.length >= 4 &&
parts[0]?.toLowerCase() === "dist" &&
parts[1]?.toLowerCase() === "extensions" &&
Boolean(parts[2]) &&
isInstallStageDirName(parts[3] ?? "")
);
}
async function collectLegacyPluginDependencyStagingDebrisPaths(
packageRoot: string,
): Promise<string[]> {
const distDirs: string[] = [];
try {
const packageRootEntries = await fs.readdir(packageRoot, { withFileTypes: true });
for (const entry of packageRootEntries) {
if (entry.isDirectory() && entry.name.toLowerCase() === "dist") {
distDirs.push(path.join(packageRoot, entry.name));
}
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return [];
}
throw error;
}
const debris: string[] = [];
for (const distDir of distDirs) {
let distEntries: import("node:fs").Dirent[];
try {
distEntries = await fs.readdir(distDir, { withFileTypes: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
continue;
}
throw error;
}
for (const distEntry of distEntries) {
if (!distEntry.isDirectory() || distEntry.name.toLowerCase() !== "extensions") {
continue;
}
const extensionsDir = path.join(distDir, distEntry.name);
let extensionEntries: import("node:fs").Dirent[];
try {
extensionEntries = await fs.readdir(extensionsDir, { withFileTypes: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
continue;
}
throw error;
}
for (const extensionEntry of extensionEntries) {
if (!extensionEntry.isDirectory()) {
continue;
}
const extensionPath = path.join(extensionsDir, extensionEntry.name);
let stagingEntries: import("node:fs").Dirent[];
try {
stagingEntries = await fs.readdir(extensionPath, { withFileTypes: true });
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
continue;
}
throw error;
}
for (const stagingEntry of stagingEntries) {
if (!isInstallStageDirName(stagingEntry.name)) {
continue;
}
debris.push(
normalizeRelativePath(
path.relative(packageRoot, path.join(extensionPath, stagingEntry.name)),
),
);
}
}
}
}
return debris.toSorted((left, right) => left.localeCompare(right));
}
async function assertNoLegacyPluginDependencyStagingDebris(packageRoot: string): Promise<void> {
const debris = await collectLegacyPluginDependencyStagingDebrisPaths(packageRoot);
if (debris.length === 0) {
return;
}
throw new Error(
`unexpected legacy plugin dependency staging debris in package dist: ${debris.join(", ")}`,
);
}
export async function writePackageDistInventory(packageRoot: string): Promise<string[]> {
await assertNoLegacyPluginDependencyStagingDebris(packageRoot);
const inventory = sortUniqueStrings(await collectPackageDistInventory(packageRoot));
const inventoryPath = path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH);
await writeJson(inventoryPath, inventory, { trailingNewline: true });
return inventory;
}
@@ -11,8 +11,8 @@
"src/commands/doctor/shared/codex-route-warnings.ts": 3,
"src/config/sessions/session-accessor.sqlite.ts": 2,
"src/config/sessions/session-accessor.ts": 12,
"src/config/sessions/store-load.ts": 5,
"src/config/sessions/store.ts": 15,
"src/config/sessions/store-load.ts": 3,
"src/config/sessions/store.ts": 10,
"src/config/sessions/test-helpers.ts": 2,
"src/config/sessions/transcript.ts": 2
},
@@ -31,7 +31,7 @@
"sessionLifecycleCleanup": {
"src/config/sessions/session-accessor.sqlite.ts": 2,
"src/config/sessions/store-maintenance-operations.ts": 2,
"src/config/sessions/store.ts": 5
"src/config/sessions/store.ts": 2
},
"transcriptWriter": {
"src/agents/embedded-agent-runner/compaction-hooks.ts": 2,
@@ -39,7 +39,6 @@
"src/agents/embedded-agent-runner/tool-result-truncation.ts": 3,
"src/agents/embedded-agent-runner/transcript-rewrite.ts": 3,
"src/config/sessions/session-accessor.sqlite.ts": 3,
"src/config/sessions/session-accessor.ts": 2,
"src/config/sessions/store.ts": 2
"src/config/sessions/session-accessor.ts": 2
}
}
+5 -5
View File
@@ -5,17 +5,17 @@ import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import {
LOCAL_BUILD_METADATA_DIST_PATHS,
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
writePackageDistInventory,
} from "../src/infra/package-dist-inventory.ts";
import {
compareReleaseVersions as compareReleaseVersionsBase,
collectReleaseVersionFloorErrors as collectReleaseVersionFloorErrorsBase,
resolveNpmDistTagMirrorAuth as resolveNpmDistTagMirrorAuthBase,
parseReleaseVersion as parseReleaseVersionBase,
} from "./lib/npm-publish-plan.mjs";
import {
LOCAL_BUILD_METADATA_DIST_PATHS,
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
writePackageDistInventory,
} from "./lib/package-dist-inventory.ts";
import { WORKSPACE_TEMPLATE_PACK_PATHS } from "./lib/workspace-bootstrap-smoke.mjs";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
+1 -1
View File
@@ -5,7 +5,7 @@ import { spawnSync, type SpawnSyncOptions } from "node:child_process";
import { existsSync, readdirSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../src/infra/errors.ts";
import { writePackageDistInventory } from "../src/infra/package-dist-inventory.ts";
import { writePackageDistInventory } from "./lib/package-dist-inventory.ts";
import { preparePackageChangelog } from "./package-changelog.mjs";
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs";
const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/iu;
+1 -1
View File
@@ -742,7 +742,7 @@ async function main() {
"tsx",
"--input-type=module",
"-e",
"const { writePackageDistInventory } = await import('./src/infra/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());",
"const { writePackageDistInventory } = await import('./scripts/lib/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());",
],
sourceDir,
{
+6 -6
View File
@@ -20,12 +20,6 @@ import { basename, dirname, join, resolve, win32 } from "node:path";
import { pathToFileURL } from "node:url";
import { expectDefined } from "../packages/normalization-core/src/expect.js";
import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../src/cli/completion-runtime.ts";
import {
isLegacyPluginDependencyInstallStagePath,
LOCAL_BUILD_METADATA_DIST_PATHS,
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
writePackageDistInventory,
} from "../src/infra/package-dist-inventory.ts";
import { escapeRegExp } from "../src/shared/regexp.js";
import { checkCliBootstrapExternalImports } from "./check-cli-bootstrap-imports.mjs";
import {
@@ -38,6 +32,12 @@ import {
listBundledPluginPackArtifacts,
} from "./lib/bundled-plugin-build-entries.mjs";
import { collectPackUnpackedSizeErrors as collectNpmPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs";
import {
isLegacyPluginDependencyInstallStagePath,
LOCAL_BUILD_METADATA_DIST_PATHS,
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
writePackageDistInventory,
} from "./lib/package-dist-inventory.ts";
import { collectBundledPluginPackageDependencySpecs } from "./lib/plugin-package-dependencies.mjs";
import {
listPluginSdkDistArtifacts,
+1 -1
View File
@@ -2,7 +2,7 @@
// Write Package Dist Inventory script supports OpenClaw repository automation.
import { pathToFileURL } from "node:url";
import { writePackageDistInventory } from "../src/infra/package-dist-inventory.ts";
import { writePackageDistInventory } from "./lib/package-dist-inventory.ts";
async function writeCurrentPackageDistInventory(): Promise<void> {
await writePackageDistInventory(process.cwd());
@@ -4,7 +4,6 @@ import { afterEach, beforeEach, expect, vi } from "vitest";
import { resetAcpManagerTaskStateForTests } from "../../../test/helpers/acp-manager-task-state.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js";
import { resetHeartbeatWakeStateForTests } from "../../infra/heartbeat-wake.js";
import { deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js";
import { resetAcpActiveTurnsForTests } from "./active-turns.js";
@@ -338,7 +337,6 @@ export function installAcpSessionManagerTestLifecycle(): void {
} else {
setTestEnvValue("OPENCLAW_STATE_DIR", ORIGINAL_STATE_DIR);
}
resetHeartbeatWakeStateForTests();
resetAcpManagerTaskStateForTests();
});
}
+7 -4
View File
@@ -37,19 +37,23 @@ let resetProcessRegistryForTests: typeof import("./bash-process-registry.js").re
let resolveExecTarget: typeof import("./bash-tools.exec-runtime.js").resolveExecTarget;
let runExecProcess: typeof import("./bash-tools.exec-runtime.js").runExecProcess;
let prepareGatewaySuspend: typeof import("../infra/gateway-suspend-coordinator.js").prepareGatewaySuspend;
let resetGatewaySuspendCoordinatorForTest: typeof import("../infra/gateway-suspend-coordinator.js").resetGatewaySuspendCoordinatorForTest;
let resetGatewaySuspendCoordinatorForLifecycleRestart: typeof import("../infra/gateway-suspend-coordinator.js").resetGatewaySuspendCoordinatorForLifecycleRestart;
let resumeGatewaySuspend: typeof import("../infra/gateway-suspend-coordinator.js").resumeGatewaySuspend;
beforeAll(async () => {
({ getActiveBackgroundExecSessionCount, markBackgrounded, resetProcessRegistryForTests } =
await import("./bash-process-registry.js"));
({ resolveExecTarget, runExecProcess } = await import("./bash-tools.exec-runtime.js"));
({ prepareGatewaySuspend, resetGatewaySuspendCoordinatorForTest, resumeGatewaySuspend } =
({
prepareGatewaySuspend,
resetGatewaySuspendCoordinatorForLifecycleRestart,
resumeGatewaySuspend,
} =
await import("../infra/gateway-suspend-coordinator.js"));
});
beforeEach(() => {
resetGatewaySuspendCoordinatorForTest();
resetGatewaySuspendCoordinatorForLifecycleRestart();
resetProcessRegistryForTests();
requestHeartbeatMock.mockClear();
enqueueSystemEventMock.mockClear();
@@ -57,7 +61,6 @@ beforeEach(() => {
});
afterEach(() => {
resetGatewaySuspendCoordinatorForTest();
resetProcessRegistryForTests();
});
@@ -5,7 +5,6 @@
*/
import { expectDefined } from "@openclaw/normalization-core";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { OPENCLAW_CLI_ENV_VALUE } from "../infra/openclaw-exec-env.js";
import type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.types.js";
import type { BashSandboxConfig } from "./bash-tools.shared.js";
import type { ExtensionContext } from "./sessions/index.js";
@@ -17,6 +16,7 @@ declare module "../plugins/hook-types.js" {
}
const CHANNEL_CONTEXT_ENV_KEY = "OPENCLAW_CHANNEL_CONTEXT";
const OPENCLAW_CLI_ENV_VALUE = "1";
type CapturedNodeHostParams = Pick<
ExecuteNodeHostCommandParams,
"env" | "requestedEnv" | "workdir"
+19 -10
View File
@@ -7,10 +7,7 @@ import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { drainFormattedSystemEvents } from "../auto-reply/reply/session-system-events.js";
import type { OpenClawConfig } from "../config/config.js";
import {
resetHeartbeatWakeStateForTests,
setHeartbeatWakeHandler,
} from "../infra/heartbeat-wake.js";
import { requestHeartbeat, setHeartbeatWakeHandler } from "../infra/heartbeat-wake.js";
import { applyPathPrepend, findPathKey } from "../infra/path-prepend.js";
import {
peekSystemEventEntries,
@@ -820,13 +817,25 @@ describe("exec exit codes", () => {
describe("exec notifyOnExit", () => {
useCapturedEnv([...SHELL_ENV_KEYS], applyDefaultShellEnv);
beforeEach(() => {
resetHeartbeatWakeStateForTests();
});
async function drainPendingHeartbeatWakes(): Promise<void> {
const handler = vi.fn(async () => ({ status: "ran" as const, durationMs: 0 }));
const dispose = setHeartbeatWakeHandler(handler);
try {
requestHeartbeat({
source: "other",
intent: "immediate",
reason: "test-cleanup",
coalesceMs: 0,
});
await expect.poll(() => handler.mock.calls.length, NOTIFY_POLL_OPTIONS).toBeGreaterThan(0);
} finally {
dispose();
}
}
afterEach(() => {
resetHeartbeatWakeStateForTests();
});
beforeEach(drainPendingHeartbeatWakes);
afterEach(drainPendingHeartbeatWakes);
it("enqueues a system event when a backgrounded exec exits", async () => {
const tool = createNotifyOnExitExecTool();
@@ -14,7 +14,7 @@ import {
claimAgentRunContext,
getAgentEventLifecycleGeneration,
getAgentRunContext,
resetAgentRunContextForTest,
resetAgentEventsForTest,
rotateAgentEventLifecycleGeneration,
withAgentRunLifecycleGeneration,
} from "../../infra/agent-events.js";
@@ -301,6 +301,7 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
});
beforeEach(() => {
resetAgentEventsForTest();
resetRunOverflowCompactionHarnessMocks();
mockedBuildEmbeddedRunPayloads.mockReturnValue([{ text: "ok" }]);
});
@@ -1117,7 +1118,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
});
it("rebinds preserved queued work to the current lifecycle generation", async () => {
resetAgentRunContextForTest();
mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
let enqueueCount = 0;
let runQueuedTask: (() => void) | undefined;
@@ -1157,7 +1157,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
expect(onExecutionStarted).toHaveBeenCalledWith({
lifecycleGeneration: currentLifecycleGeneration,
});
resetAgentRunContextForTest();
});
it("revalidates reserved harness ownership after the global queue wait", async () => {
@@ -1222,7 +1221,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
});
it("rejects background work queued across lifecycle rotation", async () => {
resetAgentRunContextForTest();
let enqueueCount = 0;
let runQueuedTask: (() => void) | undefined;
const runPromise = runEmbeddedAgent({
@@ -1256,7 +1254,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
});
it("does not claim a rebound generation after queued work was aborted", async () => {
resetAgentRunContextForTest();
let enqueueCount = 0;
let runQueuedTask: (() => void) | undefined;
const abortController = new AbortController();
@@ -1292,7 +1289,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
});
it("rejects stale descendants admitted after lifecycle rotation", async () => {
resetAgentRunContextForTest();
const preRestartGeneration = getAgentEventLifecycleGeneration();
rotateAgentEventLifecycleGeneration();
@@ -3815,7 +3811,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
});
it("does not let an old execution rotate a newer same-id run context", async () => {
resetAgentRunContextForTest();
const currentLifecycleGeneration = rotateAgentEventLifecycleGeneration();
claimAgentRunContext("shared-run", {
sessionKey: "new-session-key",
@@ -3845,7 +3840,6 @@ describe("runEmbeddedAgent overflow compaction trigger routing", () => {
lifecycleGeneration: currentLifecycleGeneration,
}),
);
resetAgentRunContextForTest();
});
it("guards thrown engine-owned overflow compaction attempts", async () => {
File diff suppressed because it is too large Load Diff
@@ -3,8 +3,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { drainSessionStoreWriterQueuesForTest } from "../config/sessions.js";
import { describe, expect, it, vi } from "vitest";
import {
readCompactionCount,
seedSessionStore,
@@ -130,10 +129,6 @@ function loggedInfoMessageAt(info: ReturnType<typeof vi.fn>, index: number): str
return message;
}
afterEach(async () => {
await drainSessionStoreWriterQueuesForTest();
});
describe("reconcileSessionStoreCompactionCountAfterSuccess", () => {
it("raises the stored compaction count to the observed value", async () => {
// Store count can lag the in-memory count after async writes; reconciliation
-2
View File
@@ -12,7 +12,6 @@ import {
} from "../../infra/diagnostic-events.js";
import {
getActiveDiagnosticTraceContext,
resetDiagnosticTraceContextForTest,
runWithDiagnosticTraceContext,
type DiagnosticTraceContext,
} from "../../infra/diagnostic-trace-context.js";
@@ -134,7 +133,6 @@ function captureDiagnosticEvents(
describe("AgentHarness lifecycle runner", () => {
afterEach(() => {
resetDiagnosticEventsForTest();
resetDiagnosticTraceContextForTest();
});
it("runs a harness attempt without changing attempt params", async () => {
@@ -16,7 +16,7 @@ import { callGateway } from "../gateway/call.js";
import {
getAgentEventLifecycleGeneration,
registerAgentRunContext,
resetAgentRunContextForTest,
resetAgentEventsForTest,
} from "../infra/agent-events.js";
import {
getActiveGatewayRootWorkCount,
@@ -69,7 +69,7 @@ let tmpDir: string;
beforeEach(async () => {
vi.clearAllMocks();
resetAgentRunContextForTest();
resetAgentEventsForTest();
resetGatewayWorkAdmission();
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-main-restart-recovery-"));
});
+1 -1
View File
@@ -7,7 +7,6 @@ import type { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js";
* Verifies SSRF-guarded fetch, scoped dispatcher behavior, and same-origin headers.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TEST_UNDICI_RUNTIME_DEPS_KEY } from "../infra/net/undici-runtime.js";
import {
buildMcpHttpFetch,
withoutMcpAuthorizationHeader,
@@ -15,6 +14,7 @@ import {
} from "./mcp-http-fetch.js";
const testGlobal = globalThis as Record<string, unknown>;
const TEST_UNDICI_RUNTIME_DEPS_KEY = "__OPENCLAW_TEST_UNDICI_RUNTIME_DEPS__";
const { lookupMock } = vi.hoisted(() => ({
lookupMock: vi.fn(),
}));
+11 -27
View File
@@ -3,10 +3,12 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { normalizeConfigPatchReplacePath } from "../config/patch-replace-paths.js";
import { GatewayClientRequestError } from "../gateway/client.js";
import { readRestartSentinel } from "../infra/restart-sentinel.js";
import { testing as restartTesting } from "../infra/restart.js";
import {
resetGatewayRestartStateForInProcessRestart,
setPreRestartDeferralCheck,
} from "../infra/restart.js";
import { withEnvAsync } from "../test-utils/env.js";
import { createGatewayTool } from "./tools/gateway-tool.js";
import { callGatewayTool } from "./tools/gateway.js";
@@ -131,7 +133,6 @@ function expectConfigMutationCall(params: {
describe("gateway tool", () => {
beforeEach(() => {
restartTesting.resetSigusr1State();
callGatewayToolMock.mockClear();
readGatewayCallOptionsMock.mockClear();
callGatewayToolMock.mockImplementation(async (method: string) => {
@@ -298,12 +299,10 @@ describe("gateway tool", () => {
);
});
it("schedules SIGUSR1 restart", async () => {
it("schedules SIGUSR1 restart and writes the routed sentinel", async () => {
resetGatewayRestartStateForInProcessRestart();
setPreRestartDeferralCheck(() => 0);
const kill = vi.spyOn(process, "kill").mockImplementation(() => true);
const restartSignalKillCalls = () =>
kill.mock.calls.filter(
([pid, signal]) => pid === process.pid && (signal === "SIGUSR1" || signal === undefined),
);
const sigusr1Handler = vi.fn();
process.on("SIGUSR1", sigusr1Handler);
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-"));
@@ -312,9 +311,7 @@ describe("gateway tool", () => {
await withEnvAsync(
{ OPENCLAW_STATE_DIR: stateDir, OPENCLAW_PROFILE: "isolated" },
async () => {
const tool = requireGatewayTool();
const result = await tool.execute("call1", {
const result = await requireGatewayTool().execute("call1", {
action: "restart",
delayMs: 0,
});
@@ -325,13 +322,11 @@ describe("gateway tool", () => {
delayMs: 0,
});
expect(restartSignalKillCalls()).toHaveLength(0);
expect(sigusr1Handler).not.toHaveBeenCalled();
await vi.waitFor(() => expect(sigusr1Handler).toHaveBeenCalledTimes(1), {
interval: 1,
timeout: 1_000,
});
expect(restartSignalKillCalls()).toHaveLength(0);
expect(kill).not.toHaveBeenCalled();
const sentinel = await readRestartSentinel();
expect(sentinel?.payload.kind).toBe("restart");
@@ -343,7 +338,8 @@ describe("gateway tool", () => {
} finally {
process.removeListener("SIGUSR1", sigusr1Handler);
kill.mockRestore();
restartTesting.resetSigusr1State();
resetGatewayRestartStateForInProcessRestart();
setPreRestartDeferralCheck(() => 0);
await fs.rm(stateDir, { recursive: true, force: true });
}
});
@@ -501,18 +497,6 @@ describe("gateway tool", () => {
});
});
it("distinguishes explicit terminal array consent from indexed consent", () => {
expect(normalizeConfigPatchReplacePath("bindings[]")).toBe("bindings");
expect(normalizeConfigPatchReplacePath("bindings[0]")).toBe("bindings[0]");
expect(normalizeConfigPatchReplacePath("agents.list[0].skills")).toBe("agents.list[].skills");
expect(normalizeConfigPatchReplacePath(normalizeConfigPatchReplacePath("bindings[]"))).toBe(
"bindings",
);
expect(normalizeConfigPatchReplacePath(normalizeConfigPatchReplacePath("bindings[0]"))).toBe(
"bindings[0]",
);
});
it("rejects config.patch when it changes safe bin approval paths", async () => {
const tool = requireGatewayTool();
+2 -1
View File
@@ -1,10 +1,11 @@
// Verifies Docker create arguments for sandbox hardening and configured passthrough.
import { describe, expect, it } from "vitest";
import { OPENCLAW_CLI_ENV_VALUE } from "../infra/openclaw-exec-env.js";
import { SANDBOX_DOCKER_CREATE_ARGS_EPOCH } from "./sandbox/constants.js";
import { buildSandboxCreateArgs } from "./sandbox/docker.js";
import type { SandboxDockerConfig } from "./sandbox/types.js";
const OPENCLAW_CLI_ENV_VALUE = "1";
describe("buildSandboxCreateArgs", () => {
function createSandboxConfig(
overrides: Partial<SandboxDockerConfig> = {},
@@ -11,15 +11,12 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { setRuntimeConfigSnapshot } from "../config/config.js";
import {
clearSessionStoreCacheForTest,
drainSessionStoreWriterQueuesForTest,
} from "../config/sessions/store.js";
import { callGateway } from "../gateway/call.js";
import { createRunningTaskRun } from "../tasks/detached-task-runtime.js";
import { resetTaskFlowRegistryForTests } from "../tasks/task-flow-registry.js";
import { findTaskByRunId, resetTaskRegistryForTests } from "../tasks/task-registry.js";
import { captureEnv } from "../test-utils/env.js";
import { cleanupSessionStateForTest } from "../test-utils/session-state-cleanup.js";
import { recoverOrphanedSubagentSessions } from "./subagent-orphan-recovery.js";
import {
addSubagentRunForTests,
@@ -84,8 +81,7 @@ describe("subagent orphan recovery — faithful restart path", () => {
afterEach(async () => {
testing.setDepsForTest();
resetSubagentRegistryForTests({ persist: false });
await drainSessionStoreWriterQueuesForTest();
clearSessionStoreCacheForTest();
await cleanupSessionStateForTest();
resetTaskRegistryForTests({ persist: false });
resetTaskFlowRegistryForTests({ persist: false });
if (tempStateDir) {
@@ -95,7 +91,7 @@ describe("subagent orphan recovery — faithful restart path", () => {
envSnapshot.restore();
});
it("finalizes a stale (>2h) aborted run in the real registry instead of resuming it", async () => {
it("finalizes a stale (>2h) aborted run instead of resuming it", async () => {
const now = Date.now();
const childSessionKey = "agent:main:subagent:stale-aborted";
const runId = "run-stale-aborted";
@@ -130,25 +126,11 @@ describe("subagent orphan recovery — faithful restart path", () => {
).not.toBeNull();
addSubagentRunForTests(record);
const before = getSubagentRunByChildSessionKey(childSessionKey);
console.log(
`[proof] before recovery: stale run endedAt=${before?.endedAt ?? "undefined"} outcome=${
before?.outcome?.status ?? "undefined"
}`,
);
const result = await recoverOrphanedSubagentSessions({
getActiveRuns: () => new Map([[runId, record]]),
});
const after = getSubagentRunByChildSessionKey(childSessionKey);
console.log(
`[proof] after recovery: result=${JSON.stringify(result)} endedAt=${
after?.endedAt ?? "undefined"
} outcome=${after?.outcome?.status ?? "undefined"}`,
);
// Stale aborted run was finalized in the real registry, not resumed.
expect(vi.mocked(callGateway)).not.toHaveBeenCalled();
expect(after?.endedAt).toBeTypeOf("number");
expect(after?.outcome?.status).toBe("error");
@@ -159,11 +141,9 @@ describe("subagent orphan recovery — faithful restart path", () => {
error: expect.stringContaining("stale aborted subagent run not resumed"),
});
// The task finalizer and session projection are durable, not only
// in-memory side effects of the recovery pass.
resetTaskRegistryForTests({ persist: false });
expect(findTaskByRunId(runId)).toMatchObject({ status: "failed" });
await drainSessionStoreWriterQueuesForTest();
await cleanupSessionStateForTest();
const persistedSession = (await readSubagentSessionStore(storePath))[childSessionKey];
expect(persistedSession).toMatchObject({
status: "failed",
@@ -5,11 +5,8 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import "./subagent-registry.mocks.shared.js";
import {
clearSessionStoreCacheForTest,
drainSessionStoreWriterQueuesForTest,
} from "../config/sessions/store.js";
import { withEnvAsync } from "../test-utils/env.js";
import { cleanupSessionStateForTest } from "../test-utils/session-state-cleanup.js";
import {
createSubagentRegistryTestDeps,
writeSubagentSessionEntry,
@@ -128,8 +125,7 @@ describe("subagent registry persistence resume", () => {
announceSpy.mockClear();
mod.testing.setDepsForTest();
mod.resetSubagentRegistryForTests({ persist: false });
await drainSessionStoreWriterQueuesForTest();
clearSessionStoreCacheForTest();
await cleanupSessionStateForTest();
if (tempStateDir) {
await fs.rm(tempStateDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
tempStateDir = null;
@@ -8,14 +8,11 @@ import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "./subagent-registry.mocks.shared.js";
import { replaceSessionEntry } from "../config/sessions/session-accessor.js";
import {
clearSessionStoreCacheForTest,
drainSessionStoreWriterQueuesForTest,
} from "../config/sessions/store.js";
import type { SessionEntry } from "../config/sessions/types.js";
import { callGateway } from "../gateway/call.js";
import { onAgentEvent } from "../infra/agent-events.js";
import { captureEnv, deleteTestEnvValue, setTestEnvValue, withEnv } from "../test-utils/env.js";
import { cleanupSessionStateForTest } from "../test-utils/session-state-cleanup.js";
import { scheduleOrphanRecovery } from "./subagent-orphan-recovery.js";
import { persistSubagentSessionTiming } from "./subagent-registry-helpers.js";
import { getSubagentRunsSnapshotForRead } from "./subagent-registry-state.js";
@@ -230,8 +227,7 @@ describe("subagent registry persistence", () => {
afterEach(async () => {
testing.setDepsForTest();
resetSubagentRegistryForTests({ persist: false });
await drainSessionStoreWriterQueuesForTest();
clearSessionStoreCacheForTest();
await cleanupSessionStateForTest();
if (tempStateDir) {
await fs.rm(tempStateDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
tempStateDir = null;
+2 -5
View File
@@ -5,10 +5,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
resetActiveNodeContextForTests,
setActiveNodeContext,
} from "../infra/active-node-context.js";
import { setActiveNodeContext } from "../infra/active-node-context.js";
import { buildSystemPromptParams } from "./system-prompt-params.js";
async function makeTempDir(label: string): Promise<string> {
@@ -35,7 +32,7 @@ function buildParams(params: { config?: OpenClawConfig; workspaceDir?: string; c
}
describe("buildSystemPromptParams", () => {
afterEach(() => resetActiveNodeContextForTests());
afterEach(() => setActiveNodeContext(null));
it("projects only the stable active-node identity", () => {
setActiveNodeContext({ nodeId: "mac-123" });
@@ -1,7 +1,7 @@
// Video generation background tests cover detached task lifecycle, keepalive
// progress, completion announcement, and direct failure delivery.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getAgentRunContext, resetAgentRunContextForTest } from "../../infra/agent-events.js";
import { getAgentRunContext, resetAgentEventsForTest } from "../../infra/agent-events.js";
import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js";
import {
announceDeliveryMocks,
@@ -28,7 +28,7 @@ const { withMediaGenerationTaskKeepalive } = await import("./media-generate-back
describe("video generate background helpers", () => {
beforeEach(() => {
resetAgentRunContextForTest();
resetAgentEventsForTest();
resetMediaBackgroundMocks({
taskExecutorMocks,
taskDeliveryRuntimeMocks,
@@ -38,7 +38,7 @@ describe("video generate background helpers", () => {
afterEach(() => {
vi.useRealTimers();
resetAgentRunContextForTest();
resetAgentEventsForTest();
});
it("creates a running task with queued progress text", () => {
@@ -1,6 +1,5 @@
// Tests command gating rules for ownership, channel, and active session state.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { isCommandFlagEnabled } from "../../config/commands.js";
import type { OpenClawConfig } from "../../config/config.js";
import { REDACTED_SENTINEL } from "../../config/redact-snapshot.js";
import type { MsgContext } from "../templating.js";
@@ -621,18 +620,6 @@ describe("command gating", () => {
});
});
it("ignores inherited command flags", () => {
const inheritedCommands = Object.create({
bash: true,
config: true,
debug: true,
}) as Record<string, unknown>;
const cfg = { commands: inheritedCommands as never } as OpenClawConfig;
expect(isCommandFlagEnabled(cfg, "bash")).toBe(false);
expect(isCommandFlagEnabled(cfg, "config")).toBe(false);
expect(isCommandFlagEnabled(cfg, "debug")).toBe(false);
});
it("blocks disallowed /config set writes", async () => {
resolveConfigWriteDeniedTextMock
.mockReturnValueOnce("Config writes are disabled")
@@ -3,7 +3,6 @@ import { vi, type Mock } from "vitest";
import { clearAgentHarnesses } from "../../agents/harness/registry.js";
import type { ChannelMessagingAdapter } from "../../channels/plugins/types.core.js";
import type { OpenClawConfig } from "../../config/config.js";
import { clearApprovalNativeRouteStateForTest } from "../../infra/approval-native-route-coordinator.js";
import type {
AcpRuntime,
AcpRuntimeEnsureInput,
@@ -459,7 +458,6 @@ export const describe0BeforeEach0 = () => {
...passiveThreadingTestPlugins,
]),
);
clearApprovalNativeRouteStateForTest();
acpManagerRuntimeMocks.getAcpSessionManager.mockReset();
acpManagerRuntimeMocks.getAcpSessionManager.mockReturnValue(createMockAcpSessionManager());
replyRunTesting.resetReplyRunRegistry();
+3 -6
View File
@@ -9,6 +9,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
import { setCliSessionBinding } from "../../agents/cli-session.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import { resetAgentEventsForTest } from "../../infra/agent-events.js";
import {
createUserTurnTranscriptRecorder,
type PersistedUserTurnMessage,
@@ -556,6 +557,7 @@ beforeAll(async () => {
});
beforeEach(() => {
resetAgentEventsForTest({ preserveListeners: true });
setFastFollowupCliBackendDeps();
replyRunTestingForTest?.resetReplyRunRegistry();
clearRuntimeConfigSnapshot?.();
@@ -616,6 +618,7 @@ beforeEach(() => {
});
afterEach(() => {
resetAgentEventsForTest({ preserveListeners: true });
cliBackendsTestingForTest?.resetDepsForTest();
replyRunTestingForTest?.resetReplyRunRegistry();
clearRuntimeConfigSnapshot?.();
@@ -1115,7 +1118,6 @@ describe("createFollowupRunner reply-lane admission", () => {
const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
"../../infra/agent-events.js",
);
realAgentEvents.resetAgentRunContextForTest();
const active = createReplyOperationForTest({
sessionKey: "main",
sessionId: "pre-compact-session",
@@ -1170,7 +1172,6 @@ describe("createFollowupRunner reply-lane admission", () => {
expect(realAgentEvents.getAgentRunContext(observedRunId ?? "")?.sessionId).toBe(
"post-compact-session",
);
realAgentEvents.resetAgentRunContextForTest();
});
it("routes preflight compaction failures before starting queued followup runs", async () => {
@@ -4695,7 +4696,6 @@ describe("createFollowupRunner compaction", () => {
const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
"../../infra/agent-events.js",
);
realAgentEvents.resetAgentRunContextForTest();
const sessionEntry: SessionEntry = {
sessionId: "old-session",
updatedAt: Date.now(),
@@ -4751,14 +4751,12 @@ describe("createFollowupRunner compaction", () => {
expect(observedRunId).toBeDefined();
expect(realAgentEvents.getAgentRunContext(observedRunId ?? "")?.sessionId).toBe("new-session");
realAgentEvents.resetAgentRunContextForTest();
});
it("captures follow-up lifecycle ownership before asynchronous preflight", async () => {
const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
"../../infra/agent-events.js",
);
realAgentEvents.resetAgentRunContextForTest();
const initialGeneration = realAgentEvents.getAgentEventLifecycleGeneration();
let releasePreflight: (() => void) | undefined;
runPreflightCompactionIfNeededMock.mockImplementationOnce(
@@ -4826,7 +4824,6 @@ describe("createFollowupRunner compaction", () => {
expect(observedLifecycleGeneration).toBe(initialGeneration);
expect(realAgentEvents.getAgentRunContext(registeredRun?.runId ?? "")).toBeUndefined();
} finally {
realAgentEvents.resetAgentRunContextForTest();
}
});
});
-2
View File
@@ -16,7 +16,6 @@ import {
createChildDiagnosticTraceContext,
freezeDiagnosticTraceContext,
getActiveDiagnosticTraceContext,
resetDiagnosticTraceContextForTest,
} from "../../infra/diagnostic-trace-context.js";
import { logMessageProcessed } from "../../logging/diagnostic.js";
import { getChildLogger, resetLogger, setLoggerOverride } from "../../logging/logger.js";
@@ -192,7 +191,6 @@ describe("channel turn kernel", () => {
beforeEach(() => {
vi.clearAllMocks();
resetDiagnosticEventsForTest();
resetDiagnosticTraceContextForTest();
resetLogger();
setLoggerOverride({ level: "info" });
resolveOutboundDurableFinalDeliverySupport.mockResolvedValue({ ok: true });
+3 -1
View File
@@ -1,7 +1,9 @@
// Daemon restart health tests cover health checks after daemon restart operations.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { GatewayService } from "../../daemon/service.js";
import type { PortListenerKind, PortUsage } from "../../infra/ports.js";
import type { PortUsage } from "../../infra/ports.js";
type PortListenerKind = ReturnType<typeof import("../../infra/ports.js").classifyPortListener>;
const inspectPortUsage = vi.hoisted(() => vi.fn<(port: number) => Promise<PortUsage>>());
const sleep = vi.hoisted(() => vi.fn(async (_ms: number) => {}));
+5 -1
View File
@@ -5,13 +5,17 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { StaleOpenClawUpdateLaunchdJob } from "../../daemon/launchd.js";
import { createMockGatewayService } from "../../daemon/service.test-helpers.js";
import type { PortConnections, PortListener, PortUsageStatus } from "../../infra/ports.js";
import type { PortListener, PortUsageStatus } from "../../infra/ports.js";
import type { GatewayRestartHandoff } from "../../infra/restart-handoff.js";
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js";
import { VERSION } from "../../version.js";
import type { GatewayRestartSnapshot } from "./restart-health.js";
import { gatherDaemonStatus } from "./status.gather.js";
type PortConnections = Awaited<
ReturnType<typeof import("../../infra/ports.js").inspectPortConnections>
>;
const callGatewayStatusProbe = vi.fn<
(opts?: unknown) => Promise<{
ok: boolean;
+3
View File
@@ -1129,6 +1129,7 @@ describe("runGatewayLoop", () => {
expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(1);
expect(markGatewayDraining).toHaveBeenCalledTimes(1);
expect(resetAllLanes).toHaveBeenCalledTimes(1);
expect(resetGatewaySuspendCoordinatorForLifecycleRestart).toHaveBeenCalledTimes(1);
expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(1);
expect(reloadTaskRuntimeStateFromStore).toHaveBeenCalledTimes(1);
} finally {
@@ -1318,6 +1319,7 @@ describe("runGatewayLoop", () => {
expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2);
expect(markGatewayDraining).toHaveBeenCalledTimes(2);
expect(resetAllLanes).toHaveBeenCalledTimes(2);
expect(resetGatewaySuspendCoordinatorForLifecycleRestart).toHaveBeenCalledTimes(2);
expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(2);
expect(reloadTaskRuntimeStateFromStore).toHaveBeenCalledTimes(2);
expect(acquireGatewayLock).toHaveBeenCalledTimes(3);
@@ -1390,6 +1392,7 @@ describe("runGatewayLoop", () => {
expect(markGatewaySigusr1RestartHandled).toHaveBeenCalledTimes(2);
expect(markGatewayDraining).toHaveBeenCalledTimes(2);
expect(resetAllLanes).toHaveBeenCalledTimes(2);
expect(resetGatewaySuspendCoordinatorForLifecycleRestart).toHaveBeenCalledTimes(2);
expect(resetGatewayRestartStateForInProcessRestart).toHaveBeenCalledTimes(2);
expect(reloadTaskRuntimeStateFromStore).toHaveBeenCalledTimes(2);
expect(acquireGatewayLock).toHaveBeenCalledTimes(3);
+1 -1
View File
@@ -7,12 +7,12 @@ import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { Command } from "commander";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { writePackageDistInventory } from "../../scripts/lib/package-dist-inventory.ts";
import { TEST_BUNDLED_RUNTIME_SIDECAR_PATHS } from "../../test/helpers/bundled-runtime-sidecars.js";
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/types.openclaw.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import { GATEWAY_SERVICE_RUNTIME_PID_ENV } from "../daemon/constants.js";
import type { ClawHubRiskAcknowledgementRequest } from "../infra/clawhub-install-trust.js";
import { writePackageDistInventory } from "../infra/package-dist-inventory.js";
import { isBetaTag } from "../infra/update-channels.js";
import {
createDeferredConfiguredPluginRepairDoctorResult,
-2
View File
@@ -30,7 +30,6 @@ import {
emitAgentEvent,
onAgentEvent,
resetAgentEventsForTest,
resetAgentRunContextForTest,
} from "../infra/agent-events.js";
import type { PluginProviderRegistration } from "../plugins/registry.test-fixtures.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js";
@@ -393,7 +392,6 @@ beforeEach(() => {
installThinkingTestProviders();
clearSessionStoreCacheForTest();
resetAgentEventsForTest();
resetAgentRunContextForTest();
acpManagerTesting.resetAcpSessionManagerForTests();
runtimeSnapshotModule.clearRuntimeConfigSnapshot();
vi.mocked(runEmbeddedAgent).mockResolvedValue(createDefaultAgentResult());
-4
View File
@@ -2,16 +2,12 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { __testing as openClawRootTesting } from "../infra/openclaw-root.js";
import { resolveSandboxScript } from "./doctor-sandbox.js";
describe("resolveSandboxScript", () => {
const created: string[] = [];
afterEach(() => {
// The shared resolver memoizes package-root lookups process-wide; reset so per-test temp dirs
// never see a stale (including negative) cache hit.
openClawRootTesting.clearOpenClawPackageRootCaches();
for (const dir of created.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
@@ -12,7 +12,6 @@ import {
} from "../config/sessions/store.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { __testing as openClawRootTesting } from "../infra/openclaw-root.js";
import { AGENT_HARNESS_SESSION_KEY_RESERVED_MESSAGE } from "../sessions/agent-harness-session-key.js";
import type { Skill } from "../skills/loading/skill-contract.js";
@@ -126,7 +125,6 @@ describe("doctor session snapshot stale runtime metadata", () => {
});
afterEach(async () => {
openClawRootTesting.clearOpenClawPackageRootCaches();
await fs.rm(root, { recursive: true, force: true });
});
+3 -12
View File
@@ -20,10 +20,7 @@ interface BackupMaintenanceFs extends BackupRotationFs {
* Missing slots are ignored so interrupted writes or first-run configs do not
* block the next config write.
*/
export async function rotateConfigBackups(
configPath: string,
ioFs: BackupRotationFs,
): Promise<void> {
async function rotateConfigBackups(configPath: string, ioFs: BackupRotationFs): Promise<void> {
if (CONFIG_BACKUP_COUNT <= 1) {
return;
}
@@ -48,10 +45,7 @@ export async function rotateConfigBackups(
* Backups are copied on mixed filesystems, so copy mode preservation is not a
* portable security guarantee.
*/
export async function hardenBackupPermissions(
configPath: string,
ioFs: BackupRotationFs,
): Promise<void> {
async function hardenBackupPermissions(configPath: string, ioFs: BackupRotationFs): Promise<void> {
if (!ioFs.chmod) {
return;
}
@@ -67,10 +61,7 @@ export async function hardenBackupPermissions(
}
/** Prunes stale `.bak.*` files that are outside the managed numbered ring. */
export async function cleanOrphanBackups(
configPath: string,
ioFs: BackupRotationFs,
): Promise<void> {
async function cleanOrphanBackups(configPath: string, ioFs: BackupRotationFs): Promise<void> {
if (!ioFs.readdir) {
return;
}
+2 -2
View File
@@ -6,7 +6,7 @@ import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
import type { PluginOrigin } from "../plugins/plugin-origin.types.js";
import type { ChannelUiMetadata, PluginUiMetadata } from "./schema.js";
export type ChannelSchemaMetadataWithOwnership = ChannelUiMetadata & {
type ChannelSchemaMetadataWithOwnership = ChannelUiMetadata & {
schemaPluginId?: string;
schemaPluginOrigin?: PluginOrigin;
};
@@ -17,7 +17,7 @@ type ChannelMetadataRecord = ChannelSchemaMetadataWithOwnership & {
type ChannelDmAllowFromMode = "topOnly" | "topOrNested" | "nestedOnly";
export type ChannelDmPolicyMetadata = {
type ChannelDmPolicyMetadata = {
id: string;
dmAllowFromMode?: ChannelDmAllowFromMode;
};
+1 -2
View File
@@ -3,9 +3,8 @@ import path from "node:path";
import { beforeEach, describe, expect, it } from "vitest";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
import { isCommandFlagEnabled, isRestartEnabled } from "./commands.flags.js";
import {
isCommandFlagEnabled,
isRestartEnabled,
isNativeCommandsExplicitlyDisabled,
resolveNativeCommandsEnabled,
resolveNativeSkillsEnabled,
-1
View File
@@ -5,7 +5,6 @@ import { resolveReadOnlyChannelCommandDefaults } from "../channels/plugins/read-
import type { ChannelId } from "../channels/plugins/types.public.js";
import type { NativeCommandsSetting } from "./types.js";
import type { OpenClawConfig } from "./types.openclaw.js";
export { isCommandFlagEnabled, isRestartEnabled } from "./commands.flags.js";
function resolveAutoDefault(
providerId: ChannelId | undefined,
@@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
import {
DiscordConfigSchema,
IMessageConfigSchema,
IrcConfigSchema,
SignalConfigSchema,
SlackConfigSchema,
TelegramConfigSchema,
@@ -95,10 +94,10 @@ describe('account dmPolicy="allowlist" uses inherited allowFrom', () => {
schema: SlackConfigSchema,
config: {
allowFrom: ["U123"],
botToken: "xoxb-top",
appToken: "xapp-top",
botToken: "fake",
appToken: "fake",
accounts: {
work: { dmPolicy: "allowlist", botToken: "xoxb-work", appToken: "xapp-work" },
work: { dmPolicy: "allowlist", botToken: "fake", appToken: "fake" },
},
},
},
@@ -112,11 +111,6 @@ describe('account dmPolicy="allowlist" uses inherited allowFrom', () => {
schema: IMessageConfigSchema,
config: { allowFrom: ["alice"], accounts: { work: { dmPolicy: "allowlist" } } },
},
{
name: "irc",
schema: IrcConfigSchema,
config: { allowFrom: ["nick"], accounts: { work: { dmPolicy: "allowlist" } } },
},
] as const)(
"accepts $name account allowlist when parent allowFrom exists",
({ schema, config }) => {
+20 -112
View File
@@ -1,20 +1,13 @@
// Covers config backup rotation limits and cleanup behavior.
import fs from "node:fs/promises";
import { describe, expect, it } from "vitest";
import {
createPreUpdateConfigSnapshot,
maintainConfigBackups,
rotateConfigBackups,
hardenBackupPermissions,
cleanOrphanBackups,
} from "./backup-rotation.js";
import { createPreUpdateConfigSnapshot, maintainConfigBackups } from "./backup-rotation.js";
import {
expectPosixMode,
IS_WINDOWS,
resolveConfigPathFromTempState,
} from "./config.backup-rotation.test-helpers.js";
import { withTempHome } from "./test-helpers.js";
import type { OpenClawConfig } from "./types.js";
async function expectRegularFile(filePath: string): Promise<void> {
expect((await fs.stat(filePath)).isFile()).toBe(true);
@@ -31,94 +24,35 @@ async function expectPathMissing(filePath: string): Promise<void> {
}
describe("config backup rotation", () => {
it("keeps a 5-deep backup ring for config writes", async () => {
it("keeps five recovery points while preserving the pre-update snapshot", async () => {
await withTempHome(async () => {
const configPath = resolveConfigPathFromTempState();
const buildConfig = (version: number): OpenClawConfig =>
({
agents: { list: [{ id: `v${version}` }] },
}) as OpenClawConfig;
const writeVersion = async (version: number) => {
const json = JSON.stringify(buildConfig(version), null, 2).trimEnd().concat("\n");
await fs.writeFile(configPath, json, "utf-8");
const writeVersion = (version: number) =>
fs.writeFile(configPath, JSON.stringify({ version }), "utf-8");
const readVersion = async (suffix = "") => {
const raw = await fs.readFile(`${configPath}${suffix}`, "utf-8");
return (JSON.parse(raw) as { version: number }).version;
};
const { existsSync } = await import("node:fs");
await writeVersion(0);
await createPreUpdateConfigSnapshot({
configPath,
fs: { writeFile: fs.writeFile, readFile: fs.readFile, existsSync },
});
for (let version = 1; version <= 6; version += 1) {
await rotateConfigBackups(configPath, fs);
await fs.copyFile(configPath, `${configPath}.bak`).catch(() => {
// best-effort
});
await maintainConfigBackups(configPath, fs);
await writeVersion(version);
}
const readName = async (suffix = "") => {
const raw = await fs.readFile(`${configPath}${suffix}`, "utf-8");
return (
(JSON.parse(raw) as { agents?: { list?: Array<{ id?: string }> } }).agents?.list?.[0]
?.id ?? null
);
};
await expect(readName()).resolves.toBe("v6");
await expect(readName(".bak")).resolves.toBe("v5");
await expect(readName(".bak.1")).resolves.toBe("v4");
await expect(readName(".bak.2")).resolves.toBe("v3");
await expect(readName(".bak.3")).resolves.toBe("v2");
await expect(readName(".bak.4")).resolves.toBe("v1");
await expect(readVersion()).resolves.toBe(6);
await expect(readVersion(".bak")).resolves.toBe(5);
await expect(readVersion(".bak.1")).resolves.toBe(4);
await expect(readVersion(".bak.2")).resolves.toBe(3);
await expect(readVersion(".bak.3")).resolves.toBe(2);
await expect(readVersion(".bak.4")).resolves.toBe(1);
await expectPathMissing(`${configPath}.bak.5`);
});
});
// chmod is a no-op on Windows — 0o600 can never be observed there.
it.skipIf(IS_WINDOWS)("hardenBackupPermissions sets 0o600 on all backup files", async () => {
await withTempHome(async () => {
const configPath = resolveConfigPathFromTempState();
// Create .bak and .bak.1 with permissive mode
await fs.writeFile(`${configPath}.bak`, "secret", { mode: 0o644 });
await fs.writeFile(`${configPath}.bak.1`, "secret", { mode: 0o644 });
await hardenBackupPermissions(configPath, fs);
const bakStat = await fs.stat(`${configPath}.bak`);
const bak1Stat = await fs.stat(`${configPath}.bak.1`);
expectPosixMode(bakStat.mode, 0o600);
expectPosixMode(bak1Stat.mode, 0o600);
});
});
it("cleanOrphanBackups removes stale files outside the rotation ring", async () => {
await withTempHome(async () => {
const configPath = resolveConfigPathFromTempState();
// Create valid backups
await fs.writeFile(configPath, "current");
await fs.writeFile(`${configPath}.bak`, "backup-0");
await fs.writeFile(`${configPath}.bak.1`, "backup-1");
await fs.writeFile(`${configPath}.bak.2`, "backup-2");
// Create orphans
await fs.writeFile(`${configPath}.bak.1772352289`, "orphan-pid");
await fs.writeFile(`${configPath}.bak.before-marketing`, "orphan-manual");
await fs.writeFile(`${configPath}.bak.99`, "orphan-overflow");
await cleanOrphanBackups(configPath, fs);
// Valid backups preserved
await expectRegularFile(`${configPath}.bak`);
await expectRegularFile(`${configPath}.bak.1`);
await expectRegularFile(`${configPath}.bak.2`);
// Orphans removed
await expectPathMissing(`${configPath}.bak.1772352289`);
await expectPathMissing(`${configPath}.bak.before-marketing`);
await expectPathMissing(`${configPath}.bak.99`);
// Main config untouched
await expect(fs.readFile(configPath, "utf-8")).resolves.toBe("current");
await expect(readVersion(".pre-update")).resolves.toBe(0);
});
});
@@ -170,31 +104,6 @@ describe("config backup rotation", () => {
});
});
it("createPreUpdateConfigSnapshot survives multiple config writes", async () => {
await withTempHome(async () => {
const configPath = resolveConfigPathFromTempState();
const original = JSON.stringify({ version: "original" });
await fs.writeFile(configPath, original, { mode: 0o600 });
const { existsSync } = await import("node:fs");
await createPreUpdateConfigSnapshot({
configPath,
fs: { writeFile: fs.writeFile, readFile: fs.readFile, existsSync },
});
// Simulate multiple config writes + backup rotations
for (let i = 0; i < 7; i++) {
await rotateConfigBackups(configPath, fs);
await fs.copyFile(configPath, `${configPath}.bak`);
await fs.writeFile(configPath, JSON.stringify({ version: `write-${i}` }));
}
// .pre-update still holds the original content
const snapshotPath = `${configPath}.pre-update`;
await expect(fs.readFile(snapshotPath, "utf-8")).resolves.toBe(original);
});
});
it("createPreUpdateConfigSnapshot replaces a preexisting snapshot once per process", async () => {
await withTempHome(async () => {
const configPath = resolveConfigPathFromTempState();
@@ -261,7 +170,6 @@ describe("config backup rotation", () => {
});
await expectPathMissing(`${configPath}.pre-update`);
// The failed attempt must not suppress the next snapshot pass.
await createPreUpdateConfigSnapshot({
configPath,
fs: { writeFile: fs.writeFile, readFile: fs.readFile, existsSync },
+6 -6
View File
@@ -16,7 +16,7 @@ import {
} from "./config-env-vars.js";
import { resolveConfigEnvVars } from "./env-substitution.js";
import { assertGatewayConfigEnvSelectionUnchanged } from "./gateway-env-selection.js";
import { collectDurableServiceEnvVars, readStateDirDotEnvVars } from "./state-dir-dotenv.js";
import { collectDurableServiceEnvVars } from "./state-dir-dotenv.js";
import { withEnvOverride, withTempHome, writeStateDirDotEnv } from "./test-helpers.js";
import type { OpenClawConfig } from "./types.js";
@@ -598,7 +598,7 @@ describe("config env vars", () => {
await writeStateDirDotEnv("BRAVE_API_KEY=BSA-test-key\nDISCORD_BOT_TOKEN=discord-tok\n", {
env: process.env,
});
const vars = readStateDirDotEnvVars(process.env);
const vars = collectDurableServiceEnvVars({ env: process.env });
expect(vars.BRAVE_API_KEY).toBe("BSA-test-key");
expect(vars.DISCORD_BOT_TOKEN).toBe("discord-tok");
});
@@ -606,7 +606,7 @@ describe("config env vars", () => {
it("returns empty record when the state-dir .env file is missing", async () => {
await withTempHome(async (_home) => {
expect(readStateDirDotEnvVars(process.env)).toStrictEqual({});
expect(collectDurableServiceEnvVars({ env: process.env })).toStrictEqual({});
});
});
@@ -616,7 +616,7 @@ describe("config env vars", () => {
"NODE_OPTIONS=--require /tmp/evil.js\nOPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS=1\nEMPTY=\nVALID=ok\n",
{ env: process.env },
);
const vars = readStateDirDotEnvVars(process.env);
const vars = collectDurableServiceEnvVars({ env: process.env });
expect(vars.NODE_OPTIONS).toBeUndefined();
expect(vars.OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS).toBeUndefined();
expect(vars.EMPTY).toBeUndefined();
@@ -631,8 +631,8 @@ describe("config env vars", () => {
stateDir: customStateDir,
});
expect(
readStateDirDotEnvVars({
OPENCLAW_STATE_DIR: customStateDir,
collectDurableServiceEnvVars({
env: { OPENCLAW_STATE_DIR: customStateDir },
}).CUSTOM_KEY,
).toBe("from-override");
});
+1 -1
View File
@@ -3,7 +3,7 @@ import type { CronConfig } from "./types.cron.js";
/** Default maximum number of cron jobs allowed to run at once. */
export const DEFAULT_CRON_MAX_CONCURRENT_RUNS = 8;
export const DEFAULT_CRON_TRIGGER_MIN_INTERVAL_MS = 30_000;
const DEFAULT_CRON_TRIGGER_MIN_INTERVAL_MS = 30_000;
/** Resolves cron concurrency config, flooring finite values and clamping to at least one. */
export function resolveCronMaxConcurrentRuns(
-1
View File
@@ -5,4 +5,3 @@ export {
createConfigRuntimeEnv,
isConfigRuntimeEnvVarAllowed,
} from "./config-env-vars.js";
export { collectDurableServiceEnvVars, readStateDirDotEnvVars } from "./state-dir-dotenv.js";
+33 -37
View File
@@ -9,9 +9,6 @@ import {
CircularIncludeError,
ConfigIncludeError,
MAX_INCLUDE_DEPTH,
MAX_INCLUDE_FILE_BYTES,
MAX_INCLUDE_PATH_LENGTH,
deepMerge,
type IncludeResolver,
resolveConfigIncludeWritePath,
resolveConfigIncludes,
@@ -722,30 +719,24 @@ describe("security: path traversal protection (CWE-22)", () => {
});
describe("prototype pollution protection", () => {
it("blocks prototype pollution vectors in shallow and nested merges", () => {
const cases = [
{
base: {},
incoming: JSON.parse('{"__proto__":{"polluted":true}}'),
expected: {},
},
{
base: { safe: 1 },
incoming: { prototype: { x: 1 }, constructor: { y: 2 }, normal: 3 },
expected: { safe: 1, normal: 3 },
},
{
base: { nested: { a: 1 } },
incoming: { nested: JSON.parse('{"__proto__":{"polluted":true}}') },
expected: { nested: { a: 1 } },
},
] as const;
it("blocks prototype pollution vectors in included and sibling config", () => {
const includePath = configPath("pollution.json");
const included = JSON.parse(
'{"__proto__":{"polluted":true},"constructor":{"hidden":true},"normal":3}',
) as Record<string, unknown>;
const sibling = JSON.parse('{"__proto__":{"alsoPolluted":true},"safe":1}') as Record<
string,
unknown
>;
for (const { base, incoming, expected } of cases) {
const result = deepMerge(base, incoming);
expect((Object.prototype as Record<string, unknown>).polluted).toBeUndefined();
expect(result).toEqual(expected);
}
const result = resolve(
{ $include: "./pollution.json", ...sibling },
{ [includePath]: included },
);
expect((Object.prototype as Record<string, unknown>).polluted).toBeUndefined();
expect((Object.prototype as Record<string, unknown>).alsoPolluted).toBeUndefined();
expect(result).toEqual({ normal: 3, safe: 1 });
});
});
@@ -762,12 +753,12 @@ describe("security: path traversal protection (CWE-22)", () => {
}
});
it("rejects include path at or over maximum length (>= MAX_INCLUDE_PATH_LENGTH)", () => {
const overLimit = "a".repeat(MAX_INCLUDE_PATH_LENGTH + 1);
expectResolveIncludeError(() => resolve({ $include: overLimit }, {}), /maximum length/);
// Boundary: length exactly 4096 must be rejected (Linux PATH_MAX includes NUL)
const atLimit = "b".repeat(MAX_INCLUDE_PATH_LENGTH);
expectResolveIncludeError(() => resolve({ $include: atLimit }, {}), /maximum length/);
it("rejects include paths at or over the platform-safe maximum", () => {
expectResolveIncludeError(() => resolve({ $include: "a".repeat(4096) }, {}), /maximum length/);
expectResolveIncludeError(
() => resolve({ $include: "b".repeat(4097) }, {}),
/maximum length/,
);
});
it("accepts include path at or under maximum length when file exists", () => {
@@ -861,16 +852,21 @@ describe("security: path traversal protection (CWE-22)", () => {
});
});
it("rejects oversized include files", async () => {
it("rejects include files larger than the guarded read limit", async () => {
await withTempDir({ prefix: "openclaw-includes-big-" }, async (tempRoot) => {
const configDir = path.join(tempRoot, "config");
await fs.mkdir(configDir, { recursive: true });
const includePath = path.join(configDir, "big.json5");
const payload = "a".repeat(MAX_INCLUDE_FILE_BYTES + 1);
await fs.writeFile(includePath, `{"blob":"${payload}"}`, "utf-8");
await fs.writeFile(
path.join(configDir, "big.json5"),
`{"blob":"${"a".repeat(2 * 1024 * 1024 + 1)}"}`,
"utf-8",
);
expect(() =>
resolveConfigIncludes({ $include: "./big.json5" }, path.join(configDir, "openclaw.json")),
resolveConfigIncludes(
{ $include: "./big.json5" },
path.join(configDir, "openclaw.json"),
),
).toThrow(/security checks|max/i);
});
});
+3 -3
View File
@@ -23,10 +23,10 @@ import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
export const INCLUDE_KEY = "$include";
export const MAX_INCLUDE_DEPTH = 10;
export const MAX_INCLUDE_FILE_BYTES = 2 * 1024 * 1024;
const MAX_INCLUDE_FILE_BYTES = 2 * 1024 * 1024;
/** Maximum length for $include path and resolved path (CWE-22 hardening). */
export const MAX_INCLUDE_PATH_LENGTH = 4096;
const MAX_INCLUDE_PATH_LENGTH = 4096;
export function hashConfigIncludeRaw(raw: string | null): string {
const hash = crypto.createHash("sha256");
@@ -136,7 +136,7 @@ export class CircularIncludeError extends ConfigIncludeError {
// ============================================================================
/** Deep merge: arrays concatenate, objects merge recursively, primitives: source wins */
export function deepMerge(target: unknown, source: unknown): unknown {
function deepMerge(target: unknown, source: unknown): unknown {
return mergeDeepValues(target, source, { arrays: "concat", undefinedValues: "replace" });
}
+42 -161
View File
@@ -9,12 +9,11 @@ import {
createConfigWriteAuditRecordBase,
finalizeConfigWriteAuditRecord,
formatConfigOverwriteLogMessage,
redactConfigAuditArgv,
resolveConfigAuditLogPath,
scrubConfigAuditLog,
} from "./io.audit.js";
function createAuditRecordBase(configPath: string) {
function createAuditRecordBase(configPath: string, argv?: string[]) {
return createConfigWriteAuditRecordBase({
configPath,
env: {} as NodeJS.ProcessEnv,
@@ -38,6 +37,17 @@ function createAuditRecordBase(configPath: string) {
gatewayModeAfter: "local",
suspicious: [],
now: "2026-04-07T08:00:00.000Z",
...(argv
? {
processInfo: {
pid: 101,
ppid: 99,
cwd: "/work",
argv,
execArgv: [],
},
}
: {}),
});
}
@@ -235,165 +245,6 @@ describe("config io audit helpers", () => {
expect(raw).not.toContain("abcd-efgh-ijkl-mnop");
});
it("redacts argv values that follow known secret flag names", () => {
const argv = [
"node",
"openclaw",
"gateway",
"--token",
"super-secret-gateway-token-12345",
"--api-key",
"sk-very-real-looking-openai-api-key-AB12CD34",
"--port",
"8080",
];
const result = redactConfigAuditArgv(argv);
expect(result).toEqual([
"node",
"openclaw",
"gateway",
"--token",
"***",
"--api-key",
"***",
"--port",
"8080",
]);
});
it("redacts the value half of `--flag=value` for secret flags", () => {
const argv = ["openclaw", "--token=ghp_realgithubtoken1234567890ABCD", "--port=8080"];
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token=***", "--port=8080"]);
});
it("redacts standalone token shapes via the shared logging redaction patterns", () => {
const argv = [
"node",
"openclaw",
"ghp_realgithubtoken1234567890ABCD",
"AIzaSyD-very-real-looking-google-api-key-123",
"987654321:AAAAAAAAAAAAAAAAAAAAAAAAAAAA",
];
const result = redactConfigAuditArgv(argv);
expect(result[0]).toBe("node");
expect(result[1]).toBe("openclaw");
for (const masked of result.slice(2)) {
expect(masked).not.toContain("ghp_realgithubtoken");
expect(masked).not.toContain("AIzaSyD-very-real-looking");
expect(masked).not.toMatch(/AAAAAAAAAAAAAA/);
}
});
it("leaves non-secret arguments untouched", () => {
const argv = ["node", "openclaw", "gateway", "--port", "8080", "--bind", "lan"];
expect(redactConfigAuditArgv(argv)).toEqual(argv);
});
it("redacts unknown but credential-suffixed flags via the heuristic classifier", () => {
const argv = [
"node",
"openclaw",
"--custom-api-key",
"real-tenant-key-AB12CD34EF56GH78",
"--alibaba-model-studio-api-key=plain-value-xyz-12345",
"--app-token",
"another-secret-value",
"--frobnicate-credential=hidden",
];
const result = redactConfigAuditArgv(argv);
expect(result).toEqual([
"node",
"openclaw",
"--custom-api-key",
"***",
"--alibaba-model-studio-api-key=***",
"--app-token",
"***",
"--frobnicate-credential=***",
]);
});
it("redacts key-valued secret flags (Nostr --private-key, Matrix --recovery-key)", () => {
const argv = [
"node",
"openclaw",
"channels",
"add",
"--channel",
"nostr",
"--private-key",
"nsec1realnostrprivatekeyvaluexyz1234567890",
"--recovery-key=EsTb-ABCD-1234-EFGH-5678-IJKL-9012-MNOP",
];
const result = redactConfigAuditArgv(argv);
expect(result).toEqual([
"node",
"openclaw",
"channels",
"add",
"--channel",
"nostr",
"--private-key",
"***",
"--recovery-key=***",
]);
});
it("redacts unknown *-key flags via the heuristic classifier (private/signing/master/etc.)", () => {
const argv = [
"node",
"openclaw",
"--my-plugin-private-key",
"tenant-private-key-material-zzz",
"--rotated-signing-key=PEM-LIKE-MATERIAL",
"--ops-master-key",
"ABCDEF1234567890",
"--secret-key",
"opaque-secret-key",
"--aws-secret-access-key=opaque-aws-secret",
"--openai_api_key",
"opaque-underscore-key",
"--aws_secret_access_key=opaque-underscore-aws-secret",
"--credentials",
"opaque-plural-credential",
"--service-credentials=opaque-inline-plural-credential",
];
const result = redactConfigAuditArgv(argv);
expect(result).toEqual([
"node",
"openclaw",
"--my-plugin-private-key",
"***",
"--rotated-signing-key=***",
"--ops-master-key",
"***",
"--secret-key",
"***",
"--aws-secret-access-key=***",
"--openai_api_key",
"***",
"--aws_secret_access_key=***",
"--credentials",
"***",
"--service-credentials=***",
]);
});
it("masks the next arg after a secret flag even when it looks like another option", () => {
const argv = ["openclaw", "--token", "--port", "8080"];
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token", "***", "8080"]);
});
it("redacts dash-leading secret values after bare secret flags", () => {
const argv = ["openclaw", "--password", "-secret-value"];
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--password", "***"]);
});
it("does not mask when a secret flag is the final arg with no value", () => {
const argv = ["openclaw", "--token"];
expect(redactConfigAuditArgv(argv)).toEqual(["openclaw", "--token"]);
});
it("caps caller-supplied processInfo argv at 8 entries before redaction", () => {
const longArgv = [
"node",
@@ -478,6 +329,36 @@ describe("config io audit helpers", () => {
expect(base.argv).toEqual(["node", "openclaw", "--token", "***"]);
});
it.each([
{
name: "inline known secret",
argv: ["openclaw", "--token=fake", "--port=8080"],
expected: ["openclaw", "--token=***", "--port=8080"],
},
{
name: "custom credential suffix",
argv: ["openclaw", "--tenant-credential", "fake", "--bind", "lan"],
expected: ["openclaw", "--tenant-credential", "***", "--bind", "lan"],
},
{
name: "underscore key suffix",
argv: ["openclaw", "--provider_api_key", "fake"],
expected: ["openclaw", "--provider_api_key", "***"],
},
{
name: "dash-leading secret value",
argv: ["openclaw", "--password", "-fake"],
expected: ["openclaw", "--password", "***"],
},
{
name: "secret flag without a value",
argv: ["openclaw", "--token"],
expected: ["openclaw", "--token"],
},
])("redacts $name in persisted audit process info", ({ argv, expected }) => {
expect(createAuditRecordBase("/tmp/openclaw.json", argv).argv).toEqual(expected);
});
it("also accepts flattened audit record params from legacy call sites", async () => {
const home = await suiteRootTracker.make("append-flat");
const record = createRenameAuditRecord(home);
+1 -1
View File
@@ -6,7 +6,7 @@ import { redactSensitiveArgv } from "./redact-argv.js";
const CONFIG_AUDIT_ARGV_CAP = 8;
export function redactConfigAuditArgv(argv: readonly string[]): string[] {
function redactConfigAuditArgv(argv: readonly string[]): string[] {
return redactSensitiveArgv(argv);
}
+1 -1
View File
@@ -34,7 +34,7 @@ export type ConfigHealthState = {
type ConfigHealthDatabase = Pick<OpenClawStateKyselyDatabase, "config_health_entries">;
export type ConfigHealthStateDeps = {
type ConfigHealthStateDeps = {
env: NodeJS.ProcessEnv;
homedir: () => string;
logger: Pick<typeof console, "warn">;
+14 -36
View File
@@ -3,9 +3,7 @@ import { describe, expect, it, vi } from "vitest";
import {
createInvalidConfigError,
formatInvalidConfigDetails,
formatInvalidConfigLogMessage,
isInvalidConfigError,
logInvalidConfigOnce,
throwInvalidConfig,
} from "./io.invalid-config.js";
@@ -28,12 +26,6 @@ describe("config io invalid config formatting", () => {
expect(details).toContain("- <root>: root problem");
});
it("formats the logger message with the escaped newline separator", () => {
expect(formatInvalidConfigLogMessage("/tmp/openclaw.json", "- gateway.port: bad")).toBe(
"Invalid config at /tmp/openclaw.json:\\n- gateway.port: bad",
);
});
it("creates INVALID_CONFIG errors with inline details", () => {
const err = createInvalidConfigError("/tmp/openclaw.json", "- gateway.port: bad") as Error & {
code?: string;
@@ -51,40 +43,26 @@ describe("config io invalid config formatting", () => {
expect(isInvalidConfigError(new Error(err.message))).toBe(false);
});
it("logs invalid config details only once per path", () => {
it("throws INVALID_CONFIG after logging the formatted details once per path", () => {
const logger = { error: vi.fn() };
const loggedConfigPaths = new Set<string>();
logInvalidConfigOnce({
configPath: "/tmp/openclaw.json",
details: "- gateway.port: bad",
logger,
loggedConfigPaths,
});
logInvalidConfigOnce({
configPath: "/tmp/openclaw.json",
details: "- gateway.port: worse",
logger,
loggedConfigPaths,
});
expect(logger.error).toHaveBeenCalledOnce();
expect(logger.error).toHaveBeenCalledWith(
"Invalid config at /tmp/openclaw.json:\\n- gateway.port: bad",
);
});
it("throws INVALID_CONFIG after logging the formatted details", () => {
const logger = { error: vi.fn() };
expect(() =>
const throwInvalid = () =>
throwInvalidConfig({
configPath: "/tmp/openclaw.json",
issues: [{ path: "nope", message: "Unknown key(s): nope" }],
logger,
loggedConfigPaths: new Set<string>(),
}),
).toThrowError("Invalid config at /tmp/openclaw.json:\n- nope: Unknown key(s): nope");
loggedConfigPaths,
});
expect(throwInvalid).toThrowError(
"Invalid config at /tmp/openclaw.json:\n- nope: Unknown key(s): nope",
);
expect(throwInvalid).toThrowError(
"Invalid config at /tmp/openclaw.json:\n- nope: Unknown key(s): nope",
);
expect(logger.error).toHaveBeenCalledOnce();
expect(logger.error).toHaveBeenCalledWith(
"Invalid config at /tmp/openclaw.json:\\n- nope: Unknown key(s): nope",
);
});
});
+2 -2
View File
@@ -23,12 +23,12 @@ export function formatInvalidConfigDetails(issues: ConfigValidationIssueLike[]):
}
/** Builds the one-line invalid-config prefix plus preformatted validation details. */
export function formatInvalidConfigLogMessage(configPath: string, details: string): string {
function formatInvalidConfigLogMessage(configPath: string, details: string): string {
return `Invalid config at ${configPath}:\\n${details}`;
}
/** Logs an invalid config message once per path during a load sequence. */
export function logInvalidConfigOnce(params: {
function logInvalidConfigOnce(params: {
configPath: string;
details: string;
logger: Pick<typeof console, "error">;
+5 -2
View File
@@ -17,13 +17,16 @@ import {
maybeRecoverSuspiciousConfigReadSync,
promoteConfigSnapshotToLastKnownGood,
recoverConfigFromLastKnownGood,
resolveLastKnownGoodConfigPath,
type ObserveRecoveryDeps,
} from "./io.observe-recovery.js";
import type { ConfigFileSnapshot } from "./types.js";
const CONFIG_CLOBBER_SNAPSHOT_LIMIT = 32;
type ConfigHealthDatabase = Pick<OpenClawStateKyselyDatabase, "config_health_entries">;
type ObserveRecoveryDeps = Parameters<typeof maybeRecoverSuspiciousConfigRead>[0]["deps"];
function resolveLastKnownGoodConfigPath(configPath: string): string {
return `${configPath}.last-good`;
}
describe("config observe recovery", () => {
let fixtureRoot = "";
+2 -2
View File
@@ -27,7 +27,7 @@ import {
import type { ConfigFileSnapshot } from "./types.openclaw.js";
/** Dependencies injected into config recovery observation for testable filesystem behavior. */
export type ObserveRecoveryDeps = {
type ObserveRecoveryDeps = {
fs: {
promises: {
stat(path: string): Promise<{
@@ -570,7 +570,7 @@ function readConfigFingerprintForPathSync(
}
}
export function resolveLastKnownGoodConfigPath(configPath: string): string {
function resolveLastKnownGoodConfigPath(configPath: string): string {
return `${configPath}.last-good`;
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { isRecord } from "../utils.js";
export type ConfigObserveSuspiciousBaseline = {
type ConfigObserveSuspiciousBaseline = {
bytes: number;
hasMeta: boolean;
gatewayMode: string | null;
+5 -4
View File
@@ -1,11 +1,12 @@
// Verifies owner display secrets stay redacted in config IO paths.
import { describe, expect, it } from "vitest";
import {
type OwnerDisplaySecretRuntimeState,
retainGeneratedOwnerDisplaySecret,
} from "./io.owner-display-secret.js";
import { retainGeneratedOwnerDisplaySecret } from "./io.owner-display-secret.js";
import type { OpenClawConfig } from "./types.openclaw.js";
type OwnerDisplaySecretRuntimeState = Parameters<
typeof retainGeneratedOwnerDisplaySecret
>[0]["state"];
function createState(): OwnerDisplaySecretRuntimeState {
return {
pendingByPath: new Map<string, string>(),
+1 -1
View File
@@ -5,7 +5,7 @@
import type { OpenClawConfig } from "./types.openclaw.js";
/** Runtime-only owner display secrets keyed by config path during config IO. */
export type OwnerDisplaySecretRuntimeState = {
type OwnerDisplaySecretRuntimeState = {
pendingByPath: Map<string, string>;
};
+40 -100
View File
@@ -8,7 +8,6 @@ import {
restoreEnvRefsFromMap,
resolvePersistCandidateForWrite,
resolveWriteEnvSnapshotForPath,
unsetPathForWrite,
} from "./io.write-prepare.js";
import type { OpenClawConfig } from "./types.js";
@@ -504,6 +503,46 @@ describe("config io write prepare", () => {
expect(persisted.agents?.defaults?.models?.["openai/gpt-5.4"]).not.toHaveProperty("params");
});
it("applies explicit unsets without mutating caller config", () => {
const input: OpenClawConfig = {
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
tools: { alsoAllow: ["exec", "fetch", "read"] },
};
const next = applyUnsetPathsForWrite(input, [
["commands", "ownerDisplay"],
["tools", "alsoAllow", "1"],
]);
expect(input).toEqual({
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
tools: { alsoAllow: ["exec", "fetch", "read"] },
});
expect(next.commands ?? {}).not.toHaveProperty("ownerDisplay");
expect(next.tools?.alsoAllow).toEqual(["exec", "read"]);
});
it.each([
["invalid array suffix", ["tools", "alsoAllow", "1abc"]],
["signed array index", ["tools", "alsoAllow", "+0"]],
["unsafe integer", ["tools", "alsoAllow", "9007199254740993"]],
["maximum array key", ["tools", "alsoAllow", "4294967294"]],
["missing key", ["commands", "missingKey"]],
["prototype key", ["commands", "__proto__"]],
["constructor key", ["commands", "constructor"]],
["prototype constructor property", ["commands", "prototype"]],
] as const)("treats %s unset paths as immutable no-ops", (_name, unsetPath) => {
const input: OpenClawConfig = {
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
tools: { alsoAllow: ["exec", "fetch"] },
};
expect(applyUnsetPathsForWrite(input, [[...unsetPath]])).toBe(input);
});
it("preserves untouched include-owned subtrees during unrelated writes", () => {
const persisted = resolvePersistCandidateForWrite({
runtimeConfig: {
@@ -822,105 +861,6 @@ describe("config io write prepare", () => {
expect(message).toContain('openclaw config set channels.telegram.dmPolicy "pairing"');
});
it("unsets explicit paths when runtime defaults would otherwise reappear", () => {
const next = unsetPathForWrite(
{
gateway: { auth: { mode: "none" } },
commands: { ownerDisplay: "hash" },
},
["commands", "ownerDisplay"],
);
expect(next.changed).toBe(true);
expect(next.next.commands ?? {}).not.toHaveProperty("ownerDisplay");
});
it("does not mutate caller config when unsetting existing config objects", () => {
const input: OpenClawConfig = {
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
} satisfies OpenClawConfig;
const next = unsetPathForWrite(input, ["commands", "ownerDisplay"]);
expect(input).toEqual({
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
});
expect(next.next.commands ?? {}).not.toHaveProperty("ownerDisplay");
});
it("keeps caller arrays immutable when unsetting array entries", () => {
const input: OpenClawConfig = {
gateway: { mode: "local" },
tools: { alsoAllow: ["exec", "fetch", "read"] },
} satisfies OpenClawConfig;
const next = unsetPathForWrite(input, ["tools", "alsoAllow", "1"]);
expect(input.tools!.alsoAllow).toEqual(["exec", "fetch", "read"]);
expect((next.next.tools as { alsoAllow?: string[] } | undefined)?.alsoAllow).toEqual([
"exec",
"read",
]);
});
it("treats invalid array-index unset paths as no-ops", () => {
const input: OpenClawConfig = {
gateway: { mode: "local" },
tools: { alsoAllow: ["exec", "fetch"] },
} satisfies OpenClawConfig;
for (const path of [
["tools", "alsoAllow", "1abc"],
["tools", "alsoAllow", "+0"],
["tools", "alsoAllow", "9007199254740993"],
["tools", "alsoAllow", "4294967294"],
]) {
const next = unsetPathForWrite(input, path);
expect(next.changed).toBe(false);
expect(next.next).toBe(input);
}
});
it("treats missing unset paths as no-op without mutating caller config", () => {
const input: OpenClawConfig = {
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
} satisfies OpenClawConfig;
const next = unsetPathForWrite(input, ["commands", "missingKey"]);
expect(next.changed).toBe(false);
expect(next.next).toBe(input);
expect(input).toEqual({
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
});
});
it("ignores blocked prototype-key unset path segments", () => {
const input: OpenClawConfig = {
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
} satisfies OpenClawConfig;
const blocked = [
["commands", "__proto__"],
["commands", "constructor"],
["commands", "prototype"],
].map((segments) => unsetPathForWrite(input, segments));
for (const result of blocked) {
expect(result.changed).toBe(false);
expect(result.next).toBe(input);
}
expect(input).toEqual({
gateway: { mode: "local" },
commands: { ownerDisplay: "hash" },
});
});
it("preserves env refs on unchanged paths while keeping changed paths resolved", () => {
const changedPaths = new Set<string>();
collectChangedPaths(
+1 -1
View File
@@ -1082,7 +1082,7 @@ function unsetPathForWriteAt(
};
}
export function unsetPathForWrite(
function unsetPathForWrite(
root: OpenClawConfig,
pathSegments: string[],
): { changed: boolean; next: OpenClawConfig } {
+2 -43
View File
@@ -4,19 +4,10 @@ import {
formatConfigIssueLine,
formatConfigIssueLines,
formatConfigIssueSummary,
normalizeConfigIssue,
normalizeConfigIssuePath,
normalizeConfigIssues,
} from "./issue-format.js";
describe("config issue format", () => {
it("normalizes empty paths to <root>", () => {
expect(normalizeConfigIssuePath("")).toBe("<root>");
expect(normalizeConfigIssuePath(" ")).toBe("<root>");
expect(normalizeConfigIssuePath(null)).toBe("<root>");
expect(normalizeConfigIssuePath(undefined)).toBe("<root>");
});
it("formats issue lines with and without markers", () => {
expect(formatConfigIssueLine({ path: "", message: "broken" }, "-")).toBe("- : broken");
expect(
@@ -63,20 +54,7 @@ describe("config issue format", () => {
).toBe("<root>: root broken; gateway.auth.password.source: Required; and 1 more");
});
it("normalizes issue metadata for machine output", () => {
expect(
normalizeConfigIssue({
path: "",
message: "invalid",
allowedValues: ["stable", "beta"],
allowedValuesHiddenCount: 0,
}),
).toEqual({
path: "<root>",
message: "invalid",
allowedValues: ["stable", "beta"],
});
it("normalizes issue collections for machine output", () => {
expect(
normalizeConfigIssues([
{
@@ -86,25 +64,6 @@ describe("config issue format", () => {
allowedValuesHiddenCount: 2,
},
]),
).toEqual([
{
path: "update.channel",
message: "invalid",
},
]);
expect(
normalizeConfigIssue({
path: "update.channel",
message: "invalid",
allowedValues: ["stable"],
allowedValuesHiddenCount: 2,
}),
).toEqual({
path: "update.channel",
message: "invalid",
allowedValues: ["stable"],
allowedValuesHiddenCount: 2,
});
).toEqual([{ path: "update.channel", message: "invalid" }]);
});
});
+2 -2
View File
@@ -16,7 +16,7 @@ type ConfigIssueSummaryOptions = ConfigIssueFormatOptions & {
};
/** Normalize missing or blank config issue paths to the root marker used in CLI output. */
export function normalizeConfigIssuePath(path: string | null | undefined): string {
function normalizeConfigIssuePath(path: string | null | undefined): string {
if (typeof path !== "string") {
return "<root>";
}
@@ -25,7 +25,7 @@ export function normalizeConfigIssuePath(path: string | null | undefined): strin
}
/** Return the public config issue shape with a normalized path and non-empty allowed values. */
export function normalizeConfigIssue(issue: ConfigValidationIssue): ConfigValidationIssue {
function normalizeConfigIssue(issue: ConfigValidationIssue): ConfigValidationIssue {
const hasAllowedValues = Array.isArray(issue.allowedValues) && issue.allowedValues.length > 0;
return {
path: normalizeConfigIssuePath(issue.path),
-5
View File
@@ -63,11 +63,6 @@ function resolveMarkdownModeFromSection(
return isMarkdownTableMode(sectionMode) ? sectionMode : undefined;
}
export type {
ResolveMarkdownTableMode,
ResolveMarkdownTableModeParams,
} from "./markdown-tables.types.js";
export function resolveMarkdownTableMode(
params: ResolveMarkdownTableModeParams,
): MarkdownTableMode {
+1 -1
View File
@@ -1,5 +1,5 @@
/** Config paths with user-facing metadata for audio understanding settings. */
export const MEDIA_AUDIO_FIELD_KEYS = [
const MEDIA_AUDIO_FIELD_KEYS = [
"tools.media.audio.enabled",
"tools.media.audio.maxBytes",
"tools.media.audio.maxChars",
+1 -3
View File
@@ -43,6 +43,7 @@ import {
resolveWriteEnvSnapshotForPath,
} from "./io.write-prepare.js";
import { ConfigMutationConflictError } from "./mutation-conflict.js";
import type { ConfigMutationBase } from "./mutation-types.js";
import { assertConfigWriteAllowedInCurrentMode } from "./nix-mode-write-guard.js";
import { resolveConfigPath } from "./paths.js";
import {
@@ -64,9 +65,6 @@ import {
import type { ConfigFileSnapshot, OpenClawConfig } from "./types.js";
import { validateConfigObjectWithPlugins } from "./validation.js";
/** Selects whether a mutation starts from runtime or source config shape. */
export type ConfigMutationBase = "runtime" | "source";
const CONFIG_MUTATION_LOCK_OPTIONS = {
retries: {
retries: 80,
+2
View File
@@ -0,0 +1,2 @@
/** Selects whether a mutation starts from runtime or source config shape. */
export type ConfigMutationBase = "runtime" | "source";
+1 -1
View File
@@ -1,5 +1,5 @@
// Normalizes config.patch replacePaths shared by Gateway and agent preflight checks.
export function normalizeConfigPatchReplacePath(value: string): string {
function normalizeConfigPatchReplacePath(value: string): string {
const trimmed = value.trim();
if (trimmed.endsWith("[]")) {
return trimmed.slice(0, -2).replace(/\[\d+\](?=\.)/g, "[]");
-4
View File
@@ -36,10 +36,6 @@ import type {
} from "./plugin-auto-enable.types.js";
import { ensurePluginAllowlisted } from "./plugins-allowlist.js";
import type { OpenClawConfig } from "./types.openclaw.js";
export type {
PluginAutoEnableCandidate,
PluginAutoEnableResult,
} from "./plugin-auto-enable.types.js";
const EMPTY_PLUGIN_MANIFEST_REGISTRY: PluginManifestRegistry = {
plugins: [],
+1 -1
View File
@@ -3,13 +3,13 @@
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { redactSnapshotTestHints as mainSchemaHints } from "../../test/helpers/config/redact-snapshot-test-hints.js";
import type { ConfigUiHints } from "../shared/config-ui-hints-types.js";
import {
REDACTED_SENTINEL,
redactConfigSnapshot,
restoreRedactedValues as restoreRedactedValues_orig,
} from "./redact-snapshot.js";
import { makeSnapshot, restoreRedactedValues } from "./redact-snapshot.test-helpers.js";
import type { ConfigUiHints } from "./schema.js";
describe("restoreRedactedValues", () => {
it("restores redacted URL endpoint fields on round-trip", () => {
+2 -1
View File
@@ -1,9 +1,10 @@
// Provides assertions for redacted snapshot tests.
import { expect } from "vitest";
import { restoreRedactedValues as restoreRedactedValues_orig } from "./redact-snapshot.js";
import type { ConfigUiHints } from "./schema.js";
import type { ConfigFileSnapshot } from "./types.openclaw.js";
type ConfigUiHints = NonNullable<Parameters<typeof restoreRedactedValues_orig>[2]>;
/** Complete snapshot shape used by redaction tests. */
export type TestSnapshot<TConfig extends Record<string, unknown>> = ConfigFileSnapshot & {
parsed: TConfig;
+2 -1
View File
@@ -4,6 +4,7 @@ import { expectDefined } from "@openclaw/normalization-core";
import JSON5 from "json5";
import { describe, expect, it } from "vitest";
import { redactSnapshotTestHints as mainSchemaHints } from "../../test/helpers/config/redact-snapshot-test-hints.js";
import type { ConfigUiHints } from "../shared/config-ui-hints-types.js";
import { materializeRuntimeConfig } from "./materialize.js";
import { REDACTED_SENTINEL, redactConfigSnapshot } from "./redact-snapshot.js";
import {
@@ -11,7 +12,7 @@ import {
restoreRedactedValues,
type TestSnapshot,
} from "./redact-snapshot.test-helpers.js";
import { buildConfigSchema, type ConfigUiHints } from "./schema.js";
import { buildConfigSchema } from "./schema.js";
import type { ConfigFileSnapshot, OpenClawConfig } from "./types.openclaw.js";
function expectNestedPairValue(
+1 -37
View File
@@ -2,49 +2,13 @@
import { beforeEach, describe, expect, it } from "vitest";
import {
GROUP_POLICY_BLOCKED_LABEL,
resetMissingProviderGroupPolicyFallbackWarningsForTesting,
resolveAllowlistProviderRuntimeGroupPolicy,
resolveDefaultGroupPolicy,
resolveOpenProviderRuntimeGroupPolicy,
resolveRuntimeGroupPolicy,
warnMissingProviderGroupPolicyFallbackOnce,
} from "./runtime-group-policy.js";
beforeEach(() => {
resetMissingProviderGroupPolicyFallbackWarningsForTesting();
});
describe("resolveRuntimeGroupPolicy", () => {
it.each([
{
title: "fails closed when provider config is missing and no defaults are set",
params: { providerConfigPresent: false },
expectedPolicy: "allowlist",
expectedFallbackApplied: true,
},
{
title: "keeps configured fallback when provider config is present",
params: { providerConfigPresent: true, configuredFallbackPolicy: "open" as const },
expectedPolicy: "open",
expectedFallbackApplied: false,
},
{
title: "ignores global defaults when provider config is missing",
params: {
providerConfigPresent: false,
defaultGroupPolicy: "disabled" as const,
configuredFallbackPolicy: "open" as const,
missingProviderFallbackPolicy: "allowlist" as const,
},
expectedPolicy: "allowlist",
expectedFallbackApplied: true,
},
])("$title", ({ params, expectedPolicy, expectedFallbackApplied }) => {
const resolved = resolveRuntimeGroupPolicy(params);
expect(resolved.groupPolicy).toBe(expectedPolicy);
expect(resolved.providerMissingFallbackApplied).toBe(expectedFallbackApplied);
});
});
beforeEach(() => {});
describe("resolveOpenProviderRuntimeGroupPolicy", () => {
it("uses open fallback when provider config exists", () => {
+1 -10
View File
@@ -19,9 +19,7 @@ type RuntimeGroupPolicyParams = {
* Resolve the effective group policy for a channel/provider runtime.
* Missing provider config can fail closed separately from configured providers.
*/
export function resolveRuntimeGroupPolicy(
params: RuntimeGroupPolicyParams,
): RuntimeGroupPolicyResolution {
function resolveRuntimeGroupPolicy(params: RuntimeGroupPolicyParams): RuntimeGroupPolicyResolution {
const configuredFallbackPolicy = params.configuredFallbackPolicy ?? "open";
const missingProviderFallbackPolicy = params.missingProviderFallbackPolicy ?? "allowlist";
const groupPolicy = params.providerConfigPresent
@@ -119,10 +117,3 @@ export function warnMissingProviderGroupPolicyFallbackOnce(params: {
);
return true;
}
/**
* Test helper. Keeps warning-cache state deterministic across test files.
*/
export function resetMissingProviderGroupPolicyFallbackWarningsForTesting(): void {
warnedMissingProviderGroupPolicy.clear();
}
+1 -1
View File
@@ -146,7 +146,7 @@ function applyNodeDocumentation(
}
}
export type BaseConfigSchemaResponse = {
type BaseConfigSchemaResponse = {
schema: ConfigSchema;
uiHints: ConfigUiHints;
version: string;
+2 -2
View File
@@ -2,7 +2,7 @@
import { expectDefined } from "@openclaw/normalization-core";
import { describe, expect, it } from "vitest";
import { MEDIA_AUDIO_FIELD_KEYS } from "./media-audio-field-metadata.js";
import { MEDIA_AUDIO_FIELD_HELP } from "./media-audio-field-metadata.js";
import { FIELD_HELP } from "./schema.help.js";
import { FIELD_LABELS } from "./schema.labels.js";
@@ -516,7 +516,7 @@ const TOOLS_HOOKS_TARGET_KEYS = [
"tools.links.models",
"tools.links.scope",
"tools.links.timeoutSeconds",
...MEDIA_AUDIO_FIELD_KEYS,
...Object.keys(MEDIA_AUDIO_FIELD_HELP),
"tools.media.concurrency",
"tools.media.image.attachments",
"tools.media.image.enabled",
+11 -18
View File
@@ -3,9 +3,8 @@ import { isSensitiveUrlConfigPath } from "@openclaw/net-policy/redact-sensitive-
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { buildSecretInputSchema } from "../plugin-sdk/secret-input-schema.js";
import { FIELD_HELP } from "./schema.help.js";
import { testApi, isPluginOwnedChannelHintPath, isSensitiveConfigPath } from "./schema.hints.js";
import { FIELD_LABELS } from "./schema.labels.js";
import { buildBaseHints, testApi } from "./schema.hints.js";
import { isSensitiveConfigPath } from "./sensitive-paths.js";
import { OpenClawSchema } from "./zod-schema.js";
import { sensitive } from "./zod-schema.sensitive.js";
@@ -23,7 +22,7 @@ const BUNDLED_CHANNEL_HINT_PREFIXES = [
describe("isSensitiveConfigPath", () => {
it("matches whitelist suffixes case-insensitively", () => {
const whitelistedPaths = [
for (const path of [
"maxTokens",
"maxOutputTokens",
"maxInputTokens",
@@ -34,8 +33,7 @@ describe("isSensitiveConfigPath", () => {
"tokenLimit",
"tokenBudget",
"channels.irc.nickserv.passwordFile",
];
for (const path of whitelistedPaths) {
]) {
expect(isSensitiveConfigPath(path)).toBe(false);
expect(isSensitiveConfigPath(path.toUpperCase())).toBe(false);
}
@@ -46,25 +44,20 @@ describe("isSensitiveConfigPath", () => {
expect(isSensitiveConfigPath("models.providers.openai.apiKey")).toBe(true);
expect(isSensitiveConfigPath("channels.irc.nickserv.password")).toBe(true);
expect(isSensitiveConfigPath("channels.feishu.encryptKey")).toBe(true);
expect(isSensitiveConfigPath("channels.feishu.accounts.default.encryptKey")).toBe(true);
expect(isSensitiveConfigPath("channels.nostr.privateKey")).toBe(true);
expect(isSensitiveConfigPath("channels.nostr.accounts.default.privateKey")).toBe(true);
expect(isSensitiveConfigPath("models.providers.local.localService.env.HF_HOME")).toBe(true);
expect(isSensitiveConfigPath("models.providers.local.localService.env.MAX_TOKENS")).toBe(true);
});
});
describe("plugin-owned channel hint paths", () => {
it("keeps bundled channel help and labels out of core tables", () => {
for (const key of [...Object.keys(FIELD_HELP), ...Object.keys(FIELD_LABELS)]) {
if (
!BUNDLED_CHANNEL_HINT_PREFIXES.some(
it("keeps bundled channel hints out of the core hint map", () => {
for (const key of Object.keys(buildBaseHints())) {
expect(
BUNDLED_CHANNEL_HINT_PREFIXES.some(
(prefix) => key === prefix || key.startsWith(`${prefix}.`),
)
) {
continue;
}
expect(isPluginOwnedChannelHintPath(key), `core still owns ${key}`).toBe(false);
),
`core still owns ${key}`,
).toBe(false);
}
});
});
+1 -4
View File
@@ -111,15 +111,13 @@ function isKernelOwnedChannelHintPath(path: string): boolean {
}
/** Return whether a channel hint path belongs to a plugin-owned channel namespace. */
export function isPluginOwnedChannelHintPath(path: string): boolean {
function isPluginOwnedChannelHintPath(path: string): boolean {
if (!path.startsWith(CHANNEL_NAMESPACE_PREFIX)) {
return false;
}
return !isKernelOwnedChannelHintPath(path);
}
export { isSensitiveConfigPath };
/** Build core config UI hints while leaving plugin-owned channel hints to plugin schemas. */
export function buildBaseHints(): ConfigUiHints {
const hints: ConfigUiHints = {};
@@ -330,4 +328,3 @@ export const testApi = {
collectMatchingSchemaPaths,
mapSensitivePaths,
};
export { testApi as __test__ };
+3 -3
View File
@@ -3,7 +3,7 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
import type { ConfigUiHint, ConfigUiHints } from "../shared/config-ui-hints-types.js";
/** Stable config UI tag vocabulary used for filtering and grouping schema hints. */
export const CONFIG_TAGS = [
const CONFIG_TAGS = [
"security",
"auth",
"network",
@@ -21,7 +21,7 @@ export const CONFIG_TAGS = [
"advanced",
] as const;
export type ConfigTag = (typeof CONFIG_TAGS)[number];
type ConfigTag = (typeof CONFIG_TAGS)[number];
const TAG_PRIORITY: Record<ConfigTag, number> = {
security: 0,
@@ -162,7 +162,7 @@ function addTags(set: Set<ConfigTag>, tags: ReadonlyArray<ConfigTag>): void {
}
/** Derive known config UI tags from a schema path and optional hint metadata. */
export function deriveTagsForPath(path: string, hint?: ConfigUiHint): ConfigTag[] {
function deriveTagsForPath(path: string, hint?: ConfigUiHint): ConfigTag[] {
const lowerPath = normalizeLowercaseStringOrEmpty(path);
const override = resolveOverride(path);
if (override) {
+22 -45
View File
@@ -3,7 +3,7 @@ import { SENSITIVE_URL_HINT_TAG } from "@openclaw/net-policy/redact-sensitive-ur
import { expectDefined } from "@openclaw/normalization-core";
import { beforeAll, describe, expect, it } from "vitest";
import { buildConfigSchema, lookupConfigSchema } from "./schema.js";
import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js";
import { applyDerivedTags } from "./schema.tags.js";
import { ToolsSchema } from "./zod-schema.agent-runtime.js";
import { OpenClawSchema } from "./zod-schema.js";
import {
@@ -574,23 +574,29 @@ describe("config schema", () => {
expect(second).toBe(first);
});
it("derives security/auth tags for credential paths", () => {
const tags = deriveTagsForPath("gateway.auth.token");
expect(tags).toContain("security");
expect(tags).toContain("auth");
it("derives tags for security, network, storage, tools, and performance paths", () => {
const tagged = applyDerivedTags({
"gateway.auth.token": {},
"proxy.tls.caFile": {},
"tools.web.fetch.timeoutSeconds": {},
});
expect(tagged["gateway.auth.token"]?.tags).toEqual(
expect.arrayContaining(["security", "auth"]),
);
expect(tagged["proxy.tls.caFile"]?.tags).toEqual(
expect.arrayContaining(["security", "network", "storage"]),
);
expect(tagged["tools.web.fetch.timeoutSeconds"]?.tags).toEqual(
expect.arrayContaining(["tools", "performance"]),
);
});
it("classifies managed proxy CA files as security-relevant config", () => {
const tags = deriveTagsForPath("proxy.tls.caFile");
expect(tags).toContain("security");
expect(tags).toContain("network");
expect(tags).toContain("storage");
});
it("derives tools/performance tags for web fetch timeout paths", () => {
const tags = deriveTagsForPath("tools.web.fetch.timeoutSeconds");
expect(tags).toContain("tools");
expect(tags).toContain("performance");
it("covers core config paths with derived tags", () => {
for (const [key, hint] of Object.entries(baseSchema.uiHints)) {
if (key.includes(".")) {
expect(hint.tags?.length ?? 0, `expected tags for ${key}`).toBeGreaterThan(0);
}
}
});
it("accepts web fetch readability and firecrawl config in the runtime zod schema", () => {
@@ -1013,35 +1019,6 @@ describe("config schema", () => {
}
});
it("keeps tags in the allowed taxonomy", () => {
const withTags = applyDerivedTags({
"gateway.auth.token": {},
"tools.web.fetch.timeoutSeconds": {},
"channels.slack.accounts.*.token": {},
});
const allowed = new Set<string>(CONFIG_TAGS);
for (const hint of Object.values(withTags)) {
for (const tag of hint.tags ?? []) {
expect(allowed.has(tag)).toBe(true);
}
}
});
it("covers core/built-in config paths with tags", () => {
const schema = baseSchema;
const allowed = new Set<string>([...CONFIG_TAGS, SENSITIVE_URL_HINT_TAG]);
for (const [key, hint] of Object.entries(schema.uiHints)) {
if (!key.includes(".")) {
continue;
}
const tags = hint.tags ?? [];
expect(tags.length, `expected tags for ${key}`).toBeGreaterThan(0);
for (const tag of tags) {
expect(allowed.has(tag), `unexpected tag ${tag} on ${key}`).toBe(true);
}
}
});
it("looks up a config schema path with immediate child summaries", () => {
const lookup = lookupConfigSchema(baseSchema, "gateway.auth");
expect(lookup?.path).toBe("gateway.auth");
+6 -8
View File
@@ -15,9 +15,7 @@ import {
} from "./schema.shared.js";
import { applyDerivedTags } from "./schema.tags.js";
export type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
export type ConfigSchema = Record<string, unknown>;
type ConfigSchema = Record<string, unknown>;
type JsonSchemaNode = Record<string, unknown>;
@@ -108,7 +106,7 @@ export type ConfigSchemaResponse = {
generatedAt: string;
};
export type ConfigSchemaLookupChild = {
type ConfigSchemaLookupChild = {
key: string;
path: string;
type?: string | string[];
@@ -119,17 +117,17 @@ export type ConfigSchemaLookupChild = {
hintPath?: string;
};
export type ConfigSchemaReloadKind = "restart" | "hot" | "none";
type ConfigSchemaReloadKind = "restart" | "hot" | "none";
export type ConfigSchemaReloadMetadata = {
type ConfigSchemaReloadMetadata = {
kind: ConfigSchemaReloadKind;
};
export type ConfigSchemaReloadMetadataResolver = (
type ConfigSchemaReloadMetadataResolver = (
path: string,
) => ConfigSchemaReloadMetadata | null | undefined;
export type ConfigSchemaLookupResult = {
type ConfigSchemaLookupResult = {
path: string;
schema: JsonSchemaNode;
reloadKind?: ConfigSchemaReloadKind;
+8 -345
View File
@@ -7,11 +7,7 @@ import * as jsonFiles from "../infra/json-files.js";
import { createCanonicalFixtureSkill } from "../skills/test-support/test-helpers.js";
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
import {
getSerializedSessionStore,
getSerializedSessionStoreCacheStatsForTest,
getSerializedSessionStorePromptRefs,
getSessionStoreSnapshotCacheStatsForTest,
getSessionStoreStringInternStatsForTest,
readSessionStoreCache,
setSerializedSessionStore,
setSerializedSessionStorePromptRefs,
@@ -20,14 +16,9 @@ import {
import {
clearSessionStoreCacheForTest,
loadSessionStore,
readSessionEntries,
readSessionEntry,
readSessionStoreSnapshot,
readSessionUpdatedAt,
saveSessionStore,
updateSessionStore,
updateSessionStoreEntry,
updateLastRoute,
} from "./sessions/store.js";
import type { SessionEntry } from "./sessions/types.js";
import type { SessionSkillPromptRef } from "./sessions/types.js";
@@ -78,34 +69,6 @@ describe("Session Store Cache", () => {
delete process.env.OPENCLAW_SESSION_SERIALIZED_CACHE_MAX_BYTES;
});
it("bounds the serialized session store cache by total bytes", () => {
process.env.OPENCLAW_SESSION_SERIALIZED_CACHE_MAX_BYTES = "64";
clearSessionStoreCacheForTest();
setSerializedSessionStore("store:1", "a".repeat(40));
setSerializedSessionStore("store:2", "b".repeat(40));
expect(getSerializedSessionStore("store:1")).toBeUndefined();
expect(getSerializedSessionStore("store:2")).toBe("b".repeat(40));
expect(getSerializedSessionStoreCacheStatsForTest().entries).toBe(1);
expect(getSerializedSessionStoreCacheStatsForTest().totalBytes).toBe(40);
});
it("bounds the serialized session store cache by path count", () => {
const maxEntries = getSerializedSessionStoreCacheStatsForTest().maxEntries;
for (let index = 0; index < maxEntries + 2; index += 1) {
setSerializedSessionStore(`store:${index}`, `serialized:${index}`);
}
expect(getSerializedSessionStore("store:0")).toBeUndefined();
expect(getSerializedSessionStore("store:1")).toBeUndefined();
expect(getSerializedSessionStore(`store:${maxEntries + 1}`)).toBe(
`serialized:${maxEntries + 1}`,
);
expect(getSerializedSessionStoreCacheStatsForTest().entries).toBe(maxEntries);
});
it("keeps serialized prompt refs on the serialized cache entry lifecycle", () => {
const promptRef: SessionSkillPromptRef = {
version: 1,
@@ -461,270 +424,6 @@ describe("Session Store Cache", () => {
stringifySpy.mockRestore();
});
it("interns duplicate large skillsSnapshot prompts across cached loads", async () => {
const largePrompt = "skill prompt ".repeat(200);
const testStore = {
"session:1": createSessionEntry({
skillsSnapshot: {
prompt: largePrompt,
skills: [{ name: "alpha" }],
},
}),
"session:2": createSessionEntry({
sessionId: "id-2",
displayName: "Test Session 2",
skillsSnapshot: {
prompt: largePrompt,
skills: [{ name: "beta" }],
},
}),
};
await saveSessionStore(storePath, testStore);
clearSessionStoreCacheForTest();
const loaded1 = loadSessionStore(storePath);
const afterFirstLoad = getSessionStoreStringInternStatsForTest();
expect(afterFirstLoad.poolSize).toBe(1);
expect(afterFirstLoad.stored).toBe(1);
expect(afterFirstLoad.reused).toBeGreaterThanOrEqual(1);
for (const skill of expectDefined(loaded1["session:1"], "loaded session").skillsSnapshot
?.skills ?? []) {
skill.name = "mutated";
break;
}
const loaded2 = loadSessionStore(storePath);
const afterSecondLoad = getSessionStoreStringInternStatsForTest();
expect(afterSecondLoad.poolSize).toBe(1);
expect(afterSecondLoad.reused).toBeGreaterThanOrEqual(afterFirstLoad.reused + 2);
expect(
expectDefined(loaded2["session:1"], 'loaded2["session:1"] test invariant').skillsSnapshot
?.skills?.[0]?.name,
).toBe("alpha");
});
it("does not intern short skillsSnapshot prompts", async () => {
const testStore = {
"session:1": createSessionEntry({
skillsSnapshot: {
prompt: "short prompt",
skills: [{ name: "alpha" }],
},
}),
"session:2": createSessionEntry({
sessionId: "id-2",
displayName: "Test Session 2",
skillsSnapshot: {
prompt: "short prompt",
skills: [{ name: "beta" }],
},
}),
};
await saveSessionStore(storePath, testStore);
clearSessionStoreCacheForTest();
loadSessionStore(storePath);
const stats = getSessionStoreStringInternStatsForTest();
expect(stats.poolSize).toBe(0);
expect(stats.skippedSmall).toBeGreaterThanOrEqual(2);
});
it("reads updatedAt from immutable session snapshots without cloning cached stores", async () => {
const updatedAt = Date.now();
const testStore = createSingleSessionStore(
createSessionEntry({
updatedAt,
}),
"agent:main:main",
);
await saveSessionStore(storePath, testStore);
clearSessionStoreCacheForTest();
readSessionStoreSnapshot(storePath);
expect(readSessionEntry(storePath, "agent:main:main")?.updatedAt).toBe(updatedAt);
const parseSpy = vi.spyOn(JSON, "parse");
expect(readSessionUpdatedAt({ storePath, sessionKey: "agent:main:main" })).toBe(updatedAt);
expect(parseSpy).not.toHaveBeenCalled();
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(1);
parseSpy.mockRestore();
});
it("builds a snapshot from disk without reparsing the mutable clone", async () => {
const testStore = createSingleSessionStore(
createSessionEntry({
skillsSnapshot: {
prompt: "snapshot skill prompt ".repeat(200),
skills: [{ name: "alpha" }],
},
}),
);
await saveSessionStore(storePath, testStore);
clearSessionStoreCacheForTest();
const parseSpy = vi.spyOn(JSON, "parse");
const snapshot = readSessionStoreSnapshot(storePath);
expect(
expectDefined(snapshot["session:1"], 'snapshot["session:1"] test invariant').sessionId,
).toBe("id-1");
expect(parseSpy).toHaveBeenCalledTimes(1);
parseSpy.mockRestore();
});
it("serves immutable session snapshots without cloning cache hits", async () => {
const testStore = createSingleSessionStore(
createSessionEntry({
origin: { provider: "openai" },
skillsSnapshot: {
prompt: "snapshot skill prompt ".repeat(200),
skills: [{ name: "alpha" }],
},
}),
);
await saveSessionStore(storePath, testStore);
clearSessionStoreCacheForTest();
const snapshot1 = readSessionStoreSnapshot(storePath);
const snapshot2 = readSessionStoreSnapshot(storePath);
expect(snapshot2).toBe(snapshot1);
expect(Object.isFrozen(snapshot1)).toBe(true);
expect(Object.isFrozen(snapshot1["session:1"])).toBe(true);
expect(
Object.isFrozen(
expectDefined(snapshot1["session:1"], 'snapshot1["session:1"] test invariant')
.skillsSnapshot?.skills,
),
).toBe(true);
expect(readSessionEntry(storePath, "session:1")?.sessionId).toBe("id-1");
expect(readSessionEntries(storePath).map(([key]) => key)).toEqual(["session:1"]);
expect(() => {
(snapshot1 as Record<string, SessionEntry>)["session:2"] = createSessionEntry({
sessionId: "id-2",
});
}).toThrow(TypeError);
const mutable = loadSessionStore(storePath);
expectDefined(mutable["session:1"], 'mutable["session:1"] test invariant').origin = {
provider: "mutated",
};
expect(
expectDefined(
readSessionStoreSnapshot(storePath)["session:1"],
'readSessionStoreSnapshot(storePath)["session:1"] test invariant',
).origin?.provider,
).toBe("openai");
});
it("reads immutable single entries without populating whole-store snapshots", async () => {
await saveSessionStore(storePath, {
"session:1": createSessionEntry({
sessionId: "id-1",
skillsSnapshot: {
prompt: "single entry prompt ".repeat(200),
skills: [{ name: "alpha" }],
},
}),
"session:2": createSessionEntry({ sessionId: "id-2" }),
});
clearSessionStoreCacheForTest();
const entry = readSessionEntry(storePath, "session:1");
expect(entry?.sessionId).toBe("id-1");
expect(Object.isFrozen(entry)).toBe(true);
expect(Object.isFrozen(entry?.skillsSnapshot?.skills)).toBe(true);
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(0);
const cached = loadSessionStore(storePath, { clone: false });
expect(() => {
(entry as SessionEntry).displayName = "mutated returned entry";
}).toThrow(TypeError);
expect(
expectDefined(cached["session:1"], 'cached["session:1"] test invariant').displayName,
).toBe("Test Session 1");
});
it("does not tag snapshots with stats from writes racing after a disk read", async () => {
await saveSessionStore(
storePath,
createSingleSessionStore(createSessionEntry({ displayName: "Before race" })),
);
clearSessionStoreCacheForTest();
const afterRaceStore = createSingleSessionStore(
createSessionEntry({ displayName: "After cross-process race" }),
);
const originalReadFileSync = fs.readFileSync.bind(fs);
let wroteAfterRead = false;
const readSpy = vi.spyOn(fs, "readFileSync").mockImplementation((file, ...args) => {
const result = originalReadFileSync(
file,
...(args as [Parameters<typeof fs.readFileSync>[1]]),
);
if (file === storePath && !wroteAfterRead) {
wroteAfterRead = true;
fs.writeFileSync(storePath, JSON.stringify(afterRaceStore, null, 2));
const bumped = new Date(Date.now() + 2_000);
fs.utimesSync(storePath, bumped, bumped);
}
return result;
});
const first = readSessionStoreSnapshot(storePath);
expect(expectDefined(first["session:1"], 'first["session:1"] test invariant').displayName).toBe(
"Before race",
);
readSpy.mockRestore();
const second = readSessionStoreSnapshot(storePath);
expect(
expectDefined(second["session:1"], 'second["session:1"] test invariant').displayName,
).toBe("After cross-process race");
});
it("publishes a new immutable snapshot after session store writes", async () => {
await saveSessionStore(storePath, createSingleSessionStore());
const before = readSessionStoreSnapshot(storePath);
await updateSessionStore(
storePath,
(store) => {
store["session:1"] = {
...expectDefined(store["session:1"], "stored session entry"),
displayName: "Updated Session",
updatedAt: Date.now() + 1,
};
},
{ skipMaintenance: true },
);
const after = readSessionStoreSnapshot(storePath);
expect(after).not.toBe(before);
expect(
expectDefined(before["session:1"], 'before["session:1"] test invariant').displayName,
).toBe("Test Session 1");
expect(expectDefined(after["session:1"], 'after["session:1"] test invariant').displayName).toBe(
"Updated Session",
);
});
it("keeps whole-store update results detached from the mutable cache by default", async () => {
await saveSessionStore(storePath, createSingleSessionStore());
@@ -821,30 +520,28 @@ describe("Session Store Cache", () => {
).toBe("Entry writer owned by default");
});
it("publishes route updates without cloning the whole object cache", async () => {
it("detaches caller-owned patch objects before publishing writer-owned caches", async () => {
await saveSessionStore(storePath, {
"session:1": createSessionEntry({ sessionId: "id-1" }),
"session:2": createSessionEntry({ sessionId: "id-2" }),
});
const before = loadSessionStore(storePath, { clone: false });
const untouched = before["session:2"];
const deliveryContext = { channel: "telegram", to: "chat-1" };
const persisted = await updateLastRoute({
await updateSessionStoreEntry({
storePath,
sessionKey: "session:1",
channel: "telegram",
to: "chat-1",
update: async () => ({ deliveryContext }),
});
deliveryContext.to = "mutated-after-persist";
const cached = loadSessionStore(storePath, { clone: false });
expect(cached["session:2"]).toBe(untouched);
expect(cached["session:1"]).not.toBe(persisted);
persisted!.lastTo = "mutated-return";
expect(expectDefined(cached["session:1"], 'cached["session:1"] test invariant').lastTo).toBe(
"chat-1",
);
expect(
expectDefined(cached["session:1"], 'cached["session:1"] test invariant').deliveryContext?.to,
).toBe("chat-1");
});
it("falls back to full projection when untouched entries need prompt blob repair", async () => {
const prompt = "skill prompt ".repeat(80);
await saveSessionStore(storePath, {
@@ -944,40 +641,6 @@ describe("Session Store Cache", () => {
expect(after).toBe(before);
});
it("builds immutable session snapshots lazily after writes", async () => {
await saveSessionStore(storePath, createSingleSessionStore());
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(0);
const first = readSessionStoreSnapshot(storePath);
const statsAfterRead = getSessionStoreSnapshotCacheStatsForTest();
const second = readSessionStoreSnapshot(storePath);
expect(first).toBe(second);
expect(Object.isFrozen(first)).toBe(true);
expect(statsAfterRead.entries).toBe(1);
await updateSessionStore(
storePath,
(store) => {
store["session:1"] = {
...expectDefined(store["session:1"], "stored session entry"),
displayName: "Updated lazily",
updatedAt: Date.now() + 1,
};
},
{ skipMaintenance: true },
);
expect(getSessionStoreSnapshotCacheStatsForTest().entries).toBe(0);
expect(
expectDefined(
readSessionStoreSnapshot(storePath)["session:1"],
'readSessionStoreSnapshot(storePath)["session:1"] test invariant',
).displayName,
).toBe("Updated lazily");
});
it("should refresh cache when store file changes on disk", async () => {
const testStore = createSingleSessionStore();
-390
View File
@@ -10,17 +10,13 @@ import {
buildGroupDisplayName,
deriveSessionKey,
loadSessionStore,
patchSessionEntry,
recordSessionMetaFromInbound,
resolveSessionFilePath,
resolveSessionFilePathOptions,
resolveSessionKey,
resolveSessionTranscriptPath,
resolveSessionTranscriptsDir,
updateLastRoute,
updateSessionStore,
updateSessionStoreEntry,
upsertSessionEntry,
} from "./sessions.js";
describe("sessions", () => {
@@ -64,14 +60,6 @@ describe("sessions", () => {
);
}
function buildMainSessionEntry(overrides: Record<string, unknown> = {}) {
return {
sessionId: "sess-1",
updatedAt: 123,
...overrides,
};
}
async function createAgentSessionsLayout(label: string): Promise<{
stateDir: string;
mainStorePath: string;
@@ -230,288 +218,6 @@ describe("sessions", () => {
});
}
it("updateLastRoute persists channel and target", async () => {
const mainSessionKey = "agent:main:main";
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute",
entries: {
[mainSessionKey]: buildMainSessionEntry({
systemSent: true,
thinkingLevel: "low",
responseUsage: "on",
queueDebounceMs: 1234,
reasoningLevel: "on",
elevatedLevel: "on",
authProfileOverride: "auth-1",
compactionCount: 2,
}),
},
});
await updateLastRoute({
storePath,
sessionKey: mainSessionKey,
deliveryContext: {
channel: "telegram",
to: " 12345 ",
},
});
const store = loadSessionStore(storePath);
expect(store[mainSessionKey]?.sessionId).toBe("sess-1");
// updateLastRoute must preserve existing updatedAt (activity timestamp)
expect(store[mainSessionKey]?.updatedAt).toBe(123);
expect(store[mainSessionKey]?.lastChannel).toBe("telegram");
expect(store[mainSessionKey]?.lastTo).toBe("12345");
expect(store[mainSessionKey]?.deliveryContext).toEqual({
channel: "telegram",
to: "12345",
});
expect(store[mainSessionKey]?.responseUsage).toBe("on");
expect(store[mainSessionKey]?.queueDebounceMs).toBe(1234);
expect(store[mainSessionKey]?.reasoningLevel).toBe("on");
expect(store[mainSessionKey]?.elevatedLevel).toBe("on");
expect(store[mainSessionKey]?.authProfileOverride).toBe("auth-1");
expect(store[mainSessionKey]?.compactionCount).toBe(2);
});
it("updateLastRoute prefers explicit deliveryContext", async () => {
const mainSessionKey = "agent:main:main";
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute",
entries: {},
});
await updateLastRoute({
storePath,
sessionKey: mainSessionKey,
channel: "demo-chat",
to: "111",
accountId: "legacy",
deliveryContext: {
channel: "telegram",
to: "222",
accountId: "primary",
},
});
const store = loadSessionStore(storePath);
expect(store[mainSessionKey]?.lastChannel).toBe("telegram");
expect(store[mainSessionKey]?.lastTo).toBe("222");
expect(store[mainSessionKey]?.lastAccountId).toBe("primary");
expect(store[mainSessionKey]?.deliveryContext).toEqual({
channel: "telegram",
to: "222",
accountId: "primary",
});
});
it("updateLastRoute clears threadId when explicit route omits threadId", async () => {
const mainSessionKey = "agent:main:main";
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute",
entries: {
[mainSessionKey]: buildMainSessionEntry({
deliveryContext: {
channel: "telegram",
to: "222",
threadId: "42",
},
lastChannel: "telegram",
lastTo: "222",
lastThreadId: "42",
}),
},
});
await updateLastRoute({
storePath,
sessionKey: mainSessionKey,
deliveryContext: {
channel: "telegram",
to: "222",
},
});
const store = loadSessionStore(storePath);
expect(store[mainSessionKey]?.deliveryContext).toEqual({
channel: "telegram",
to: "222",
});
expect(store[mainSessionKey]?.lastThreadId).toBeUndefined();
});
it("updateLastRoute records origin + group metadata when ctx is provided", async () => {
const sessionKey = "agent:main:demo-chat:group:room-123";
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute",
entries: {},
});
await updateLastRoute({
storePath,
sessionKey,
deliveryContext: {
channel: "demo-chat",
to: "room-123",
},
ctx: {
Provider: "demo-chat",
ChatType: "group",
GroupSubject: "Family",
From: "room-123",
},
});
const store = loadSessionStore(storePath);
expect(store[sessionKey]?.subject).toBe("Family");
expect(store[sessionKey]?.channel).toBe("demo-chat");
expect(store[sessionKey]?.groupId).toBe("room-123");
expect(store[sessionKey]?.origin?.label).toBe("Family");
expect(store[sessionKey]?.origin?.provider).toBe("demo-chat");
expect(store[sessionKey]?.origin?.chatType).toBe("group");
});
it("updateLastRoute skips missing sessions when creation is disabled", async () => {
const sessionKey = "agent:main:demo-chat:group:room-123";
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute-no-create",
entries: {},
});
const result = await updateLastRoute({
storePath,
sessionKey,
deliveryContext: {
channel: "demo-chat",
to: "room-123",
},
createIfMissing: false,
});
const store = loadSessionStore(storePath);
expect(result).toBeNull();
expect(store[sessionKey]).toBeUndefined();
});
it("updateLastRoute updates existing sessions when creation is disabled", async () => {
const sessionKey = "agent:main:demo-chat:group:room-123";
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute-existing-no-create",
entries: {
[sessionKey]: buildMainSessionEntry(),
},
});
await updateLastRoute({
storePath,
sessionKey,
deliveryContext: {
channel: "demo-chat",
to: "room-123",
},
createIfMissing: false,
});
const store = loadSessionStore(storePath);
expect(store[sessionKey]?.lastChannel).toBe("demo-chat");
expect(store[sessionKey]?.lastTo).toBe("room-123");
});
it("updateLastRoute does not bump updatedAt on existing sessions (#49515)", async () => {
const mainSessionKey = "agent:main:main";
const frozenUpdatedAt = 1000;
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute-preserve-activity",
entries: {
[mainSessionKey]: buildMainSessionEntry({
updatedAt: frozenUpdatedAt,
}),
},
});
await updateLastRoute({
storePath,
sessionKey: mainSessionKey,
deliveryContext: {
channel: "telegram",
to: "99999",
},
});
const store = loadSessionStore(storePath);
// Route updates must not refresh activity timestamps; idle/daily reset
// evaluation relies on updatedAt from actual session turns.
expect(store[mainSessionKey]?.updatedAt).toBe(frozenUpdatedAt);
// Routing fields should still be updated
expect(store[mainSessionKey]?.lastChannel).toBe("telegram");
expect(store[mainSessionKey]?.lastTo).toBe("99999");
});
it("updateLastRoute skips persistence when the route is unchanged", async () => {
const mainSessionKey = "agent:main:main";
const entry = buildMainSessionEntry({
route: {
channel: "telegram",
target: { to: "99999" },
},
deliveryContext: {
channel: "telegram",
to: "99999",
},
lastChannel: "telegram",
lastTo: "99999",
});
const { storePath } = await createSessionStoreFixture({
prefix: "updateLastRoute-noop",
entries: {
[mainSessionKey]: entry,
},
});
const before = await fs.readFile(storePath, "utf-8");
const result = await updateLastRoute({
storePath,
sessionKey: mainSessionKey,
deliveryContext: {
channel: "telegram",
to: "99999",
},
});
expect(result).toEqual(entry);
if (result) {
result.lastTo = "mutated";
}
expect(loadSessionStore(storePath, { clone: false })[mainSessionKey]?.lastTo).toBe("99999");
await expect(fs.readFile(storePath, "utf-8")).resolves.toBe(before);
});
it("recordSessionMetaFromInbound skips persistence when there is no metadata patch", async () => {
const mainSessionKey = "agent:main:main";
const entry = buildMainSessionEntry();
const { storePath } = await createSessionStoreFixture({
prefix: "recordSessionMetaFromInbound-noop",
entries: {
[mainSessionKey]: entry,
},
});
const before = await fs.readFile(storePath, "utf-8");
const result = await recordSessionMetaFromInbound({
storePath,
sessionKey: mainSessionKey,
ctx: {},
});
expect(result).toEqual(entry);
if (result) {
result.sessionId = "mutated";
}
expect(loadSessionStore(storePath, { clone: false })[mainSessionKey]?.sessionId).toBe("sess-1");
await expect(fs.readFile(storePath, "utf-8")).resolves.toBe(before);
});
it("updateSessionStoreEntry preserves existing fields when patching", async () => {
const sessionKey = "agent:main:main";
const { storePath } = await createSessionStoreFixture({
@@ -600,102 +306,6 @@ describe("sessions", () => {
expect(loadSessionStore(storePath)[sessionKey]?.displayName).toBe("after");
});
it("patchSessionEntry can preserve activity for metadata-only updates", async () => {
const sessionKey = "agent:main:main";
const { storePath } = await createSessionStoreFixture({
prefix: "patchSessionEntry-preserve-activity",
entries: {
[sessionKey]: {
sessionId: "sess-1",
updatedAt: 100,
pluginDebugEntries: [{ pluginId: "other", lines: ["keep"] }],
},
},
});
await patchSessionEntry({
storePath,
sessionKey,
preserveActivity: true,
update: () => ({
pluginDebugEntries: [{ pluginId: "active-memory", lines: ["status"] }],
}),
});
const store = loadSessionStore(storePath);
expect(store[sessionKey]?.updatedAt).toBe(100);
expect(store[sessionKey]?.pluginDebugEntries).toEqual([
{ pluginId: "active-memory", lines: ["status"] },
]);
});
it("patchSessionEntry can replace an entry so deleted fields stay deleted", async () => {
const sessionKey = "agent:main:main";
const { storePath } = await createSessionStoreFixture({
prefix: "patchSessionEntry-replace-entry",
entries: {
[sessionKey]: {
sessionId: "sess-1",
updatedAt: 100,
model: "old-model",
modelProvider: "old-provider",
},
},
});
await patchSessionEntry({
storePath,
sessionKey,
replaceEntry: true,
update: (entry) => {
const next = { ...entry, providerOverride: "openai" };
delete next.model;
delete next.modelProvider;
return next;
},
});
const store = loadSessionStore(storePath);
expect(store[sessionKey]?.providerOverride).toBe("openai");
expect(store[sessionKey]?.model).toBeUndefined();
expect(store[sessionKey]?.modelProvider).toBeUndefined();
});
it("upsertSessionEntry drops legacy embedded ACP metadata", async () => {
const sessionKey = "agent:main:main";
const acp = {
backend: "codex",
agent: "main",
runtimeSessionName: "runtime-session",
mode: "persistent" as const,
state: "idle" as const,
lastActivityAt: 100,
};
const { storePath } = await createSessionStoreFixture({
prefix: "upsertSessionEntry-acp",
entries: {
[sessionKey]: {
sessionId: "sess-1",
updatedAt: 100,
acp,
},
},
});
await upsertSessionEntry({
storePath,
sessionKey,
entry: {
sessionId: "sess-2",
updatedAt: 200,
},
});
const store = loadSessionStore(storePath);
expect(store[sessionKey]?.sessionId).toBe("sess-2");
expect(store[sessionKey]?.acp).toBeUndefined();
});
it("updateSessionStore preserves concurrent additions", async () => {
const dir = await createCaseDir("updateSessionStore");
const storePath = path.join(dir, "sessions.json");
-9
View File
@@ -16,15 +16,6 @@ export {
patchSessionEntryWithKey,
resetSessionEntryLifecycle,
resolveSessionEntryCandidateTarget,
type CanonicalizeSessionEntryAliasesResult,
type DeleteSessionEntryLifecycleParams,
type DeleteSessionEntryLifecycleResult,
type ResolvedSessionEntryCandidateTarget,
type ResetSessionEntryLifecycleParams,
type ResetSessionEntryLifecycleResult,
type SessionEntryCandidateAccessScope,
type SessionLifecycleArchivedTranscript,
type SessionLifecycleStoreTarget,
} from "./sessions/session-accessor.js";
export * from "./sessions/session-key.js";
export * from "./sessions/store.js";
+9 -22
View File
@@ -6,11 +6,8 @@ import {
isPrimarySessionTranscriptFileName,
isSessionArchiveArtifactName,
isSessionStoreTempArtifactName,
isTrajectoryPointerArtifactName,
isTrajectoryRuntimeArtifactName,
isTrajectorySessionArtifactName,
isUsageCountedSessionTranscriptFileName,
parseCompactionCheckpointTranscriptFileName,
parseUsageCountedSessionIdFromFileName,
parseSessionArchiveTimestamp,
} from "./artifacts.js";
@@ -65,13 +62,20 @@ describe("session artifact helpers", () => {
});
it("classifies trajectory sidecar artifacts", () => {
expect(isTrajectoryRuntimeArtifactName("abc.trajectory.jsonl")).toBe(true);
expect(isTrajectoryPointerArtifactName("abc.trajectory-path.json")).toBe(true);
expect(isTrajectorySessionArtifactName("abc.trajectory.jsonl")).toBe(true);
expect(isTrajectorySessionArtifactName("abc.trajectory-path.json")).toBe(true);
expect(isTrajectorySessionArtifactName("abc.jsonl")).toBe(false);
});
it("recognizes exact compaction checkpoint transcript names", () => {
expect(
isCompactionCheckpointTranscriptFileName(
"abc.checkpoint.11111111-1111-4111-8111-111111111111.jsonl",
),
).toBe(true);
expect(isCompactionCheckpointTranscriptFileName("abc.checkpoint.not-a-uuid.jsonl")).toBe(false);
});
it("classifies usage-counted transcript files", () => {
expect(isUsageCountedSessionTranscriptFileName("abc.jsonl")).toBe(true);
expect(
@@ -110,23 +114,6 @@ describe("session artifact helpers", () => {
expect(parseUsageCountedSessionIdFromFileName("abc.trajectory.jsonl")).toBeNull();
});
it("parses exact compaction checkpoint transcript file names", () => {
expect(
parseCompactionCheckpointTranscriptFileName(
"abc.checkpoint.11111111-1111-4111-8111-111111111111.jsonl",
),
).toEqual({
sessionId: "abc",
checkpointId: "11111111-1111-4111-8111-111111111111",
});
expect(isCompactionCheckpointTranscriptFileName("abc.checkpoint.not-a-uuid.jsonl")).toBe(false);
expect(
isCompactionCheckpointTranscriptFileName(
"abc.checkpoint.11111111-1111-4111-8111-111111111111.jsonl.deleted.2026-01-01T00-00-00.000Z",
),
).toBe(false);
});
it("formats and parses archive timestamps", () => {
const now = Date.parse("2026-02-23T12:34:56.000Z");
const stamp = formatSessionArchiveTimestamp(now);
+3 -3
View File
@@ -72,7 +72,7 @@ export function isSessionStoreTempArtifactName(fileName: string, storeBasename:
}
/** Parses a compaction checkpoint transcript filename into session/checkpoint ids. */
export function parseCompactionCheckpointTranscriptFileName(fileName: string): {
function parseCompactionCheckpointTranscriptFileName(fileName: string): {
sessionId: string;
checkpointId: string;
} | null {
@@ -88,12 +88,12 @@ export function isCompactionCheckpointTranscriptFileName(fileName: string): bool
}
/** Returns true for trajectory runtime jsonl artifacts. */
export function isTrajectoryRuntimeArtifactName(fileName: string): boolean {
function isTrajectoryRuntimeArtifactName(fileName: string): boolean {
return fileName.endsWith(".trajectory.jsonl");
}
/** Returns true for trajectory pointer artifacts. */
export function isTrajectoryPointerArtifactName(fileName: string): boolean {
function isTrajectoryPointerArtifactName(fileName: string): boolean {
return fileName.endsWith(".trajectory-path.json");
}
+2 -2
View File
@@ -51,7 +51,7 @@ export type SessionsCleanupOptions = SessionStoreSelectionOptions & {
fixDmScope?: boolean;
};
export type SessionCleanupAction =
type SessionCleanupAction =
| "keep"
| "prune-missing"
| "prune-model-run"
@@ -88,7 +88,7 @@ export type SessionsCleanupResult =
stores: SessionCleanupSummary[];
};
export type SessionsCleanupRunResult = {
type SessionsCleanupRunResult = {
mode: ResolvedSessionMaintenanceConfig["mode"];
previewResults: Array<{
summary: SessionCleanupSummary;
+2 -2
View File
@@ -23,7 +23,7 @@ import { projectSessionStoreForPersistence } from "./skill-prompt-blobs.js";
import { shouldPreserveMaintenanceEntry } from "./store-maintenance.js";
import type { SessionEntry } from "./types.js";
export type SessionDiskBudgetConfig = {
type SessionDiskBudgetConfig = {
maxDiskBytes: number | null;
highWaterBytes: number | null;
};
@@ -46,7 +46,7 @@ export type SessionUnreferencedArtifactSweepResult = {
olderThanMs: number;
};
export type SessionDiskBudgetLogger = {
type SessionDiskBudgetLogger = {
warn: (message: string, context?: Record<string, unknown>) => void;
info: (message: string, context?: Record<string, unknown>) => void;
};
+3 -3
View File
@@ -15,19 +15,19 @@ import {
import { loadSessionEntry, type SessionAccessScope } from "./session-accessor.js";
import type { SessionEntry } from "./types.js";
export type ResolveSessionEntryResetFreshnessParams = SessionAccessScope & {
type ResolveSessionEntryResetFreshnessParams = SessionAccessScope & {
now?: number;
resetOverride?: SessionResetConfig;
resetType: SessionResetType;
sessionCfg?: SessionConfig;
};
export type SessionEntryLifecycleTimestamps = {
type SessionEntryLifecycleTimestamps = {
sessionStartedAt?: number;
lastInteractionAt?: number;
};
export type ResolvedSessionEntryResetFreshness =
type ResolvedSessionEntryResetFreshness =
| {
state: "missing";
entry: undefined;
+1 -1
View File
@@ -9,7 +9,7 @@ import { loadSessionEntry, patchSessionEntry } from "./session-accessor.js";
import { resolveFreshSessionTotalTokens } from "./types.js";
import type { SessionEntry, SessionGoal, SessionGoalStatus } from "./types.js";
export type SessionGoalSnapshot = {
type SessionGoalSnapshot = {
status: "missing" | "found";
goal?: SessionGoal;
};
+1 -1
View File
@@ -26,7 +26,7 @@ type SessionWorkStartOptions = {
/** Stable Gateway error detail for stale session lifecycle requests. */
export const SESSION_LIFECYCLE_CHANGED_ERROR_REASON = "session-changed";
export const SESSION_WORK_START_INVALIDATED_ERROR_CODE = "SESSION_WORK_START_INVALIDATED";
const SESSION_WORK_START_INVALIDATED_ERROR_CODE = "SESSION_WORK_START_INVALIDATED";
export class SessionWorkStartInvalidatedError extends Error {
readonly code = SESSION_WORK_START_INVALIDATED_ERROR_CODE;
+1 -11
View File
@@ -61,7 +61,7 @@ export function resolveSessionFilePathOptions(params: {
return undefined;
}
export const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
export function validateSessionId(sessionId: string): string {
const trimmed = sessionId.trim();
@@ -294,16 +294,6 @@ export function resolveSessionTranscriptPath(
): string {
return resolveSessionTranscriptPathInDir(sessionId, resolveAgentSessionsDir(agentId), topicId);
}
export function resolveExplicitSessionFilePath(
sessionFile: string,
opts?: SessionFilePathOptions,
): string {
return resolvePathWithinSessionsDir(resolveSessionsDir(opts), sessionFile, {
agentId: opts?.agentId,
});
}
export function resolveSessionFilePath(
sessionId: string,
entry?: { sessionFile?: string },
+1 -1
View File
@@ -5,7 +5,7 @@ import { normalizeSessionEntrySlotKey } from "../../plugins/session-entry-slot-k
import type { SessionEntry } from "./types.js";
/** Cleanup variants owned by plugin host lifecycle paths. */
export type PluginHostSessionCleanupMode = "plugin-owned-state" | "promoted-slots";
type PluginHostSessionCleanupMode = "plugin-owned-state" | "promoted-slots";
export type PluginHostSessionCleanupStoreParams = {
/** Agent that owns the resolved session store target. */
+3 -3
View File
@@ -19,11 +19,11 @@ export type SessionFreshness = {
staleReason?: SessionResetMode;
};
export const DEFAULT_RESET_MODE: SessionResetMode = "daily";
export const DEFAULT_RESET_AT_HOUR = 4;
const DEFAULT_RESET_MODE: SessionResetMode = "daily";
const DEFAULT_RESET_AT_HOUR = 4;
/** Returns the most recent daily reset boundary for the supplied wall-clock time. */
export function resolveDailyResetAtMs(now: number, atHour: number): number {
function resolveDailyResetAtMs(now: number, atHour: number): number {
const normalizedAtHour = normalizeResetAtHour(atHour);
const resetAt = new Date(now);
resetAt.setHours(normalizedAtHour, 0, 0, 0);
+13 -11
View File
@@ -1,24 +1,26 @@
// Session reset tests cover clearing persisted session state.
// Session reset tests cover conversation-aware reset classification.
import { beforeEach, describe, expect, it } from "vitest";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createSessionConversationTestRegistry } from "../../test-utils/session-conversation-registry.js";
import { isThreadSessionKey, resolveSessionResetType } from "./reset.js";
import { resolveSessionResetType } from "./reset.js";
describe("session reset thread detection", () => {
beforeEach(() => {
setActivePluginRegistry(createSessionConversationTestRegistry());
});
it("does not treat feishu conversation ids with embedded :topic: as thread suffixes", () => {
const sessionKey =
"agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user";
expect(isThreadSessionKey(sessionKey)).toBe(false);
expect(resolveSessionResetType({ sessionKey })).toBe("group");
it("does not treat Feishu conversation ids with embedded :topic: as thread suffixes", () => {
expect(
resolveSessionResetType({
sessionKey:
"agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
}),
).toBe("group");
});
it("still treats telegram :topic: suffixes as thread sessions", () => {
const sessionKey = "agent:main:telegram:group:-100123:topic:77";
expect(isThreadSessionKey(sessionKey)).toBe(true);
expect(resolveSessionResetType({ sessionKey })).toBe("thread");
it("still treats Telegram :topic: suffixes as thread sessions", () => {
expect(
resolveSessionResetType({ sessionKey: "agent:main:telegram:group:-100123:topic:77" }),
).toBe("thread");
});
});
+1 -4
View File
@@ -8,10 +8,7 @@ import { normalizeMessageChannel } from "../../utils/message-channel.js";
import type { SessionConfig, SessionResetConfig } from "../types.base.js";
/** Public reset policy exports plus helpers that classify direct, group, and thread sessions. */
export {
DEFAULT_RESET_AT_HOUR,
DEFAULT_RESET_MODE,
evaluateSessionFreshness,
resolveDailyResetAtMs,
resolveSessionResetPolicy,
type SessionFreshness,
type SessionResetMode,
@@ -23,7 +20,7 @@ import type { SessionResetType } from "./reset-policy.js";
const GROUP_SESSION_MARKERS = [":group:", ":channel:"];
/** Returns true when a session key is known to represent a thread. */
export function isThreadSessionKey(sessionKey?: string | null): boolean {
function isThreadSessionKey(sessionKey?: string | null): boolean {
return Boolean(resolveLoadedSessionThreadInfo(sessionKey).threadId);
}
-1
View File
@@ -9,7 +9,6 @@ export type ReadSessionUpdatedAt = (params: {
storePath: string;
sessionKey: string;
}) => number | undefined;
export type RecordSessionMetaFromInbound = (params: {
storePath: string;
sessionKey: string;
@@ -45,11 +45,9 @@ import {
} from "./session-accessor.js";
import {
appendSqliteTranscriptEvent,
appendSqliteTranscriptEvents,
appendSqliteTranscriptMessage,
branchSqliteCompactionCheckpointSession,
cleanupSqliteSessionLifecycleArtifacts,
deleteSqliteTranscript,
forkSqliteSessionEntryFromParentTarget,
listSqliteSessionEntries,
loadExactSqliteSessionEntry,
@@ -63,7 +61,6 @@ import {
replaceSqliteSessionEntrySync,
replaceSqliteTranscriptEvents,
restoreSqliteCompactionCheckpointSession,
sqliteTranscriptExists,
upsertSqliteSessionEntry,
} from "./session-accessor.sqlite.js";
import { parseSqliteSessionFileMarker } from "./sqlite-marker.js";
@@ -1868,40 +1865,6 @@ describe("sqlite session normalization", () => {
expect(upsertRow?.updated_at).toBe(upsertEntry.updatedAt);
});
it("replaces, appends, checks, and deletes SQLite transcript rows without filesystem artifacts", async () => {
const env = { ...process.env, OPENCLAW_STATE_DIR: paths.stateDir };
const scope = {
agentId: "main",
env,
sessionId: "transcript-state-session",
sessionKey: "agent:main:main",
storePath: paths.sqlitePath,
};
expect(sqliteTranscriptExists(scope)).toBe(false);
await replaceSqliteTranscriptEvents(scope, [
{ type: "session", id: "transcript-state-session", cwd: paths.tempDir },
{ type: "message", id: "msg-1", parentId: null, message: { content: "one" } },
]);
await appendSqliteTranscriptEvents(scope, [
{ type: "message", id: "msg-2", parentId: "msg-1", message: { content: "two" } },
]);
expect(sqliteTranscriptExists(scope)).toBe(true);
await expect(loadSqliteTranscriptEvents(scope)).resolves.toEqual([
{ type: "session", id: "transcript-state-session", cwd: paths.tempDir },
{ type: "message", id: "msg-1", parentId: null, message: { content: "one" } },
{ type: "message", id: "msg-2", parentId: "msg-1", message: { content: "two" } },
]);
await expect(deleteSqliteTranscript(scope)).resolves.toBe(true);
expect(sqliteTranscriptExists(scope)).toBe(false);
await expect(loadSqliteTranscriptEvents(scope)).resolves.toEqual([]);
expect(fs.existsSync(paths.sqlitePath)).toBe(true);
expect(fs.readdirSync(paths.tempDir)).not.toContain("transcript-state-session.jsonl");
});
it("branches a checkpoint by copying SQLite rows and creating the entry transactionally", async () => {
const env = { ...process.env, OPENCLAW_STATE_DIR: paths.stateDir };
const sourceScope = {
@@ -1718,37 +1718,6 @@ function parseLatestAssistantMessageEvent(
};
}
/** Checks whether the additive SQLite transcript store has rows for a transcript. */
export function sqliteTranscriptExists(scope: SessionTranscriptReadScope): boolean {
const resolved = resolveSqliteTranscriptReadScope(scope);
const database = openOpenClawAgentDatabase(toDatabaseOptions(resolved));
const db = getSessionKysely(database.db);
const row = executeSqliteQueryTakeFirstSync(
database.db,
db
.selectFrom("transcript_events")
.select("seq")
.where("session_id", "=", resolved.sessionId)
.limit(1),
);
return row !== undefined;
}
/** Deletes rows for one transcript from the additive SQLite transcript store. */
export async function deleteSqliteTranscript(scope: SessionTranscriptReadScope): Promise<boolean> {
const resolved = resolveSqliteTranscriptReadScope(scope);
return await runExclusiveSqliteSessionWrite(resolved, async () => {
let deleted = false;
runOpenClawAgentWriteTransaction((database) => {
deleted = deleteSqliteTranscriptEventsInTransaction(database, resolved.sessionId);
if (deleted) {
touchTranscriptMutationInTransaction(database, resolved.sessionId);
}
}, toDatabaseOptions(resolved));
return deleted;
});
}
/** Fully replaces rows for one transcript in the additive SQLite transcript store. */
export async function replaceSqliteTranscriptEvents(
scope: SessionTranscriptAccessScope,
@@ -1883,22 +1852,6 @@ export function appendSqliteTranscriptEventSync(
}, toDatabaseOptions(resolved));
}
/** Appends raw transcript events to the additive SQLite transcript store in one transaction. */
export async function appendSqliteTranscriptEvents(
scope: SessionTranscriptAccessScope,
events: TranscriptEvent[],
): Promise<void> {
if (events.length === 0) {
return;
}
const resolved = resolveSqliteTranscriptScope(scope);
await runExclusiveSqliteSessionWrite(resolved, async () => {
runOpenClawAgentWriteTransaction((database) => {
appendTranscriptEventsInTransaction(database, resolved, events);
}, toDatabaseOptions(resolved));
});
}
/** Appends a guarded transcript turn and touches its session row in one queued write. */
export async function appendSqliteExpectedSessionTranscriptTurn(
scope: SessionTranscriptWriteScope,
@@ -11,14 +11,14 @@ import { collectActiveSessionWorkAdmissionKeys } from "./store-maintenance-prese
import { pruneStaleEntries } from "./store.js";
import type { SessionEntry } from "./types.js";
export type SessionRegistryMaintenanceStoreSummary = {
type SessionRegistryMaintenanceStoreSummary = {
afterCount: number;
beforeCount: number;
preservedRunning: number;
pruned: number;
};
export type SessionRegistryMaintenanceStoreOptions = {
type SessionRegistryMaintenanceStoreOptions = {
/** Apply pruning to the backing store; false previews against a clone. */
apply: boolean;
/** Retention window for cron-run session entries. */
@@ -81,7 +81,7 @@ function readMessageText(message: unknown): string | undefined {
* assistant message text is indexed; tool results, reasoning blocks, and
* images stay out of the index by construction.
*/
export function extractTranscriptIndexEntry(
function extractTranscriptIndexEntry(
event: unknown,
fallbackTimestamp: number,
): TranscriptIndexEntry | undefined {

Some files were not shown because too many files have changed in this diff Show More