fix(sessions): add context-engine fallback session-size guard (#76940)#76950
fix(sessions): add context-engine fallback session-size guard (#76940)#76950100yenadmin wants to merge 2 commits intoopenclaw:mainfrom
Conversation
…aw#76940) When a configured context-engine plugin (e.g. lossless-claw) fails to resolve and the gateway falls back to the default `legacy` engine, walk the affected agent's transcript directory and apply a configured action to any session jsonl exceeding the size threshold. Surfaces the real failure mode (engine disabled / unregistered / contract violation) instead of letting next-load context overflow stall the gateway. Real-world trigger: the openclaw 2026.5.2 npm install silently dropped several configured extensions from the runtime plugin set, including a context-engine slot plugin. The next gateway boot loaded an existing session at 808 messages / 6.3 MB, which immediately hit context overflow on the first turn. Larger sessions in the same install (200 MB jsonl files from prior work) would have been unrecoverable without manual jsonl rotation. Defaults: - sizeBytes: 1mb (small enough to catch realistic overflow cases) - action: "auto" (archive when an engine sqlite store is present and the engine has the source-of-truth; warn otherwise) Config surface (session.maintenance.contextFallbackGuard): - sizeBytes: number | string (e.g. "1mb", "512kb") - action: "warn" | "archive" | "block" | "auto" Implementation: - New module src/context-engine/fallback-guard.ts walks the agent transcript dir, applies action per oversized file, dedups warnings per process, treats archive-rename failure as warn so signal isn't lost. - Wired into all four resolver fallback sites in src/context-engine/registry.ts (engine-not-registered, factory throw, contract validation throw, contract validation error) via a single fallbackToDefault helper. - "block" action throws from the resolver with a structured message naming the offending transcripts and the failed engine id. - Plumbed agentId through ResolveContextEngineOptions so the guard inspects the correct agent's sessions; updated the main embedded runner call site. Other call sites continue to default to the primary agent id (existing behavior). Tests: - 12 unit tests in fallback-guard.test.ts cover warn/archive/block, auto resolution in both directions, threshold parsing, default threshold, dedup, archive-failure-falls-back-to-warn, transcript-name filtering (skip .bak / .reset / .archived / .deleted / .trim-backup), and missing sessions dir. - All 34 existing src/context-engine tests pass unchanged. Closes openclaw#76940
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Codex review: needs changes before merge. Summary Reproducibility: yes. for the review findings: source inspection of PR head shows the logger type mismatch, stale default docs, default-agent scan gap, classifier divergence, and invalid recovery command. The underlying gateway-stall class is supported by linked reports and logs, but I did not live-reproduce it in this read-only pass. Next step before merge Security Review findings
Review detailsBest possible solution: Land a revised version that fixes the type/build issue, aligns config defaults, scans the intended agent sessions, reuses existing transcript artifact helpers, points operators to real recovery commands, and keeps broader transcript-size caps tracked separately. Do we have a high-confidence way to reproduce the issue? Yes for the review findings: source inspection of PR head shows the logger type mismatch, stale default docs, default-agent scan gap, classifier divergence, and invalid recovery command. The underlying gateway-stall class is supported by linked reports and logs, but I did not live-reproduce it in this read-only pass. Is this the best way to solve the issue? No, not as currently written. The guard direction is plausible, but the patch should fix the concrete blockers and get maintainer agreement on the startup auto-archive policy before merge. Full review comments:
Overall correctness: patch is incorrect Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against e5ec14a06a67. |
There was a problem hiding this comment.
Pull request overview
Adds a defensive “context-engine fallback session-size guard” so that when a configured context engine fails to resolve and the gateway falls back to legacy, the system scans the affected agent’s session transcript directory and applies a configurable policy to oversized .jsonl transcripts.
Changes:
- Add
applyContextEngineFallbackGuard()(withwarn/archive/block/auto) plus unit tests. - Invoke the guard from
resolveContextEngine()at each fallback site; plumbagentIdfrom the embedded runner. - Introduce new config surface
session.maintenance.contextFallbackGuard.{sizeBytes,action}across schema/types/help/labels and document it inCHANGELOG.md.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/context-engine/registry.ts | Runs the fallback guard before returning the default context engine; adds agentId to resolver options. |
| src/context-engine/fallback-guard.ts | Implements transcript-dir scan + size threshold policy actions (warn/archive/block/auto). |
| src/context-engine/fallback-guard.test.ts | Unit tests covering action behaviors, parsing, filtering, dedup, and failure paths. |
| src/config/zod-schema.session.ts | Adds Zod validation for session.maintenance.contextFallbackGuard and validates sizeBytes. |
| src/config/types.base.ts | Adds typed config definitions for the new guard. |
| src/config/schema.labels.ts | Adds labels for the new config keys. |
| src/config/schema.help.ts | Adds help text describing the new guard behavior and defaults. |
| src/config/schema.base.generated.ts | Regenerates the base schema output to include the new keys. |
| src/agents/pi-embedded-runner/run.ts | Passes params.agentId into resolveContextEngine() so the guard scans the correct agent. |
| CHANGELOG.md | Documents the new guard config and semantics. |
…rd + operator recovery prompt (openclaw#76940) Three follow-ups to the initial guard addition based on operator feedback: 1) Default threshold 1mb → 2mb. 1MiB jsonl is roughly 250k tokens of message content; 2MiB is roughly 500k tokens. 500k tokens already overflows every shipping context window — for models in the 200-256k effective-window range it overflows much sooner. Operators on smaller-context models can still dial down via session.maintenance.contextFallbackGuard.sizeBytes. 2) Boot-time guard (applyContextEngineBootGuard). The on-fallback path only catches "configured engine failed to load." It misses the much more common case: no context engine was ever configured. The legacy engine windows the prompt in-memory at request time but never shrinks the on-disk jsonl, so an unmanaged session grows append-only until the gateway stalls on next start. The boot guard runs once at startup and applies the same policy when slots.contextEngine is unset/legacy or the configured plugin is missing from loadedPluginIds. Both triggers funnel into the same applyContextEngineFallbackGuard implementation; one config knob, one policy, two entry points. 3) Operator-facing message rewrite. The terse single-line warn/archive log is replaced with a structured block that names the file, the engine that failed, the size, the available repair commands (openclaw doctor --fix / sessions archive / config set slots), AND a copy-pasteable recovery prompt for the next agent turn. The prompt instructs the agent to read the archived tail (last ~200 non-system messages, group into chunks of 1-2k tokens each, stop at ~40k tokens aggregate), giving the fresh session enough context to continue meaningfully. Sized so we use the fresh session's available context window — not so miserly that the user loses their working state, not so generous that we eat the whole window. Tests: - 17 unit tests pass (12 original + 5 new for boot-guard / recovery prompt) - Existing 34 src/context-engine tests unchanged - Lint clean on changed files Wiring: - fallback-guard.ts: bump DEFAULT_FALLBACK_GUARD_SIZE_BYTES, add renderWarnMessage / renderArchiveMessage / renderRecoveryPrompt, add applyContextEngineBootGuard - server-startup-post-attach.ts: invoke boot guard right after logGatewayStartup; never let guard exceptions stall startup - CHANGELOG: expanded entry covering both trigger paths and threshold rationale Refs openclaw#76940.
|
Pushed 1. Default threshold 1mb → 2mb
2. Boot-time guard in addition to on-fallbackThe original PR only caught "configured engine failed to load." It missed the much more common case: no context engine was ever configured. The legacy engine windows the prompt in-memory at request time but never shrinks the on-disk jsonl — so an unmanaged session grows append-only until the gateway stalls on next start. (See cross-referenced reports: #64767 444MB jsonl, #66360 unbounded growth, #73691 MEMORY.md gateway freeze — all this same shape.) The boot guard runs once at startup (
Both trigger paths funnel into the same When the configured engine is loaded and active, the boot guard short-circuits — the engine itself is responsible for size management (LCM rewrites the jsonl on every compaction, so its sessions stay bounded indefinitely). 3. Operator-facing message rewriteThe terse single-line warn/archive log is replaced with a structured block that names the file, the engine that failed, the size, the repair commands, AND a copy-pasteable recovery prompt for the next agent turn: Sizing rationale on the prompt:
Validation
Operator notesThe boot guard runs in a The on-fallback guard remains for the request-time path (resolver fails mid-conversation rather than at boot), so a regression that takes down the engine after boot is still caught. |
|
@steipete @vincentkoc I recommend this fix in the hot fix to prevent context engines from blowing up gateways if disabled or deleted. |
…rd (openclaw#76940) Addresses Copilot's 5 inline review comments on PR openclaw#76950 plus 13 additional findings from a parallel adversarial sweep. Single consolidated commit; per-finding diff lives below. == Copilot review comments == (1) Archive-failure path used optional `logger.error?.()` so the fallback signal could be silently lost on loggers without `error`. Now ALWAYS emits a `warn` (deduped via warnedPaths) in addition to the optional error, so the signal survives loggers that only have warn. File: src/context-engine/fallback-guard.ts (archive catch block) (2) The block-action error thrown from resolveContextEngine() didn't name the offending transcripts. Now includes basenames of the blocked files (capped at 5 + "+N more"), so operators see exactly which sessions trip the gate without leaking absolute paths into error stacks shared in bug reports. File: src/context-engine/registry.ts (fallbackToDefault helper) (3) `defaultHasContextEngineHistory()` walked path.dirname THREE times from sessionsDir, landing at ~/.openclaw (global state) instead of the agent state dir. The auto-action heuristic mis-detected history. Now walks two dirs (sessionsDir → agentDir) and ALSO checks the global state root for legacy LCM installs that keep a shared sqlite store. Function additionally takes the injected `ioFs` so tests can stub it (previously bare `fs.existsSync`). File: src/context-engine/fallback-guard.ts (4) The custom `isLiveSessionTranscript` filter accepted non-primary artifacts (.trajectory.jsonl, .checkpoint.<uuid>.jsonl) and could exclude legitimate primaries with `.deleted.` in id. Replaced with `isPrimarySessionTranscriptFileName` from src/config/sessions/artifacts.ts — the same predicate every other transcript code path uses. Custom filter deleted. File: src/context-engine/fallback-guard.ts (5) `ApplyFallbackGuardOptions.agentDir` was documented + accepted by callers but never read. Now used to derive sessionsDir (`<agentDir>/sessions`) when supplied, before falling back to agentId-based resolution. Embedded runners that scope to a non-default state root now have the guard scoped correctly. File: src/context-engine/fallback-guard.ts (resolution order block) == Additional adversarial-sweep findings == (6) [P0] Archive filename `<id>.archived-no-context-engine-<ts>.jsonl` was NOT recognized by `isPrimarySessionTranscriptFileName`, so the archived file got loaded as a live session on next gateway start — the guard accomplished nothing in the case it exists to fix. Now extends `SessionArchiveReason` with "context-fallback" and uses the canonical `<id>.jsonl.context-fallback.<iso-ts>-<nonce>` shape. Existing disk-budget pruning + transcript helpers correctly recognize and exclude these files. Files: src/config/sessions/artifacts.ts, src/context-engine/fallback-guard.ts (buildArchivePath) (7) [P0] Doc-drift: schema.help.ts + types.base.ts JSDoc said "Default `1mb`" after the bump to 2mb in the prior commit. Both sites updated; types.base.ts now points readers at the constant in fallback-guard.ts so future changes stay in sync. Files: src/config/schema.help.ts, src/config/types.base.ts (8) [P0] `action: "block"` was silently downgraded to warn at gateway boot — the boot wrapper discarded the outcome and never called `fallbackGuardOutcomeIsBlocking`. Now collects blocking outcomes across all agents and sets `process.exitCode = 1` with a structured error log, so launchd / systemd / docker treat the boot as unhealthy. Startup itself does not throw (degrades to "no protection this boot" if the guard faults internally). File: src/gateway/server-startup-post-attach.ts (9) [P0] Boot guard was hardcoded to the "main" agent. Multi-agent installs (concierge, support, etc.) had no boot-time protection. Now iterates every agent under ~/.openclaw/agents/ via readdirSync(withFileTypes); falls back to DEFAULT_AGENT_ID for fresh installs / test environments. File: src/gateway/server-startup-post-attach.ts (10) [P0] Four resolveContextEngine call sites passed no agentId, so when fallback fired for a non-default agent the guard walked the wrong agent's session directory. Plumbed agentId through: - src/agents/pi-embedded-runner/compact.queued.ts (hoisted resolveSessionAgentIds early) - src/agents/command/cli-compaction.ts (extended cliCompactionDeps.resolveContextEngine signature) - src/agents/subagent-spawn.ts (parseAgentSessionKey on requesterInternalKey) - src/agents/subagent-registry.ts (parseAgentSessionKey on childSessionKey) (11) [P1] `renameSync` race: archive timestamp was millisecond- resolution; two archives in the same ms (rapid fallback across agents, or two large transcripts in one pass) collided and `renameSync` silently overwrites. Now appends a 6-hex random nonce. File: src/context-engine/fallback-guard.ts (buildArchivePath) (12) [P1] `sizeBytes: 0` (or "0b" / "0kb") passed schema validation but was silently downgraded to the default in resolveGuardConfig — opposite of operator intent (trying to disable or tighten the guard would actually loosen it to default). Now logs an explicit warn so the misconfig is visible. File: src/context-engine/fallback-guard.ts (resolveGuardConfig) (13) [P1] `openclaw sessions archive <id>` doesn't exist as a subcommand — operator-facing message printed a fake command. Replaced with `openclaw sessions cleanup --enforce`. `openclaw config set ... ""` likewise replaced with the canonical `openclaw config unset ...` form. File: src/context-engine/fallback-guard.ts (renderWarnMessage, renderArchiveMessage) (14) [P1] Multi-line operator block destroyed by JSON log encoding (the JSON file logger encodes \n literally; the box-drawing characters became one mile-long line). Replaced unicode box drawing with explicit `----- BEGIN RECOVERY PROMPT -----` / `----- END RECOVERY PROMPT -----` delimiters that survive JSON encoding and are easy to grep / extract. File: src/context-engine/fallback-guard.ts (15) [P1] Home-dir leaked into operator-facing paths. When operators paste the message into bug reports / GitHub issues / chat the username was exposed. Added `redactHomePrefix()` that substitutes `~` for the user's home prefix in the rendered prompt + Archived block. The structured `summary` line keeps the absolute path for grep-by-path. File: src/context-engine/fallback-guard.ts (renderRecoveryPrompt, renderWarnMessage, renderArchiveMessage) (16) [P1] Two structurally-identical action union types (`SessionContextFallbackGuardAction` in types.base.ts and `FallbackGuardAction` in fallback-guard.ts). Now a single source of truth: `FallbackGuardAction` aliases the public type. Files: src/context-engine/fallback-guard.ts (17) [P1] Recovery prompt assumed agent could safely Read a multi-MiB jsonl. Now explicitly tells the agent to use Read with offset/limit, skip individual messages over ~10k tokens, and bound itself to ~40k aggregate. Also added a "next likely action" bullet so the fresh session resumes work cleanly. File: src/context-engine/fallback-guard.ts (renderRecoveryPrompt) (18) [P1] `agent="(default)"` printed in summary line when agentId was unset, but actual on-disk path uses `main`. Operators grepping by the printed label couldn't find the path. Now uses `DEFAULT_AGENT_ID` ("main") consistently. File: src/context-engine/fallback-guard.ts (renderAgentLabel) (19) [P2] platform-specific restart hint: `launchctl` was unconditional. Now branches on process.platform to suggest `launchctl` on macOS, `systemctl --user restart` on Linux, `Restart-Service` on Windows. File: src/context-engine/fallback-guard.ts (platformRestartHint) (20) [P2] `lstatSync` used in place of `statSync` so a symlink in the sessions dir doesn't get archived (previously could rename the link and orphan the target). File: src/context-engine/fallback-guard.ts (21) [P2] Aggregated stat-error count is now logged at end of pass so an operator sees a signal when (e.g.) every file in the sessions dir is unreadable due to permission/quarantine drift — previously the swallowed errors produced an empty outcome with no clue why. File: src/context-engine/fallback-guard.ts (statErrors counter) == Tests == - 25 fallback-guard.test.ts cases pass (was 17 — 8 added covering canonical archive name, prior archives ignored, home-dir redact, nonce collision-avoidance, sizeBytes:0 warns, agentDir option used, archive-failure-also-warns, lstat for symlinks) - 6 zod-schema.session-maintenance-extensions.test.ts cases for contextFallbackGuard (valid/absent/casing/typo/malformed/wrong-level) - All 142 src/config/sessions tests pass unchanged - 95 total context-engine + maintenance-extension tests pass - Lint clean on changed files (the 2 remaining `__testing` warnings are pre-existing in v2026.5.2 and unrelated) Refs openclaw#76940. Addresses inline review on PR openclaw#76950.
|
Thanks for jumping on this and for the detailed incident write-up. For the hotfix, we are not going to take this core guard as-is. The immediate failure is a Closing this PR for now. If we revisit core hardening, the safer shape is likely a narrower diagnostic/warn-only guard first, with docs and recovery paths aligned to existing session maintenance. |
|
@steipete not sure if your AI wrote that but the issue isn't lossless claw here. We have no control over the session file management. LCM manages session only when enabled. Nothing we can do when plugin system is rewritten and mass disables plugins (plugin builders can't conform to new requirements that come out in new patch without notice- normal process is to put out requirements in update and let them know new system is phasing in on X date). We lose a lot of good will constantly rebuilding plugin infra. That being said when it is disabled the session file explodes the gateway because LCM handles session management when it is enabled. When it is disabled or deleted, the session file breaks the gateway. I was in the middle of the commits for this fix but got blocked by it closing. We can on LCM side start to truncate and manage session file while LCM is enabled but that is on your decision if you would prefer we do that. |
… dead activeContextEngineId, accurate token estimate, boot-message phrasing (openclaw#76940) Three additional Copilot/sweep findings on the prior consolidated commit: (22) [P1] params.log.error doesn't typecheck on the gateway post-attach surface (the typed log here is { info, warn } only — error is on the richer subsystem logger but not on this narrowed shape). Switched the boot-guard logger sink to omit `error` (the guard always emits a `warn` fallback when the optional error is absent) and the structured "block" startup line to use `warn`. The non-zero process.exitCode is the real signal external supervisors pick up, so demoting the channel doesn't lose anything. File: src/gateway/server-startup-post-attach.ts (23) [P1] `activeContextEngineId` was required on ApplyBootGuardOptions but applyContextEngineBootGuard ignored it (re-read the same value from `options.config.plugins.slots.contextEngine`). Easy to misinterpret as "the boot guard respects the explicit activeContextEngineId override" when it does not. Removed the dead field; callers now pass only the loaded set + config. File: src/context-engine/fallback-guard.ts (ApplyBootGuardOptions) (24) [P1] Operator messages printed `RECOVERY_PROMPT_MAX_SUMMARY_TOKENS` ("40k+ tokens of message content") for every transcript regardless of file size. Now estimates from the actual byte size with a simple ~4-chars-per-token heuristic, formatted with reasonable significant digits ("~580k tokens (estimated)" / "~1.4M tokens (estimated)"). File: src/context-engine/fallback-guard.ts (estimateTokensFromBytes) (25) [P2] The "Reason: Context engine X is configured but failed (...)" line in operator messages reads incorrectly when the boot guard fires for the no-engine case (failedEngineId is the synthetic "(legacy/none)" label, not a real configured engine). Branch on that label and emit a no-engine-specific reason block instead. File: src/context-engine/fallback-guard.ts (renderReasonLines) Tests: all 25 fallback-guard cases still pass; the GuardMessageContext type now carries `sizeBytes` so the token estimate has the source data without re-deriving from the formatted MiB string. Refs openclaw#76940. Addresses Copilot review on PR openclaw#76950.
Adversarial-review pass: Copilot's review + 3 internal sub-agent sweepsTwo new commits address everything the Codex bot review found, plus 13 additional findings from a parallel adversarial-agent sweep I ran in 4 dimensions (concurrency / fs, config validation, integration / multi-agent, UX / recovery prompt). Commits in this push
Each Copilot inline comment is replied with the specific commit + file:line + relevant test that covers the fix. Highest-impact fix (would-have-killed-the-PR-purpose level)The previous archive shape was Other significant fixes beyond Copilot's 5
Test coverage
Adversarial methodology3 sub-agents ran in parallel with focused scopes (concurrency/fs, config/back-compat, integration/UX). Each was given the PR diff + Copilot's existing 5 findings up front so they didn't duplicate. Results merged into the consolidated commit; per-finding rationale and file:line in the commit body of The "single conversation, three perspectives" pattern surfaced bugs Copilot missed — particularly the archive-shape-not-recognized P0 (which would have nullified the whole PR), the multi-agent-boot-guard-hardcoded-main P0, and the 4-other-call-sites-pass-no-agentId P0. Worth doing on guard-style PRs that touch multiple subsystems. |
TLDR: when a context engine fails, is disabled, or breaks. It breaks gateway and session > and OC gets blamed for bad experience. This fixes that. No plugin should be able to break or disable gateway or leave it hanging 20+ min.
Summary
Implements the defensive guard proposed in #76940. When a configured context-engine plugin (e.g.
lossless-claw) fails to resolve and the gateway falls back to the defaultlegacyengine, walks the affected agent's transcript directory and applies a configured action to any session jsonl exceeding the size threshold.Real-world trigger that motivated this: the
2026.5.2npm install silently dropped several configured extensions from the runtime plugin set, including a context-engine slot plugin. The next gateway boot loaded an existing session at 808 messages / 6.3 MB, which immediately hit context overflow on the first turn:Larger sessions in the same install (200 MB jsonl files from prior work) would have been unrecoverable without manual jsonl rotation. Cross-references in the issue (#64767 — 444 MB jsonl hangs gateway, #66360, #73691, #75740) show this is a recurring class of failure.
Config surface
Action semantics:
warn— log a structured, actionable warning naming file + size + applied action. Per-process dedup so repeated resolves don't spam.archive— rename the jsonl to<basename>.archived-no-context-engine-<ISO>.jsonl. Recoverable via existing archive-recovery work (Recover archived (.reset) session transcripts in memory hook + session-logs skill #71537, [codex] Include reset archives in session log searches #76119).block— throw from the resolver with a structured message naming the offending transcripts and the failed engine id, refusing to fall back until an operator takes action.auto(default) — archive when the agent's state dir contains a known context-engine sqlite store (lcm.db,lossless-claw.db,context-engine.db), warn otherwise. Rationale: when an engine like LCM is in use, the jsonl is just the live buffer — the engine has the source-of-truth in SQLite, so archiving the jsonl loses at most the fresh tail (~32-64 messages, equivalent blast radius to a forced compaction). When no engine has run, the jsonl IS the only record, soautoconservatively warns.Threshold note:
2 MBdefault is small intentionally — 1mb is roughly 250k tokens which would overflow gpt-5.5 in the wild. Operators can raise this via config when their workflow tolerates more.Implementation
src/context-engine/fallback-guard.ts— pure function that walks the agent transcript dir, applies action per oversized file, dedups warnings per process, falls back towarnwhen archive rename fails (so we never silently lose the signal). All filesystem and resolver calls are injectable for testing.src/context-engine/registry.ts— singlefallbackToDefaulthelper closure insideresolveContextEngineruns the guard before each of the four fallback sites (engine-not-registered, factory throw, contract validation throw, contract validation error). Theblockaction throws from the resolver with a structured message;warn/archive/autocontinue to the default engine.src/agents/pi-embedded-runner/run.ts— plumbparams.agentIdthroughResolveContextEngineOptionsso the guard inspects the correct agent's sessions. Other resolver call sites continue to default to the primary agent id (existing behavior — opt-in plumbing for the future).src/config/zod-schema.session.ts,types.base.ts,schema.labels.ts,schema.help.ts,schema.base.generated.ts— config schema, types, labels, help text.session.maintenance.contextFallbackGuard.{sizeBytes,action}validated alongside the existing maintenance fields.Filename filter ignores
.archived-*,.bak,.reset,.deleted,.trim-backupso we never re-archive our own archives or interfere with other rotation systems.Tests
12 new unit tests in
src/context-engine/fallback-guard.test.tscover:1 MiBthreshold when config absentfallbackGuardOutcomeIsBlockinghelperAll 34 existing
src/context-engine/*.test.tstests pass unchanged. The 1 failing test insrc/config/io.compat.test.ts("logs validation warnings with real line breaks") fails on bareupstream/maintoo — pre-existing, unrelated.Validation
pnpm exec vitest run src/context-engine/→ 58 tests passedpnpm exec vitest run src/config/→ 1226 passed / 1 pre-existing failurepnpm exec oxlint --type-awareon changed files → 0 errorspnpm check:base-config-schema→ clean (regeneratedschema.base.generated.ts)Change Type
Scope
Closes #76940