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
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ All tracedecay users contribute to an anonymous aggregate counter. `tracedecay s

## Index Freshness

tracedecay keeps the graph up to date without a background daemon or an OS-level file watcher.
tracedecay keeps the graph up to date without an OS-level file watcher. The optional MCP daemon reuses server state across clients; it does not watch files in the background.

**On-demand staleness check.** Every MCP tool call checks whether any indexed files have been modified since the last sync. If stale files are found, they are re-extracted before the tool response is returned. A 30-second cooldown prevents back-to-back calls from re-walking the tree on every keystroke.

Expand All @@ -699,13 +699,23 @@ cp scripts/post-commit .git/hooks/post-commit
chmod +x .git/hooks/post-commit
```

### Daemon Debugging

The daemon is a long-running MCP server for clients that share one local socket. Its logs go to stderr; when installed as a Linux user service, systemd captures them in journald.

```bash
tracedecay daemon install-service # install/start Linux systemd user service
tracedecay daemon status # service path, socket state, log command
systemctl --user status tracedecay.service --no-pager
journalctl --user -u tracedecay.service -f
tracedecay status --runtime --json # process + DB/WAL/SHM telemetry snapshot
```

Scheduler logs use stable `event=... key=value` fields such as `event=scheduler_tick`, `event=scheduler_task`, `task=memory_curator`, `outcome=skipped`, and `reason=not_configured`, so they can be filtered directly from journald.

### Upgrading from 5.x

The standalone daemon command from 5.x-era installs and its
launchd/systemd/Windows Service autostart were removed in 6.0.0. The embedded
OS-level file watcher that replaced the daemon was itself removed in 6.1.0 (it
caused runaway CPU and memory on large monorepos with deep `node_modules` or
`target` trees). The on-demand staleness model above is the current design.
The old cross-platform watcher/autostart daemon from 5.x-era installs was removed in 6.0.0. The embedded OS-level file watcher that replaced it was itself removed in 6.1.0 (it caused runaway CPU and memory on large monorepos with deep `node_modules` or `target` trees). The current daemon is MCP transport/process reuse, not file watching; the on-demand staleness model above is still the freshness design.

---

