Skip to content

Complete catalog-led configuration boundaries - #1293

Merged
malpern merged 15 commits into
masterfrom
codex/config-admission-freshness
Sep 13, 2026
Merged

malpern merged 15 commits into
masterfrom
codex/config-admission-freshness

Conversation

@malpern

@malpern malpern commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • refresh persisted rule sources at root mutation admission and journal remaining generation inputs
  • protect managed global files, reconcile watcher revisions, and make catalog updates recoverable
  • render from immutable generation inputs and extract Help/API discovery into a Foundation-only CLI boundary

Validation

  • focused slice tests passed during implementation, including 36 generation tests and dedicated Help output-contract tests
  • swift build --target KeyPathCLIHelp --jobs 4 passed
  • swift test --test-product KeyPathCLIHelpTests --filter HelpOutputContractTests --jobs 4 passed
  • broad CLI test compilation is currently blocked by unrelated existing Swift 6 Sendable closure errors in Pack, Rule, and Collection commands

Review

  • Astra reviewed each slice; Slice 8 was approved after an AppKit-transitive-dependency and CLI error-contract regression were corrected.
  • The local review gate invoked an unavailable thermo-nuclear-swift-review command and returned success incorrectly. Treat this as remote review gate selected; the GitHub claude-review check must pass before merge.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T03:45:46.779395Z a2ca21d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Automated review of the supplied diff (note: diff was truncated after ~60KB, so ~40% of changed files — e.g. CatalogUpdateReviewSheet.swift, RuleCollectionsManager+PublicAPI.swift, DeviceSelectionStore.swift, PreferencesService.swift, ConfigFileWatcher.swift, and most test files — were not reviewable).

