Skip to content

Change daemon capacity live and retire the old unit on rename - #873

Merged
alexeyzimarev merged 17 commits into
mainfrom
alexeyzimarev/ai-2535-extend-desktop-app-settings-to-configure-the-daemon
Sep 11, 2026
Merged

alexeyzimarev merged 17 commits into
mainfrom
alexeyzimarev/ai-2535-extend-desktop-app-settings-to-configure-the-daemon

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Part of #791 — AI-2535

What & why

The desktop app is getting a Settings window to change the daemon's agent capacity and its name. Capacity is a live field the daemon reads per launch and the server overwrites on a repeat connect, so a new local-socket frame pair (DaemonSettingsPut 23 / DaemonSettingsAck 81, capability settings/1) applies it without a restart and republishes the registration. The name is the daemon's identity locally and on the server and is baked into the service unit, so a rename is a reinstall: install --replace --verify --retire <old-id> removes the old unit inside the same transaction, keeping the app to one verb. The profile's max_agents now applies whenever --max-agents is absent; comparing against the default 5 made an explicit 5 read as unset. The window itself is the next PR.

Where to look

ServiceVerify.RetireAsync: the retired label's lock is taken before its plist is read, and the retire runs on its own budget ahead of the install's forward cutoff, so a rename can take one forward budget plus a lock wait longer than a plain replace. A retired unit is never restored, and an absent unit is a no-op, so a detached daemon under the old name is left running.

Verification

Core, daemon and CLI unit suites green locally; the daemon suite's one failure is the codex vendored-pin skew on this machine (installed 0.154.0, pin 0.147.0). dotnet publish -c Release 2>&1 | grep -E 'IL[23][01][0-9]{2}': empty. Real-socket tests cover a put changing the live cap, a second status frame reaching an existing subscriber, the next launch over the cap being refused, and exactly one re-register.

alexeyzimarev and others added 14 commits September 10, 2026 16:11
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The counting double and the sequenced double expose disjoint evidence: register calls on one, rejections on the other.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…791)

The ack returns once the live value is set; the server learns through the same single-flighted re-register the vendor-CLI watcher uses, so a burst of puts publishes once with the newest value.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comparing the live value with the default 5 made a profile of exactly 5 read as unset and let the profile override an explicit --max-agents 5.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The unit bakes the daemon name in as --name, so a rename is a reinstall under a new label; retiring the old one in the same transaction keeps the app to one verb.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Retire now runs before the install's own forward budget starts, and spends its own budget, so a slow retire can never starve the install's readiness poll.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Refuse a --retire value that sanitizes to the fallback daemon name,
guard the engine against retiring the install's own target, and make
the two command-level refusal tests assert on the real message instead
of the launchd-only gate they were accidentally passing through.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 10, 2026

Copy link
Copy Markdown

AI-2535

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 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-10T20:55:18.801088Z 032fcdc 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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Change daemon capacity live and retire the old unit on rename

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds live daemon capacity updates through versioned local IPC and registration republishing.
• Retires old same-profile service units transactionally during verified daemon renames.
• Fixes profile capacity precedence when --max-agents is absent.
Diagram