Expand All @@ -732,6 +742,7 @@ tracedecay sync --doctor [path] # Sync and list added/modified/removed files
tracedecay status [path] # Show statistics + cost summary
tracedecay status [path] --json # Show statistics (JSON output)
tracedecay status --details # Include node-kind breakdown
tracedecay status --runtime # Capture process + DB/WAL/SHM telemetry
tracedecay dashboard # Start local web dashboard (default port 7341)
tracedecay dashboard --port 8080 # Start dashboard on custom port
tracedecay dashboard --port 0 # Auto-select a free port
Expand All @@ -748,7 +759,10 @@ tracedecay reinstall # Refresh settings for all installed agents
tracedecay update-plugin # Refresh generated plugin code/assets only; never writes agent configs
tracedecay uninstall [--agent NAME] [--profile NAME] # Remove agent integration
tracedecay serve # Start MCP server
tracedecay daemon status # Show daemon service/socket/log hints
tracedecay daemon install-service # Install/start Linux systemd user service
tracedecay monitor # Live TUI showing MCP calls across all projects
tracedecay update # Refresh binary, generated plugins, and daemon
tracedecay upgrade # Self-update to latest version
tracedecay channel [stable|beta] # Show or switch update channel
tracedecay doctor [--agent NAME] # Check installation health
Expand Down Expand Up @@ -1013,6 +1027,7 @@ Large projects take longer on the first full index.
| `TRACEDECAY_BIN` | Path to the tracedecay binary (used by Hermes wrapper for spawn mode). |
| `TRACEDECAY_DASHBOARD_PROJECT` | Project root path for Hermes dashboard spawn mode (defaults to Hermes' cwd). |
| `TRACEDECAY_DASHBOARD_URL` | Full URL to an already-running dashboard (Hermes external URL mode). |
| `TRACEDECAY_DAEMON_SOCKET` | Override the Unix socket used by MCP daemon clients. |
| `HERMES_HOME` | Path to Hermes profile directory for profile-scoped plugin installation. |
| `TRACEDECAY_OFFLINE` | Set to `1` to skip network requests for pricing data (Savings & Cost tab uses bundled fallback). |
| `DISABLE_TRACEDECAY` | Set to `true` to disable the MCP server entirely (exits cleanly without initializing). |
Expand Down
30 changes: 30 additions & 0 deletions docs/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,20 @@ Returns feature flags and server configuration. Used by the UI and wrappers to d
- `features.llm_curation`: Whether TraceDecay can run LLM-backed curation through standalone automation. Delegated hosts keep planning host-owned and submit ops through `POST /curate/apply`.
- `automation.mode`: `"disabled"`, `"standalone_backend"`, or `"delegated_host"`; `delegated_host` is provider-neutral and may be used by Hermes, Codex app-server orchestration, Claude Code CLI, Cursor Agent CLI, or another host that owns the intelligence layer.

### Automation Scheduler Debugging

The dashboard scheduler panel reads `GET /api/automation/scheduler/status` and can pause or resume the scheduler with `/pause` and `/resume`. The status response includes the effective automation config, control file path, tick cadence, and per-task due/skip reasons.

When the daemon runs the scheduler, its stderr/journald logs use stable `event=... key=value` fields:

```bash
tracedecay daemon status
journalctl --user -u tracedecay.service -f
journalctl --user -u tracedecay.service --since "1 hour ago" | grep 'event=scheduler'
```

Useful events include `event=scheduler_tick`, `event=scheduler_sleep`, `event=scheduler_task`, and `event=scheduler_task_error`.

---

### Holographic Memory API
Expand Down Expand Up @@ -1333,6 +1347,22 @@ export TRACEDECAY_GLOBAL_DB=/path/to/sessions.db
tracedecay dashboard
```

### Automation Scheduler Not Running

```bash
# Check effective automation config and backend availability
tracedecay automation config explain --json

# Check run history for memory_curator/session_reflector/skill_writer
tracedecay automation runs list --json

# Check daemon socket/service/log path
tracedecay daemon status
journalctl --user -u tracedecay.service --since "1 hour ago" | grep 'event=scheduler'
```

If the config shows `enabled: false`, `backend: "disabled"`, `host_mode: "delegated_host"`, or task schedules set to `manual`, the daemon scheduler will skip work. The scheduler status panel shows the same skip reasons without requiring shell access.

### Frontend Assets Not Updating

```bash
Expand Down
60 changes: 47 additions & 13 deletions src/automation/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,7 @@ impl AgentTaskBackend for CodexAppServerBackend {
let output_json = request
.contract
.strict_json
.then(|| {
let value = extract_single_json_object(&summary.text)?;
validate_response_schema(&value, &request.contract)?;
Ok::<Value, TraceDecayError>(value)
})
.then(|| extract_response_json_object(&summary.text, &request.contract))
.transpose()?;
Ok(AgentTaskResponse {
run_id: request.run_id.clone(),
Expand All @@ -368,11 +364,53 @@ impl AgentTaskBackend for CodexAppServerBackend {
}
}

pub fn extract_single_json_object(text: &str) -> Result<Value> {
pub fn extract_json_object_prefix(text: &str) -> Result<Value> {
let candidate = strip_optional_json_fence(text)?;
let mut deserializer = serde_json::Deserializer::from_str(candidate);
let value = Value::deserialize(&mut deserializer)?;
deserializer.end()?;
parse_json_object_prefix(candidate)
}

fn extract_response_json_object(text: &str, contract: &AgentTaskContract) -> Result<Value> {
let mut schema_error = None;
for (start, _) in text.char_indices().filter(|(_, ch)| *ch == '{') {
if !is_json_object_candidate_boundary(&text[..start]) {
continue;
}
let Ok(value) = parse_json_object_prefix(&text[start..]) else {
continue;
};
if let Err(err) = validate_response_schema(&value, contract) {
if schema_error.is_none() {
schema_error = Some(err);
}
continue;
}

return Ok(value);
}

if let Some(err) = schema_error {
return Err(err);
}

let value = extract_json_object_prefix(text)?;
validate_response_schema(&value, contract)?;
Ok(value)
}

fn is_json_object_candidate_boundary(prefix: &str) -> bool {
prefix
.chars()
.rev()
.find(|ch| !ch.is_whitespace())
.is_none_or(|ch| matches!(ch, '}' | ']'))
}

fn parse_json_object_prefix(candidate: &str) -> Result<Value> {
let mut stream = serde_json::Deserializer::from_str(candidate).into_iter::<Value>();
let value = match stream.next() {
Some(value) => value?,
None => return config_error("automation backend output must be a JSON object"),
};
if !value.is_object() {
return config_error("automation backend output must be a JSON object");
}
Expand Down Expand Up @@ -405,10 +443,6 @@ fn strip_optional_json_fence(text: &str) -> Result<&str> {
let Some(closing_start) = after_opening.rfind("```") else {
return config_error("automation backend JSON fence is missing closing fence");
};
let trailing = after_opening[closing_start + "```".len()..].trim();
if !trailing.is_empty() {
return config_error("automation backend JSON fence has trailing content");
}
let mut inner = &after_opening[..closing_start];
if let Some(rest) = inner.strip_prefix("json") {
inner = rest;
Expand Down
4 changes: 2 additions & 2 deletions src/automation/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use serde_json::{json, Value};

use super::artifacts::{sha256_json, write_improvement_artifacts};
use super::backend::{
agent_task_contract, classify_agent_task_error_message, extract_single_json_object,
agent_task_contract, classify_agent_task_error_message, extract_json_object_prefix,
prompt_version, task_key, AgentTaskKind, AgentTaskRequest, AgentTaskResponse,
};
use super::config::{AutomationBackend, AutomationConfig, AutomationHostMode};
Expand Down Expand Up @@ -450,7 +450,7 @@ impl<'a> AgentRunFinalizer<'a> {
match response
.output_json
.clone()
.map_or_else(|| extract_single_json_object(&response.output_text), Ok)
.map_or_else(|| extract_json_object_prefix(&response.output_text), Ok)
{
Ok(output) => Ok(output),
Err(err) => {
Expand Down
Loading
Loading