Skip to content

feat: reliability improvements - agent settings, unified sidebar, test coverage - #11

Merged
jzila merged 33 commits into
mainfrom
reliability
Feb 1, 2026
Merged

jzila merged 33 commits into
mainfrom
reliability

Conversation

@jzila

@jzila jzila commented Feb 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Plumb agent settings (model/timeout) into running agents with proper JSON serialization
  • Fix data races and guard against empty model in agent executor
  • Combine beads and rules into unified left sidebar panel in dashboard
  • Fix dashboard rules save to call correct /rules/persist-all endpoint
  • Add Patch and Truncated fields to GitCommit struct
  • Add default rule to deny implementing epics
  • Extensive test coverage for agent config handlers, settings enforcement, JSON contracts, and merge coordinator

Test plan

  • Tests added for agent config handlers (JSON casing, validation)
  • Tests for agent settings enforcement in orchestrator
  • JSON serialization contract tests
  • Coverage for handleAgentCommit patch/truncated fields
  • go test ./... passes
  • go build ./... passes

jzila added 30 commits January 29, 2026 15:53
investigation

FailPermanently now adds the needs-investigation label and records the
failure reason in notes, but no longer closes the bead. This allows
humans to investigate failed tasks and manually retry them when needed,
rather than silently closing them.
beads

Add a built-in default rule that automatically excludes tasks with
needs-investigation, needs-input, or other needs-* labels from canopy
runs. These tasks require manual attention and should not be picked up
by automated orchestration.

Implementation:
- Define DefaultCustomRules in pkg/config/config.go with the
  exclude-needs-labels rule using wildcard pattern 'needs-*' in labels
- Update NewEngineWithDefaults to handle shadowing: config rules can
  disable default rules by adding a rule with the same name and
  enabled=false
- Update all rules engine creation sites to use NewEngineWithDefaults
  with the default rules

Users can disable the default rule by adding to their config:
  [rules]
  custom = [{ name = "exclude-needs-labels", enabled = false }]
Add [agents] section to .canopy/config.toml schema for model configuration.

- AgentSettings struct with DefaultModel and per-type settings
- AgentTypeSettings struct with Model, Enabled, and Timeout fields
- DefaultAgentSettings() for sensible defaults (empty model = CLI default)
- Validate() method for AgentSettings (validates timeout formats)
- Tests for loading, saving, and validating agent configuration
Add Model field to agent.Config and pass it to the Claude CLI via
--model flag in both Execute() and ExecuteResume() methods.

The model is propagated through the configuration chain:
- orchestrator.Config.Model -> executor, mergecoordinator
- mergecoordinator.Config.Model -> resolver
- mergequeue.Processor.model -> repairagent

This allows callers to specify which Claude model to use for all
agents including workers, resolvers, and repair agents.
executors

Wire up config loading to pass agent-specific model settings to each
executor type (worker, resolver, repair).

Changes:
- Add GetWorkerModel(), GetResolverModel(), GetRepairModel() helper
  methods to AgentSettings for resolving effective model per agent type
- Update orchestrator.Config to include AgentSettings field
- Update orchestrator.New() to determine per-agent-type models from
  AgentSettings (CLI --model flag still takes precedence)
- Split mergecoordinator.Config.Model into ResolverModel and RepairModel
- Pass repair model to processor in mergecoordinator.New()

Model resolution priority:
1. CLI --model flag (highest)
2. Per-agent-type model from config.toml (e.g., agents.worker.model)
3. DefaultModel from config.toml (agents.default_model)
4. Empty string (use Claude CLI default)
…2dkq)

Add API endpoints for managing agent settings at runtime with persistence:
- GET  /api/repos/:repo_id/config/agents - Get current agent settings
- POST /api/repos/:repo_id/config/agents - Update agent settings (partial)
- POST /api/repos/:repo_id/config/agents/persist - Persist to disk

Implementation follows the rules engine pattern:
- Track runtime vs persisted state in RepoAPI implementations
- Broadcast agent_config:changed events via WebSocket
- Support partial updates for default_model and per-agent-type settings
- Validate settings before applying changes

Files:
- pkg/daemon/agent_config_handlers.go - NEW: API handlers
- pkg/daemon/config_handlers.go - Route agent config requests
- pkg/daemon/server.go - Add AgentConfigHandler
- pkg/events/events.go - Add EventAgentConfigChanged
- pkg/orchestrator/api.go - Add RepoAPI agent config methods
…0sik)

