Skip to content

fleet: per-program configuration status - #9

Closed
LutzLange wants to merge 6 commits into
copilot/pr3c-settings-jsoncfrom
copilot/pr4-program-status
Closed

LutzLange wants to merge 6 commits into
copilot/pr3c-settings-jsoncfrom
copilot/pr4-program-status

Conversation

@LutzLange

Copy link
Copy Markdown
Owner

Stacked on this fork so the diff is this PR only (base: copilot/pr3c-settings-jsonc). Eighth change in the series; James gets one integration PR from copilot/integration when the series is ready. Product-wide: every managed programs.* key, not only Copilot.

Why. The controller kept one configuration status per device (applied or failed, one error string), and one conflicting file blocks every managed file on the device. An admin could not tell which program conflicted, which were blocked by it, or which were configured but not pointed at the gateway because the local LLM proxy was not running.

What it does.

  • Agent. Each apply reports an outcome per managed program, by precedence failed > conflict > blocked > inactive > applied/removed > unchanged. Provider gains a required id(). Reconciler::plan_with_report plans every provider and keeps planning past a failing one; AttributedPlan::apply applies with the same all-or-nothing rules and returns the same first error as before (Reconciler::apply now wraps it). ReconcilePlan::apply says where it stopped (conflict, changed file, private directory, failed write), so the failing program is failed, programs already written applied, the rest blocked. A program counts as changed when bytes on disk differ or it recorded a create/update/remove, so a sidecar rewritten with the same bytes does not make an unchanged program look applied. The Copilot CLI and both VS Code files report inactive with the reason. Reported programs: configured ones (an exhaustive destructure of ProgramsConfig) and ones with something to clean up, conflict or fail. Details are at most 1024 bytes; a sidecar parse error no longer quotes the file's values. Every apply logs one program configuration outcome line per program that is not applied or unchanged (warnings for failed, conflict, blocked).
  • Proto (additive). ConfigStatus.programs (ProgramStatus { program, state, detail }, ProgramState) and ConfigStatus.programs_reported. Older controllers ignore them; older agents send neither.
  • Controller. Migration 0003: device_program_status (with the report position) and device_config_status.programs_reported. A report is validated (at most 32 programs, IDs [a-z0-9-] up to 64 bytes, no duplicates, details up to 2048 bytes) and written with the device row in one transaction, so a program missing from a report disappears. A report that fails validation does not break the stream: the device status is stored and the last accepted program rows stay. GET /api/v1/devices/{id} returns programs and programs_reported; the device list and overview are unchanged.
  • UI. A "Managed programs" card on the device page (tool, state badge, detail, time; rows from an earlier revision show it); "Per-program status is not reported by this agent version" for older agents; storybook fixtures for every state.
  • README. "Configuration status" under "Start locally, grow into a fleet": the states, precedence, all-or-nothing, the log line, what a restart reports, rollout in either order, and that the migration is one-way (roll back from a database backup).

Process. Spec reviewed (Opus, 14 findings) and revised to rev 3 (the test author found attribution needs Provider::id); baseline tests first (f1e6e15, rebased as 6684bf5: 13 reconciler, 2 remote, 5 controller), unchanged through every revision (the test fake gained id()). Three review lenses: conformance conforms; maintainer one major (Reconciler::apply kept its own loop), adversarial minors (a sidecar value could reach the detail, a rejected report looped the stream, two stop paths untested), all fixed with tests. Delta 1 and delta 2 passed with minors (fallback only for invalid reports and keeping the last accepted rows; private-directory stop path tested; earlier-revision rows marked). The PR 3b test capi_tunnel_closes_when_the_cached_credential_expires failed under parallel load (a 300 ms cache window); fixed without weakening what it checks (ledger). Gate: agent 269, core 18, controller 18, storybook 52, clippy, fmt, schema, frontend.

