Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/SELF-IMPROVING-LOOPS-CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ This is the durable contract for TraceDecay-owned self-improvement loops. Hermes
| Kiro | Config, ledgers, managed-agent prompt-index content, MCP skill body serving | Managed-agent file ownership and host execution | Existing managed-agent path with prompt index plus `tracedecay_skill_view` |
| Prompt-only agents | Config, ledgers, prompt-index generation, MCP skill body serving | Prompt ingestion and execution | Compact prompt index plus `tracedecay_skill_view` |

## Cadence And Automation Defaults

Hermes is the reference behavior for self-improvement cadence, but TraceDecay owns its own scheduler in standalone mode. Hermes memory review and skill review are turn/iteration nudges: memory defaults to every 10 user turns when memory is enabled, skill review defaults to every 10 tool-calling iterations, and both run as a whitelisted background review fork after the foreground response. Hermes skill-library curator is separate: it runs after `curator.interval_hours` elapses, defaults to 168 hours, requires the idle gate (`curator.min_idle_hours`, default 2 hours), seeds the first run instead of mutating immediately, snapshots before real runs, and archives rather than deletes.

TraceDecay standalone automation is time-scheduled by the daemon, not by Codex native automations or Hermes cron. The default scheduler tick is 60 seconds. `tracedecay install --agent codex --automation` enables the Codex app-server backend with `require_dashboard_approval=false`, `auto_apply_memory_ops=true`, `auto_enable_skills=false`, and these task cadences:

| Task | Default cadence | Default mutation behavior |
| --- | --- | --- |
| `memory_curator` | Every 15 minutes, with a 5-minute cooldown | Validated accepted curation ops auto-apply when `auto_apply_memory_ops=true` and dashboard approval is disabled. |
| `session_reflector` | Every 15 minutes, with a 5-minute cooldown | Validated accepted session facts auto-apply under the same memory auto-apply policy; otherwise they stay as dashboard fact proposals. |
| `skill_writer` | Every 60 minutes, after a 15-minute idle window, with a 5-minute cooldown | Creates or updates managed skill drafts; skills are not auto-enabled while `auto_enable_skills=false`. |

The daemon loop is the host for these jobs. It should not create Codex top-level chats for scheduler work, and it should not rely on Codex native recurring automations for liveness. Host backends provide the model call; TraceDecay owns evidence collection, validation, ledgers, apply policy, and scheduler state.

## Standalone And Delegated Modes

`standalone` means TraceDecay owns backend calls, evidence collection, validation, run ledger writes, approval staging, dashboard review payloads, and optional scheduler execution. Backend output can propose changes, but TraceDecay validates every proposed mutation before it can be applied.
Expand Down
114 changes: 101 additions & 13 deletions src/agent_cmd.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use std::io;
use std::path::{Path, PathBuf};

use tracedecay::automation::config::{
apply_project_config_patch, project_config_path, AutomationBackend, AutomationConfigPatch,
AutomationHostMode, AutomationTaskPatch,
};