graph TD
  Client["Desktop Client"] --> IPC["Control IPC"] --> Handler["Settings Handler"] --> Config["Live Config"]
  Handler --> Outputs["Status and Registration"]
  CLI["Service CLI"] --> Verify["Verify Transaction"] --> Units["launchd Units"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Live daemon rename protocol
  • ➕ Avoids reinstalling the service unit and relaunching the desktop app.
  • ➕ Could preserve daemon availability throughout the identity change.
  • ➖ Requires coordinated migration of locks, sockets, state paths, launchd labels, and server identity.
  • ➖ Needs server-side rename semantics to prevent stale name-to-connection mappings.
  • ➖ Existing app services are bound to the daemon name for the process lifetime.
2. App-managed uninstall and install
  • ➕ Avoids extending the verified install command.
  • ➕ Uses existing lifecycle verbs independently.
  • ➖ Exposes the app to races and partial failure between destructive commands.
  • ➖ Could leave no managed daemon or allow the old unit to restart at login.
  • ➖ Moves lifecycle safety and rollback orchestration outside the CLI transaction boundary.
3. Restart for every capacity change
  • ➕ Avoids adding a new IPC contract and daemon handler.
  • ➕ Naturally reloads the persisted profile value.
  • ➖ Disrupts running daemon connectivity for a value already read dynamically per launch.
  • ➖ Provides a worse settings experience and unnecessary lifecycle risk.

Recommendation: Keep the PR's hybrid strategy: apply capacity live because it is already mutable runtime state, while treating name changes as identity replacement inside one verified CLI transaction. A true live rename would require broad server and local-state migration, and app-sequenced lifecycle commands would weaken transactional safety.

Files changed (36) +2518 / -37

Enhancement (13) +224 / -13
WizardLateBinding.csForward daemon settings operations through late binding +3/-0

Forward daemon settings operations through late binding

• Implements the new settings-put interface member by forwarding it to the bound local-control client.

src/Capacitor.App/Services/Onboarding/WizardLateBinding.cs

FrameCodec.csEncode and decode daemon settings frames +4/-2

Encode and decode daemon settings frames

• Routes daemon settings request and acknowledgement payloads through the codec's UTF-8 text handling.

src/Capacitor.Cli.Core/LocalIpc/FrameCodec.cs

FrameType.csReserve daemon settings wire frame identifiers +3/-0

Reserve daemon settings wire frame identifiers

• Adds append-only frame values 23 and 81 for settings puts and acknowledgements.

src/Capacitor.Cli.Core/LocalIpc/FrameType.cs

LocalControlOps.csExpose the daemon settings socket operation +16/-0

Expose the daemon settings socket operation

• Adds 'PutDaemonSettingsAsync' to the control interface and implements request serialization, acknowledgement parsing, and transport error mapping.

src/Capacitor.Cli.Core/LocalIpc/LocalControlOps.cs

LocalFrame.csAdd a settings JSON frame constructor +4/-0

Add a settings JSON frame constructor

• Introduces a dedicated helper for constructing daemon settings request and acknowledgement frames.

src/Capacitor.Cli.Core/LocalIpc/LocalFrame.cs

SettingsIpc.csDefine daemon settings wire contracts +33/-0

Define daemon settings wire contracts

• Adds nullable request and acknowledgement DTOs, refusal reasons, the 'settings/1' capability, structural validation, and source-generated snake-case JSON metadata.

src/Capacitor.Cli.Core/LocalIpc/SettingsIpc.cs

AgentOrchestrator.csExpose coalesced registration republishing +7/-0

Expose coalesced registration republishing

• Adds a runtime registration refresh entry point that reuses the existing single-flight capability refresh mechanism.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs

DaemonSettingsIpc.csApply daemon capacity over the local socket +41/-0

Apply daemon capacity over the local socket

• Validates settings puts, mutates live capacity, pulses status subscribers, republishes registration, and returns the effective value or refusal reason.

src/Capacitor.Cli.Daemon/Services/DaemonSettingsIpc.cs

LocalControlCapabilities.csAdvertise daemon settings support +3/-2

Advertise daemon settings support

• Appends 'settings/1' to the daemon's local-control capability list and documents its route.

src/Capacitor.Cli.Daemon/Services/LocalControlCapabilities.cs

LocalControlServer.csRoute daemon settings frames +3/-1

Route daemon settings frames

• Injects the settings handler and dispatches incoming 'DaemonSettingsPut' frames to it.

src/Capacitor.Cli.Daemon/Services/LocalControlServer.cs

DaemonServiceCommands.csAdd the verified install retirement option +24/-2

Add the verified install retirement option

• Parses and sanitizes '--retire', enforces its flag and identity constraints, and forwards the retired service ID into the verified install transaction.

src/Capacitor.Cli/Commands/DaemonServiceCommands.cs

ServiceEnsure.csMap service retirement refusals +6/-1

Map service retirement refusals

• Maps the new coded retirement refusal into recovery output and the existing not-configured flow outcome.

src/Capacitor.Cli/Commands/ServiceEnsure.cs

ServiceVerify.csRetire old units within verified installation +77/-5

Retire old units within verified installation

• Adds a coded retirement refusal and a separately budgeted retirement phase. The phase locks the old label before reading its plist, verifies profile ownership, confirms removal, and preserves target collision safety.

src/Capacitor.Cli/Services/ServiceVerify.cs

Tests (17) +680 / -21
ScriptedLocalControlOps.csScript daemon settings calls in app tests +22/-0

Script daemon settings calls in app tests

• Adds queued acknowledgements and failures plus call and payload capture for the new local-control operation.

test/Capacitor.App.Tests.Unit/ScriptedLocalControlOps.cs

FrameCodecSettingsTests.csPin and round-trip settings frame types +29/-0

Pin and round-trip settings frame types

• Verifies UTF-8 payload round trips and protects frame identifiers 23 and 81 from wire-breaking changes.

test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/FrameCodecSettingsTests.cs

LocalControlOpsTests.csTest the daemon settings client operation +56/-0

Test the daemon settings client operation

• Covers snake-case requests, successful and refused acknowledgements, malformed replies, and daemon error frames.

test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/LocalControlOpsTests.cs

SettingsWireContractsTests.csVerify daemon settings JSON contracts +42/-0

Verify daemon settings JSON contracts

• Pins request and acknowledgement shapes, nullable compatibility behavior, structural validation, and the capability string.

test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/SettingsWireContractsTests.cs

DaemonConfigProfileTests.csCover profile capacity precedence +33/-3

Cover profile capacity precedence

• Verifies ordinary and default-valued profile capacities, explicit CLI overrides, and missing profile sections.

test/Capacitor.Cli.Daemon.Tests.Unit/DaemonConfigProfileTests.cs

AgentOrchestratorLocalAttachTests.csWire settings IPC into attach test servers +7/-3

Wire settings IPC into attach test servers

• Supplies the new handler dependency to local-control server fixtures used by attach and list tests.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/AgentOrchestratorLocalAttachTests.cs

ConsentRulesPutV2Tests.csWire settings IPC into consent-rule tests +4/-2

Wire settings IPC into consent-rule tests

• Shares a status notifier and supplies the settings handler to the local-control server harness.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/ConsentRulesPutV2Tests.cs

DaemonSettingsIpcTests.csExercise live settings through real sockets +183/-0

Exercise live settings through real sockets

• Tests live capacity mutation, pushed status updates, launch refusal at the new cap, one registration republish, invalid payloads, client round trips, and capability advertisement.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/DaemonSettingsIpcTests.cs

DaemonStatusIpcTests.csWire settings IPC into status tests +2/-1

Wire settings IPC into status tests

• Adds the settings handler dependency using the status harness's existing notifier.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/DaemonStatusIpcTests.cs

LaunchConsentIpcTests.csWire settings IPC into launch-consent tests +4/-2

Wire settings IPC into launch-consent tests

• Updates the real local-control server harness to share a notifier with the settings handler.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/LaunchConsentIpcTests.cs

LocalControlCapabilitiesTests.csPin the settings capability +1/-1

Pin the settings capability

• Updates the exact advertised capability list to include 'settings/1' in append order.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/LocalControlCapabilitiesTests.cs

LocalControlHelloTests.csExpect settings support in hello replies +7/-5

Expect settings support in hello replies

• Wires the new handler into hello fixtures and updates capability assertions for 'settings/1'.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/LocalControlHelloTests.cs

LocalControlOpsV2PutTests.csWire settings IPC into v2 put tests +4/-2

Wire settings IPC into v2 put tests

• Updates the local-control server harness with a shared notifier and settings handler.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/LocalControlOpsV2PutTests.cs

LocalControlProbeTests.csWire settings IPC into probe tests +4/-2

Wire settings IPC into probe tests

• Supplies the new settings handler dependency to the probe test server.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/LocalControlProbeTests.cs

DaemonCommandsServiceInstallTests.csValidate retirement command arguments +28/-0

Validate retirement command arguments

• Covers missing required flags, retirement of the target itself, sanitized case equivalence, and invalid fallback service IDs.

test/Capacitor.Cli.Tests.Unit/Commands/DaemonCommandsServiceInstallTests.cs

ServiceEnsureTests.csPin retirement refusal flow mapping +1/-0

Pin retirement refusal flow mapping

• Adds the new retirement exit code to the exhaustive recovery-reason mapping test.

test/Capacitor.Cli.Tests.Unit/Commands/ServiceEnsureTests.cs

ServiceVerifyRetireTests.csTest transactional old-unit retirement +253/-0

Test transactional old-unit retirement

• Covers self-retirement rejection, absent and same-profile units, foreign or unreadable plists, target collisions, unconfirmed stops, separate budgets, and non-restoration after install rollback.

test/Capacitor.Cli.Tests.Unit/Services/ServiceVerifyRetireTests.cs

Documentation (5) +1604 / -1
README.mdDocument transactional service retirement +3/-0

Document transactional service retirement

• Adds an example and behavioral guidance for 'install --replace --verify --retire', including profile ownership and target contention rules.

README.md

CHANGES.mdRecord daemon settings and rename semantics +16/-0

Record daemon settings and rename semantics

• Explains live capacity application, registration republishing, transactional retirement, and the corrected profile precedence rule.

docs/CHANGES.md

2026-09-10-ai2535-daemon-settings-backend.mdAdd the daemon settings backend implementation plan +1343/-0

Add the daemon settings backend implementation plan

• Provides the task-by-task plan, constraints, interfaces, tests, and verification steps for the IPC, daemon, and service-retirement work.

docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md

2026-09-10-ai2535-desktop-daemon-settings-design.mdDefine the desktop daemon settings design +236/-0

Define the desktop daemon settings design

• Documents capacity persistence and live apply, rename lifecycle behavior, IPC contracts, the future settings window, and rejected alternatives.

docs/superpowers/specs/2026-09-10-ai2535-desktop-daemon-settings-design.md

help-daemon.txtDescribe the service retirement option +6/-1

Describe the service retirement option

• Updates daemon help with '--retire ID', its required flags, same-profile restriction, and collision behavior.

src/Capacitor.Cli.Core/Resources/help-daemon.txt

Other (1) +10 / -2
DaemonRunner.csRegister settings IPC and fix capacity precedence +10/-2

Register settings IPC and fix capacity precedence

• Registers the daemon settings handler and tracks whether capacity came from CLI arguments. Profile capacity now applies whenever '--max-agents' was not explicitly supplied.

src/Capacitor.Cli.Daemon/DaemonRunner.cs

@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: 032fcdc396

ℹ️ 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 +791 to +793
if (validatedDaemonPid(serviceId) is not null) {
Say(VerifyExit.ContendedToken);
return VerifyExit.Contended;

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 Recheck the target owner after retiring the old unit

If a daemon under the new serviceId starts after this one-shot probe while RetireAsync waits for or removes the old unit, the code never enforces this collision check again. The subsequent replace matrix can classify that newly started daemon as the owner and clear or kill it because replace is true, after the old unit has already been deleted. Recheck for a validated target owner immediately after retirement and before any target mutation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. Fixed in c8c62f8: the collision check is repeated right after the retire returns, and the replace matrix now takes a refuseLiveOwner flag (set whenever a retire was requested) that returns verify_contended at both points of destruction, the owning-label clear and the validated-owner kill, instead of taking the daemon over. Three tests pin the window during the retire, the owner seen by the pre-query, and a label-less live owner.

Comment on lines +785 to +799
if (retireServiceId is not null) {
if (retireServiceId == spec.ServiceId)
throw new ArgumentException("retireServiceId must differ from the service being installed");

// A rename's target must be free: a live daemon under the new name is another daemon,
// not a stale unit for --replace to take over. No pre-query needed for this check.
if (validatedDaemonPid(serviceId) is not null) {
Say(VerifyExit.ContendedToken);
return VerifyExit.Contended;
}
// Retire spends its OWN forward budget — never the install's — so a late-but-successful
// retire can never starve the install's own readiness poll and strand the operator with
// no daemon at all (a retired unit is never restored on the install's own timeout).
var retireBy = time.GetUtcNow() + _forwardBudget;
if (await RetireAsync(retireServiceId, spec, retireBy) is { } retireExit) return retireExit;

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 Validate retirement before recovering the target

Because this retirement block runs after RecoverLeftoverMarker, a request naming an unreadable or foreign-profile retired unit can return RetireRefused only after recovery has already deleted the new target's marker or uninstalled its matching residue. That violates the exit's stated guarantee that nothing is touched on retirement refusal and can leave the target changed despite the refusal. Validate and lock the retired unit before performing target recovery.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Recovery here is scoped to residue this engine's own earlier transaction for the new id provably left behind (a marker whose fingerprint matches the plist on disk), and it runs identically for every install, retire or not. Running the retire validation ahead of it would mean holding the retired label's lock across recovery for no gain: the target ends in the same verified-safe absent state the refused transaction would have left it in. What was wrong is the exit's doc claim that nothing is touched; it now says nothing is written for the new id and that the id's own residue recovery has already run.

Comment on lines +798 to +799
var retireBy = time.GetUtcNow() + _forwardBudget;
if (await RetireAsync(retireServiceId, spec, retireBy) is { } retireExit) return retireExit;

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 Start the retirement budget after acquiring its lock

The retirement deadline is calculated before RetireAsync spends up to LockWait acquiring the retired label's lock, so lock contention is deducted from the promised independent forward budget. With the defaults, a near-10-second lock wait leaves only about half of the 20-second retirement budget; the code can then uninstall the old unit but fail before confirming the stop. Establish the forward deadline after the retired lock is acquired.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. Fixed in c8c62f8: the retire's deadline is now taken inside RetireAsync after its lock is acquired, so a lock wait no longer eats the budget; the AdvertisedBound note says a caller allows one lock wait plus one forward budget for it.

@qodo-code-review

qodo-code-review Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Local starts ignore the new capacity ✗ Dismissed 🐞 Bug ≡ Correctness
Description
HandleLocalSpawnAsync creates and publishes an agent without comparing EffectiveCount with the
newly mutable MaxConcurrentAgents value. After capacity is lowered, local agent start
requests—and concurrent local socket requests—can continue creating agents beyond the configured
limit while only server-originated launches are refused.
Code

src/Capacitor.Cli.Daemon/Services/DaemonSettingsIpc.cs[29]

+            config.MaxConcurrentAgents = max;
Relevance

●● Moderate

The race is technically plausible, but no close historical precedent establishes that local launches
must enforce live capacity.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new handler changes the shared capacity live, but the local spawn path proceeds from validation
through process creation and PublishAgent without reading that value. The repository's only
capacity comparison is in the separate server-command launch path, while local socket connections
are handled concurrently.

src/Capacitor.Cli.Daemon/Services/DaemonSettingsIpc.cs[28-34]
src/Capacitor.Cli.Daemon/Services/LocalControlServer.cs[31-46]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[217-284]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2033-2038]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Live daemon capacity is enforced for server-originated launches but not for agents spawned through the local control socket, allowing local starts to exceed the configured limit.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/DaemonSettingsIpc.cs[28-34]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[217-284]

## Recommended Fix
Route local and server launch admission through one concurrency-safe capacity gate. Reject a local spawn before creating its worktree or process when the effective agent count has reached the live limit, and add tests covering a lowered limit plus simultaneous local spawn requests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Lowered limits still admit a new agent ✗ Dismissed 🐞 Bug ≡ Correctness
Description
DaemonSettingsIpc.Apply writes config.MaxConcurrentAgents without synchronizing with
AgentOrchestrator's one-time capacity check. When a launch has passed that check but has not yet
published its agent, reducing the limit to the current agent count can acknowledge success while the
in-flight launch later adds another agent above the new limit.
Code

src/Capacitor.Cli.Daemon/Services/DaemonSettingsIpc.cs[R29-31]

+            config.MaxConcurrentAgents = max;
+            notifier.Pulse();
+            orchestrator.RepublishRegistration("settings");
Relevance

●● Moderate

This is a subtle in-flight admission race; evidence supports correctness concerns but lacks a close
team precedent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Local-control requests run concurrently, while launch admission reads the limit only once and does
not count the launch until much later. The new settings handler therefore creates a path for the
limit to change between admission and publication.

src/Capacitor.Cli.Daemon/Services/LocalControlServer.cs[31-34]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2033-2038]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2309-2310]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2391-2418]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[1311-1318]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
`DaemonSettingsIpc.Apply` changes the live capacity independently of agent admission. A launch checks capacity before its awaited setup and only later publishes its agent, so a capacity reduction can complete and acknowledge before that already-in-flight launch increases the active count beyond the new limit.

Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/DaemonSettingsIpc.cs[28-34]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2033-2038]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2391-2418]

