feature: loops and bugs and ux#40
Open
JheisonMB wants to merge 204 commits into
Open
Conversation
…e session working directory
…le passphrase error never hides fresh input
…t panic or mis-wrap
…ow instead of freezing them
…agent instructions
…stall - agents.rs: replace manual ANSI parser (depth 5) with regex (depth 1) - process.rs: extract parse_pids_from_ss and terminate_process (depth 6 → 3) - vector_store.rs: extract shared query_file_paths helper, deduplicate logic - service_install.rs: extract ensure_linger_enabled and reload_and_enable_service Also applies cargo fmt to 16 other files with inconsistent formatting. All 421 tests pass, clippy clean.
…ystem_context_parts
… advancing retry_current_node was a no-op — pool dispatch only picks pending specs, so the running spec was invisible and the next queue member ran instead. Changes: - db/pools.rs: pool_running_spec_id() finds first running pool member - loop_engine.rs: run_loop_dispatch checks for running spec before pending-picker loop when is_resume=true - db/loops.rs: reconcile_stranded_pool_specs() resets pool specs stuck running with no active run, called at startup - handler.rs: retry_current_node validates a running spec exists - server.rs: call reconcile_stranded_pool_specs at startup 7 new tests, all 1150 pass.
A prompt-only rule telling non-committer nodes not to commit has been broken by three different models. Move the rule into the engine. A node may commit only if its config declares `commit_rights: true`. The engine snapshots HEAD before a node runs and compares it after; a node that moved history without the right fails, overriding whatever verdict it reported — a clean exit code and a `loop_complete_node` self-report of `pass` are both overridden — and the failure is persisted on the run row so `canopy loop info` shows it. Enforcement is per-graph opt-in: it activates only once some node in the graph declares the right, because a graph that designates nobody cannot be told apart from one whose committer predates this key, and enforcing there would fail the very node the graph relies on to land work. Ensembles are enforced at quorum granularity — members run concurrently against one workdir, so a moved HEAD cannot be attributed to a single member. Non-git workdirs are skipped. 6 tests, 1156 pass.
The B19/B26 infra-crash retry treated every quick agent failure as transient, so a deterministic spawn failure — chiefly a missing CLI binary — burned the whole retry budget and its doubling backoff waiting for a state that cannot change between attempts. Classify spawn failures by kind, not by matching the rendered message: - cli_strategy::resolve_binary now returns a typed BinaryResolutionError instead of a bare anyhow string, so a missing binary is distinguishable at the source. - A new SpawnError wraps build/spawn failures and derives a permanent reason from the error kind (typed BinaryResolutionError, io NotFound / PermissionDenied); everything else stays transient. - agent_spawn_failure records spawn_permanent plus an infra_retry_skipped reason on the run output, so an operator can tell 'failed fast on purpose' from 'retried and gave up'. - is_infra_crash returns false when spawn_permanent is set, so a permanent failure produces exactly one run row with no infra_attempt marker. The transient path is unchanged: retry limit, backoff, and the iteration-budget exemption are untouched, and the existing B19/B26 tests pass as-is.
…e (B42) Terminating a node run because a newer attempt superseded it is engine bookkeeping, not a node failure. It was being finalized as a `Fail` row and then routed down the graph's fail edge, which — with a resilience node on that edge — manufactured a fresh triage run per killed attempt, and each triage pass spawned the next attempt: an unbounded runaway that let no node complete. - run_spec now recognises the supersede marker on a resolved run and returns a new terminal-and-silent `SpecExecutionOutcome::Superseded`: the owning dispatch evaluates no edge, fails nothing, and completes nothing, leaving the loop to whichever dispatch now owns it. - run_loop_dispatch claims the loop via a single-statement compare-and-set (fireable -> running only if not already running) instead of an unconditional status write. This is the one guarded entry point every launch path funnels through, so the autorun-vs-resume check-then-act race (one reads the loop as failed, the other hasn't written running yet) resolves to exactly one winner; the loser no-ops rather than starting a duplicate, superseding run. loop_continue no longer pre-flips the status — it converges on the claim. Genuine agent failures (nonzero exit, self-reported fail, timeout) and the B19/B26 infra-retry path are untouched. Adds a regression test that drives the exact chain (in-flight run + supersede -> no edge traversed, no resilience run created) and one for the duplicate-launch no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Node launches, completions, terminations and edge traversals produced no log output, so diagnosing a loop meant reading ps and SQLite. Emit INFO lines for each transition: - launch: loop/spec/node ids, node name, run id, platform, model - completion: run id, status - edge route: run id, from/to node, status (or no edge matched) - termination: run id, node id, reason Every line carries the run id so a single grep reconstructs one run end to end. Ensemble member runs log the same lifecycle with their ensemble id. Only ids and status are logged, never prompts or agent output, so volume tracks transitions rather than agent chatter.
agent_models now returns, for each platform, the literal model string that platform's CLI accepts. Previously opencode ids were emitted bare (`mimo-v2.5-free`) with the provider only as a parenthetical label, but opencode's `-m` flag requires the `provider/model` form (`opencode/mimo-v2.5-free`); the bare form fails at runtime with a generic server error, which was misdiagnosed as a flaky gateway for days. Platform- native models absent from models.dev (`mimo-v2.5-free`, `big-pickle`) were also missing entirely. The rule is registry-driven, not a CLI-name check: `CliConfig` gains `models_list_cmd` (opencode: `models`). When a platform declares an enumeration command, agent_models runs it as the authoritative source of passable ids — each output line is the exact string the model flag accepts, prefix and all — and skips models.dev, which cannot know a gateway's prefixed form or its private zen catalog. Platforms without an enumeration command (claude) keep the bare models.dev-derived ids, which are correct. The native enumeration is cached like the models.dev catalog (its own `models_native_<cli>.json`, same TTL and live|cache|stale provenance) and runs off the hot path in spawn_blocking, so a fresh cache serves with no CLI call. Output stays bounded by the existing per-provider caps. The rendered id is always the first token on the line with any human label parenthesized, so a caller can copy it verbatim. Tested with a mocked fetch and a mocked enumeration asserting the emitted id form per platform (opencode prefixed, claude bare), plus the native cache staleness policy; cross-checked against real `opencode models` output.
…install (B38) Binary resolution now converges on setup's model: absolute path -> PATH lookup -> not found. The `~/.<binary>/bin/<binary>` install-dir guess is removed; it was derived from the binary name and wrong precisely when needed (e.g. mimo lives at ~/.mimocode/bin/mimo, not ~/.mimo/bin/mimo). The real fix is the daemon's PATH. `canopy daemon install` now captures PATH from the installing user's process environment and writes it into the generated systemd unit, so a service-managed daemon sees the same CLIs the user does. Regenerating merges rather than shrinks: existing Environment=PATH= entries are preserved so a reinstall never breaks already-working CLIs. BinaryResolutionError collapses to a single struct whose message names the binary and the exact PATH searched.
Daemon-startup reconciliation now marks a node run interrupted by a restart/kill with a distinct, recorded marker instead of a plain fail so canopy loop info never attributes the damage to whichever node runs next. Any uncommitted worktree changes are stashed via `git stash push -u`, bounded by the spec's recorded spec_start_head, rather than reverted or left to be silently overwritten — the stash is idempotent and recoverable with `git stash list` / `git stash pop`. reconcile_stranded_pool_specs now also catches a loop_runs row still stuck at 'running' from a stale boot or dead pid for a pool member whose loop already paused, applying the same distinct marking and quarantine before resetting the spec to pending.
Setup and doctor detection called which::which(binary) directly while the spawner resolved through resolve_binary_with_path, so a CLI could report as installed at setup time yet fail to spawn from the daemon. Route every detection call site (setup_module::models, skills_module::download, cli_config::CliConfig) through the same resolve_binary_in primitive the spawner uses, so the two can never disagree. canopy doctor now reports each configured CLI's resolution step and absolute path, and compares against the daemon's captured Environment=PATH= (read from the systemd user unit) to warn when a CLI only resolves under the interactive shell's PATH and would fail under the daemon.
Windows Action Center piled up duplicate toasts for the same subject (e.g. a mission unlock) because send_wsl never set Tag/Group, so every Show() was additive. Derive a stable per-subject tag from the title (already the natural subject key at every call site) so re-sending the same subject replaces its entry instead of adding a new one; distinct subjects and the 24h additive retention are unaffected. This is now the authoritative anti-pile-up mechanism, holding regardless of exit path; startup cleanup remains as best-effort hygiene and now logs failures instead of swallowing them.
Replace the flat sidebar (Projects/Loops/Backlog/Knowledge/History as top-level siblings) with three collapsible layers between the pinned RAG summary and sysinfo dashboard: Live (interactive agents + terminals), Automation (background agents + loops, global and always visible), and Knowledge (the projects list). Everything project-scoped now lives inside each project: a cheap cached Preview summary card on highlight, and an Overview | Backlog | Knowledge | History tab bar on Enter, reusing the sidebar's existing mouse hit-test/dispatch infrastructure for tabs and rows. The project History tab now reads finished loops and past interactive/terminal sessions straight from SQLite (scoped to the project's workdir), fixing the old behavior where History only showed what the live provider had observed since the TUI started. Also fixes a bug found in review: draw_projects_list/draw_knowledge_body took &App instead of &mut App, so project_click_map was never populated and a mouse click on a sidebar project row silently did nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…phaned artifacts The 2026-07-12 incident required an emergency SQL delete of 829 stale orphaned/error interactive_sessions rows flooding the TUI after WSL crashes. `canopy clean` (soft mode, default) removes stale orphaned/error/completed sessions and their orphaned log/terminal/RAG artifacts within a configurable retention window (default 7 days), never touching active/resumed sessions, and reports (without deleting) projects whose workdir no longer exists. Supports --dry-run and --older-than. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…MCP tools Replaces static global-skill installation with on-demand fetch: skills are cloned from configurable [skills] git sources into ~/.canopy/skills/, kept fresh via a TTL'd commit-hash check (network failure serves the stale copy with a WARN), and served to any harness via new skill_list/skill_get MCP tools. Reuses skills_module's find_skill_instructions/list_skill_dirs and leaves the existing global-symlink standard untouched. Validates skill_get's caller-supplied name as a single path component before it reaches store_dir.join()/remove_dir_all, closing a path-traversal gap in an MCP tool surface reachable by any connected agent.
…t at spawn (S2) Agent-node and blueprint configs can now carry an optional `skills: [...]` array. At spawn time, after prompt/preset interpolation and on both cold-start and resume paths, each pinned skill is resolved through S1's dynamic skill store (same fetch/TTL-cache path skill_get uses) and appended to the prompt under a `## Skill: <name>` section in listed order. A skill that can't be resolved — or no store configured at all — degrades to a WARN plus an inline note instead of failing the spawn. Oversized injected content is still caught by the existing B9 stdin-transport threshold. blueprint config merge and loop_get's node listing already pass `skills` through untouched, verified with tests rather than reimplemented.
The UniverLab/skills catalog renamed loop-design to canopy-loop-design and split execution-mindset's Canopy tooling into new canopy-intelligence and canopy-sync skills, alongside new architect-mindset/canopy-capabilities entries gated by requires = "canopy". Updates the prompt-builder guidance text to describe the family instead of the old two-skill summary, renames the stale loop-design references in sync_policy's tests, and adds an explicit one-time migration that moves a machine's existing ~/.agents/skills/loop-design/ to canopy-loop-design/ before the normal per-file sync runs — skipping the move (with a WARN) if both directories already exist rather than guessing which one should win, so B4's never-clobber-newer-local-content rule stays intact.
First step of the theme system: introduces Theme with classic()/default() mirroring today's hardcoded colors (BORDER_COLOR, ACCENT, BG_SELECTED, DIM, and the Rgb(18,18,18) background). Not yet wired into any renderer.
…r constants Thread Theme::classic() through draw_header, draw_footer, and draw_sidebar render paths. Replace direct uses of ACCENT, DIM, BG_SELECTED, and BORDER_COLOR with theme.header_color, theme.dim_text, theme.selected_bg, and theme.border_color respectively. Purely mechanical — no visual change with Theme::classic(). Panel/dashboard/dialog files remain on the old constants (T3-T4).
Thread Theme::classic() through draw_log_panel and all panel render paths (mod.rs, loop_live.rs, sync.rs). Replace direct uses of ACCENT, DIM, and BORDER_COLOR with theme.header_color, theme.dim_text, and theme.border_color respectively. Purely mechanical — no visual change with Theme::classic(). Dialog files remain on the old constants (T4).
… constants Thread Theme::classic() through all dialog render paths and system dashboard. Replace direct uses of ACCENT, DIM, BORDER_COLOR, and BG_SELECTED with theme.header_color, theme.dim_text, theme.border_color, and theme.selected_bg respectively. Remove the old free-standing color constants from ui/mod.rs — every renderer now reads from Theme. Inline the actual Color values in Theme::classic() instead of referencing the removed constants. Purely mechanical — no visual change with Theme::classic(). Completes T2-T4 theme migration; only modern theme (T5) and persistence (T6) remain.
…l renderers Add Theme::modern() with borderless, background-contrast styling: - panel_bg: Rgb(25, 25, 35) - sidebar_bg: Rgb(18, 18, 25) - selected_bg: Rgb(40, 40, 55) - header_color: Rgb(160, 160, 170) - dim_text: Rgb(90, 90, 100) - show_borders: false Add borders_for(theme) helper returning Borders::ALL for classic, Borders::NONE for modern. Replace all Borders::ALL with borders_for(theme) in sidebar, panels, and dialogs. Sidebar fills its area with sidebar_bg for background contrast. Add 6 tests: modern_is_borderless, modern_reproduces_spec_values, borders_for_classic_draws_all_sides, borders_for_modern_draws_no_sides, test_backend_classic_draws_box_glyphs_modern_does_not, and modern_theme_drops_border_glyphs_where_classic_shows_them. No theme selection/persistence yet (T6). Purely additive — classic theme unchanged.
Add CanopyConfig::theme field (String, default 'classic') with serde for forward-compat. Add Theme::resolve() mapping config string to Theme — 'classic'/'modern' resolve to their palettes, unknown values fall back to classic with a warning. Add App::theme field resolved from config at startup. draw() now uses app.theme instead of hardcoded Theme::classic(). app_methods.rs uses self.theme for accent colors. Setup wizard gains a theme selection step (step 2.25) with Classic/ Modern options. Tests: config round-trips theme via TOML, old configs without theme field default to classic, Theme::resolve handles all three cases, wizard theme_choice_to_config_value maps labels correctly.
Implement deferred resume for paused loops via loop_schedule_continue, allowing users to pause a loop (e.g. to stop burning quota) and have it automatically resume at a scheduled time without manual intervention. - Add auto_continue_at + auto_continue_action fields to Loop domain model - Add is_auto_continue_due() that fires only when status == Paused - Add fire_due_auto_continue_loops() scheduler branch that routes through loop_continue (retry_current_node/skip_next_spec) + resume_background, never loop_reset or fresh dispatch - Clear stale schedules when loop leaves Paused before fire time - Add loop_schedule_continue MCP tool with cancel support (at=null) - Add DB migration for new columns (idempotent) - Add 17 tests covering domain, scheduler, DB migration, and handler Distinct from autorun_at: autorun resets-and-relaunches failed loops, auto-continue resumes paused loops in place preserving cursor/context.
Extend `canopy clean` with `--hard` flag that deletes orphaned projects (registered workdir no longer exists) and all their dependent rows: interactive sessions, terminal sessions, loops (with specs/nodes/edges/ runs/ensembles), project-scoped intelligence, sync state, prompts, and scheduled sends. Features: - Transactional cascade deletion per project (no partial deletes) - Guards: skip projects with running loops or active/resumed sessions - Interactive confirmation prompt (default: no) - `--yes` flag bypasses prompt for scripting - `--dry-run --hard` prints plan without deleting - Projects with existing workdirs are never deleted - Standalone specs and pools preserved (not project-owned) Implementation: - DB layer: `cascade_delete_orphan_project`, `project_hard_cascade_counts`, `project_hard_cascade_skip_reason` - Domain layer: `plan_hard_cascade` (pure decision function) - CLI layer: `run_hard_cascade` with confirmation flow - Full test coverage (53 clean-related tests) Depends on C1 (soft clean, commit 105108c).
Defect A — node-initiated loop_schedule_autorun now responds promptly: - Single-statement UPDATE with WAL mode, no transaction held across await - scheduler_notify.notify_one() wakeup is non-blocking - loop_run/loop_reset fast-fail against running loops with explanatory errors - Scheduling is idempotent (retry after lost ack overwrites same column) Defect B — deterministic reset-time parsing replaces model arithmetic: - New domain/quota_reset.rs parses 'resets 1pm (America/Bogota)' format - Handles 12h clock (1pm, 1:10pm, 9:20pm, 2:30am), IANA timezones via chrono_tz - Adds 2-min safety margin, validates strictly-future and ≤24h - Midnight rollover: 2:30am at 22:40 resolves to next day - quota_reset_message param feeds raw CLI text to parser engine-side Regression tests: - 10 unit tests in quota_reset.rs covering all time formats and edge cases - node_initiated_autorun_call_during_own_dispatch_over_http: <5s response - quota_shaped_failure_schedules_correct_autorun_instant: end-to-end path - retried_schedule_call_is_idempotent: double-call leaves one schedule - loop_run/loop_reset fast-fail against running loop All 1348 tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Why
Related issue
Checklist
Notes (optional)