Lab (Linux VM, user mode, controller-pushed config, API and admin UI). First run by an independent agent on v1: the device page lists claude-code, copilot, vscode; a comment in chatLanguageModels.json gives vscode conflict with the path, claude-code blocked ("not applied: vscode conflicted"), copilot unchanged, device failed, file untouched; without the proxy listener copilot and vscode are inactive with the reason; removing copilot gives removed, then no row; an older agent shows "not reported by this agent version"; a newer agent against an older controller applies without errors (an older controller refuses a database migrated by this one, documented). One checklist expectation was wrong by design (after a restart the report says unchanged, since the startup apply wrote the files first; documented, local issue 014 for the startup report). Repeat on v3 with lab/checks/pr4-status.sh: 14 of 14. The lab also found that its build script could install a stale binary when worktrees share a cargo target directory; fixed in the lab tooling.

Not covered: Postgres (tests and lab ran SQLite; the migration uses the same statements as 0001/0002); a check that no pairing value appears in the stored API responses (the lab agent was not allowed to read the pairing file; to be run by hand); retry after a cleared conflict (the reconcile tick, PR 5).

Follow-ups (local issue tracker): 014 report the startup apply; 002 reconcile tick; 006 sidecar rewritten on every apply.

🤖 Generated with Claude Code

LutzLange and others added 6 commits September 30, 2026 12:40
Adds failing tests against the PR-4.md interface contract (reconcile::{ProgramState,
ProgramOutcome, ApplyReport, AttributedPlan}, Reconciler::{plan_with_report,
apply_with_report}, ReconcilePlan::inactive, reconcile::configured_programs; the
controller's device_program_status storage; the proto's ProgramStatus/ProgramState).
New symbols the contract names are compile-only todo!() stubs; existing tests are
unmodified and still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The controller kept one configuration status per device (applied or
failed, one error string), and one conflicting file blocks every managed
file on the device. An admin could not tell which program conflicted,
which were blocked by it, or which were configured but not pointed at the
gateway because the local LLM proxy was not running.

Agent. Each apply now reports an outcome per managed program, by
precedence failed > conflict > blocked > inactive > applied/removed >
unchanged:
- `Provider` gains a required `id()` (the existing `ID` constants, the
  inventory's agent kind).
- `Reconciler::plan_with_report` plans every provider and keeps planning
  past a failing one; `AttributedPlan::apply` applies with the same
  all-or-nothing rules and returns the same first error as
  `Reconciler::apply` (unchanged, still used by tests).
- `ReconcilePlan` records, per provider before it is appended, whether the
  plan changes anything on disk (differing bytes, a removal, or a recorded
  create/update/remove; a sidecar rewritten with the same bytes does not
  count), its conflict lines, an inactive reason, and the paths it owns;
  `apply` now also says where it stopped (conflict, changed observed file,
  private directory, or the index of the failed write), so the failing
  program is `failed`, programs already written are `applied` and the rest
  `blocked`.
- The Copilot CLI and both VS Code files record `inactive` with the reason
  where they already warned about a missing proxy.
- Programs are reported when configured (an exhaustive destructure of
  `ProgramsConfig`, so a new key does not compile until mapped) or when
  they have something to clean up, conflict or fail. Details are truncated
  to 1024 bytes on a char boundary.
- The remote apply, the startup apply and `--once` log one `program
  configuration outcome` line per program that is not applied or unchanged
  (before a startup failure stops the daemon).

Proto (additive): `ConfigStatus.programs` (`ProgramStatus { program,
state, detail }`, `ProgramState` enum) and `ConfigStatus.programs_reported`.
Older controllers ignore them; older agents send neither.

Controller. Migration 0003: `device_program_status` (per device and
program, with the report position) and
`device_config_status.programs_reported`. `update_config_status` validates
the report (at most 32 programs, IDs `[a-z0-9-]` up to 64 bytes, no
duplicates, details up to 2048 bytes; an invalid report is an error, as
for telemetry) and writes the device row and the program rows in one
transaction, so a program missing from a report disappears. `GET
/api/v1/devices/{id}` returns `programs` (ordered as reported, unknown
states as `unspecified`) and `programs_reported` (null before any status).
The device list and overview are unchanged.

UI: a "Managed programs" card on the device page (tool, state badge,
detail, time); "Per-program status is not reported by this agent version"
for older agents; storybook fixtures for every state.

README: "Configuration status" under "Start locally, grow into a fleet".

Tests: baseline 6684bf5 (test author, 20 tests: 13 reconciler, 2 remote, 5
controller), unchanged; the test fake gained `id()`. Writer: Docker test
`program_status_reports_inactive_without_the_proxy` (VS Code image, the
outcome line in the daemon log) and two storybook stories. Agent lib 266,
controller 17, core 18, storybook 52.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed sidecar values, stable tunnel test)