Recommended Fix
Make capacity changes and launch admission share a synchronization or reservation mechanism. Reserve a capacity slot atomically before awaited launch setup, and ensure a settings update either observes that reservation before acknowledging or an in-flight launch rechecks the current limit before publication and is rejected when the new limit leaves no slot; release reservations on every failed launch path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. A future retire can remove the wrong unit ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The planned RetireAsync reads and validates the retired plist before it acquires that service's
transaction lock. A concurrent install can replace the unit after the profile check and before the
later lock acquisition, so following this plan can delete a different unit and uses the install
budget rather than the implemented separate retirement budget.
Code

docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md[R1165-1167]

+    async Task<int?> RetireAsync(string retireId, ServiceSpec spec, DateTimeOffset deadline) {
+        var (status, content) = _discriminatedPlistRead(manager.UnitPath(retireId));
+        if (status == LaunchdUnit.PlistRead.Absent) return null;
Relevance

●●● Strong

The PR explicitly states the correct lock ordering and separate retirement budget, matching the
finding’s requested fix.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added plan explicitly reads the plist before taking the retired-label lock and passes the
install forward deadline. The implemented transaction instead gives retirement its own deadline
before the install forward phase, while RetireAsync locks before reading because the source
comment identifies the concurrent-replacement race.

docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md[1143-1154]
docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md[1165-1188]
src/Capacitor.Cli/Services/ServiceVerify.cs[785-805]
src/Capacitor.Cli/Services/ServiceVerify.cs[1001-1012]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
The implementation plan's `RetireAsync` snippet reads the retired unit before acquiring its transaction lock. This contradicts the implemented ordering and can lead a future implementation to authorize one unit then remove a replacement written concurrently.

Fix Focus Areas
- docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md[1143-1154]
- docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md[1165-1188]

Recommended Fix
Update the plan to acquire `ServiceTxnLock.TryAcquire(store, retireId, LockWait)` as the first operation in `RetireAsync`, then read and validate the plist while holding that lock. Move the retirement step ahead of the install forward phase and give it its own `_forwardBudget` deadline, matching the current `ServiceVerify` implementation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. PR metadata fails issue linking ✗ Dismissed 📘 Rule violation § Compliance
Description
The PR description begins with Part of #791 — AI-2535 instead of placing a GitHub closing keyword
and both issue references on the same reference line. The implementation plan explicitly instructs
authors not to use Closes #791, so the submitted metadata follows that noncompliant path.
Code

docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md[1343]

+Body: follow `.github/PULL_REQUEST_TEMPLATE.md`. Reference line: `Closes #791` is **not** used, since PR 2 finishes the issue; write `Part of #791` and `AI-2535`. Push with `git push https://github.com/kurrent-io/kcap-cli.git alexeyzimarev/ai-2535-extend-desktop-app-settings-to-configure-the-daemon`.
Relevance

●●● Strong

Recent repository guidance treats a closing keyword plus both identifiers as required PR metadata.

PR-#659

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2897991 requires one reference line containing a GitHub closing keyword plus the
GitHub and Linear identifiers. The cited plan mandates Part of #791 and the supplied PR
description uses exactly Part of #791 — AI-2535, leaving the required closing keyword absent.

Rule 2897991: PR description must contain both GitHub and Linear issue references; PR title must not contain issue IDs
docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md[1343-1343]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR reference line uses `Part of #791` rather than the required GitHub closing keyword followed by the GitHub and Linear references on one line.

## Fix Focus Areas
- docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md[1343-1343]

## Recommended Fix
Update the PR description reference line to `Closes #791 AI-2535` and revise the implementation-plan instruction so it no longer directs authors to use the noncompliant `Part of` form.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Settings test heading adds no rationale ✗ Dismissed 📘 Rule violation ⚙ Maintainability
Description
LocalControlOpsTests adds // ---- PutDaemonSettingsAsync ----, which only repeats the method
under test for the following block. The adjacent test names already identify that operation, so the
heading gives a later reader no behavioral constraint or usage warning.
Code

test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/LocalControlOpsTests.cs[365]

+    // ---- PutDaemonSettingsAsync ----
Relevance

●●● Strong

Recent test-review precedents accept removing comments that merely restate nearby assertions or test
behavior.

PR-#591

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2762993 disallows comments that merely restate nearby code or signature information.
The cited heading is solely the name of the operation exercised by the immediately following tests.

Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/LocalControlOpsTests.cs[365-365]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The added settings-test section heading only names the method exercised by the tests below it.

## Fix Focus Areas
- test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/LocalControlOpsTests.cs[365-365]

## Recommended Fix
Remove the section heading and rely on the descriptive test method names to organize the settings operation coverage.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
6. Bad retirement input creates crash reports ✓ Resolved 🐞 Bug ☼ Reliability
Description
Install calls DaemonCommands.ExtractFlagValue(args, "--retire") outside an ArgumentException
handler. When --retire has no value or is followed by another flag, the parser throws and the
top-level crash guard logs the otherwise-invalid command as a crash instead of returning through the
command’s normal validation path.
Code

src/Capacitor.Cli/Commands/DaemonServiceCommands.cs[110]

+        var retire   = DaemonCommands.ExtractFlagValue(args, "--retire");
Relevance

●●● Strong

The repository accepts fixes preventing malformed CLI arguments from escaping into generic crash
handling.

PR-#383
PR-#240

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The newly added parser call is unguarded, while the parser explicitly throws for the malformed forms
and other command handlers catch this exception. An uncaught exception reaches the process-wide
crash reporter, which records it as a crash.

src/Capacitor.Cli/Commands/DaemonServiceCommands.cs[110-111]
src/Capacitor.Cli/Commands/DaemonCommands.cs[837-850]
src/Capacitor.Cli/Commands/DaemonCommands.cs[306-315]
src/Capacitor.Cli/Program.cs[202-207]
src/Capacitor.Cli/CrashReporter.cs[51-77]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`--retire` parsing can throw `ArgumentException` for a missing or flag-like value, but `Install` does not catch it. Catch this validation failure locally, print its message, and return exit code 1 rather than letting it reach crash reporting.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/DaemonServiceCommands.cs[110-111]

## Recommended Fix
Wrap the `ExtractFlagValue(args, "--retire")` call in a `try`/`catch (ArgumentException ex)` matching the existing command-parser handling pattern. Write `ex.Message` to stderr and return `1`; only sanitize the parsed value after successful extraction. Add unit coverage for `--retire` with no following token and with another flag as its following token.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. Capacity test heading adds no rationale ⊘ Outdated 📘 Rule violation ⚙ Maintainability
Description
DaemonConfigProfileTests adds // ── max_agents from profile ──, which only labels the block of
capacity tests that immediately follows. A later reader learns no constraint or rationale beyond
what the four test names already state.
Code

test/Capacitor.Cli.Daemon.Tests.Unit/DaemonConfigProfileTests.cs[27]

+    // ── max_agents from profile ──────────────────────────────────────────────
Relevance

● Weak

A recent precedent rejected removing a test comment that restated the test’s behavior, making this
rule application uncertain but unfavorable.

PR-#672

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2762993 permits comments only when they document non-obvious, behavior-critical
constraints. The cited heading supplies organization only and does not explain behavior or
rationale.

Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
test/Capacitor.Cli.Daemon.Tests.Unit/DaemonConfigProfileTests.cs[27-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The added capacity-test section heading merely repeats the subject already expressed by the adjacent test names.

## Fix Focus Areas
- test/Capacitor.Cli.Daemon.Tests.Unit/DaemonConfigProfileTests.cs[27-27]

## Recommended Fix
Remove the decorative heading; retain the nearby behavioral comment about an explicit default value because that comment documents a non-obvious constraint.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Five public types share one source file 📘 Rule violation ⚙ Maintainability
Description
SettingsIpc.cs declares two public records, two public static classes, and a public serializer
context even though its filename matches none of them. Searching for or extending one wire contract
now requires navigating a mixed-purpose file whose ownership is ambiguous.
Code

src/Capacitor.Cli.Core/LocalIpc/SettingsIpc.cs[R8-11]

+public sealed record DaemonSettingsPutDto(int? MaxAgents);
+
+/// MaxAgents echoes the value in effect after the put, on success and on refusal alike.
+public sealed record DaemonSettingsAckDto(bool Ok, string? Reason, int? MaxAgents);
Relevance

● Weak

A same-day precedent rejected enforcing one-type-per-file for a partial source file, despite the
naming rule.

PR-#865

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3162234 requires one primary top-level type per file with a matching filename. The
cited source defines DaemonSettingsPutDto, DaemonSettingsAckDto, DaemonSettingsReasons,
SettingsWire, and SettingsIpcJsonContext together, and none qualifies for the listed narrow
exceptions.

Rule 3162234: One primary type per file, with only narrow documented exceptions
src/Capacitor.Cli.Core/LocalIpc/SettingsIpc.cs[8-33]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SettingsIpc.cs` contains five public top-level types and does not match any primary type name.

## Fix Focus Areas
- src/Capacitor.Cli.Core/LocalIpc/SettingsIpc.cs[8-33]

## Recommended Fix
Move each public record, static contract class, and serializer context into a source file named after that type while preserving the existing namespace and generated JSON metadata.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/kcap-server (sha: 01e241aa)
Review mode: 🧠 Deep: This is a high-density, cross-cutting change spanning local IPC contracts, live daemon state, service install/retirement transactions, configuration precedence, and multiple operational paths, creating many independent opportunities for subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread test/Capacitor.Cli.Core.Tests.Unit/LocalIpc/LocalControlOpsTests.cs
Comment thread docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md
Comment thread src/Capacitor.Cli.Daemon/Services/DaemonSettingsIpc.cs
Comment thread docs/superpowers/plans/2026-09-10-ai2535-daemon-settings-backend.md Outdated
Comment thread src/Capacitor.Cli/Commands/DaemonServiceCommands.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/DaemonSettingsIpc.cs
alexeyzimarev and others added 3 commits September 11, 2026 11:07
…-extend-desktop-app-settings-to-configure-the-daemon

# Conflicts:
#	docs/CHANGES.md
Its own budget now starts only once its lock is held, and a
collision under the new name is rechecked after the retire settles
and inside the replace matrix, not just before the retire starts.
Also rejects a missing/flag-like --retire value instead of throwing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 5's snippets and placement prose lagged the actual insertion
point and the refuseLiveOwner matrix parameter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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