Autonomy: level dial, goals, wakes, and the autonomy channel - #631
Conversation
Adds TaskStatus::Failed with InProgress->Failed and Failed->Ready edges, and rebuilds the global tasks table so assigned_agent_id is nullable. Unassigned tasks surface through Task::effective_agent_id, which falls back to the owner for events and notifications.
SystemEvent is a closed enum with a string round-trip so unknown event names fail at config load. The wake_events queue coalesces pending duplicates in SQL via a partial unique index and claims consumption with a CAS-guarded update.
Channels now carry a ChannelKind (User, Cron, Autonomy) with policy methods for self-exit, reflection suppression, and retrigger capping, replacing the accidental cron_outcome.is_some() and id-prefix checks. Autonomy exists for the upcoming autonomy channel; nothing constructs it yet.
Goals are persistent user objectives distinct from tasks: the goals table lives in the global DB with goal_id linking on tasks. Branches and cortex chat get goal_create/goal_update/goal_list; channels get read-only goal_list. Active goals render as a compact list in every channel prompt. Completion is user-initiated through the API.
Adds AutonomyLevel (off/observe/suggest/act) and AutonomyConfig with load-time validation and hot reload. The cortex tick starts a run when the interval elapses or wake events are pending; runs consume the wake queue at start, brief from tasks, goals, and prior run summaries, and exit through autonomy_complete under a retry-enforced completion contract. Ready-task pickup is now gated on level act.
GET /agents/autonomy (status + fleet + runs) reads through the agent registry; level and tuning writes ride the agent-config path with TOML persistence and hot reload. The panel now runs on real data: the dial writes config with optimistic updates, the approval queue is actual pending_approval tasks with approve/dismiss-to-backlog, goals and run history come from their stores. Wake defs and the fleet ceiling remain labeled design previews.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
WalkthroughThis change adds autonomous agent execution with scheduled and event-driven wakes, durable wake and run storage, goals, task ownership updates, configuration, APIs, tools, prompts, and autonomy interface pages. ChangesAutonomy platform
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
wake_defs table + WakeDefStore (CAS schedule claiming, event subscriber lookup, webhook token lookup), [[agents.wakes]] config validated at load and reconciled as a seed with the DB as source of truth, task-approved builtin, and run briefings now carry each wake's instructions with level-gated events rendered as observations.
Phase 2 of the wakes design:
- emit_system_event producer helper: subscriber lookup, coalescing
enqueue, doorbell ring. Wired at task transitions (approve/execute/
update now use update_with_status_transition so the pending_approval
-> ready edge fires task.approved to the owning agent), goal
create/update (API + tools), and worker completion.
- Shared ScheduleSpec trigger layer in src/schedule.rs — one definition
of 5-field cron expansion, used by both the cron scheduler and the new
schedule-wake producer riding the cortex tick with CAS-claimed cursors.
- Wakes API: list (with virtual interval-survey row), tune, manual fire,
delete, plus public webhook ingress at /hooks/wakes/{token} outside
the auth middleware — the per-wake token is the authority boundary.
- Instance ceiling: [autonomy] ceiling in config.toml, one ArcSwap
shared by the API and every agent, capping both the run gate and
ready-task pickup as min(ceiling, dial). Fleet API reports ceiling +
effective_level.
- Panel: WakesCard and CeilingCard are real now (mock deleted); ceiling
writes persist with optimistic updates.
…amiepine/autonomy-panel
There was a problem hiding this comment.
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/tasks/store.rs (1)
845-849: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winThe test schema still declares
assigned_agent_id TEXT NOT NULL.
migrations/global/20260809000001_tasks_nullable_assignment.sqlmakesassigned_agent_idnullable, andTask::assigned_agent_idis nowOption<String>. The in-memory test schema insetup_test_storekeeps theNOT NULLconstraint. Every test that runs against this fixture therefore cannot create an unowned task, and the new unassigned-task paths (effective_agent_id,render_task_line's(unowned)branch, autonomyclaim_unowned) stay untested. An insert withassigned_agent_id = NULLfails in tests but succeeds in production.Relax the constraint so the fixture matches the migrated schema, then add a test that creates a task with
assigned_agent_id: None.🐛 Proposed fix
owner_agent_id TEXT NOT NULL, - assigned_agent_id TEXT NOT NULL, + assigned_agent_id TEXT, subtasks TEXT,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tasks/store.rs` around lines 845 - 849, Update the in-memory schema in setup_test_store so assigned_agent_id is nullable, matching migration 20260809000001_tasks_nullable_assignment.sql and Task::assigned_agent_id. Add a test that creates a task with assigned_agent_id: None and verifies it persists and exercises the unowned-task behavior.src/api/tasks.rs (1)
365-396: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftPreserve an explicit unassignment value.
UpdateTaskRequest.assigned_agent_idusesOption<String>, so the client cannot send an explicitnullto clear the existing assignee without also being ambiguous when the field is omitted. A tri-stateassigned_agent_idvalue is needed throughUpdateTaskRequest,UpdateTaskInput, the client/schema type, and the SQL update. Add coverage for omitted, string, and null values.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/api/tasks.rs` around lines 365 - 396, Make assigned_agent_id tri-state across UpdateTaskRequest, UpdateTaskInput, the client/schema type, and the SQL update so omitted preserves the current assignee, a string assigns it, and explicit null clears it. Update the task mutation flow around update_with_status_transition to propagate all three states without conflating omission with unassignment, and add coverage for omitted, string, and null inputs.
🟠 Major comments (21)
src/wakes/mod.rs-1-23 (1)
1-23: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winMove this module root out of
mod.rs.The coding guidelines forbid
mod.rsfiles. Move this file tosrc/wakes.rsand keep the contents unchanged.src/goals.rsin this same PR already follows that pattern.git mv src/wakes/mod.rs src/wakes.rsAs per coding guidelines: "Don't use
mod.rsfiles. Usesrc/memory.rsas the module root, notsrc/memory/mod.rs."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/mod.rs` around lines 1 - 23, Move the wakes module root from src/wakes/mod.rs to src/wakes.rs, preserving the module contents unchanged. Keep the existing submodule declarations and re-exports in the moved root so the public API remains identical, matching the module layout used by src/goals.rs.Source: Coding guidelines
tests/bulletin.rs-119-125 (1)
119-125: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winUse the instance pool for
GoalStorein these tests.
Db::connectonly applies./migrationstodb.sqlite, so the per-agent pool does not definegoals.GoalStorequeries thegoalstable from global migrations, while the wake stores are on per-agent tables. Initializedb.instance/connect_instance_dbfor these tests and pass that pool togoals::GoalStore::new, matching production.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/bulletin.rs` around lines 119 - 125, The test setup currently constructs GoalStore with the global db.sqlite pool, but production expects goals on the instance database. Initialize the instance database via db.instance/connect_instance_db in the test setup and pass that pool to goals::GoalStore::new; leave the wake stores using the existing global pool.src/wakes/events.rs-11-23 (1)
11-23: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAlign
SystemEventserde values withas_str.
#[serde(rename_all = "snake_case")]makesTaskApprovedserialize to"task_approved", whileas_str,parse,WakeTrigger::spec,WakeDefserialization, and TOML wake config all use"task.approved". This causes a JSON round-trip viaWakeDef/WakeTriggerto fail parsing. Rename each variant explicitly to its dotted form and add a serde round-trip test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/events.rs` around lines 11 - 23, Update the SystemEvent enum’s serde configuration so every variant serializes to the dotted value used by as_str, parse, WakeTrigger::spec, WakeDef, and TOML wake configuration instead of snake_case names. Add a serde round-trip test covering the SystemEvent values through the relevant WakeDef/WakeTrigger path, ensuring serialization and deserialization preserve each event.src/wakes/defs.rs-133-177 (1)
133-177: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPreserve
webhook_tokenduring wake definition updates.
reconcile_config_wakesbuildsWakeDefvalues withwebhook_token: Noneand then callsstore.upsert, soDO UPDATE SET webhook_token = excluded.webhook_tokenoverwrites an existing token withNULL. Keep the existing token for config-owned/updated wakes the same way schedule cursor columns are preserved, so published webhook URLs do not break authentication.🔒️ Proposed fix
- webhook_token = excluded.webhook_token, \ + webhook_token = COALESCE(excluded.webhook_token, wake_defs.webhook_token), \🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/defs.rs` around lines 133 - 177, Update WakeStore::upsert so the ON CONFLICT update path preserves the existing webhook_token instead of assigning excluded.webhook_token, particularly for config-owned wakes created by reconcile_config_wakes with None; leave insertion behavior unchanged and ensure existing published webhook authentication tokens survive reconfiguration.migrations/20260809000003_wake_defs.sql-18-18 (1)
18-18: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winStore a hashed webhook credential instead of
webhook_token.
webhook_tokenis a public bearer credential stored directly inwake_defs. Database reads expose it becauseWAKE_DEF_COLUMNSselects it from rows, anditem_from_defexposes it by buildingwebhook_urlfrom the cleartext value. Store the credential as a secret only for one creation response, hash it for the ingress lookup, and compare the hash forPOST /hooks/wakes/{token}. If the stored lookup stays value-based, add a unique index on(trigger_kind, webhook_token)to prevent duplicate tokens or duplicate keys that can overload the scan path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@migrations/20260809000003_wake_defs.sql` at line 18, Replace the webhook_token column in the wake definition schema with a hashed credential field, and update the wake creation, WAKE_DEF_COLUMNS, item_from_def, and POST /hooks/wakes/{token} lookup flows to hash incoming tokens, return the secret only in the creation response, and compare hashes during ingress without exposing them in normal row-derived URLs. If lookup remains value-based, add a unique index on trigger_kind and the hashed token field.src/wakes/runs.rs-93-101 (1)
93-101: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMake autonomy run starts atomic before changing the schema.
maybe_run_autonomycallshas_active_run()separately frombegin_run(). Two concurrent ticks can observe no running row and both insert another row. The schema has onlyidas primary key, so there is no status-based guard. Move the active-row check intobegin_run()and makehas_active_run()a read-only check, or otherwise make the start conditional in one statement.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/runs.rs` around lines 93 - 101, The autonomy run start must combine the active-run check with insertion atomically. Update begin_run to conditionally insert only when no active run exists, while keeping has_active_run read-only and preserving the existing Result behavior and run ID generation.interface/src/routes/Autonomy.tsx-20-20 (1)
20-20: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not default the ceiling to
actwhile the fleet query loads.
fleetDataisundefinedon first render and after a cache miss. The page then displaysact, the least restrictive value, as the instance-wide ceiling. The ceiling is a safety control, so the placeholder must not overstate the permitted level. A user who reads the page during loading sees a limit that may be wrong, andCeilingCardrendersactas the active selection.Render a loading state, or default to
off.🔧 Proposed fix
- const ceiling = fleetData?.ceiling ?? "act"; + const ceiling = fleetData?.ceiling ?? "off";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@interface/src/routes/Autonomy.tsx` at line 20, Update the ceiling initialization in the Autonomy route to avoid defaulting undefined fleet data to “act”; while fleetData is loading or unavailable, render the page’s loading state or use the safer “off” value so CeilingCard never presents an overstated ceiling.src/api/autonomy.rs-288-291 (1)
288-291: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winStore the new ceiling before you release the config mutex.
drop(config_guard)runs beforestate.autonomy_ceiling.store(...). Two concurrent ceiling updates can therefore serialize their file writes in one order and their in-memory stores in the opposite order. The result is aconfig.tomlvalue that disagrees with the liveArcSwapvalue until the next reload.Keep the store inside the critical section.
🔧 Proposed fix
- drop(config_guard); - state.autonomy_ceiling.store(Arc::new(request.ceiling)); + drop(config_guard); tracing::info!(ceiling = %request.ceiling, "instance autonomy ceiling updated via API");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/api/autonomy.rs` around lines 288 - 291, Move the state.autonomy_ceiling.store(...) call before drop(config_guard) in the ceiling update handler, keeping it within the config mutex critical section so file persistence and the live value remain ordered consistently. Leave the tracing::info! call after releasing the guard.src/agent/ingestion.rs-498-498 (1)
498-498: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRestrict goal mutation tools on the ingestion profile.
process_chunkbuildsBranchToolProfile::MemoryPersistenceand passesdeps.goal_storeintocreate_branch_tool_server. That path registersGoalCreateToolandGoalUpdateTool, which callGoalStore::create/updatedirectly. Keep goal mutation tools off this profile, or wire the profile throughcreate_branch_tool_serverso ingestion can receive a non-mutating goal handle.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/agent/ingestion.rs` at line 498, Update process_chunk’s BranchToolProfile::MemoryPersistence setup and create_branch_tool_server integration so the ingestion profile cannot register or invoke GoalCreateTool and GoalUpdateTool through deps.goal_store. Remove the mutable goal store from this profile or pass a non-mutating goal handle, while preserving any read-only goal functionality.interface/src/components/autonomy/RunHistoryCard.tsx-158-163 (1)
158-163: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd a fallback for unknown action kinds.
ACTION_CONFIGcovers onlyenriched,created, andexecuted. The persistence layer stores the kind as a free-form string (kind: kind.to_string()insrc/wakes/runs.rslines 282-288), so a new or legacy kind reaches this component as a value with no entry.ACTION_CONFIG[action.kind]is thenundefined, and destructuring it throws a TypeError that unmounts the whole run-history card.🔧 Proposed fix
+const FALLBACK_ACTION = { + icon: Lightning, + iconClass: "text-ink-faint", + label: "Action", +}; + function formatTimeAgo(iso: string): string {- } = ACTION_CONFIG[action.kind]; + } = ACTION_CONFIG[action.kind] ?? FALLBACK_ACTION;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@interface/src/components/autonomy/RunHistoryCard.tsx` around lines 158 - 163, Update the action lookup in the run.actions map within RunHistoryCard so unknown action kinds use a safe fallback configuration instead of destructuring undefined. Preserve the existing ACTION_CONFIG behavior for enriched, created, and executed actions, and provide fallback icon, iconClass, and label values that allow the card to render without throwing.src/api/server.rs-340-345 (1)
340-345: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winThe public webhook ingress buffers up to 10 MiB before the 64 KiB check. The route is registered outside the auth middleware and inherits the global
DefaultBodyLimit::max(10 * 1024 * 1024).webhook_ingresscollects the whole body intoaxum::body::Bytesand only then compares againstMAX_WEBHOOK_BODY_BYTES. An unauthenticated caller can therefore force repeated 10 MiB allocations. The single fix is a route-scoped body limit at registration.
src/api/server.rs#L340-L345: chain.layer(DefaultBodyLimit::max(wakes::MAX_WEBHOOK_BODY_BYTES))onto thepost(wakes::webhook_ingress)route, and changeMAX_WEBHOOK_BODY_BYTEStopub(super).src/api/wakes.rs#L424-L432: keep the explicit size check as a defense-in-depth guard, and add a comment noting that the transport-level limit is enforced at the route registration.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/api/server.rs` around lines 340 - 345, The public webhook route in src/api/server.rs lines 340-345 must enforce a route-scoped DefaultBodyLimit capped at wakes::MAX_WEBHOOK_BODY_BYTES; make that constant pub(super) so registration can access it. In src/api/wakes.rs lines 424-432, retain the explicit size check in webhook_ingress and document that the transport-level limit is enforced at route registration.src/api/wakes.rs-436-450 (1)
436-450: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy liftThe unauthenticated token scan amplifies load, and one store error aborts the whole lookup.
Two problems in this loop:
- Amplification. Every request runs one
find_by_webhook_tokenquery per registered agent. An invalid token costs N queries and always walks the full registry. The endpoint is public, so an attacker controls this cost directly. Consider an in-memory token index, or a single instance-wide lookup table keyed by token.- Error handling. If the lookup fails for the first agent, the handler returns
500and never checks the remaining agents, even when the token belongs to a later agent. Log the failure and continue the scan, then return500only if no agent matched and at least one lookup failed.🛡️ Proposed fix for the error handling
+ let mut lookup_failed = false; for deps in registry { let def = match deps.wake_def_store.find_by_webhook_token(&token).await { Ok(Some(def)) => def, Ok(None) => continue, Err(error) => { tracing::warn!(%error, agent_id = %deps.agent_id, "webhook token lookup failed"); - return Err(error_response( - StatusCode::INTERNAL_SERVER_ERROR, - "wake lookup failed", - )); + lookup_failed = true; + continue; } };Then return
500instead of404at the end whenlookup_failedis true.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/api/wakes.rs` around lines 436 - 450, Replace the per-agent token lookup in the webhook handler’s registry loop with a shared in-memory token index or instance-wide lookup keyed by token, so each request performs at most one lookup. For the existing find_by_webhook_token error path, log the error and continue scanning instead of returning immediately; track whether any lookup failed, and return 500 only after the scan finds no match and at least one lookup failed, otherwise preserve the successful match and 404 behavior.src/api/config.rs-821-877 (1)
821-877: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReject invalid autonomy values before applying the API update.
update_autonomy_tableonly checksactive_hours, sointerval_secs: 0accepts the patched key directly.warn_secs >= timeout_secsis clamped rather than rejected assrc/config/types.rsdoes. Apply the same checks here, includinginterval_secs >= 60, thentimeout_secs <= interval_secs, and reject invalidwarn_secsseparately.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/api/config.rs` around lines 821 - 877, Update update_autonomy_table to validate autonomy interval_secs, timeout_secs, and warn_secs before applying any table mutations. Reject interval_secs values below 60, reject timeout_secs values at or below interval_secs, and independently reject warn_secs values at or above timeout_secs, matching the validation rules in src/config/types.rs. Return BAD_REQUEST for each invalid case and avoid writing the patched values when validation fails.src/agent/autonomy.rs-39-62 (1)
39-62: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winMove the autonomy prompt text into
prompts/.
AUTONOMY_CONTRACT_RETRY_PROMPT,HARD_TIMEOUT_PROMPT, and thesoft_warning_textbody are system prompt text injected into the LLM conversation. The coding guidelines require system prompts to live inprompts/as markdown files and be loaded at startup or on demand. The comparable cron and memory-persistence wrap-up prompts already render throughPromptEnginefragments (for examplefragments/system/memory_persistence_contract_retry).Add three templates under
prompts/en/fragments/system/and render them throughPromptEngine, following the existing fragment pattern.soft_warning_textbecomes a render call that passesremaining_minutes.
AUTONOMY_FALLBACK_SUMMARYis a stored summary value, not prompt text, so it can stay in Rust.As per coding guidelines: "Don't store prompts as string constants in Rust. System prompts live in
prompts/as markdown files. Load at startup or on demand."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/agent/autonomy.rs` around lines 39 - 62, Move the system prompt text from AUTONOMY_CONTRACT_RETRY_PROMPT, HARD_TIMEOUT_PROMPT, and soft_warning_text into three markdown fragments under prompts/en/fragments/system/. Update the autonomy prompt injection paths to render these fragments through PromptEngine, passing remaining_minutes to the soft-warning template, following the existing memory_persistence_contract_retry pattern. Keep AUTONOMY_FALLBACK_SUMMARY as the Rust constant.Source: Coding guidelines
src/agent/autonomy.rs-346-369 (1)
346-369: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winReplace
let _ =onResultwith.ok()or explicit handling.Three sites discard a
Resultwithlet _ =:
- Line 346:
channel_tx.send(...)for the soft warning.- Line 359:
channel_tx.send(...)for the hard-timeout wrap-up.- Line 369: awaiting
channel_handleafterabort().The coding guidelines forbid
let _ =onResultand allow only.ok()on channel sends where the receiver may be dropped. Lines 346 and 359 qualify for.ok(). Line 369 is aJoinHandleawait, not a channel send, so handle or log it.A dropped soft-warning send is also worth a log line: it means the channel already exited, which changes how the following timeout is interpreted.
♻️ Proposed change
- let _ = channel_tx - .send(system_message(deps, soft_warning_text(remaining_secs))) - .await; + channel_tx + .send(system_message(deps, soft_warning_text(remaining_secs))) + .await + .ok();- let _ = channel_tx - .send(system_message(deps, HARD_TIMEOUT_PROMPT.to_string())) - .await; + channel_tx + .send(system_message(deps, HARD_TIMEOUT_PROMPT.to_string())) + .await + .ok();Err(_elapsed) => { channel_handle.abort(); - let _ = (&mut channel_handle).await; + if let Err(join_error) = (&mut channel_handle).await + && !join_error.is_cancelled() + { + tracing::warn!( + %join_error, + run_id = %run_id, + "autonomy channel join failed after abort" + ); + } None }As per coding guidelines: "Don't silently discard errors. No
let _ =on Results. Handle them, log them, or propagate them. The only exception is.ok()on channel sends where the receiver may be dropped."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/agent/autonomy.rs` around lines 346 - 369, Replace the soft-warning and hard-timeout channel send `let _ =` statements with `.ok()`, but log when the soft-warning send fails because the receiver has already exited. After `channel_handle.abort()`, explicitly handle the JoinHandle await result by logging or otherwise reporting a join failure instead of discarding it with `let _ =`; leave the existing timeout and wrap-up behavior unchanged.Source: Coding guidelines
src/agent/channel.rs-3520-3539 (1)
3520-3539: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winSpawn the wake-event emission instead of awaiting it in the event loop.
crate::wakes::emit_system_eventperforms a wake-definition lookup and a wake-event insert against SQLite. This code awaits it inline inhandle_event, so the channel event loop blocks on two database round trips for every worker completion. The function returns()and logs its own errors, so nothing downstream consumes its result.Move it into
tokio::spawn.♻️ Proposed change
- crate::wakes::emit_system_event( - &self.deps, - worker_event, - &format!("worker:{worker_id}"), - &serde_json::json!({ - "worker_id": worker_id.to_string(), - "success": *success, - "summary": crate::summarize_first_non_empty_line( - result, - crate::EVENT_SUMMARY_MAX_CHARS, - ), - }), - ) - .await; + let event_deps = self.deps.clone(); + let dedupe_key = format!("worker:{worker_id}"); + let payload = serde_json::json!({ + "worker_id": worker_id.to_string(), + "success": *success, + "summary": crate::summarize_first_non_empty_line( + result, + crate::EVENT_SUMMARY_MAX_CHARS, + ), + }); + tokio::spawn(async move { + crate::wakes::emit_system_event( + &event_deps, + worker_event, + &dedupe_key, + &payload, + ) + .await; + });As per coding guidelines: "Don't block the channel." and "Use
tokio::spawnfor fire-and-forget database writes (conversation history saves, memory writes, worker log persistence) so the user gets their response immediately."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/agent/channel.rs` around lines 3520 - 3539, Update the worker completion handling around emit_system_event to launch the wake-event emission with tokio::spawn instead of awaiting it inline. Preserve the existing dependencies, worker_event, payload, and error-logging behavior while allowing handle_event’s channel loop to continue immediately.Source: Coding guidelines
src/wakes/config.rs-125-146 (1)
125-146: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPreserve the stored webhook token during config reconciliation.
Reconciliation upserts the config-provided
WakeDef, andWakeDefStore::upsertwriteswebhook_token = excluded.webhook_token. The configto_defpath sets this toNone, so every restart writesNULLback to persisted config-owned webhook wakes. Matchupsert_preserves_schedule_cursorwith an equivalentwebhook_tokentest, and keepupsertfrom overwritingwebhook_tokenif reconciliation should only preserve live identity fields.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/config.rs` around lines 125 - 146, Update WakeConfig::to_def to preserve the existing webhook_token during config reconciliation instead of always setting it to None, and adjust WakeDefStore::upsert so reconciliation does not overwrite a stored token with NULL. Add an equivalent test to upsert_preserves_schedule_cursor verifying webhook_token remains unchanged after upsert.src/tools.rs-940-942 (1)
940-942: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not expose goal mutation tools to memory-persistence branches.
src/agent/ingestion.rscreates this server withBranchToolProfile::MemoryPersistencewhile processing file chunks. This unconditional registration lets untrusted ingested content create or update durable user goals. Register goal mutation tools only for user-directed branch profiles.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tools.rs` around lines 940 - 942, Update the tool registration around goal_create, GoalListTool::new, and goal_update so goal mutation tools are excluded when the server uses BranchToolProfile::MemoryPersistence. Register these tools only for user-directed branch profiles, while preserving the existing goal tool behavior for those profiles.src/tools/autonomy_complete.rs-134-148 (1)
134-148: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftMake autonomy completion a terminal lifecycle state.
mark_completed()only affects the channel exit check. The same agent turn can still callSpawnWorkerToolorBranchToolafter this tool returns, while the run is already persisted as completed. Reject new work after completion, or terminate the agent loop immediately after a successful completion call.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tools/autonomy_complete.rs` around lines 134 - 148, The autonomy completion flow around mark_completed must make completion terminal for the current agent turn. Update the lifecycle guard used by SpawnWorkerTool and BranchTool, or terminate the agent loop immediately after complete_run succeeds, so no new work can be started after autonomy_complete returns while preserving the existing completion persistence and warning behavior.src/tools.rs-814-819 (1)
814-819: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHandle tool-removal failures explicitly.
These calls discard
Resultvalues. Preserve an expected missing-tool case explicitly, then log or propagate every other removal failure.As per coding guidelines, “Don't silently discard errors. No
let _ =on Results.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tools.rs` around lines 814 - 819, Update the tool-removal calls in this cleanup block to handle each Result explicitly: tolerate only the expected missing-tool case, while logging or propagating all other failures. Remove the `let _ =` patterns for `CronTool::NAME`, `SendMessageTool::NAME`, `SendAgentMessageTool::NAME`, `AttachmentRecallTool::NAME`, `SetOutcomeTool::NAME`, and `AutonomyCompleteTool::NAME`.Source: Coding guidelines
prompts/en/channel.md.j2-195-199 (1)
195-199: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winTreat
active_goalsas untrusted reference data.Goal text is injected into the system prompt. A goal title or description can contain instructions that affect later conversations. State that active goals provide context only and must not provide executable instructions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@prompts/en/channel.md.j2` around lines 195 - 199, Update the active_goals prompt section to explicitly identify goal titles and descriptions as untrusted reference data. State that they provide background context only and must not be treated as executable instructions or override higher-priority guidance.
🟡 Minor comments (6)
src/wakes/schedule.rs-148-179 (1)
148-179: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAn
enqueuefailure drops the occurrence.The CAS advances the cursor at line 150 before the event is enqueued at line 167. If
enqueuefails, the code logs a warning and returns. The cursor already points at the next occurrence, so this fire is lost until the following occurrence. For a daily schedule that loses a full day.Roll the cursor back to
cursoron enqueue failure, so the next pass retries the same occurrence.🔁 Proposed fix
Err(error) => { tracing::warn!(wake_id = %def.id, %error, "failed to enqueue schedule wake event"); + // Restore the cursor so the next pass retries this occurrence. + if let Err(error) = defs + .claim_schedule_fire(&def.id, Some(&next_text), cursor) + .await + { + tracing::warn!(wake_id = %def.id, %error, "failed to roll back schedule wake cursor"); + } false }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/schedule.rs` around lines 148 - 179, Update the enqueue failure branch in the schedule-fire flow after claim_schedule_fire to roll the schedule cursor back to cursor before returning false. Reuse the existing schedule-definition update mechanism, handle and log any rollback error, and preserve the current enqueue warning and return behavior so the next pass retries the failed occurrence.src/schedule.rs-52-55 (1)
52-55: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winGuard the
u64→i64cast oninterval_secs.
interval_secsis stored asu64, butsrc/schedule.rscasts it toi64beforechrono::Duration::seconds. Values from unbounded API/database intervals abovei64::MAXcan wrap to negative, andchrono::Duration::secondspanics for seconds that would exceed the internal milliseconds range. Keep this path falling back to “no occurrence” for malformed intervals.🛡️ Proposed fix
let interval = self.interval_secs.filter(|secs| *secs > 0)?; - Some(after + chrono::Duration::seconds(interval as i64)) + let duration = chrono::Duration::try_seconds(i64::try_from(interval).ok()?)?; + after.checked_add_signed(duration)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/schedule.rs` around lines 52 - 55, Update the schedule calculation around interval and the chrono::Duration::seconds call to reject any positive interval_secs that cannot be represented safely as i64 or would exceed chrono’s supported duration range, returning None for those malformed values. Preserve the existing zero-interval filtering and normal Some(after + duration) behavior for valid intervals.src/api/goals.rs-250-253 (1)
250-253: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winBoth goal write handlers map every store error to
400 Bad Request.GoalStore::createandGoalStore::updatefail for invalid input and for infrastructure problems such as a database outage. Both handlers collapse those cases into a client error, which breaks client retry logic and hides outages from alerting.
src/api/goals.rs#L250-L253: returnStatusCode::INTERNAL_SERVER_ERRORfor store failures increate_goal, and validate the request fields before the store call.src/api/goals.rs#L302-L305: returnStatusCode::INTERNAL_SERVER_ERRORfor store failures inupdate_goal, and keep400only for rejected status transitions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/api/goals.rs` around lines 250 - 253, In src/api/goals.rs lines 250-253, update create_goal to validate request fields before calling GoalStore::create and map store failures to StatusCode::INTERNAL_SERVER_ERROR. In src/api/goals.rs lines 302-305, update update_goal to map GoalStore::update failures to INTERNAL_SERVER_ERROR while retaining BAD_REQUEST only for rejected status transitions.prompts/en/autonomy_channel.md.j2-44-56 (1)
44-56: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winAdd a fallback branch for an unrecognized level.
The three branches match the literal strings
"observe","suggest", and"act". IfAutonomyLevel::as_str()ever returns a value outside that set — a new level, or a casing change — every branch is skipped. The rendered briefing then contains no level rules at all, while the "Hard rules" section below still tells the agent it may enrich, create, and execute tasks. The failure is silent and removes the restriction text that gates autonomous execution.Add an
{% else %}branch that states the most restrictive behavior.🛡️ Proposed fix
{% elif level == "act" %} Your autonomy level is **act**: the full loop. You may: - **Enrich pending_approval tasks** — investigate and record findings so the user reviews a fully reasoned brief. - **Execute ready tasks** — tasks the user has approved. Use your tools directly; spawn workers for genuine parallelism. - **Create new tasks** — propose follow-on work as `pending_approval` tasks{% if claim_unowned %} (you may also claim unowned tasks){% endif %}. +{% else %} +Your autonomy level is unrecognized. Treat this run as **observe**: survey and summarize only. Do not mutate anything. {% endif %}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@prompts/en/autonomy_channel.md.j2` around lines 44 - 56, Add an `{% else %}` fallback after the `level == "act"` branch in the autonomy-level template, using the same restrictive wording and behavior as the `"observe"` branch so unrecognized levels cannot enable autonomous actions. Keep the existing branches unchanged.src/wakes/config.rs-153-191 (1)
153-191: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winReconciliation aborts mid-loop, which contradicts the documented per-row tolerance.
The doc comment states this function has "the same tolerance cron config seeding has for per-row failures". The code does not match.
store.upsert(...).await?at line 181 andstore.delete(...).await?at line 186 propagate the first error and return.A single failing upsert therefore skips every remaining upsert and the whole delete pass. The caller in
src/main.rslogs a warning and continues startup. The agent then runs with a partially reconciled wake set: wakes removed from config keep firing, and later config entries are missing. Nothing retries until the next restart.Log and continue per row, and return an error only if the caller must know that reconciliation was incomplete.
♻️ Proposed change
- store.upsert(&config.to_def(trigger)).await?; + if let Err(error) = store.upsert(&config.to_def(trigger)).await { + tracing::warn!(wake_id = %config.id, %error, "failed to upsert config wake, skipping"); + } } for def in &existing { if def.config_owned && !def.builtin && !config_ids.contains(def.id.as_str()) { - store.delete(&def.id).await?; + if let Err(error) = store.delete(&def.id).await { + tracing::warn!(wake_id = %def.id, %error, "failed to delete removed config wake"); + } } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/config.rs` around lines 153 - 191, Update reconcile_config_wakes so per-row store.upsert and store.delete failures are logged with the wake ID and processing continues through all remaining configs and stale definitions. Track whether any row failed, then return an error after both reconciliation passes only when reconciliation was incomplete, preserving successful rows and existing validation/collision behavior.src/tools/autonomy_complete.rs-105-110 (1)
105-110: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winEnforce the documented summary bounds.
The tool accepts a one-line summary and any number of lines. The completion contract requires a 2-5 line summary for run continuity. Validate the count of non-empty lines before persisting it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tools/autonomy_complete.rs` around lines 105 - 110, Update summary validation in the autonomy completion flow to count non-empty lines after trimming, and reject summaries with fewer than 2 or more than 5 such lines before persistence. Preserve the existing minimum character-length validation and error behavior while enforcing the documented 2–5 line contract.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2d0461cd-3119-4125-9e13-f07a9ef74be5
📒 Files selected for processing (81)
docs/design-docs/autonomy.mddocs/design-docs/bundled-skills.mddocs/design-docs/dormancy.mddocs/design-docs/durable-transcript.mddocs/design-docs/import-tool.mddocs/design-docs/prompt-stability.mddocs/design-docs/wakes.mdinterface/src/api/client.tsinterface/src/api/schema.d.tsinterface/src/components/Sidebar.tsxinterface/src/components/autonomy/ApprovalQueueCard.tsxinterface/src/components/autonomy/AutonomyDialCard.tsxinterface/src/components/autonomy/CeilingCard.tsxinterface/src/components/autonomy/FleetCard.tsxinterface/src/components/autonomy/GoalsCard.tsxinterface/src/components/autonomy/RunHistoryCard.tsxinterface/src/components/autonomy/WakesCard.tsxinterface/src/components/autonomy/index.tsinterface/src/components/autonomy/levels.tsxinterface/src/router.tsxinterface/src/routes/AgentAutonomy.tsxinterface/src/routes/Autonomy.tsxmigrations/20260809000001_wake_events.sqlmigrations/20260809000002_autonomy_runs.sqlmigrations/20260809000003_wake_defs.sqlmigrations/global/20260809000001_tasks_nullable_assignment.sqlmigrations/global/20260809000002_goals.sqlprompts/en/autonomy_channel.md.j2prompts/en/channel.md.j2prompts/en/tools/autonomy_complete_description.md.j2prompts/en/tools/goal_create_description.md.j2prompts/en/tools/goal_list_description.md.j2prompts/en/tools/goal_update_description.md.j2src/agent.rssrc/agent/autonomy.rssrc/agent/channel.rssrc/agent/channel_dispatch.rssrc/agent/cortex.rssrc/agent/ingestion.rssrc/api.rssrc/api/agents.rssrc/api/autonomy.rssrc/api/channels.rssrc/api/config.rssrc/api/goals.rssrc/api/server.rssrc/api/state.rssrc/api/tasks.rssrc/api/wakes.rssrc/cli/task.rssrc/config/load.rssrc/config/runtime.rssrc/config/toml_schema.rssrc/config/types.rssrc/cron/scheduler.rssrc/goals.rssrc/goals/store.rssrc/lib.rssrc/main.rssrc/prompts/engine.rssrc/prompts/text.rssrc/schedule.rssrc/tasks/store.rssrc/tools.rssrc/tools/autonomy_complete.rssrc/tools/goal_create.rssrc/tools/goal_list.rssrc/tools/goal_update.rssrc/tools/send_agent_message.rssrc/tools/task_create.rssrc/tools/task_update.rssrc/wakes/config.rssrc/wakes/defs.rssrc/wakes/emit.rssrc/wakes/events.rssrc/wakes/mod.rssrc/wakes/runs.rssrc/wakes/schedule.rssrc/wakes/store.rstests/bulletin.rstests/context_dump.rs
👮 Files not reviewed due to content moderation or server errors (10)
- docs/design-docs/bundled-skills.md
- docs/design-docs/dormancy.md
- docs/design-docs/prompt-stability.md
- docs/design-docs/wakes.md
- interface/src/api/client.ts
- interface/src/api/schema.d.ts
- src/config/load.rs
- src/config/runtime.rs
- src/config/toml_schema.rs
- src/config/types.rs
- wake_defs upsert no longer nulls a minted webhook token; reconcile tolerates per-row failures like its doc claims - SystemEvent serde names now match as_str (dotted form) - schedule producer rolls the cursor back when enqueue fails; interval cast guarded against overflow - goal mutation tools gated to default branches only — ingestion and memory-persistence branches get read-only goal_list - autonomy run prompts moved to prompts/ fragments (also registers the memory_persistence_contract_retry fragment, which had never been wired and always fell back to the Rust constant) - worker wake emission moved off the channel event loop; let _ = Results replaced with logged handling - webhook ingress gets a route-scoped body limit; token scan survives per-agent store errors - autonomy config API validates merged values; goal API returns 500 for store failures instead of 400 - ceiling stored inside the config mutex; UI no longer shows act while the fleet query loads; RunHistoryCard tolerates unknown action kinds - src/wakes/mod.rs -> src/wakes.rs per module conventions; test fixtures use the instance pool for instance-level stores and allow unassigned tasks
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/wakes/defs.rs (2)
138-158: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReset the schedule cursor when the schedule definition changes.
upsertpreservesnext_run_atfor every conflict. If hot reload changestrigger_spec, the old cursor can be due at a time that does not match the new cron expression or interval.fire_schedule_wakethen fires using that stale cursor before it calculates the following occurrence.Clear
next_run_atwhen the trigger kind or trigger specification changes. Add a regression test that changes a persisted schedule and verifies that the next pass initializes the new schedule.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/defs.rs` around lines 138 - 158, Update WakeDefStore::upsert so an existing row clears next_run_at when trigger_kind or trigger_spec changes, while preserving it for unchanged schedules. Add a regression test covering a persisted schedule update and verify that the next fire_schedule_wake pass initializes the new schedule before firing.
234-242: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPrevent deletion of built-in wakes.
WakeDefdocuments that built-in wakes can be tuned or disabled but not deleted. This query deletesTASK_APPROVED_WAKE_IDlike any other row. Approved tasks then stop causing an immediate wake until the next restart reseeds the definition.Reject deletion when
builtin = 1. Add a test for the built-in wake.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/defs.rs` around lines 234 - 242, Update WakeDef::delete to reject rows marked builtin = 1 before executing the DELETE, while preserving deletion for user-defined wakes and the existing Result<bool> behavior. Add a test covering an attempted deletion of a built-in wake and verify the wake remains present.prompts/en/autonomy_channel.md.j2 (1)
3-11: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftTreat wake payloads as untrusted data.
Webhook ingress accepts arbitrary JSON, and line 7 inserts the payload into an autonomy run prompt. A sender with a valid webhook token can place instructions in that payload. In
actmode, the model can treat those instructions as authority and invoke tools.Render payloads as clearly delimited untrusted data. State that instructions in payloads must not be followed. Keep tool authorization independent from prompt content.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@prompts/en/autonomy_channel.md.j2` around lines 3 - 11, Update the wake-event rendering in the autonomy prompt template to clearly delimit each event payload as untrusted data and explicitly instruct the model not to follow commands contained within it. Preserve payload visibility for reasoning, while ensuring tool authorization remains determined by the autonomy level and existing controls rather than prompt-provided content.
🧹 Nitpick comments (1)
src/wakes/defs.rs (1)
138-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse full variable names for wake definitions.
src/wakes/defs.rs#L138-L138: Renamedeftowake_definition.src/wakes/schedule.rs#L60-L65: Renamedeftowake_definition.As per coding guidelines, “Don't abbreviate variable names.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/wakes/defs.rs` at line 138, Rename the abbreviated def parameter and all its references to wake_definition in src/wakes/defs.rs lines 138-138, including the upsert method. Apply the same rename in src/wakes/schedule.rs lines 60-65, updating every use consistently.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@prompts/en/autonomy_channel.md.j2`:
- Around line 3-11: Update the wake-event rendering in the autonomy prompt
template to clearly delimit each event payload as untrusted data and explicitly
instruct the model not to follow commands contained within it. Preserve payload
visibility for reasoning, while ensuring tool authorization remains determined
by the autonomy level and existing controls rather than prompt-provided content.
In `@src/wakes/defs.rs`:
- Around line 138-158: Update WakeDefStore::upsert so an existing row clears
next_run_at when trigger_kind or trigger_spec changes, while preserving it for
unchanged schedules. Add a regression test covering a persisted schedule update
and verify that the next fire_schedule_wake pass initializes the new schedule
before firing.
- Around line 234-242: Update WakeDef::delete to reject rows marked builtin = 1
before executing the DELETE, while preserving deletion for user-defined wakes
and the existing Result<bool> behavior. Add a test covering an attempted
deletion of a built-in wake and verify the wake remains present.
---
Nitpick comments:
In `@src/wakes/defs.rs`:
- Line 138: Rename the abbreviated def parameter and all its references to
wake_definition in src/wakes/defs.rs lines 138-138, including the upsert method.
Apply the same rename in src/wakes/schedule.rs lines 60-65, updating every use
consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 465561b9-fb71-4441-a98d-193c072a9599
📒 Files selected for processing (29)
interface/src/components/autonomy/CeilingCard.tsxinterface/src/components/autonomy/FleetCard.tsxinterface/src/components/autonomy/RunHistoryCard.tsxinterface/src/components/autonomy/levels.tsxinterface/src/routes/Autonomy.tsxprompts/en/autonomy_channel.md.j2prompts/en/channel.md.j2prompts/en/fragments/system/autonomy_contract_retry.md.j2prompts/en/fragments/system/autonomy_hard_timeout.md.j2prompts/en/fragments/system/autonomy_soft_warning.md.j2src/agent/autonomy.rssrc/agent/channel.rssrc/api/autonomy.rssrc/api/config.rssrc/api/goals.rssrc/api/server.rssrc/api/wakes.rssrc/prompts/engine.rssrc/prompts/text.rssrc/schedule.rssrc/tasks/store.rssrc/tools.rssrc/wakes.rssrc/wakes/config.rssrc/wakes/defs.rssrc/wakes/events.rssrc/wakes/schedule.rstests/bulletin.rstests/context_dump.rs
🚧 Files skipped from review as they are similar to previous changes (13)
- src/api/server.rs
- interface/src/components/autonomy/CeilingCard.tsx
- src/prompts/text.rs
- src/schedule.rs
- prompts/en/channel.md.j2
- interface/src/routes/Autonomy.tsx
- src/api/wakes.rs
- interface/src/components/autonomy/levels.tsx
- src/api/autonomy.rs
- src/agent/autonomy.rs
- src/tasks/store.rs
- src/agent/channel.rs
- src/wakes/config.rs
…eding clippy (-Dwarnings) rejected the nested if in reconcile_config_wakes; folded into a let-chain with identical behavior. reload_skills seeded usage rows in a detached task, so a seed captured from a stale skill snapshot could land after a user delete removed the row and resurrect it — the flake in agent_delete_archives_and_user_delete_removes. Seeding is now awaited inside reload_skills, which every mutation path calls after its store write, making the order deterministic.
Builds the autonomy headline: a dial for how active each agent is, backed by a real autonomy loop in the core.
Panel
/autonomy— fleet view: per-agent dial status, live run indicators, merged approval queue and run history with agent attribution/agents/:id/autonomy— the per-agent screen where the dial writes config (optimistic update, TOML persistence, hot reload)pending_approvaltasks (approve hits the approve endpoint, dismiss moves to backlog so enrichment survives); goals and run history read their storesCore
The dial:
ChannelKind::Autonomywith no reply tool, and exit throughautonomy_completeunder a retry-enforced completion contract. Observe surveys, suggest enriches and proposes, act executes.wake_eventstable with SQL coalescing (partial unique index + upsert bumpsdelivery_count) and CAS-guarded consumption; closedSystemEventvocabulary so unknown event names fail at config load. Producers land in phase 2.goal_create/goal_update/goal_list(channels read-only), compact injection into every channel prompt,/goalsAPI,goal_idlinking on tasks.ChannelKind {User, Cron, Autonomy}replacing thecron_outcome.is_some()discriminators,TaskStatus::Failed, nullableassigned_agent_id.Behavior change
Ready-task pickup is now gated on
level = act, and the default isoff: agents no longer auto-execute approved or link-delegated tasks until dialed up. Existing installs that rely on auto-execution needlevel = "act".Design docs
wakes.md— the trigger model (schedule / webhook / event / condition), grounded against the existing cron, wake-substrate, and webhook machinery. Plus the trilogy wakes unlocks:dormancy.md,durable-transcript.md,prompt-stability.md.autonomy.mdandgoals.mdrevised to match what actually got built.Testing
Lib suite went 962 → 987: config validation matrix, run store CAS + stale-run reaping, the pure due-decision function (active hours incl. midnight wrap, wake-event pull-forward), act-only pickup gating, queue coalescing/claiming, goals transitions.
Phase 2 — wakes go live
wake_defsper-agent table withWakeDefStore;[[agents.wakes]]config entries validated at load (exactly one trigger, event names against the closed enum, cron expressions through the shared parser) and reconciled as a seed with the DB as source of truth, cron-style. Built-intask-approvedwake seeded per agent. Run briefings now render each wake's instructions, and events below the current level show as observations only.emit_system_event(subscriber lookup, coalescing enqueue, doorbell ring). The approve/execute/update task endpoints switched toupdate_with_status_transition, so thepending_approval -> readyedge finally firestask.approvedinto the owning agent's queue — approval latency drops from the interval to the next doorbell. Goal create/update and worker completion emit too.ScheduleSpeclayer insrc/schedule.rs(single definition of the 5-field cron expansion; the cron scheduler now composes the same primitives). Producer rides the cortex tick with CAS-claimed cursors: startup initializes without firing, out-of-window occurrences skip like cron, stale cursors fast-forward.POST /hooks/wakes/{token}outside the auth middleware; the per-wake token is the authority boundary. Payload stored as data, never interpreted. Bursts coalesce into one pending event with a delivery count. Tokens minted lazily, CAS-guarded.interval-surveyrow derived from the dial config), tune, manual test-fire, delete; WakesCard is real now (toggles persist, webhook URLs copy on click), mock deleted.[autonomy] ceilingin config.toml, oneArcSwapshared by the API and every agent: effective level = min(ceiling, dial) at both the run gate and ready-task pickup. Fleet API reportsceiling+ per-agenteffective_level; CeilingCard writes persist.Lib suite now 1027. Remaining from the wakes doc: conditions, debounce/rearm, the persisted circuit breaker, and creator authority re-resolution (phases 3-4).