Skip to content

feat: implement decoupled orchestrator with agent lifecycle state machine - #8

Merged
jzila merged 75 commits into
mainfrom
decoupled-orchestrator
Jan 24, 2026
Merged

jzila merged 75 commits into
mainfrom
decoupled-orchestrator

Conversation

@jzila

@jzila jzila commented Jan 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Agent lifecycle state machine: Adds a formal state machine (pkg/lifecycle/state.go) to track agent states (Initializing → Running → Merging → MergeComplete → Terminal) with proper transitions and event handling
  • Orchestrator decoupling: Refactors the orchestrator to run as a daemon-managed component via OrchestratorManager, removing the watch mode in favor of continuous polling
  • Dashboard improvements: Adds orchestrator state indicator, retry attempt display, validation status badges, and Details drawer for completed tasks
  • Rules engine: Implements three-tier precedence hierarchy for task filtering rules
  • Persistence: Adds run_configs table and lifecycle state persistence/restoration

Test plan

  • Unit tests for lifecycle state machine (pkg/lifecycle/*_test.go)
  • Handler tests for daemon orchestration (pkg/daemon/handlers_test.go, pkg/daemon/server_test.go)
  • Manual testing of dashboard with orchestrator state transitions
  • Integration testing of full agent lifecycle (spawn → run → merge → complete)

jzila added 30 commits January 23, 2026 20:35
Add new 'off' state to distinguish 'not activated' from 'idle but
watching'.

State semantics:
- off: No orchestrator instance, no goroutine running, not watching
- idle: Orchestrator running, polling for work, none available
- active: Orchestrator running, processing tasks
- paused: Orchestrator running but scheduler paused

Changes:
- Add OrchestratorOff constant
- RegisterRepo() initializes in StateOff (not idle)
- StopRun() and runOrchestrator() completion transition to StateOff
- Error cleanup in StartRun() resets to StateOff
Rename the per-repo state/lifecycle wrapper from RepoOrchestrator to
OrchestratorLifecycle. This name better reflects that the struct manages
the orchestrator's lifecycle (Off/Idle/Active/Paused states), not the
execution itself.

Updated files:
- pkg/daemon/orchestrator_manager.go (struct definition and all references)
- docs/design/active-orchestrator-model.md (documentation references)
…-uxle)

When using bwrap, setResourceLimits was not being called, which meant
Setpgid=true was not set on the command. This caused killProcessGroup
to fail silently on context cancellation because the process wasn't
its own process group leader.

Now setResourceLimits is always called regardless of bwrap usage.
This ensures that when the orchestrator is stopped, the context
cancellation properly terminates all running agents.

The bwrap --die-with-parent flag is a backup for unexpected parent
death, but for graceful shutdown we need process group isolation.
The kill agent button in the web UI was not working because the daemon
was created with nil scheduler. The decoupled orchestrator model stores
schedulers within OrchestratorManager, not the daemon.

Changes:
- Add KillAgent method to OrchestratorManager that looks up the agent's
  run and routes the kill request to the correct scheduler
- Update HandleKillAgent to use OrchestratorManager when available,
  falling back to legacy scheduler interface
- Add GetOrchestratorManager to DaemonInterface
- Update mock daemon in tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reflect refined understanding of the orchestrator model:

- Add 'Off' state (no instance exists) to state machine
- Clarify 'Idle' = activated, watching, no work available
- Remove 'run completes → idle' transition (orchestrator never exits)
- Keep 'runs' terminology (not 'sessions')
- Document OrchestratorLifecycle wrapper (RepoOrchestrator renamed)
- Update watch mode as default behavior with polling loop
- Correct 'What's Already Implemented' section (canopy-o7ww scope)
- Update epic reference from canopy-i4sm to canopy-2360
- Add ASCII state transition diagram
continuously

The orchestrator now always runs in a continuous polling loop:
- Check for work
- If work: process tasks (Active state)
- If no work: sleep PollInterval (Idle state)
- Repeat until context cancelled

Changes:
- Remove Watch field from RunConfig struct in api.go
- Update config.go comment to remove "watch mode" reference
- Add KillAgent method to OrchestratorManager (was missing)
manager

- Add missing nil argument for paused parameter in UpdateRunConfig call
- Add KillAgent method to OrchestratorManager to route kill requests
  through active orchestrators

Note: Activate/Deactivate methods were already implemented on
OrchestratorLifecycle. StartRun/StopRun already use them as aliases.
observation

Add StateCallbacks struct to orchestrator with OnIdle/OnActive functions.
The Orchestrator calls OnIdle() when no work is available (entering poll-wait)
and OnActive() when starting to process tasks.

OrchestratorLifecycle (via OrchestratorManager.createStateCallbacks) registers
callbacks to update its State field accordingly:
- OnActive: State = OrchestratorActive
- OnIdle: State = OrchestratorIdle

Also fix build error in orchestration_handler.go where UpdateRunConfig was
called with an extra nil argument.
The token counter in the dashboard header now shows a clickable dropdown
that displays a detailed breakdown of all token types:
- Input tokens (blue)
- Output tokens (green)
- Cache read tokens (purple, shown when > 0)
- Cache write tokens (orange, shown when > 0)
- Total tokens (bold)

This allows users to see exactly how tokens are being used across
all agents in the run, including cache efficiency metrics.

Also adds the missing KillAgent method to OrchestratorManager
to fix the build.
…xhd)

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

Replace the old RunControlBar with a unified OrchestratorStateIndicator
that displays the current orchestrator state (Off/Idle/Active/Paused)
and provides appropriate controls.

Layout: [Repo Selector] | ● Active (N agents) [Pause] [Deactivate] | [Run History ▾]

Changes:
- Add StateCallbacks to orchestrator for Idle/Active transitions
- Add OrchestratorStateIndicator component with state-dependent buttons
- Integrate indicator into DashboardHeader between repo and run selectors
- Add orch:state_changed event handling in WebSocket hook
- Add orchestratorState and activeAgentCount to state store
- Remove unused RunControlBar component
handleAgentMergeStatus

The handleAgentMergeStatus function was only extracting 3 fields from
the event payload (merge_status, queue_pos, error), but the IPC server
sends many more fields including validation and repair tracking data.

This caused the dashboard's worker agent Details drawer to not appear
because the hasWorkerChainData check depends on validation_status and
repair_attempts fields that were never populated.

Now extracting all fields sent by the IPC server:
- commits_applied
- had_conflict
- resolver_spawned
- validation_status
- validation_error
- validation_duration_ms
- validation_steps
- repair_attempts
- last_repair_output

Added getInt64FromMap helper function to handle numeric extraction from
nested map[string]interface{} (needed for validation_steps parsing).
…fmu)

When max_retries > 0 and an agent fails, the orchestrator retries the
task but the UI had no indication this was a retry. This change adds:

- AgentStartPayload now includes attempt and max_retries fields
- Orchestrator exposes GetTaskAttempt() and GetMaxRetries() methods
- IPC client has SendAgentStartWithOptions() for passing retry info
- Dashboard shows "Attempt N of M" badge for retry attempts
- Uses orange badge styling consistent with warning indicators

The retry badge only appears when attempt > 1 (i.e., actual retries).
For infinite retries (-1), shows "Retry N" instead of "Attempt N of M".
When merge completes, the agent could appear 'running' in the UI and
canopy ps even though the task is complete with merged commits. This
was due to a race condition where:

1. Merge processor marks task 'done' and sends TaskUpdated/MergeStatus
2. EventAgentCompleted comes later from a separate callback path
3. RuntimeState processes these independently

During the window between merge completion and EventAgentCompleted,
the task shows 'completed' but agent shows 'running'.

Fix by updating AgentStatus to 'completed' or 'failed' atomically when
handleAgentMergeStatus receives a terminal merge status (merged,
merged_needs_repair, resolved, skipped, or failed). Also update task
status and recalculate stats in the same handler.

Added missing MergeStatus constant aliases (MergeStatusResolved,
MergeStatusSkipped, MergeStatusMergedNeedsRepair) to the daemon package.
When validation fails and a repair agent is spawned, the parent agent's
status in `canopy ps` would remain "running" with no indication that
repair was in progress. Users would see their work product merged but
the agent appearing stuck.

Changes:
- Add ValidationStatus and RepairAttempts fields to WorkerInfo and
  AgentInfo structs to capture repair state from daemon
- Update status display logic to show "repairing (N)" when validation
  status is "repairing", where N is the repair attempt number
- Prioritize validation status over merge status in display

Now when repair is in progress, `canopy ps` shows:
  STATUS: repairing (1)
instead of:
  STATUS: running

This provides clear visibility that the agent is actively being repaired
rather than appearing stuck.
Defines a unified state machine to replace the current multi-field
agent status model (Status, MergeStatus, ValidationStatus). This
addresses race conditions where agents appear "running" after merge
completes, and lack of UI feedback during validation/repair.

Key design elements:
- Single AgentLifecycleState enum with 13 states
- Explicit transition table with validation
- Backwards-compatible legacy field derivation
- 4-phase migration strategy with parallel tracking

Architect review complete. Resolves design for canopy-jygc, canopy-vf1u.
…vfap)

The Activate button was appearing twice because:
1. Button visibility was based solely on orchestratorState === 'off'
2. During activation, isActivating was true but orchestratorState
   remained 'off' until the WebSocket state update arrived

Fix by:
- Hide Activate button when isActivating is true
- Hide Pause/Resume/Deactivate buttons when isDeactivating is true
- Show transitioning visual feedback (yellow icon, "Activating..." text)
  during state transitions
Add orchestrator state (off, idle, active, paused) to the canopy ps
output. When the orchestrator is active with running agents, the
display shows the count: "Orchestrator: active (4 agents)".

This helps operators understand the current orchestration state from
CLI without needing the dashboard.
Implement pkg/lifecycle package with a unified state machine that replaces
the multi-field status model (Status, MergeStatus, ValidationStatus).

Key features:
- 13 states covering the full agent lifecycle from starting to terminal
- 17 events that trigger state transitions
- Atomic transitions protected by mutex
- Bounded state history (100 entries) for debugging
- TransitionCallback for EventBus integration
- Legacy compatibility functions for backwards compatibility

This is Phase 1 of the agent lifecycle state machine migration.
See docs/design/agent-lifecycle-state-machine.md for full design.
OrchestratorLifecycle
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Implements the always-active orchestrator model as specified in
docs/design/active-orchestrator-model.md:

- Rename RepoOrchestrator → OrchestratorLifecycle with improved docs
- Add Activate/Deactivate methods as preferred API for lifecycle control
- Add DeactivateByRepo to stop orchestrator by repo path
- Add new API endpoints: POST /api/orchestrator/activate,
  POST /api/orchestrator/deactivate
- Update dashboard API client with activate/deactivate functions
- Keep legacy StartRun/StopRun endpoints for backwards compatibility

State machine: Off → Idle ↔ Active, with Paused as temporary state
- Off: Not activated, no orchestrator instance
- Idle: Activated, watching for work, none available
- Active: Processing tasks
- Paused: Temporarily stopped by user/agent

The orchestrator polls continuously when activated - it never exits on
its own, only through explicit Deactivate() call.
…p9uc)

events

Remove agent status setting from handleAgentMergeStatus to prevent
race conditions when agent:merge_status and agent:completed events
arrive in close succession. Only EventAgentCompleted should set
terminal status (completed/failed) to maintain a single source of
truth for agent completion state.

The merge status event continues to update merge-specific fields
(merge_status, queue_pos, validation fields, etc.) and task status.
Users identify repositories by path, not internal UUID. Look up the
repository path from the ID returned by the daemon API using
repository.FromID(), with fallback to ID if lookup fails.
'running'

Validation status was jumping directly to 'running' without first
sending 'pending', causing the frontend state machine to miss the
intermediate transition. This matches the pattern used for merge
status (MergeStatusPending) at line 196.

The fix sends validation_status: "pending" immediately after confirming
validation is enabled, before entering the validation loop where
"running" status is sent.
…gro)

tests

Add exhaustive tests for all state machine transitions:
- All 40+ valid transitions with context variations
- Invalid transition rejection from all states
- Property tests: terminal states never transition out, history integrity
- Concurrency tests: parallel transitions, concurrent reads/writes
- Race detector passes with all tests

Coverage: 100% of nextState() logic
(canopy-03bs)

Update `canopy ps` to display the agent lifecycle state from the state
machine instead of deriving status from multiple legacy fields.

Changes:
- Add LifecycleState field to AgentState struct and GetSnapshot
- Add LifecycleState field to AgentInfo and WorkerInfo in ps command
- Update filter logic to use lifecycle state for determining active agents
- Update display logic to prefer lifecycle state over derived status
- Maintain backwards compatibility by falling back to legacy status fields
  when lifecycle_state is not set
Add merge queue visibility to the `canopy ps` command. Operators can now
see which agents are waiting to merge and their order in the queue.

Output format:
- Shows "Merge Queue: empty" when no agents are queued
- Shows ordered list with position, agent ID, task ID, and status (merging/waiting)

Example:
  Merge Queue:
    Position 1: agent-abc123 (beads-xyz) - merging
    Position 2: agent-def456 (beads-uvw) - waiting

Changes:
- Add MergeQueueSnapshot and MergeQueueEntry types to daemon handlers
- Include merge queue info in /api/state response via buildMergeQueueSnapshot()
- Update ps command to consume and display merge queue from state API
- Add merge_queue to ProcessInfo for JSON output support
- Add tests for buildMergeQueueSnapshot and state response inclusion
AgentState

Add AgentLifecycle field to AgentState to run the new state machine in
parallel with legacy Status/MergeStatus fields during Phase 1 rollout.

Changes:
- Move AgentStatus to pkg/types to break import cycle between daemon
  and lifecycle packages
- Add Lifecycle field to AgentState (not serialized to JSON)
- Initialize lifecycle in handleAgentStarted() and transition to running
- Call lifecycle.Transition() alongside legacy field updates in
  handleAgentMergeStatus() and handleAgentCompleted()
- Add checkLifecycleDivergence() to log warnings when states diverge
- Add canopy_lifecycle_divergence_total Prometheus metric for monitoring

This enables detecting transition logic bugs before Phase 2 migration.
lifecycle (canopy-4qsj)

When an agent starts, the state now goes through 'starting' before 'running'.
This allows the UI to show the correct state if an agent crashes during
initialization before actually beginning execution.

Changes:
- agent:started event now sets status to 'starting' (not 'running')
- New agent:running event transitions from 'starting' to 'running'
- agent:running is emitted on first live feed event from the agent
- Frontend handles both states with proper transitions
- Persistence layer tracks the intermediate state

Also fixes a build error in orchestration_handler.go where Deactivate/
DeactivateByRepo methods were removed but still referenced.
…-y18w)

flow

Add granular validation status states during repair agent lifecycle:
- pending_repair: Deciding to spawn repair agent (after validation failure)
- spawning_repair: Creating repair agent and context
- repairing: Repair agent actively executing

This replaces the immediate jump from 'failed' to 'repairing' with
intermediate states that provide better UI visibility and allow
tracking of slow repair agent spawning.

Updated files:
- pkg/mergequeue/processor.go: Send pending_repair and spawning_repair
- pkg/repairagent/spawner.go: Send repairing when execution begins
- pkg/ipc/protocol.go: Document new status values
- web/dashboard: Add ValidationStatusBadge config for new states
Adds a 150ms debounce delay before transitioning from active to idle
state. This prevents UI flickering when a task completes and new work
becomes available almost simultaneously (e.g., when a dependency is
resolved).

Active transitions remain immediate so the UI reflects new work quickly.
Only the transition to idle is debounced, allowing the task polling to
discover newly-ready work before emitting the idle state.

The debounce timer is cancelled if new work arrives before it fires.
jzila added 28 commits January 24, 2026 10:39
completed tasks

The commit counter on agent cards was showing inconsistently because
`commits_applied` from the `agent:merge_status` event was not being
passed through to the state store.

Changes:
- Add `commits_applied` field to AgentMergeStatusEvent interface
- Pass `commits_applied` through updateAgentMergeStatus function
- Update commit counter to prefer `merge_commits_applied` for merged
  tasks (most accurate), falling back to `agent.commits` for pre-merge
The agent detail pane previously only showed the commit count, not the
actual commits. Users had to switch to the Commits tab in the terminal
panel to see commit details.

Added a collapsible Commits section using the existing CommitList
component. The section shows a badge with the commit count and expands
to show full commit details (hash, message, author, files changed).
…xqqu)

Connect existing retry infrastructure to event callbacks:

- Add retry logging in completionCallback when merge fails but will retry
- Add Attempt and MaxRetries fields to AgentState struct
- Pass orchestrator to createEventCallbacks to access attempt info
- Include attempt/max_retries in OnAgentStartFn event payload
- Extract attempt/max_retries in handleAgentStarted state handler

The IPC protocol already had these fields defined - this change populates
them so retry information flows through to the state and dashboard.
When restoring agents with non-terminal lifecycles during Init(),
lifecycle callbacks are registered that reference r.eventBus. Previously,
SubscribeToEventBus (which sets r.eventBus) was called in Start() after
Init() returned, so any lifecycle callback that fired during restoration
would silently drop the event due to the nil check at makeLifecycleCallback.

This fix moves the SubscribeToEventBus call into Init(), right after the
eventBus and state are created but before state restoration occurs. This
ensures r.eventBus is set when lifecycle callbacks are registered, so
agent transitions publish events properly to the UI.
…gnz)

[1] fix(daemon): add agent to state before lifecycle transition

The lifecycle transition publishes events via callback. If we transition
before AddAgent(), clients receiving the event may request a state snapshot
that doesn't include the agent yet, causing stale 'Starting' state in UI.

Reorder operations so AddAgent happens before lifecycle transitions to
ensure atomicity of the add+transition sequence.
[2] fix(orchestrator): complete truncated mergeRulesSettings

function

The function was truncated at line 779, missing the final clause that
applies CLI override flags. Restore the complete function implementation.
…e3j3)

Add staleness detection before applying patches directly. When HEAD has
moved ahead of the overlay's base commit, skip direct patch application
and spawn a resolver for proper 3-way merge.

This prevents long-running agents with stale overlays from silently
reverting recent commits when their patches are merged.
The beadsClient.Fail() method called `bd reopen`, which put failed
tasks back in the ready queue even after max retries was exhausted.
This created infinite retry loops.

Changes:
- Add FailPermanently() method to BeadsClient interface and Client
- FailPermanently() closes the task and adds needs-investigation label
- Update orchestrator to call FailPermanently() when retries exhausted
- Keep existing Fail() for temporary failures (retries remaining)
- Add mock support and test coverage for FailPermanently()

The needs-investigation label ensures the task won't be picked up
by future canopy runs until manually reviewed.
The scheduler stores contexts by task.ID (e.g., "canopy-xyz"), but
KillAgent() was passing the full agentID (e.g., "agent-abc12345-canopy-xyz").
This caused 500 errors because the scheduler couldn't find the context.

Changes:
- Extract taskID from agentID parts in orchestrator_manager.go
- Improve error handling in handlers.go to return 404 for "not found" errors

Test plan: go test ./... and go build ./... pass
Refactor `canopy run` to delegate orchestrator lifecycle to the daemon
instead of running a blocking local orchestrator. The CLI now:

1. POSTs to daemon's /api/orchestrator/run endpoint
2. Polls for run status and streams output
3. Handles Ctrl+C by calling stop endpoint

Also adds rules override support to daemon API so CLI flags like
--type, --exclude-type, --label, --exclude-label, and --assignee
are properly passed through to the orchestrator.
Daemon shutdown now properly cleans up active overlays by calling
scheduler.CleanupAll() on all running orchestrators. This prevents
orphaned FUSE processes and overlay directories.

Changes:
- OrchestratorManager.CleanupAllOverlays() iterates through all active
  runs, calls scheduler.CleanupAll() with 30s timeout
- Daemon.Stop() calls CleanupAllOverlays() before stopping other components
- Added time import to daemon.go

The cleanup happens before IPC/HTTP shutdown to ensure graceful cleanup
while the orchestrators are still active.
When UnregisterRepo is called, the orchestrator context was cancelled
but the function returned immediately without waiting for active
overlays to be cleaned up. This could leave orphaned FUSE mounts.

Changes:
- Add Shutdown() method to Orchestrator that calls scheduler.CleanupAll()
- Update UnregisterRepo to call Shutdown() before returning
- Move cleanup logic outside the lock to avoid blocking other operations
- Log overlay cleanup results for debugging

The 30-second timeout prevents hanging on stuck mounts while giving
adequate time for normal cleanup to complete.
…f7af-canopy-nsdl-resolver)

settings

Add SQLite table and CRUD methods for persisting per-repository
orchestrator configuration settings:

- Add RunConfig struct with concurrency, max_priority, use_bwrap,
  max_retries fields
- Add migration V16 creating run_configs table with appropriate defaults
- Implement GetRunConfig() returning defaults if no record exists
- Implement SaveRunConfig() using upsert pattern
- Add DefaultRunConfig() helper for default values

Resolves merge conflict for canopy-nsdl by applying patch to current HEAD.
…f-canopy-ktmx-resolver)

persistence

Add task-level retry tracking fields to track retry attempts separately
from validation repair attempts (RepairAttempts).

Changes:
- Add Attempt and MaxRetries fields to daemon.AgentState
- Add Attempt and MaxRetries fields to persistence.Agent
- Add migration V17 for new database columns (V16 was taken by run_configs)
- Update CreateAgent to persist new fields
- Update agentColumns and scan functions to read new fields
- Update ConvertPersistenceAgentToState to restore fields
- Update GetSnapshot to copy new fields

Resolves: canopy-ktmx
…65cbf7af-canopy-37jl-resolver)

lifecycle states

When a WebSocket client connects, it receives a state:sync snapshot.
However, events may have been published after the snapshot was taken
but before the client processed it. This could cause the UI to show
stale states by processing events that were already reflected in the
snapshot.

Add monotonic sequence numbers to events:
- EventBus assigns increasing sequence numbers to each published event
- RuntimeStateSnapshot includes the current sequence at snapshot time
- Dashboard frontend discards events with sequence <= snapshot sequence

This ensures that events occurring before the snapshot are ignored,
preventing out-of-order state updates that could cause UI glitches.
…opy-wyy1-resolver)

Add GET /api/config and PUT /api/config endpoints for loading and
saving run configuration per repository.

- GET returns saved config or defaults if none saved
- PUT persists to SQLite, reloads into daemon memory, broadcasts
  config_updated event via WebSocket so dashboard updates

Implementation:
- Add EventConfigUpdated event type in pkg/events/events.go
- Add HandleGetConfig and HandlePutConfig handlers in orchestration_handler.go
- Register /api/config route in server.go
- Use persistence.Store.GetRunConfig/SaveRunConfig for storage
…bf7af-canopy-p4te-resolver)

Add onTerminal callback mechanism to lifecycle state machine that fires
when any terminal state is reached (completed, failed, needs_attention,
cancelled, timed_out).

This provides a safety net for overlay cleanup:
- TerminalCallback type and WithTerminalCallback option added to lifecycle
- Callback fires from both Transition() and SetState() methods
- Scheduler.CleanupOverlay() method for explicit cleanup by task ID
- Orchestrator.CleanupOverlayByTaskID() exposes cleanup to callers

The terminal callback ensures cleanup happens regardless of which path
led to the terminal state (normal completion, context cancellation,
timeout, or forced state recovery).

This is defense-in-depth: the existing merge coordinator callback chain
handles most cases, but the terminal callback provides a backup mechanism
for edge cases where the callback chain might be bypassed.
…cbf7af-canopy-ye8z-resolver)

Add Retry Information section to AgentDetail that shows:
- Current attempt number and max retries
- Previous failure reason (merge_error or validation_error)
- Last repair output if available

Only displayed when attempt > 1 (i.e., the agent is a retry).
…f-canopy-uzmw-resolver)

persistence

Add a Configure button adjacent to Activate that opens the run config
dialog in "configure" mode, allowing users to save run settings without
starting a run. Settings are persisted via PUT /api/config and synced
across browser tabs via WebSocket config_updated events.

Changes:
- api/client.ts: Add getRunConfig/saveRunConfig API functions
- OrchestratorStateIndicator: Add Configure button (outline style)
- DashboardHeader: Pass onConfigure prop
- Dashboard: Add config dialog mode state and handleSaveConfig handler
- RunConfigDialog: Support dual-mode (start vs configure)
- useWebSocket: Handle config:updated events
- stateStore: Add loadSavedConfig and updateRunConfigFromServer actions

Resolves canopy-uzmw
…cbf7af-canopy-ulxm-resolver)

Wire up attempt/max_retries fields through the full event pipeline:

- state.go: Extract attempt/max_retries from AgentStarted event payload
  and populate AgentState fields
- persistence_handler.go: Extract and persist retry info when agent starts
- useWebSocket.ts: Add attempt/max_retries to BackendAgentState interface
  and transform to frontend AgentState in state:sync handler

The UI components (AgentCard, AgentDetail) were already implemented but
had no data flowing through. Now when an agent restarts after a merge
failure, the retry badge is displayed with attempt number and max retries.

Merge conflict resolution: Preserved concurrent config:updated event
handling that was added to the codebase between BASE and HEAD commits.
…af-canopy-ea8q-resolver)

callback

When the orchestrator runs inside the daemon process, the merge
processor's ipcClient was never set. This caused all merge status
events to be silently dropped, leaving agents stuck in
queued_for_merge state with null merge_status.

Add MergeStatusCallback to mergequeue.Processor as an alternative to
IPC for daemon-mode operation. The callback is invoked whenever merge
status would be sent via IPC, and the daemon wires this to publish
events directly to EventBus.

Changes:
- Add MergeStatusEvent struct and MergeStatusCallback type to processor
- Update sendMergeStatus/sendMergeStatusFull to use callback when set
- Wire callback through MergeCoordinator and Orchestrator
- Add createMergeStatusCallback in daemon's orchestrator_manager.go
  that publishes to EventBus and handles lifecycle transitions
loading

When a new orchestrator run starts, a WebSocket run:started event may
arrive before the initial getRuns() API call completes. Previously,
setRuns() would replace the entire runs array with the API response,
causing any recently-added runs to be lost if they weren't yet in the
database.

Add mergeRuns() function that combines API results with existing state:
- Preserves runs added via WebSocket that aren't in the API response
- Uses API data as the source of truth for runs that exist in both
- Maintains descending sort order by started_at timestamp

This fixes the issue where newly started runs wouldn't appear in the
'All runs' dropdown until the page was refreshed.
…py-72x8-resolver)

repair/resolver agents

Previously, repair and resolver agents created synthetic beads tasks
with agent IDs (e.g., agent-65cbf7af-canopy-ea8q-resolver) as the task
ID for execution tracking. This agent ID was then incorrectly used in
commit messages instead of the original bead ID.

The fix adds a BeadID field to agent.Result that repair/resolver agents
set to the original bead ID after execution. The merge code now uses
BeadID if set, falling back to TaskID for normal agents.

This ensures commit messages use the format:
  type(component): description (canopy-abc)
instead of:
  type(component): description (agent-65cbf7af-canopy-abc-resolver)
…3772cec2-canopy-k6zq-resolver)

resolvers (canopy-k6zq)

- Display 'Resolving' status on AgentCard when an active resolver is
  working on the task, replacing the normal lifecycle state
- Add Related Agents section to AgentDetail showing parent and child
  agents with clickable rows to switch between them
- Pass onSelectAgent callback through TerminalPanel to AgentDetail
  allowing navigation to resolver/repair agent logs
- The TerminalPanel already rendered independently of grid filtering
  since it uses the full agents object, not filtered agents

This fixes the issue where clicking on a task in the merge queue would
not show details if the user was filtering by a different agent status.
…-canopy-qt1v-resolver)

dashboard display (canopy-qt1v)

In daemon mode, agent:commit events were being lost because
sendMergedCommits() only used the IPC client (which is nil in daemon
mode). The MergeStatusCallback pattern was added previously for merge
status events, but commits were never given the same treatment.

This adds a CommitCallback mechanism mirroring MergeStatusCallback:
- CommitEvent struct to hold commit data
- CommitCallback type for the callback function
- SetCommitCallback methods on Processor, MergeCoordinator, Orchestrator
- createCommitCallback in OrchestratorManager to publish to EventBus

The dashboard now receives agent:commit events which populate the
git_commits array, fixing the commits list display in the agent
detail panel.
…qp4)

run-configured overrides

Adds a three-tier rules precedence system for the orchestrator:
1. Override rules (run-configured, highest priority) - set via Configure dialog
2. Config rules (from .canopy/config.toml)
3. Default rules (built-in, lowest priority)

Changes:
- Add RuleSource type to track rule origin (default/config/override)
- Update rules Engine to track source for each rule
- Add ApplyOverrides() and ClearOverrides() methods to Engine
- Update RunConfig to accept rule_overrides for run-scoped rules
- Add RuleOverrideRequest type to orchestration handler API
- Add rules editing UI to RunConfigDialog with collapsible section
- Update TypeScript interfaces to include source field
- Fix test fixtures to include required source field
…kqav)

rules

The Configure dialog now pre-populates with rules from the config file,
allowing users to modify them for a specific run:

- Load rules from getRules API when opening the configure dialog
- Display config rules in a "From Config" section with toggle switches
- Visual indicators distinguish config rules from runtime-only overrides
- Modified config rules show a "modified" badge and "Reset" button
- Runtime-only rules shown in separate "Runtime Only" section with delete button
- Rules can be enabled/disabled per-run without affecting persisted config
- Only truly modified rules are sent as overrides when starting the run

Files changed:
- RunConfigDialog.tsx: Added DialogRule type, config rules display logic
- Dashboard.tsx: Added configRulesForDialog state, loadRulesForDialog handler
- EditRuleModal.test.tsx: Fixed test to expect source field in saved rule
- Check error return values in test files (errcheck)
- Remove unused allStates variable and expectError field (unused)
- Use type conversion instead of struct literal in ps.go (gosimple)
- Fix unused variable assignments in state_test.go (staticcheck)
- Remove unused STATUS_COLORS in AgentCard.tsx
- Remove unused currentRunId in Dashboard.tsx
- Remove unused filteredIndex parameter in RunConfigDialog.tsx
@jzila
jzila merged commit 10f9564 into main Jan 24, 2026
7 checks passed
@jzila
jzila deleted the decoupled-orchestrator branch January 24, 2026 23:01
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