pub(crate) fn hermes_profile_targets(
home: &Path,
) -> tracedecay::errors::Result<Vec<Option<String>>> {
Expand Down Expand Up @@ -93,11 +98,19 @@ pub(crate) fn validate_hermes_project_root_flag(
Ok(Some(path))
}

/// How `install --agent codex --automation` should configure the daemon loop.
#[derive(Debug, Clone, Copy)]
pub(crate) struct CodexAutomationInstall {
/// Apply accepted memory-curation ops without dashboard approval
/// (`--auto-apply`).
pub(crate) auto_apply: bool,
}

fn validate_codex_automation_flags(
agent: Option<&str>,
automation: bool,
automation: Option<CodexAutomationInstall>,
) -> tracedecay::errors::Result<()> {
if !automation {
if automation.is_none() {
return Ok(());
}
if agent != Some("codex") {
Expand All @@ -121,6 +134,87 @@ fn validate_codex_automation_project_path() -> tracedecay::errors::Result<PathBu
})
}

async fn install_codex_daemon_automation(
project_path: &Path,
home: &Path,
options: CodexAutomationInstall,
) -> tracedecay::errors::Result<PathBuf> {
let auto_apply = options.auto_apply;
if tracedecay::agents::codex::remove_legacy_codex_native_automation(home)? {
eprintln!(
"\x1b[32m✔\x1b[0m Removed the legacy Codex-native scheduled automation; the TraceDecay daemon loop replaces it."
);
}

let cg = open_or_init_codex_daemon_automation_project(project_path).await?;
let dashboard_root = cg.store_layout().dashboard_root.clone();
let patch = AutomationConfigPatch {
enabled: Some(true),
backend: Some(AutomationBackend::CodexAppServer),
host_mode: Some(AutomationHostMode::Standalone),
model: Some(Some("gpt-5.5".to_string())),
// Unattended memory-op apply is opt-in: without --auto-apply these
// stay unset so AutomationConfig::default() keeps dashboard approval
// required, and re-running the installer never weakens stricter
// settings a user already chose.
require_dashboard_approval: auto_apply.then_some(false),
auto_apply_memory_ops: auto_apply.then_some(true),
memory_curator: codex_daemon_interval_task(15 * 60),
session_reflector: codex_daemon_interval_task(15 * 60),
skill_writer: AutomationTaskPatch {
min_idle_secs: Some(Some(15 * 60)),
..codex_daemon_interval_task(60 * 60)
},
..AutomationConfigPatch::default()
};

let global = tracedecay::user_config::UserConfig::load().automation;
apply_project_config_patch(&dashboard_root, &global, patch).await?;
let path = project_config_path(&dashboard_root);
eprintln!(
"\x1b[32m✔\x1b[0m Enabled TraceDecay daemon automation loop at {}",
path.display()
);
eprintln!(
" The daemon scheduler will run memory_curator, session_reflector, and skill_writer via the Codex app-server backend."
);
if auto_apply {
eprintln!(
"\x1b[33m⚠\x1b[0m --auto-apply: accepted memory-curation ops (deletes and merges) will be applied without dashboard approval. There is no archive; removals are permanent."
);
}
if !tracedecay::daemon::daemon_reachable() {
eprintln!(
"\x1b[33m⚠\x1b[0m The TraceDecay daemon is not running, so the automation loop will stay idle. Enable it with `tracedecay daemon install-service`."
);
}
Ok(path)
}

async fn open_or_init_codex_daemon_automation_project(
project_path: &Path,
) -> tracedecay::errors::Result<tracedecay::tracedecay::TraceDecay> {
if tracedecay::tracedecay::TraceDecay::has_initialized_store(project_path).await {
tracedecay::tracedecay::TraceDecay::open(project_path).await
} else {
eprintln!(
"No TraceDecay store found for {}; initializing one (equivalent to `tracedecay init`).",
project_path.display()
);
tracedecay::tracedecay::TraceDecay::init(project_path).await
}
}

fn codex_daemon_interval_task(interval_secs: u64) -> AutomationTaskPatch {
AutomationTaskPatch {
enabled: Some(true),
schedule: Some(Some("interval".to_string())),
interval_secs: Some(Some(interval_secs)),
cooldown_secs: Some(Some(5 * 60)),
..AutomationTaskPatch::default()
}
}

pub(crate) fn hermes_selected_profile_targets(
home: &Path,
profile: &Option<String>,
Expand All @@ -140,7 +234,7 @@ pub(crate) async fn handle_install_command(
all_profiles: bool,
project_root: Option<String>,
no_dashboard: bool,
automation: bool,
automation: Option<CodexAutomationInstall>,
) -> tracedecay::errors::Result<()> {
validate_hermes_profile_flags(agent.as_deref(), &profile, all_profiles)?;
let pinned_project_root = validate_hermes_project_root_flag(agent.as_deref(), &project_root)?;
Expand Down Expand Up @@ -186,12 +280,9 @@ pub(crate) async fn handle_install_command(
};
ag.install_local(&ctx, &project_path)?;
ag.post_install(Some(&project_path)).await;
if automation && id == "codex" {
if let Some(options) = automation.filter(|_| id == "codex") {
let scoped_project_path = validate_codex_automation_project_path()?;
tracedecay::agents::codex::install_codex_native_automation(
&home,
&scoped_project_path,
)?;
install_codex_daemon_automation(&scoped_project_path, &home, options).await?;
}
}
installed_names.push(ag.name().to_string());
Expand Down Expand Up @@ -244,12 +335,9 @@ pub(crate) async fn handle_install_command(
};
ag.install(&ctx)?;
ag.post_install(project_path.as_deref()).await;
if automation && id == "codex" {
if let Some(options) = automation.filter(|_| id == "codex") {
let scoped_project_path = validate_codex_automation_project_path()?;
tracedecay::agents::codex::install_codex_native_automation(
&home,
&scoped_project_path,
)?;
install_codex_daemon_automation(&scoped_project_path, &home, options).await?;
}
}
if !user_cfg.installed_agents.contains(&id) {
Expand Down
Loading
Loading