diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 5f16932759fb..67048e135e74 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -d2eded996abf00045c50ad400648f6e36565a7f158fc2000e15b805052436004 config-baseline.json -2179eacdae1e28fccf05beb4f89c24d3822e93897dd343a0ca2373f20e55ed85 config-baseline.core.json -82596aeb4c8d4cd18fefae554ff4d0c9e2cd82895b53d460db8f8ceb819f0ef6 config-baseline.channel.json -168e3c52da484c235fbc451dead71af16c3bee0c0186c460e5b4b4af70a496df config-baseline.plugin.json +3ad1269afb060076125275c04db3ce432020d6a90d969982806cc6019d57b78d config-baseline.json +0883f41da0e064a58ac557e244a97d6052c3d262225d309cffbbef9e7300f48e config-baseline.core.json +12ce7ba45d75c1753ce090fca8359a15708343cb2aeabb3dd99e8f6915062628 config-baseline.channel.json +eff5bf88593d81104b23578ba5255fa4cb9dd6ca3cb0566e07942190da68a589 config-baseline.plugin.json diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 1d6e724edca5..8d5fae1c01eb 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -5ddfbeaef2941a299f3677c7f8dd951f112f80ceefef7c6cbd8e4b1d5ec0670f plugin-sdk-api-baseline.json -df6272053970ab3c15f586f7a19c2abe0c9709b8938187e1ec0a85368e5ccd6c plugin-sdk-api-baseline.jsonl +ae6151f3cd57676e269374d916691e838e284b66404ee8d7a905654f444d528d plugin-sdk-api-baseline.json +7d7eafc52679b982474e2616878bbaf601ba3f1682ce5f3dcd4a0fdcaf4c83d6 plugin-sdk-api-baseline.jsonl diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index 8a0f2ce0e964..60cb8c3beede 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -1327,7 +1327,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden - `pruneAfter`: age cutoff for stale entries (default `30d`). - `maxEntries`: maximum number of SQLite session entries (default `500`). Runtime writes batch cleanup with a small high-water buffer for production-sized caps; `openclaw sessions cleanup --enforce` applies the cap immediately. - Short-lived gateway model-run probe sessions use fixed `24h` retention, but cleanup is pressure-gated: it only removes stale strict model-run probe rows when session-entry maintenance/cap pressure is reached. Only strict explicit probe keys matching `agent:*:explicit:model-run-` are eligible; normal direct, group, thread, cron, hook, heartbeat, ACP, and sub-agent sessions do not inherit this 24h retention. When model-run cleanup runs, it runs before the broader `pruneAfter` stale-entry cleanup and `maxEntries` cap. - - `rotateBytes`: deprecated and ignored; `openclaw doctor --fix` removes it from older configs. + - Legacy `rotateBytes` is rejected by the current schema; `openclaw doctor --fix` removes it from older configs. - `resetArchiveRetention`: retention for `*.reset.` transcript archives. Defaults to `pruneAfter`; set `false` to disable. - `maxDiskBytes`: optional sessions-directory disk budget. In `warn` mode it logs warnings; in `enforce` mode it removes oldest artifacts/sessions first. - `highWaterBytes`: optional target after budget cleanup. Defaults to `80%` of `maxDiskBytes`. diff --git a/docs/reference/session-management-compaction.md b/docs/reference/session-management-compaction.md index a24aa2990db8..ef08ae0f02d0 100644 --- a/docs/reference/session-management-compaction.md +++ b/docs/reference/session-management-compaction.md @@ -78,7 +78,7 @@ Maintenance keeps durable external conversation pointers such as group sessions Normal Gateway writes flow through the session accessor, which serializes per-agent SQLite mutations through the runtime writer path. Runtime code should prefer the accessor helpers in `src/config/sessions/session-accessor.ts`; legacy `sessions.json` helpers are migration and offline-maintenance tools. When a Gateway is reachable, non-dry-run `openclaw sessions cleanup` and `openclaw agents delete` delegate store mutations to the Gateway so cleanup joins the same writer queue; `--store ` is the explicit offline repair path for a selected legacy store and always stays local (as does `--dry-run`). `maxEntries` cleanup is batched for production-sized stores, so a store may briefly exceed the configured cap before the next high-water cleanup rewrites it down. Reads never prune or cap entries during Gateway startup - only writes or `openclaw sessions cleanup --enforce` do, and the latter also applies the cap immediately and prunes old unreferenced legacy transcript, checkpoint, and trajectory artifacts even with no disk budget configured. -OpenClaw no longer creates automatic `sessions.json.bak.*` rotation backups during Gateway writes. The legacy `session.maintenance.rotateBytes` key is ignored and `openclaw doctor --fix` removes it from older configs. +OpenClaw no longer creates automatic `sessions.json.bak.*` rotation backups during Gateway writes. The current schema rejects the legacy `session.maintenance.rotateBytes` key, and `openclaw doctor --fix` removes it from older configs. Transcript mutations use the session write queue for the SQLite transcript target: diff --git a/extensions/policy/src/doctor/register.test.ts b/extensions/policy/src/doctor/register.test.ts index 5be6e0e808f5..fa8b832d427a 100644 --- a/extensions/policy/src/doctor/register.test.ts +++ b/extensions/policy/src/doctor/register.test.ts @@ -1657,7 +1657,7 @@ describe("registerPolicyDoctorChecks", () => { tools: { elevated: { enabled: true } }, gateway: { mode: "remote", - remote: { enabled: true, url: "wss://remote.example.test:18789" }, + remote: { url: "wss://remote.example.test:18789" }, controlUi: { allowInsecureAuth: true, dangerouslyDisableDeviceAuth: true, @@ -1725,9 +1725,7 @@ describe("registerPolicyDoctorChecks", () => { tools: { elevated: { enabled: false } }, gateway: { mode: "local", - remote: { - enabled: true, - }, + remote: {}, controlUi: { allowInsecureAuth: false, dangerouslyDisableDeviceAuth: false, @@ -7943,7 +7941,6 @@ describe("registerPolicyDoctorChecks", () => { ...cfgWithPolicy(), gateway: { remote: { - enabled: true, url: "wss://remote.example.test:18789", }, }, diff --git a/scripts/lib/policy-config-coverage.jsonc b/scripts/lib/policy-config-coverage.jsonc index 2702380f526c..34cafe2da8a3 100644 --- a/scripts/lib/policy-config-coverage.jsonc +++ b/scripts/lib/policy-config-coverage.jsonc @@ -27,7 +27,6 @@ "gateway.http.endpoints.*.enabled", "gateway.mode", "gateway.nodes.denyCommands.*", - "gateway.remote.enabled", "gateway.tailscale.mode", "gateway.tailscale.preserveFunnel", "logging.redactSensitive", @@ -380,13 +379,6 @@ "policy": "gateway.remote.allow", "reason": "Policy observes whether Gateway remote mode is enabled.", }, - { - "pattern": "gateway.remote.enabled", - "status": "observed", - "area": "gateway", - "policy": "gateway.remote.allow", - "reason": "Policy observes explicit remote Gateway enablement.", - }, { "pattern": "gateway.http.endpoints.*.enabled", "status": "observed", diff --git a/scripts/ts-max-loc-baseline-v2.json b/scripts/ts-max-loc-baseline-v2.json index a6448a114728..221d4f377c0f 100644 --- a/scripts/ts-max-loc-baseline-v2.json +++ b/scripts/ts-max-loc-baseline-v2.json @@ -817,8 +817,8 @@ "src/config/mutate.ts": 1240, "src/config/plugin-auto-enable.shared.ts": 1158, "src/config/redact-snapshot.ts": 890, - "src/config/schema.help.ts": 2089, - "src/config/schema.labels.ts": 1133, + "src/config/schema.help.ts": 2087, + "src/config/schema.labels.ts": 1132, "src/config/schema.ts": 847, "src/config/sessions/cleanup-service.ts": 694, "src/config/sessions/disk-budget.ts": 848, @@ -831,13 +831,13 @@ "src/config/sessions/transcript.ts": 868, "src/config/sessions/types.ts": 821, "src/config/types.agent-defaults.ts": 606, - "src/config/types.gateway.ts": 604, + "src/config/types.gateway.ts": 602, "src/config/types.tools.ts": 784, "src/config/validation.ts": 2130, "src/config/zod-schema.agent-runtime.ts": 1165, "src/config/zod-schema.core.ts": 1157, "src/config/zod-schema.providers-core.ts": 1748, - "src/config/zod-schema.ts": 1649, + "src/config/zod-schema.ts": 1648, "src/context-engine/registry.ts": 1075, "src/context-engine/types.ts": 523, "src/crestodian/chat-engine.ts": 1104, diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index cf5136cbdbcc..1f894a04dca6 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -846,7 +846,6 @@ describe("gateway.remote.transport", () => { const res = validateConfigObject({ gateway: { remote: { - enabled: true, transport: "direct", url: "wss://gateway.example.ts.net", }, diff --git a/src/config/dead-config-keys.test.ts b/src/config/dead-config-keys.test.ts new file mode 100644 index 000000000000..d46ebf95cf06 --- /dev/null +++ b/src/config/dead-config-keys.test.ts @@ -0,0 +1,37 @@ +// Verifies schema-only config keys stay outside the canonical config contract. +import { describe, expect, it } from "vitest"; +import { validateConfigObjectRaw } from "./validation.js"; + +function expectUnknownKey(params: { config: Record; path: string; key: string }) { + const result = validateConfigObjectRaw(params.config); + expect(result.ok).toBe(false); + if (result.ok) { + return; + } + const issue = result.issues.find( + (candidate) => + candidate.path === params.path && + candidate.message.includes(`Unrecognized key: "${params.key}"`), + ); + if (!issue) { + throw new Error(`Expected unknown ${params.path}.${params.key} validation issue`); + } +} + +describe("dead config keys", () => { + it("rejects legacy session.maintenance.rotateBytes", () => { + expectUnknownKey({ + config: { session: { maintenance: { rotateBytes: "10mb" } } }, + path: "session.maintenance", + key: "rotateBytes", + }); + }); + + it("rejects unused gateway.remote.enabled", () => { + expectUnknownKey({ + config: { gateway: { remote: { enabled: false } } }, + path: "gateway.remote", + key: "enabled", + }); + }); +}); diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 600668778123..be1f01bc5ee3 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -188,7 +188,6 @@ const TARGET_KEYS = [ "session.maintenance.pruneAfter", "session.maintenance.pruneDays", "session.maintenance.maxEntries", - "session.maintenance.rotateBytes", "session.maintenance.resetArchiveRetention", "session.maintenance.maxDiskBytes", "session.maintenance.highWaterBytes", @@ -840,13 +839,6 @@ describe("config help copy quality", () => { expect(pruneAfter.includes("30d")).toBe(true); expect(pruneAfter.includes("12h")).toBe(true); - const rotate = expectDefined( - FIELD_HELP["session.maintenance.rotateBytes"], - 'FIELD_HELP["session.maintenance.rotateBytes"] test invariant', - ); - expect(/deprecated/i.test(rotate)).toBe(true); - expect(rotate.includes("doctor --fix")).toBe(true); - const deprecated = expectDefined( FIELD_HELP["session.maintenance.pruneDays"], 'FIELD_HELP["session.maintenance.pruneDays"] test invariant', diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index fae1b2e2aa05..8d13bfdb7be4 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -1792,8 +1792,6 @@ export const FIELD_HELP: Record = { "Deprecated age-retention field kept for compatibility with legacy configs using day counts. Use session.maintenance.pruneAfter instead so duration syntax and behavior are consistent.", "session.maintenance.maxEntries": "Caps total session entry count retained in the store to prevent unbounded growth over time. Use lower limits for constrained environments, or higher limits when longer history is required.", - "session.maintenance.rotateBytes": - 'Deprecated and ignored. Do not use for `sessions.json` growth control; OpenClaw no longer creates automatic rotation backups, and "openclaw doctor --fix" removes this key.', "session.maintenance.resetArchiveRetention": "Age-based retention for archived transcripts (`*.reset.` and `*.deleted.`). Defaults to keeping archives until the disk budget evicts them oldest-first; set a duration (for example `30d`) to opt into wall-clock deletion, or `false` to disable it explicitly.", "session.maintenance.maxDiskBytes": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index ebec78d2e7cc..cb61f31c9e4f 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -897,7 +897,6 @@ export const FIELD_LABELS: Record = { "session.maintenance.pruneAfter": "Session Prune After", "session.maintenance.pruneDays": "Session Prune Days (Deprecated)", "session.maintenance.maxEntries": "Session Max Entries", - "session.maintenance.rotateBytes": "Deprecated Session Rotate Size", "session.maintenance.resetArchiveRetention": "Session Reset Archive Retention", "session.maintenance.maxDiskBytes": "Session Max Disk Budget", "session.maintenance.highWaterBytes": "Session Disk High-water Target", diff --git a/src/config/sessions/store.pruning.integration.test.ts b/src/config/sessions/store.pruning.integration.test.ts index 305b3a3e12a2..dc9a00767d5f 100644 --- a/src/config/sessions/store.pruning.integration.test.ts +++ b/src/config/sessions/store.pruning.integration.test.ts @@ -1657,7 +1657,6 @@ describe("Integration: saveSessionStore with pruning", () => { mode: "enforce", pruneAfter: "365d", maxEntries: 100, - rotateBytes: 200, }, }, }); @@ -1696,7 +1695,6 @@ describe("Integration: saveSessionStore with pruning", () => { mode: "enforce", pruneAfter: "365d", maxEntries: 1, - rotateBytes: 200, }, }, }); diff --git a/src/config/types.base.ts b/src/config/types.base.ts index dba59b7573eb..0180d7e718fd 100644 --- a/src/config/types.base.ts +++ b/src/config/types.base.ts @@ -265,8 +265,6 @@ export type SessionMaintenanceConfig = { pruneDays?: number; /** Maximum number of session entries to keep. Default: 500. */ maxEntries?: number; - /** @deprecated Ignored. Run `openclaw doctor --fix` to remove. */ - rotateBytes?: number | string; /** * Age-based retention for archived transcripts (`*.reset.` and * `*.deleted.`). Default and `false`: keep archives until the diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index 1c7cc787c939..a66719a3ac25 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -251,8 +251,6 @@ export type GatewayTailscaleConfig = { }; export type GatewayRemoteConfig = { - /** Whether remote gateway surfaces are enabled. Default: true when absent. */ - enabled?: boolean; /** Remote Gateway WebSocket URL (ws:// or wss://). */ url?: string; /** Transport for macOS remote connections (ssh tunnel or direct WS). */ diff --git a/src/config/zod-schema.session.ts b/src/config/zod-schema.session.ts index 51297851a06e..674d48d07fbe 100644 --- a/src/config/zod-schema.session.ts +++ b/src/config/zod-schema.session.ts @@ -88,7 +88,6 @@ export const SessionSchema = z /** @deprecated Use pruneAfter instead. */ pruneDays: z.number().int().positive().optional(), maxEntries: z.number().int().positive().optional(), - rotateBytes: z.union([z.string(), z.number()]).optional(), resetArchiveRetention: z.union([z.string(), z.number(), z.literal(false)]).optional(), maxDiskBytes: z.union([z.string(), z.number()]).optional(), highWaterBytes: z.union([z.string(), z.number()]).optional(), diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index d57818344508..867c1b955f7f 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -58,7 +58,6 @@ type ConfigSchemaShape = { }; const GatewayRemoteSchemaShape = { - enabled: z.boolean().optional(), url: z.string().optional(), transport: z.union([z.literal("ssh"), z.literal("direct")]).optional(), remotePort: z.number().int().min(1).max(65_535).optional(), diff --git a/src/secrets/runtime-gateway-local-surfaces.test.ts b/src/secrets/runtime-gateway-local-surfaces.test.ts index 669ec34dbc35..3818eb1a8609 100644 --- a/src/secrets/runtime-gateway-local-surfaces.test.ts +++ b/src/secrets/runtime-gateway-local-surfaces.test.ts @@ -56,7 +56,6 @@ describe("secrets runtime gateway local surfaces", () => { password: "local-password", }, remote: { - enabled: true, token: { source: "env", provider: "default", id: "MISSING_REMOTE_TOKEN" }, password: { source: "env", provider: "default", id: "MISSING_REMOTE_PASSWORD" }, }, @@ -181,7 +180,6 @@ describe("secrets runtime gateway local surfaces", () => { password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD_REF" }, }, remote: { - enabled: true, token: "remote-token", }, }, @@ -199,7 +197,6 @@ describe("secrets runtime gateway local surfaces", () => { mode, }, remote: { - enabled: true, token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN_REF" }, password: { source: "env", @@ -234,7 +231,6 @@ describe("secrets runtime gateway local surfaces", () => { gateway: { mode: "local", remote: { - enabled: true, token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN_REF" }, }, }, @@ -256,7 +252,6 @@ describe("secrets runtime gateway local surfaces", () => { gateway: { mode: "local", remote: { - enabled: true, password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD_REF" }, }, }, @@ -281,7 +276,6 @@ describe("secrets runtime gateway local surfaces", () => { mode: "local", tailscale: { mode: "serve" }, remote: { - enabled: true, token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN" }, password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD" }, },