Findings from the visible portion:

  1. Misleading error on invalid caller state (ConfigurationService.swift, the deviceSelections/packRecord mutation entry point):

    guard packRecord == nil || deviceSelections == nil else {
        throw RecoverableRuleWrite.Failure.invalidJournal
    }

    This is a caller-contract violation (mixing pack and device writes in one call), not a journal-integrity failure. Reusing .invalidJournal will surface a confusing "invalid journal" error/log line for what's actually a programming error at the call site. Consider a dedicated error case.

  2. Possible stuck needsRecoveredRuntimeRefresh flag (ConfigurationService.swift): applyRecoveredRuntimeRefreshIfNeeded now early-returns without clearing needsRecoveredRuntimeRefresh when needsRecoveredDeviceRuntimeRestart is also true:

    guard needsRecoveredRuntimeRefresh, !needsRecoveredDeviceRuntimeRestart, let reloadHandler else { return nil }

    Only applyRecoveredDeviceRuntimeRestartIfNeeded clears both flags on success. If any recovery caller invokes the former without also invoking the latter on the same path (call sites aren't visible in this diff), the runtime refresh obligation would never be discharged. Worth confirming both are always paired at every call site.

  3. Potential brittle single-line-block assumption in device-targeting diffing (ConfigurationService.swift, deviceTargetingRegion / removingDynamicDeviceTargeting): these scan line-by-line for macos-dev-names-include ( / macos-dev-names-exclude ( and only stop capturing on a line that trims to exactly ). If the renderer in KanataConfigurationGenerator.swift ever emits either directive as a single-line form (e.g. macos-dev-names-exclude (\"a\" \"b\")), capturing would never terminate and would swallow the rest of the file into the "device targeting region," corrupting both the manifest comparison and matchesGlobalManagedContent. The generator's full body wasn't in the visible diff, so please confirm the renderer always emits the closing paren on its own line before relying on this.

Nothing else in the visible diff stood out as a clear correctness/security/perf bug; the added reproducibility guard (ensureExistingGlobalConfigurationIsReproducible) and legacy-journal-role handling in RecoverableRuleWrite look sound based on what's shown.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Automated review of the supplied diff (note: diff was truncated after ~60000 bytes at RuntimeCoordinator.swift, so anything past that point — including RuleCollectionsManager+PublicAPI.swift, CatalogUpdateReviewSheet.swift, PreferencesService.swift, DeviceSelectionStore.swift, etc. — was not reviewed here).

1. Likely journal-recovery regression for pre-existing version-2 journals (RecoverableRuleWrite.swift)

Scope.roles for .rules/.appKeymaps/.packRules now always includes "deviceTargetingManifest". The new backward-compat check is:

let usesLegacyRoles = journal.version == 1 && journalRoles == scope.legacyRoles
guard journal.version == 1 || journal.version == 2,
      (usesLegacyRoles || (journal.entries.count == files.count && journalRoles == Set(files.keys))),
      ...

This only forgives version == 1 journals for the old (smaller) role set. But the surrounding code has always accepted version == 2 journals too (journal.version == 1 || journal.version == 2), presumably written by pre-this-PR code with the same 3/4-role sets that predate deviceTargetingManifest. If a crash left a version == 2 journal on disk before this PR shipped, upgrading will now hit entries.count != files.count (3 vs 4) and journalRoles != Set(files.keys), falling through to throw Failure.invalidJournal instead of recovering the pending write. Consider extending usesLegacyRoles (or legacyRoles) to also match version == 2 journals that predate the manifest role, unless there's a reason v2 journals are guaranteed to already carry that key.

2. Possible permanent block on recovered-runtime-refresh (ConfigurationService.swift)

guard needsRecoveredRuntimeRefresh, !needsRecoveredDeviceRuntimeRestart, let reloadHandler else { return nil }

Once needsRecoveredDeviceRuntimeRestart is set (device-scope recovery happened), ordinary TCP-reload recovery via applyRecoveredRuntimeRefreshIfNeeded is gated off until something calls the new applyRecoveredDeviceRuntimeRestartIfNeeded(...) and its restartHandler succeeds. That call site isn't visible in the supplied diff/truncated region. If no caller invokes it (or the restart handler can fail/never runs), needsRecoveredDeviceRuntimeRestart stays true forever and normal rule-write recovery notifications silently stop firing on every subsequent launch. Worth double-checking the wiring in RuntimeCoordinator/startup path (not visible in this diff) actually calls this method unconditionally on startup, and that a failed restart doesn't leave the app stuck.

3. Minor perf note (ConfigurationService.stageRuleWrite)

ensureExistingGlobalConfigurationIsReproducible now regenerates the full Kanata configuration (via generateConfiguration) as a pre-check before preparedConfiguration generates it again for the actual write, whenever the in-memory cache doesn't already match. This doubles config-generation cost on the common "no in-memory cache" path for every global rule save. Likely an acceptable safety/perf tradeoff, but flagging in case config generation is expensive for large rule sets.

No issues found with the KanataGenerationInputs plumbing itself (replacing live DeviceSelectionCache/UserDefaults reads with point-in-time inputs) — that's a solid purity improvement for the generator.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2ca21d0a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +548 to +549
let changed = hasFileActuallyChanged()
pendingAtomicWriteEvent = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve atomic replacement events through reconciliation

When an external editor saves keypath.kbd by rename/delete, rebindFileMonitor() calls setupFileMonitoring(), which records the replacement's fingerprint as the new baseline before the debounce fires. This makes hasFileActuallyChanged() return false here, and clearing pendingAtomicWriteEvent removes the previous forced-callback path, so common atomic saves never reach the hot-reload callback and the running configuration remains stale. Preserve the pre-rebind fingerprint or honor the atomic-write marker during reconciliation.

AGENTS.md reference: AGENTS.md:L342-L344

Useful? React with 👍 / 👎.

public static func generateFromMappings(_ mappings: [KeyMapping]) -> String {
let collections = [RuleCollection].collection(named: "Custom Mappings", mappings: mappings)
return generateFromCollections(collections)
return generateFromCollections(collections, inputs: .empty)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve device targeting in production fallback configs

When connected-device state has been populated, production callers such as ConfigurationManager.handleInvalidStartupConfig, ConfigurationService.backupFailedConfigAndApplySafe, and RuntimeCoordinator.resetToDefaultConfig still use this convenience API to write a live fallback. Passing .empty now silently removes the selected-device directives and VirtualHID exclusion that this API previously obtained from the cache; applying such a fallback can therefore remap disabled keyboards or let Kanata grab its VirtualHID output and enter a feedback loop. These callers need a captured DeviceGenerationInput rather than the empty compatibility input.

Useful? React with 👍 / 👎.

let candidate = ShortcutListGenerationInput(
triggerMode: triggerMode ?? self.triggerMode,
holdDelayPreset: holdDelayPreset ?? self.holdDelayPreset,
customHoldDelayMs: customHoldDelayMs ?? self.customHoldDelayMs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp custom hold delays before staging them

When a user enters a custom delay below 100 ms or above 2000 ms, this candidate bypasses the existing PreferencesService clamp and is journaled into both defaults and the generated config unchanged. The later reload only clamps the in-memory preference, while this view restores the unbounded candidate and the persisted configuration remains out of range, so the UI, defaults, and runtime disagree. Normalize the candidate with the same 100–2000 ms bounds before starting the retained write.

Useful? React with 👍 / 👎.

let proposedKeys = normalizedKeys(for: proposed)
let affectedKeys = existingKeys.symmetricDifference(proposedKeys).sorted()
let affectedLayers = Set([existing.targetLayer.displayName, proposed.targetLayer.displayName]).sorted()
let conflict = conflictInfo(for: proposed)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip conflict checks for disabled catalog updates

For a persisted collection that is disabled, conflictInfo(for:) still compares its proposed mappings against enabled collections because that helper does not inspect the candidate's isEnabled flag. Thus a disabled catalog entry that overlaps an active rule is marked canApply == false, even though updating it cannot affect the generated configuration; users cannot keep disabled built-ins current unless they first disturb unrelated active rules. Only perform this conflict check when the proposed collection is enabled.

Useful? React with 👍 / 👎.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Reviewed the diff (note: it was truncated after 60000 bytes, so this covers the Config/RuntimeCoordinator portion only).

1. Notification-driven config regeneration appears to be dropped without a visible replacement (RuntimeCoordinator.swift). The .configAffectingPreferenceChanged and .deviceSelectionChanged observers — which previously auto-regenerated config and restarted Kanata — are deleted, replaced by explicit applyShortcutListGenerationInput / applyDeviceSelections methods (RuntimeCoordinator+RuleCollections.swift). This diff doesn't show every call site being switched over. Please confirm all producers of those two notifications (e.g. ContextHUDSettingsSection, DeviceSelectionView/DeviceSelectionStore) were updated to call the new methods directly — otherwise a preference or device-selection change could silently stop propagating to the running config/daemon.

2. onWillStageConfigurationWrite fires before the write is actually committed (ConfigurationService.swift). It's invoked immediately before RecoverableRuleWrite.stage(...) at several call sites, and wired to fileWatcher.claimInternalContent(content). If staging subsequently throws or the caller discards the pending write, the watcher has already claimed that exact content. If a later legitimate external edit happens to produce byte-identical content, it could be mistaken for our own write and ignored. Worth confirming claimInternalContent has bounded/one-shot semantics so a failed/discarded stage can't cause a missed external-change detection later.

3. Wrong error case for a parameter-misuse guard (ConfigurationService.swift, stageMutation). guard packRecord == nil || deviceSelections == nil else { throw RecoverableRuleWrite.Failure.invalidJournal } throws the same error used for actually-corrupted recovery journals. Since callers/telemetry may branch on .invalidJournal to mean "journal on disk is corrupt," conflating it with "caller passed an invalid combination of parameters" could produce misleading diagnostics/recovery behavior. Consider a distinct error case (e.g. .invalidRequest).

Minor nit: ConfigurationOperationGate.Permit.operationID is added with a comment describing a use case ("avoid repeating preparation work for trusted nested calls") but no call site appears in this diff — worth double-checking it isn't dead code, or that its consumer is just outside the truncated portion.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review note: the diff was truncated before I could see the rest of RuntimeCoordinator.swift and later files (ConfigFileWatcher.swift, RuleCollectionsManager+*.swift, CatalogUpdateReviewSheet.swift, etc.), so this only covers what's visible up through ConfigurationService.swift / RecoverableRuleWrite.swift / KanataConfigurationGenerator.swift / the start of RuntimeCoordinator.swift.

Correctness: needsRecoveredDeviceRuntimeRestart can get stuck true forever after a failed boot recovery

In ConfigurationService.applyRecoveredDeviceRuntimeRestartIfNeeded, the flag is only cleared on success:

guard needsRecoveredDeviceRuntimeRestart else { return false }
guard await restartHandler() else {
    throw KeyPathError.configuration(.loadFailed(reason: "Recovered device targeting could not be restarted"))
}
needsRecoveredDeviceRuntimeRestart = false
needsRecoveredRuntimeRefresh = false
return true

RuntimeCoordinator's bootstrap task calls this once and, on throw, just records lastError — it never retries or clears the flag. Meanwhile applyRecoveredConfigurationIfNeeded gates all future recovered-config TCP reloads on !needsRecoveredDeviceRuntimeRestart:

guard needsRecoveredRuntimeRefresh, !needsRecoveredDeviceRuntimeRestart, let reloadHandler else { return nil }

So a single restart failure right after crash recovery (e.g. Kanata briefly unable to restart) permanently disables recovered-config reload for the rest of the process lifetime, with no retry path visible in the supplied diff. Worth adding either a retry on next save/reload attempt or surfacing this as a user-facing "needs restart" state rather than silently no-op'ing forever.

Minor: stagePendingRuleWrite's new mutual-exclusion guard (packRecord == nil || deviceSelections == nil) throws RecoverableRuleWrite.Failure.invalidJournal for what's actually a programmer precondition violation (both set), which will be a confusing error message if it's ever hit — consider a dedicated failure case or a precondition/assert instead.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Correctness: pre-existing v2 recovery journals will fail to recover after this upgrade (Sources/KeyPathAppKit/Infrastructure/Config/RecoverableRuleWrite.swift)

WriteScope.roles now unconditionally adds "deviceTargetingManifest" for .rules, .appKeymaps, and .packRules, and a new legacyRoles set (without that key) was added for backward compatibility. But the compatibility check is gated on journal.version == 1:

let usesLegacyRoles = journal.version == 1 && journalRoles == scope.legacyRoles
guard journal.version == 1 || journal.version == 2,
      usesLegacyRoles || (journal.entries.count == files.count && journalRoles == Set(files.keys)),

The surrounding code already accepted journal.version == 2 before this PR, meaning version-2 journals (written by the previously shipped app, without the new deviceTargetingManifest role) are a real, pre-existing on-disk format. After a user upgrades to this build with an in-flight/crashed write (a .keypath-rule-write.json, .keypath-app-write.json, or .keypath-pack-rule-write.json left over from a crash mid-save), journal.version is 2 and journalRoles == scope.legacyRoles, so usesLegacyRoles evaluates false (wrong version), and the fallback comparison against Set(files.keys) (which now includes deviceTargetingManifest) also fails on both count and role-set. recover() throws Failure.invalidJournal instead of recovering, likely leaving the stale lock/journal and blocking subsequent saves for that user.

Suggest widening the legacy check to (journal.version == 1 || journal.version == 2) && journalRoles == scope.legacyRoles, or bumping to a new journal version specifically to disambiguate "v2 with manifest" from "v2 without manifest."

Lower-confidence note (context was truncated in the diff for this file, so I can't fully verify): RuntimeCoordinator.swift replaces ruleCollectionsManager.onBeforeSave — previously used to suppress the config file watcher for 1s during internal saves to avoid double-reload — with nil, and separately removes the .configAffectingPreferenceChanged notification observer that triggered config regeneration. If the new applyShortcutListGenerationInput/applyDeviceSelections paths don't fully replace both of these, this could reintroduce double-reload-on-save or drop preference-change-triggered regeneration. Worth double-checking against the non-truncated file.

(Diff was truncated after 60000 bytes; review covers only the supplied portion.)

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Automated review (partial — the supplied diff was truncated after 60000 bytes, so files after RuntimeCoordinator.swift such as ConfigFileWatcher.swift, PreferencesService.swift, DeviceSelectionStore.swift, RuleCollectionsManager+Mutation.swift, CatalogUpdateReviewSheet.swift, etc. were not reviewed.)

Redundant generation-input reload on every rule write (Sources/KeyPathAppKit/Infrastructure/Config/ConfigurationService.swift)

In stageRuleWrite, persistedGlobalRuleGenerationInputs(...) is computed once and passed into ensureExistingGlobalConfigurationIsReproducible. Immediately after, preparedConfiguration(...) is called to build newConfig, but preparedConfiguration has no appSpecificKeys parameter, so it always calls generateConfiguration(appSpecificKeys: nil, ...). Inside generateConfiguration:

let persistedInputs: GlobalRuleGenerationInputs? = if appSpecificKeys == nil || shortcutListGenerationInput == nil || deviceGenerationInput == nil {
    try await persistedGlobalRuleGenerationInputs(preferenceDefaults: PreferencesService.canonicalDefaults)
} else {
    nil
}

Since appSpecificKeys is always nil from this call path (even though shortcutListGenerationInput/deviceGenerationInput are already resolved and passed in), this condition is always true, so persistedGlobalRuleGenerationInputs runs a second time — re-reading AppKeymaps.json, re-loading the device selection store, and re-reading UserDefaults — purely to obtain appSpecificKeys, on every single rule save. Threading appSpecificKeys through preparedConfiguration (or forwarding persistedInputs.appSpecificKeys from the caller) would avoid the duplicate disk I/O per write.

Worth double-checking: watcher-suppression behavior removed (Sources/KeyPathAppKit/Managers/RuntimeCoordinator.swift)

ruleCollectionsManager.onBeforeSave previously suppressed the file watcher for 1s ("Internal rule change") before saves to avoid a double-reload; this PR sets it to nil outright:

ruleCollectionsManager.onBeforeSave = nil

ConfigurationService gained a new onWillStageConfigurationWrite hook that fires at each staging point, which looks like the intended replacement — but the wiring of that hook to ConfigFileWatcher.suppressEvents isn't visible in the truncated portion of this diff. Please confirm onWillStageConfigurationWrite is actually connected to watcher suppression somewhere (likely in ConfigFileWatcher.swift, which changed but wasn't included in this diff), otherwise rule saves could trigger a spurious double-reload via the file watcher.

No other actionable correctness/security issues found in the visible portion; the recovery-journal legacy-role handling and new device-targeting reproducibility checks in ConfigurationService/RecoverableRuleWrite look internally consistent.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Reviewed the supplied diff (note: it was truncated after ~60KB, so files after RuntimeCoordinator.swift — e.g. ConfigFileWatcher.swift, DeviceSelectionStore.swift, RuleCollectionsManager+Mutation/PublicAPI.swift, CatalogUpdateReviewSheet.swift — were not visible and aren't covered below).

1. New reproducibility guard may block existing users' first save after upgrading (ConfigurationService.ensureExistingGlobalConfigurationIsReproducible, matchesGlobalManagedContent)
This check now runs on every global rule-collection write, not just device-related ones. It skips only when collections.json is missing (fresh install). For an already-installed user upgrading to this build, collections.json exists but the new keypath-device-targeting.manifest sidecar does not, since it's only ever written by the new staging path. If the in-memory currentConfiguration cache doesn't byte-match the on-disk keypath.kbd (e.g. because connected devices changed since the file was last written, or the cache was invalidated by an earlier recovery/rollback), the fallback in matchesGlobalManagedContent tries to read the manifest, fails (file doesn't exist), and the whole check fails — surfacing "Your configuration was preserved... Convert it explicitly with a backup before editing global rules," even though no manual edit occurred. There's no described conversion flow for the global config (unlike the app-keymap case). Worth confirming there's a migration/backfill step that writes the manifest for existing installs before this guard goes live, or that the guard tolerates a missing manifest as "trust it" rather than "fail closed."

2. Duplicated UserDefaults key string literals in RecoverableRuleWrite.PreferenceRole
contextHUDTriggerMode/contextHUDHoldDelayPreset/contextHUDHoldDelayCustomMs hardcode \"KeyPath.ContextHUD.TriggerMode\" etc. as new literals rather than referencing the constants actually used to read/write these prefs elsewhere (e.g. in PreferencesService/ContextHUDSettingsSection). If those diverge even by a typo, journal-based recovery will silently read/restore the wrong key with no compiler check to catch it.

3. ConfigurationService.generateConfiguration and KanataConfiguration.generateFromCollections lost public
Both became internal in this diff. If any other module (CLI, tests outside @testable import, etc.) called these directly, this is a build break not visible from this diff slice — worth double-checking call sites across targets.

Everything else in the visible portion (CI concurrency/mise pinning, RecoverableRuleWrite legacy-journal-role handling, KanataGenerationInputs refactor to pass device/layout state explicitly instead of reading global caches) looks like a solid correctness improvement.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Reviewed the supplied diff (note: it was truncated after ~60000 bytes, so this covers only the portion shown).

Findings:

  1. RuntimeCoordinator.swift — removed file-watcher suppression on internal saves (possible regression).
    The old code set:

    ruleCollectionsManager.onBeforeSave = { [weak self] in
        self?.configFileWatcher?.suppressEvents(for: 1.0, reason: "Internal rule change")
    }

    and this is now replaced with ruleCollectionsManager.onBeforeSave = nil. That suppression existed specifically to prevent a double-reload when RuleCollectionsManager writes the config file internally and the file watcher then fires again for the same change (the ci.yml comment in this same PR even references "the race" this class of bug caused). Nothing in the shown diff replaces this protection — the new device-restart recovery logic added in the same block is orthogonal (it runs once at bootstrap, not on every internal save). If there isn't an equivalent guard elsewhere (e.g. inside ConfigFileWatcher itself, given it was also touched in this PR), every internal collection/custom-rule save will now trigger a duplicate reload via the watcher. Worth confirming this was intentionally superseded rather than dropped.

  2. ConfigurationService.matchesGlobalManagedContent swallows read errors as "manifest absent."

    guard let manifest = try? String(contentsOf: deviceTargetingManifestURL, encoding: .utf8),
          manifest == deviceTargetingRegion(from: existing)
    else { return false }

    A transient read failure (permissions, disk hiccup, encoding issue) is indistinguishable here from "no manifest was ever written," and both cause ensureExistingGlobalConfigurationIsReproducible to treat the existing keypath.kbd as unreproducible, throwing AppConfigError.validationFailed and blocking the save. This turns a transient I/O error into a hard user-facing "your config can't be reproduced" failure. Consider distinguishing "file doesn't exist" (expected, pre-migration) from other read errors (should probably surface/log distinctly rather than silently degrading to the strictest failure path).

  3. Minor: ConfigurationOperationGate.Permit.operationID is added but not used anywhere in the shown diff — worth double-checking it's actually consumed by the (truncated) remainder of the change, otherwise it's dead API surface.

No blocking correctness issues found in the parts of the diff I could see beyond #1, which is worth a maintainer confirmation before merge.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown

Reviewed the supplied diff (note: the diff was truncated after ~60000 bytes, so files after RuntimeCoordinator.swift — e.g. RuleCollectionsManager+Mutation/PublicAPI, ContextHUDSettingsSection, DeviceSelectionView, CatalogUpdateReviewSheet, the KeyPathCLICommon extraction — were not reviewed).

1. needsRecoveredDeviceRuntimeRestart can get stuck true forever, permanently silencing rule-recovery reloads (ConfigurationService.swift)

applyRecoveredDeviceRuntimeRestartIfNeeded only clears needsRecoveredDeviceRuntimeRestart on the success path:

guard await restartHandler() else {
    throw KeyPathError.configuration(.loadFailed(reason: "Recovered device targeting could not be restarted"))
}
needsRecoveredDeviceRuntimeRestart = false
needsRecoveredRuntimeRefresh = false

If restartHandler() fails once (e.g. transient failure during the startup recovery call in RuntimeCoordinator.swift, which just logs to lastError and does not retry), the flag is left true with no other code path to reset it.

Meanwhile applyRecoveredRuntimeRefreshIfNeeded now silently no-ops whenever that flag is set:

guard needsRecoveredRuntimeRefresh, !needsRecoveredDeviceRuntimeRestart, let reloadHandler else { return nil }

So after one failed device-restart recovery, every subsequent normal rule-recovery reload becomes a silent no-op (return nil, no error) instead of applying the already-recovered rule files to the running daemon — the app is left running stale config indefinitely with no user-visible signal beyond the one-time lastError at startup.

2. Precondition violation reuses .invalidJournal, an on-disk-corruption error case (ConfigurationService.swift / RecoverableRuleWrite.swift)

guard packRecord == nil || deviceSelections == nil else {
    throw RecoverableRuleWrite.Failure.invalidJournal
}

Everywhere else .invalidJournal means "the journal read from disk doesn't match expectations" (version/role/path mismatches in RecoverableRuleWrite.recover). Reusing it here for a caller-side argument-combination bug conflates two very different failure classes; any code that branches on .invalidJournal to mean "corrupted recovery state" (e.g. to trigger cleanup/backup flows) would misfire on what is actually a programming error. Consider a distinct precondition/error case (or a fatalError/assertionFailure, since this combination should never occur from valid call sites).

No other actionable correctness/security issues found in the reviewed portion; the added test coverage (ConfigurationRuleWriteTests, DurableConfigPreferenceRecoveryTests, RuleCollectionsManagerTests) looks reasonably thorough for the new device-targeting/reproducibility logic.

@malpern
malpern merged commit d69e0ad into master Sep 13, 2026
6 checks passed
@malpern
malpern deleted the codex/config-admission-freshness branch September 13, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant