Skip to content

Autonomy: level dial, goals, wakes, and the autonomy channel - #631

Merged
jamiepine merged 18 commits into
mainfrom
jamiepine/autonomy-panel
Aug 10, 2026
Merged

Autonomy: level dial, goals, wakes, and the autonomy channel#631
jamiepine merged 18 commits into
mainfrom
jamiepine/autonomy-panel

Conversation

@jamiepine

@jamiepine jamiepine commented Aug 9, 2026

Copy link
Copy Markdown
Member

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)
  • The approval queue is real pending_approval tasks (approve hits the approve endpoint, dismiss moves to backlog so enrichment survives); goals and run history read their stores
  • The wakes card and fleet ceiling are labeled design previews — phase 2

Core

The dial:

[agents.main.autonomy]
level = "suggest"        # off | observe | suggest | act
interval_secs = 1800
active_hours = [8, 22]
max_tasks_per_run = 2
  • Autonomy channel — the cortex tick starts a run when the interval elapses or wake events are pending. Runs consume the wake queue at start (crash-safe provenance), brief from tasks + goals + prior run summaries, execute as ChannelKind::Autonomy with no reply tool, and exit through autonomy_complete under a retry-enforced completion contract. Observe surveys, suggest enriches and proposes, act executes.
  • Wake queue — per-agent wake_events table with SQL coalescing (partial unique index + upsert bumps delivery_count) and CAS-guarded consumption; closed SystemEvent vocabulary so unknown event names fail at config load. Producers land in phase 2.
  • Goals — instance-scoped objectives with goal_create/goal_update/goal_list (channels read-only), compact injection into every channel prompt, /goals API, goal_id linking on tasks.
  • Precursors — explicit ChannelKind {User, Cron, Autonomy} replacing the cron_outcome.is_some() discriminators, TaskStatus::Failed, nullable assigned_agent_id.

Behavior change

Ready-task pickup is now gated on level = act, and the default is off: agents no longer auto-execute approved or link-delegated tasks until dialed up. Existing installs that rely on auto-execution need level = "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.md and goals.md revised 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 definitionswake_defs per-agent table with WakeDefStore; [[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-in task-approved wake seeded per agent. Run briefings now render each wake's instructions, and events below the current level show as observations only.
[[agents.wakes]]
id = "morning-brief"
name = "Morning brief"
schedule = "0 8 * * *"
instructions = "Summarize overnight activity and what needs attention."
min_level = "observe"
  • Producersemit_system_event (subscriber lookup, coalescing enqueue, doorbell ring). The approve/execute/update task endpoints switched to update_with_status_transition, so the pending_approval -> ready edge finally fires task.approved into the owning agent's queue — approval latency drops from the interval to the next doorbell. Goal create/update and worker completion emit too.
  • Schedule wakes — shared ScheduleSpec layer in src/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.
  • Webhook ingressPOST /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.
  • Wakes API + panel — list (with a virtual interval-survey row derived from the dial config), tune, manual test-fire, delete; WakesCard is real now (toggles persist, webhook URLs copy on click), mock deleted.
  • Instance ceiling — top-level [autonomy] ceiling in config.toml, one ArcSwap shared by the API and every agent: effective level = min(ceiling, dial) at both the run gate and ready-task pickup. Fleet API reports ceiling + per-agent effective_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).

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.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: da82a3f5-60dc-4cd9-8f32-02b62981ff97

📥 Commits

Reviewing files that changed from the base of the PR and between f8da371 and c9e5492.

📒 Files selected for processing (6)
  • src/api/skills.rs
  • src/config/runtime.rs
  • src/config/watcher.rs
  • src/tools/install_skill.rs
  • src/tools/skill_manage.rs
  • src/wakes/config.rs

Walkthrough

This 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.

Changes

Autonomy platform

Layer / File(s) Summary
Design specifications
docs/design-docs/*
Adds specifications for autonomy, wakes, dormancy, durable transcripts, prompt stability, importing, and bundled skills.
Contracts and configuration
src/config/*, interface/src/api/*, src/schedule.rs, src/cron/scheduler.rs
Adds autonomy levels, scheduling, limits, wake configuration, goal and run schemas, nullable task assignment, and shared schedule handling.
Persistence and runtime
migrations/*, src/goals/*, src/wakes/*, src/tasks/store.rs, src/agent/autonomy.rs, src/agent/channel.rs
Persists goals, wakes, wake events, and autonomy runs. Schedules runs, consumes wake events, renders briefings, enforces timeouts, and requires autonomy_complete.
APIs and tools
src/api/*, src/tools/*
Adds autonomy, goal, and wake endpoints, webhook ingress, goal tools, autonomy completion, and effective-agent task notifications.
Interface and wiring
interface/src/components/autonomy/*, interface/src/routes/*, interface/src/router.tsx, src/main.rs, src/lib.rs
Adds autonomy navigation and dashboards, wires shared stores and runtime dependencies, and updates test fixtures and prompt context.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the autonomy dial, goals, wakes, and autonomy channel added by the changeset.
Description check ✅ Passed The description directly explains the autonomy system, interface panels, core behavior, APIs, persistence, testing, and deferred phases.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jamiepine/autonomy-panel

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

jamiepine and others added 4 commits August 9, 2026 16:27
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.
@jamiepine
jamiepine marked this pull request as ready for review August 10, 2026 00:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

The test schema still declares assigned_agent_id TEXT NOT NULL.

migrations/global/20260809000001_tasks_nullable_assignment.sql makes assigned_agent_id nullable, and Task::assigned_agent_id is now Option<String>. The in-memory test schema in setup_test_store keeps the NOT NULL constraint. 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, autonomy claim_unowned) stay untested. An insert with assigned_agent_id = NULL fails 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 lift

Preserve an explicit unassignment value.

UpdateTaskRequest.assigned_agent_id uses Option<String>, so the client cannot send an explicit null to clear the existing assignee without also being ambiguous when the field is omitted. A tri-state assigned_agent_id value is needed through UpdateTaskRequest, 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 win

Move this module root out of mod.rs.

The coding guidelines forbid mod.rs files. Move this file to src/wakes.rs and keep the contents unchanged. src/goals.rs in this same PR already follows that pattern.

git mv src/wakes/mod.rs src/wakes.rs

As per coding guidelines: "Don't use mod.rs files. Use src/memory.rs as the module root, not src/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 win

Use the instance pool for GoalStore in these tests.

Db::connect only applies ./migrations to db.sqlite, so the per-agent pool does not define goals. GoalStore queries the goals table from global migrations, while the wake stores are on per-agent tables. Initialize db.instance/connect_instance_db for these tests and pass that pool to goals::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 win

Align SystemEvent serde values with as_str.

#[serde(rename_all = "snake_case")] makes TaskApproved serialize to "task_approved", while as_str, parse, WakeTrigger::spec, WakeDef serialization, and TOML wake config all use "task.approved". This causes a JSON round-trip via WakeDef/WakeTrigger to 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 win

Preserve webhook_token during wake definition updates.

reconcile_config_wakes builds WakeDef values with webhook_token: None and then calls store.upsert, so DO UPDATE SET webhook_token = excluded.webhook_token overwrites an existing token with NULL. 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 win

Store a hashed webhook credential instead of webhook_token.

webhook_token is a public bearer credential stored directly in wake_defs. Database reads expose it because WAKE_DEF_COLUMNS selects it from rows, and item_from_def exposes it by building webhook_url from the cleartext value. Store the credential as a secret only for one creation response, hash it for the ingress lookup, and compare the hash for POST /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 win

Make autonomy run starts atomic before changing the schema.

maybe_run_autonomy calls has_active_run() separately from begin_run(). Two concurrent ticks can observe no running row and both insert another row. The schema has only id as primary key, so there is no status-based guard. Move the active-row check into begin_run() and make has_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 win

Do not default the ceiling to act while the fleet query loads.

fleetData is undefined on first render and after a cache miss. The page then displays act, 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, and CeilingCard renders act as 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 win

Store the new ceiling before you release the config mutex.

drop(config_guard) runs before state.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 a config.toml value that disagrees with the live ArcSwap value 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 win

Restrict goal mutation tools on the ingestion profile.

process_chunk builds BranchToolProfile::MemoryPersistence and passes deps.goal_store into create_branch_tool_server. That path registers GoalCreateTool and GoalUpdateTool, which call GoalStore::create/update directly. Keep goal mutation tools off this profile, or wire the profile through create_branch_tool_server so 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 win

Add a fallback for unknown action kinds.

ACTION_CONFIG covers only enriched, created, and executed. The persistence layer stores the kind as a free-form string (kind: kind.to_string() in src/wakes/runs.rs lines 282-288), so a new or legacy kind reaches this component as a value with no entry. ACTION_CONFIG[action.kind] is then undefined, 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 win

The 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_ingress collects the whole body into axum::body::Bytes and only then compares against MAX_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 the post(wakes::webhook_ingress) route, and change MAX_WEBHOOK_BODY_BYTES to pub(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 lift

The unauthenticated token scan amplifies load, and one store error aborts the whole lookup.

Two problems in this loop:

  1. Amplification. Every request runs one find_by_webhook_token query 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.
  2. Error handling. If the lookup fails for the first agent, the handler returns 500 and never checks the remaining agents, even when the token belongs to a later agent. Log the failure and continue the scan, then return 500 only 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 500 instead of 404 at the end when lookup_failed is 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 win

Reject invalid autonomy values before applying the API update.

update_autonomy_table only checks active_hours, so interval_secs: 0 accepts the patched key directly. warn_secs >= timeout_secs is clamped rather than rejected as src/config/types.rs does. Apply the same checks here, including interval_secs >= 60, then timeout_secs <= interval_secs, and reject invalid warn_secs separately.

🤖 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 win

Move the autonomy prompt text into prompts/.

AUTONOMY_CONTRACT_RETRY_PROMPT, HARD_TIMEOUT_PROMPT, and the soft_warning_text body are system prompt text injected into the LLM conversation. The coding guidelines require system prompts to live in prompts/ as markdown files and be loaded at startup or on demand. The comparable cron and memory-persistence wrap-up prompts already render through PromptEngine fragments (for example fragments/system/memory_persistence_contract_retry).

Add three templates under prompts/en/fragments/system/ and render them through PromptEngine, following the existing fragment pattern. soft_warning_text becomes a render call that passes remaining_minutes.

AUTONOMY_FALLBACK_SUMMARY is 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 win

Replace let _ = on Result with .ok() or explicit handling.

Three sites discard a Result with let _ =:

  • Line 346: channel_tx.send(...) for the soft warning.
  • Line 359: channel_tx.send(...) for the hard-timeout wrap-up.
  • Line 369: awaiting channel_handle after abort().

The coding guidelines forbid let _ = on Result and allow only .ok() on channel sends where the receiver may be dropped. Lines 346 and 359 qualify for .ok(). Line 369 is a JoinHandle await, 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 win

Spawn the wake-event emission instead of awaiting it in the event loop.

crate::wakes::emit_system_event performs a wake-definition lookup and a wake-event insert against SQLite. This code awaits it inline in handle_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::spawn for 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 win

Preserve the stored webhook token during config reconciliation.

Reconciliation upserts the config-provided WakeDef, and WakeDefStore::upsert writes webhook_token = excluded.webhook_token. The config to_def path sets this to None, so every restart writes NULL back to persisted config-owned webhook wakes. Match upsert_preserves_schedule_cursor with an equivalent webhook_token test, and keep upsert from overwriting webhook_token if 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 win

Do not expose goal mutation tools to memory-persistence branches.

src/agent/ingestion.rs creates this server with BranchToolProfile::MemoryPersistence while 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 lift

Make autonomy completion a terminal lifecycle state.

mark_completed() only affects the channel exit check. The same agent turn can still call SpawnWorkerTool or BranchTool after 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 win

Handle tool-removal failures explicitly.

These calls discard Result values. 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 win

Treat active_goals as 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 win

An enqueue failure drops the occurrence.

The CAS advances the cursor at line 150 before the event is enqueued at line 167. If enqueue fails, 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 cursor on 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 win

Guard the u64i64 cast on interval_secs.

interval_secs is stored as u64, but src/schedule.rs casts it to i64 before chrono::Duration::seconds. Values from unbounded API/database intervals above i64::MAX can wrap to negative, and chrono::Duration::seconds panics 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 win

Both goal write handlers map every store error to 400 Bad Request. GoalStore::create and GoalStore::update fail 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: return StatusCode::INTERNAL_SERVER_ERROR for store failures in create_goal, and validate the request fields before the store call.
  • src/api/goals.rs#L302-L305: return StatusCode::INTERNAL_SERVER_ERROR for store failures in update_goal, and keep 400 only 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 win

Add a fallback branch for an unrecognized level.

The three branches match the literal strings "observe", "suggest", and "act". If AutonomyLevel::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 win

Reconciliation 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 and store.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.rs logs 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 win

Enforce 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

📥 Commits

Reviewing files that changed from the base of the PR and between 04e048e and 3844c11.

📒 Files selected for processing (81)
  • docs/design-docs/autonomy.md
  • docs/design-docs/bundled-skills.md
  • docs/design-docs/dormancy.md
  • docs/design-docs/durable-transcript.md
  • docs/design-docs/import-tool.md
  • docs/design-docs/prompt-stability.md
  • docs/design-docs/wakes.md
  • interface/src/api/client.ts
  • interface/src/api/schema.d.ts
  • interface/src/components/Sidebar.tsx
  • interface/src/components/autonomy/ApprovalQueueCard.tsx
  • interface/src/components/autonomy/AutonomyDialCard.tsx
  • interface/src/components/autonomy/CeilingCard.tsx
  • interface/src/components/autonomy/FleetCard.tsx
  • interface/src/components/autonomy/GoalsCard.tsx
  • interface/src/components/autonomy/RunHistoryCard.tsx
  • interface/src/components/autonomy/WakesCard.tsx
  • interface/src/components/autonomy/index.ts
  • interface/src/components/autonomy/levels.tsx
  • interface/src/router.tsx
  • interface/src/routes/AgentAutonomy.tsx
  • interface/src/routes/Autonomy.tsx
  • migrations/20260809000001_wake_events.sql
  • migrations/20260809000002_autonomy_runs.sql
  • migrations/20260809000003_wake_defs.sql
  • migrations/global/20260809000001_tasks_nullable_assignment.sql
  • migrations/global/20260809000002_goals.sql
  • prompts/en/autonomy_channel.md.j2
  • prompts/en/channel.md.j2
  • prompts/en/tools/autonomy_complete_description.md.j2
  • prompts/en/tools/goal_create_description.md.j2
  • prompts/en/tools/goal_list_description.md.j2
  • prompts/en/tools/goal_update_description.md.j2
  • src/agent.rs
  • src/agent/autonomy.rs
  • src/agent/channel.rs
  • src/agent/channel_dispatch.rs
  • src/agent/cortex.rs
  • src/agent/ingestion.rs
  • src/api.rs
  • src/api/agents.rs
  • src/api/autonomy.rs
  • src/api/channels.rs
  • src/api/config.rs
  • src/api/goals.rs
  • src/api/server.rs
  • src/api/state.rs
  • src/api/tasks.rs
  • src/api/wakes.rs
  • src/cli/task.rs
  • src/config/load.rs
  • src/config/runtime.rs
  • src/config/toml_schema.rs
  • src/config/types.rs
  • src/cron/scheduler.rs
  • src/goals.rs
  • src/goals/store.rs
  • src/lib.rs
  • src/main.rs
  • src/prompts/engine.rs
  • src/prompts/text.rs
  • src/schedule.rs
  • src/tasks/store.rs
  • src/tools.rs
  • src/tools/autonomy_complete.rs
  • src/tools/goal_create.rs
  • src/tools/goal_list.rs
  • src/tools/goal_update.rs
  • src/tools/send_agent_message.rs
  • src/tools/task_create.rs
  • src/tools/task_update.rs
  • src/wakes/config.rs
  • src/wakes/defs.rs
  • src/wakes/emit.rs
  • src/wakes/events.rs
  • src/wakes/mod.rs
  • src/wakes/runs.rs
  • src/wakes/schedule.rs
  • src/wakes/store.rs
  • tests/bulletin.rs
  • tests/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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Reset the schedule cursor when the schedule definition changes.

upsert preserves next_run_at for every conflict. If hot reload changes trigger_spec, the old cursor can be due at a time that does not match the new cron expression or interval. fire_schedule_wake then fires using that stale cursor before it calculates the following occurrence.

Clear next_run_at when 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 win

Prevent deletion of built-in wakes.

WakeDef documents that built-in wakes can be tuned or disabled but not deleted. This query deletes TASK_APPROVED_WAKE_ID like 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 lift

Treat 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 act mode, 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 win

Use full variable names for wake definitions.

  • src/wakes/defs.rs#L138-L138: Rename def to wake_definition.
  • src/wakes/schedule.rs#L60-L65: Rename def to wake_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

📥 Commits

Reviewing files that changed from the base of the PR and between 3844c11 and f8da371.

📒 Files selected for processing (29)
  • interface/src/components/autonomy/CeilingCard.tsx
  • interface/src/components/autonomy/FleetCard.tsx
  • interface/src/components/autonomy/RunHistoryCard.tsx
  • interface/src/components/autonomy/levels.tsx
  • interface/src/routes/Autonomy.tsx
  • prompts/en/autonomy_channel.md.j2
  • prompts/en/channel.md.j2
  • prompts/en/fragments/system/autonomy_contract_retry.md.j2
  • prompts/en/fragments/system/autonomy_hard_timeout.md.j2
  • prompts/en/fragments/system/autonomy_soft_warning.md.j2
  • src/agent/autonomy.rs
  • src/agent/channel.rs
  • src/api/autonomy.rs
  • src/api/config.rs
  • src/api/goals.rs
  • src/api/server.rs
  • src/api/wakes.rs
  • src/prompts/engine.rs
  • src/prompts/text.rs
  • src/schedule.rs
  • src/tasks/store.rs
  • src/tools.rs
  • src/wakes.rs
  • src/wakes/config.rs
  • src/wakes/defs.rs
  • src/wakes/events.rs
  • src/wakes/schedule.rs
  • tests/bulletin.rs
  • tests/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.
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