vs conditions)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Frontend used `conditions: string[]` (plural, array) while Go backend
uses `condition: string` (singular, string). This mismatch caused rules
added via the web UI to silently fail—the Go JSON unmarshaler never found
the `condition` key, so validation rejected the empty string.

Changed all TypeScript interfaces and components to use `condition: string`
matching the Go `CustomRule` JSON tag.
AgentChainTimeline

Both prior-attempt and current agents perform the same implementor role.
Using 'Worker' for retries was inconsistent and confusing.
overlap detection

Adds a function that returns the list of files modified between two commits
using `git diff --name-only`. This enables the stale overlay optimization
to check if concurrent changes overlap with agent changes before spawning
a resolver.
file overlap

When HEAD has moved ahead of an overlay's base commit, check if the
agent's changed files overlap with concurrently merged files before
spawning a resolver. If there's no overlap, proceed with direct git am
(fast path), avoiding the expensive resolver agent spawn. Falls back
to resolver when overlap exists or when the file check fails.
…2wq)

viewer

The dashboard diff viewer showed 'No diff available' because patch data
wasn't extracted or propagated through the pipeline. Three fixes:

1. Use GetCommitInfoFromDirWithOptions with IncludePatch: true in
   sequential.go to actually extract diffs during merge
2. Add Patch and Truncated fields to CommitEvent struct and populate
   them in sendMergedCommits() with MaxPatchSize truncation
3. Include patch and truncated in the event payload in
   createCommitCallback()
…3nag)

store

The rules:changed event handler was logging events but not updating the
UI state. Now handles added/updated/deleted/persisted actions by calling
the appropriate store mutations, and refetches the full rules list from
the API for bulk actions (config_updated, persisted_all, reordered).
…tes on daemon restart

Extend MarkOrphanedAgentsFailed() to detect agents in non-terminal
merge-related lifecycle states (merging, queued_for_merge, validating,
repairing, resolving) and mark them as failed during state restoration.
Add AgentConfigPanel modal for viewing and editing per-agent-type model
settings (Worker, Resolver, Repair). Integrates with existing backend
API endpoints (GET/POST /api/repos/:id/config/agents, POST .../persist).

- New AgentConfigPanel.tsx with default model selector, per-type model
  overrides, enabled toggles, and timeout fields
- New useAgentConfig.ts hook for API integration and WebSocket reactivity
- Agent config types and API functions added to client.ts
- WebSocket handler for agent_config:changed events
- Bot icon button in dashboard header to open the panel
…mlvb)

process handles

When a daemon restarts, agents restored from the database may appear in
RuntimeState but have no corresponding process in the orchestrator.
Previously, `canopy kill` would fail with 'not found' for these agents,
leaving them permanently stuck.

Now when the orchestrator returns 'not found' but the agent exists in
RuntimeState with a non-terminal status, HandleKillAgent falls back to
marking the agent as failed directly in both RuntimeState and persistence,
then returns success.
ImplementorSystemPrompt

Align system prompt naming with the 'Implementor' terminology used
elsewhere in the codebase.
Add 'exclude-epics' rule to DefaultCustomRules that denies tasks with
type == epic. Epics define acceptance criteria and should not be directly
implemented; agents should work their child tasks instead.
compliance

AgentTypeSettings had toml tags but no json tags, causing Go's default
capitalized key names (Model, Enabled, Timeout) in API responses. Added
json:"model", json:"enabled", json:"timeout" tags and updated the
TypeScript interface to use snake_case field names.
…-emub)

Runtime agent settings changes via the dashboard API were validated and
stored but never propagated to the running executor, resolver, or repair
agent creation paths. Changes appeared to save but had no effect until
restart.

- Add SetModel/SetTimeout methods to agent.Executor
- Add SetModel to resolver.Resolver
- Add SetResolverModel to mergecoordinator.MergeCoordinator
- Add GetExecutor to scheduler.Scheduler
- Add UpdateAgentSettings to Orchestrator that propagates to all components
- Call UpdateAgentSettings from repoAPI.UpdateAgentConfig for live updates
- Plumb worker timeout from AgentTypeSettings into executor at construction
AgentTypeSettings
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Addresses roborev finding 668 (testing gap): verifies that
AgentTypeSettings marshals to snake_case JSON keys (model, enabled,
timeout) and not PascalCase, preventing accidental API contract breaks.

The backward-compatibility concern (Medium) is not applicable—this is
an internal API between the Go backend and React dashboard, both
updated in the same commit (cfe396b).
agent settings

- Add sync.RWMutex to Executor to protect Model/Timeout from concurrent
  read/write between Execute() and SetModel()/SetTimeout()