Review round 1 on pr4-v1 (conformance conforms; maintainer one major;
adversarial minors).

- `Reconciler::apply` wraps `apply_with_report` instead of keeping its own
  planning loop, so the all-or-nothing rule exists once and the tests that
  call `apply` run the code production runs (maintainer M1, adversarial n1).
- `AttributedPlan::apply` builds every outcome in one place from how the
  apply ended (`Stop`: completed, refused before writes, or failed at a
  point with its owners); same results (maintainer m2).
- A per-program report the controller rejects no longer breaks the stream:
  `store_config_status` keeps the device-wide status and drops the program
  rows (`programs_reported` false) with a warning; before, the agent
  reconnected, got the configuration again and sent the same report
  (maintainer m1, adversarial m2).
- `json_merge` names only the position when a sidecar does not parse: the
  serde message could quote a value from the sidecar, which holds managed
  content such as a pairing value, and that text now reaches the program
  detail, the controller database and the UI (adversarial m1). The proto
  comment no longer claims more than the code does.
- Outcome lines for failed, conflict and blocked programs are warnings
  (maintainer n2); removed and inactive stay info.
- UI: a long detail wraps instead of being cut to one line (maintainer m5);
  an unused CSS class removed.
- README: the controller migration is one-way; roll back from a database
  backup (maintainer m4).
- Test code names no PR or spec; a stale comment in daemon.rs (maintainer
  m3, n3; adversarial n2).
- `capi_tunnel_closes_when_the_cached_credential_expires` (PR 3b test)
  failed under parallel load: the cache entry's 300 ms reuse window ran out
  before the request arrived, so the proxy fetched a new credential (424).
  The entry is now reusable for 60 s while the credential still expires in
  300 ms, which is the deadline the test checks. Local issue 013.

Writer tests: `two_programs_planning_the_same_path_both_fail_and_the_rest_is_blocked`,
`a_wrongly_typed_sidecar_field_does_not_put_its_value_in_the_detail`
(reconciler), `a_rejected_program_report_keeps_the_device_status`
(controller service). Agent lib 268, controller 18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate-directory stop path tested)

- `store_config_status` falls back only when the report fails validation,
  and then stores the device-wide status and keeps the last accepted program
  rows and `programs_reported` (new `Database::update_device_config_state`).
  Before, any database error took the fallback, and the device page then
  claimed the agent did not report per-program status. Database errors are
  returned as they are.
- Writer test
  `a_private_directory_that_cannot_be_created_fails_its_owner_and_blocks_the_rest`
  covers the private-directory stop path of `AttributedPlan::apply`.
- Proto comment: details carry error chains of the kind sent in
  `ConfigStatus.error`.
- README (previous commit): what a restart reports (lab S4).

Agent lib 269, controller 18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n rejected reports

Delta 2 minor and nit: after a rejected report the device page kept the
last accepted rows under the latest device status; rows whose revision
differs from the device's now show it. README says what a rejected report
keeps and which startup outcomes are logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LutzLange

Copy link
Copy Markdown
Owner Author

Superseded: landed upstream in agentdesktop-dev#146 (per-program status).

@LutzLange LutzLange closed this Oct 20, 2026
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