- Snapshot model and timeout under lock in Execute/ExecuteResume
- Guard UpdateAgentSettings against empty model strings from Get*Model()
  which can return "" when no default is configured
- Allow timeout clearing: SetTimeout(0) resets to DefaultTimeout
- Log warnings for invalid duration strings instead of silently ignoring
- Add tests for SetModel/SetTimeout including race detector coverage

Addresses roborev findings from job 670 (canopy-a74a601f).
partial updates)

Cover JSON serialization contract (snake_case keys in request/response),
partial update behavior (nil vs false, single-field updates, empty body),
routing dispatch, request/response key roundtrip consistency, writeJSON
helper, and nil eventBus safety.
orchestrator flows

Add 14 tests covering agent settings (model/timeout) propagation through
worker, resolver, and repair agent flows in pkg/orchestrator:

- Worker/resolver/repair model propagation via UpdateAgentSettings
- CLI model override takes precedence over config-based settings
- Worker timeout parsing and propagation
- DefaultModel fallback when per-agent model is empty
- Nil settings is a safe no-op
- Invalid timeout is ignored without crash
- Initial settings from config are correctly plumbed at construction
- RepoAPI.UpdateAgentConfig propagates to executor and merge coordinator
- RepoAPI rejects invalid timeout values
- PersistAgentConfig roundtrips through config.toml

Supporting changes:
- Export GetModelAndTimeout on agent.Executor (was unexported)
- Add GetModel to resolver.Resolver and mergequeue.Processor
- Add GetResolverModel/GetRepairModel to MergeCoordinator
- Add .codex/ to .gitignore and remove tracked .codex/ artifacts
  (session logs, system skills, temp binaries)
- Allow empty model to clear previously set model in UpdateAgentSettings,
  so removing agents.*.model from config returns to CLI default
- Reject negative duration strings in UpdateAgentSettings with warning
- Add doc comment to SetTimeout noting negative durations are ignored
- Add tests for empty-model clearing and negative timeout edge cases
- Guard against nil snapshot in mockRepoAPIForConfig.UpdateAgentConfig
- Remove unused testAgentConfigHandler/newTestHandler scaffolding
- Add handler-level tests for GET/POST/persist success and error paths
- Add repoAPIOverride field to AgentConfigHandler for test injection
- Add nil guard to GetRepairModel to prevent panic when processor is
  uninitialized, mirroring existing GetResolverModel pattern
- Protect Processor.model field with sync.RWMutex to prevent data race
  between concurrent SetModel/GetModel calls
settings (canopy-5f51)

Address roborev finding #680: document that UpdateAgentSettings expects
complete AgentSettings (not partial updates). The caller UpdateAgentConfig
already merges partial PATCH-style updates before calling this function,
so the empty-model-clears behavior is safe and intentional.
The dashboard git diff view was broken because patch data was dropped
in handleAgentCommit(). The IPC layer correctly forwarded patch data
but RuntimeState never extracted or stored it.

Two changes in pkg/daemon/state.go:
1. Added Patch and Truncated fields to GitCommit struct
2. Extract and assign patch/truncated from payload in handleAgentCommit()
endpoint

The Save button in RulesPanel was calling POST /rules/save which doesn't
exist on the server. The server only has /rules/persist-all. The /save
path fell through to the rule-name matcher treating 'save' as a rule
name, returning 405 Method Not Allowed. Fixed by updating the client
endpoint to match the server route.
jzila added 3 commits February 1, 2026 12:45
fields

Address roborev finding #690: adds tests verifying that
handleAgentCommit preserves patch and truncated fields through
runtime state, including the case where these fields are absent.
…46py)

accordion

Merge the separate BeadsPane (left) and RulesPanel (right) into a single
left-side SidebarPanel with expandable accordion sections. This frees the
right side entirely for the agent grid and provides a simpler layout.

- Create SidebarPanel component with collapsible accordion sections
- Add embedded rendering mode to BeadsPane and RulesPanel
- On small screens (<=768px), sidebar renders as overlay with backdrop
- Accordion open/closed state persisted per-section in localStorage
- Single resize handle for the unified sidebar width
- Fix executor_test.go to match renamed ImplementorSystemPrompt
- Remove unused vars in AgentConfigPanel.tsx and SidebarPanel.tsx
- Add missing useCallback dependencies in useWebSocket.ts
@jzila
jzila merged commit 675af36 into main Feb 1, 2026
7 checks passed
@jzila
jzila deleted the reliability branch February 1, 2026 19:55
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