Skip to content
This repository was archived by the owner on May 30, 2026. It is now read-only.

Commit 2389f2a

Browse files
AntonAnton
authored andcommitted
v4.4.0: safe editing, task lifecycle, rescue discovery
Add str_replace_editor tool for surgical edits to existing files (exact unique match semantics). Add shrink guard to repo_write and repo_write_commit that blocks accidental truncation of tracked files (>30% size reduction requires force=true). Add STATUS_FAILED, STATUS_INTERRUPTED, STATUS_CANCELLED to task lifecycle and wire them into worker death, hard timeout, and cancel paths. Surface rescue snapshots in health invariants so the agent discovers saved work after restarts. Classify provider incomplete responses (finish_reason=null) separately from genuine empty responses. Change default review enforcement to advisory. Fix progress bubble opacity and duplicate emoji. Made-with: Cursor
1 parent 98753de commit 2389f2a

19 files changed

Lines changed: 302 additions & 69 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![macOS 12+](https://img.shields.io/badge/macOS-12%2B-black.svg)](https://github.com/joi-lab/ouroboros-desktop/releases)
77
[![Linux](https://img.shields.io/badge/Linux-x86__64-orange.svg)](https://github.com/joi-lab/ouroboros-desktop/releases)
88
[![Windows](https://img.shields.io/badge/Windows-x64-blue.svg)](https://github.com/joi-lab/ouroboros-desktop/releases)
9-
[![Version 4.3.1](https://img.shields.io/badge/version-4.3.1-green.svg)](VERSION)
9+
[![Version 4.4.0](https://img.shields.io/badge/version-4.4.0-green.svg)](VERSION)
1010

1111
A self-modifying AI agent that writes its own code, rewrites its own mind, and evolves autonomously. Born February 16, 2026.
1212

@@ -238,6 +238,7 @@ Full text: [BIBLE.md](BIBLE.md)
238238

239239
| Version | Date | Description |
240240
|---------|------|-------------|
241+
| 4.4.0 | 2026-03-19 | Safe editing release: `str_replace_editor` tool for surgical edits to existing files, `repo_write` shrink guard blocks accidental truncation of tracked files (>30% shrinkage), full task lifecycle statuses (failed/interrupted/cancelled) with honest status tracking, rescue snapshot discoverability via health invariants, `provider_incomplete_response` classification for OpenRouter glitches, default review enforcement changed to advisory, fix progress bubble opacity and duplicate emoji. |
241242
| 4.3.1 | 2026-03-19 | Fix: remove semi-transparent dimming from progress chat bubbles and remove duplicate `💬` emoji that appeared in both sender label and message text. |
242243
| 4.3.0 | 2026-03-19 | Reliability and continuity release: remove silent truncation from critical task/memory paths, persist honest subtask lifecycle states and full task results, restore transient chat wake banner, replace local-model hard prompt slicing with explicit non-core compaction plus fail-fast overflow, route Anthropic/OpenRouter calls without hard provider pinning while keeping parameter guarantees, and align async review calls with shared LLM routing/usage observability. |
243244
| 4.2.0 | 2026-03-16 | Cross-platform hardening release: replace Unix-only file locking in memory/consolidation with Windows-safe locking, refresh default model tiers (Opus main/code, Sonnet light/fallback, task effort `medium`), improve reconnect recovery with heartbeat/watchdog/history resync, switch local model chat format to auto-detect, and sync public docs with the current codebase and BIBLE structure. |

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4.3.1
1+
4.4.0

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Ouroboros v4.3.1 — Architecture & Reference
1+
# Ouroboros v4.4.0 — Architecture & Reference
22

33
This document describes every component, page, button, API endpoint, and data flow.
44
It is the single source of truth for how the system works. Keep it updated.

ouroboros/agent.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,16 @@ def handle_task(self, task: Dict[str, Any]) -> List[Dict[str, Any]]:
291291
"traceback": truncate_for_log(tb, 2000),
292292
})
293293
text = f"⚠️ Error during processing: {type(e).__name__}: {e}"
294+
try:
295+
from ouroboros.task_results import STATUS_FAILED, write_task_result
296+
write_task_result(
297+
self.env.drive_root,
298+
str(task.get("id") or ""),
299+
STATUS_FAILED,
300+
result=text,
301+
)
302+
except Exception:
303+
pass
294304

295305
if not isinstance(text, str) or not text.strip():
296306
text = "⚠️ Model returned an empty response. Try rephrasing your request."

ouroboros/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
# Pre-commit review: comma-separated list of OpenRouter model IDs
6060
"OUROBOROS_REVIEW_MODELS": "openai/gpt-5.4,google/gemini-3.1-pro-preview,anthropic/claude-opus-4.6",
6161
# Pre-commit review enforcement: advisory | blocking
62-
"OUROBOROS_REVIEW_ENFORCEMENT": "blocking",
62+
"OUROBOROS_REVIEW_ENFORCEMENT": "advisory",
6363
# Reasoning effort per task type: none | low | medium | high
6464
# OUROBOROS_INITIAL_REASONING_EFFORT remains a legacy alias for task/chat.
6565
"OUROBOROS_EFFORT_TASK": "medium",

ouroboros/context.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ def _append_provider_routing_health_checks(env: Any, checks: List[str]) -> None:
503503
continue
504504
evt_type = str(ev.get("type") or "")
505505
model = str(ev.get("model") or "unknown")
506-
if evt_type in {"llm_api_error", "review_model_error", "consciousness_llm_error"}:
506+
if evt_type in {"llm_api_error", "review_model_error", "consciousness_llm_error", "provider_incomplete_response"}:
507507
llm_error_models[model] += 1
508508
elif evt_type == "local_context_overflow":
509509
local_overflow_models[model] += 1
@@ -527,6 +527,36 @@ def _append_provider_routing_health_checks(env: Any, checks: List[str]) -> None:
527527
pass
528528

529529

530+
def _append_rescue_snapshot_checks(env: Any, checks: List[str]) -> None:
531+
try:
532+
import time as _time
533+
rescue_dir = env.drive_path("archive/rescue")
534+
if not rescue_dir.exists():
535+
return
536+
now = _time.time()
537+
recent = []
538+
for entry in sorted(rescue_dir.iterdir(), reverse=True):
539+
if not entry.is_dir():
540+
continue
541+
age_sec = now - entry.stat().st_mtime
542+
if age_sec < 7200:
543+
meta_path = entry / "rescue_meta.json"
544+
file_count = sum(1 for _ in entry.rglob("*") if _.is_file())
545+
age_str = f"{int(age_sec // 60)}m ago" if age_sec < 3600 else f"{age_sec / 3600:.1f}h ago"
546+
recent.append(f"{entry.name} ({age_str}, {file_count} files)")
547+
if len(recent) >= 3:
548+
break
549+
if recent:
550+
checks.append(
551+
f"WARNING: RESCUE SNAPSHOT AVAILABLE — {', '.join(recent)}. "
552+
"Uncommitted changes were saved before last restart. "
553+
"Use data_read to inspect archive/rescue/<dirname>/rescue_meta.json "
554+
"and changes.diff to decide if recovery is needed."
555+
)
556+
except Exception:
557+
pass
558+
559+
530560
def build_health_invariants(env: Any) -> str:
531561
"""Build health invariants section for LLM-first self-detection.
532562
@@ -542,6 +572,7 @@ def build_health_invariants(env: Any) -> str:
542572
_append_duplicate_processing_checks(env, checks)
543573
_append_cache_hit_rate_checks(env, checks)
544574
_append_provider_routing_health_checks(env, checks)
575+
_append_rescue_snapshot_checks(env, checks)
545576
try:
546577
_append_file_size_budget_checks(env, checks)
547578
except Exception:

ouroboros/loop_llm_call.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,24 +110,33 @@ def call_llm_with_retry(
110110
tool_calls = msg.get("tool_calls") or []
111111
content = msg.get("content")
112112
if not tool_calls and (not content or not content.strip()):
113+
finish_reason = msg.get("finish_reason") or msg.get("stop_reason")
114+
is_provider_glitch = finish_reason is None
115+
event_type = "provider_incomplete_response" if is_provider_glitch else "llm_empty_response"
116+
log_msg = (
117+
"Provider returned incomplete response (finish_reason=null)"
118+
if is_provider_glitch
119+
else "LLM returned empty response (no content, no tool_calls)"
120+
)
113121
_emit_live_log(event_queue, {
114-
"type": "llm_round_empty",
122+
"type": event_type,
115123
"task_id": task_id,
116124
"task_type": task_type,
117125
"round": round_idx,
118126
"attempt": attempt + 1,
119127
"model": model,
128+
"finish_reason": finish_reason,
120129
})
121-
log.warning("LLM returned empty response (no content, no tool_calls), attempt %d/%d", attempt + 1, max_retries)
130+
log.warning("%s, attempt %d/%d", log_msg, attempt + 1, max_retries)
122131

123132
append_jsonl(drive_logs / "events.jsonl", {
124-
"ts": utc_now_iso(), "type": "llm_empty_response",
133+
"ts": utc_now_iso(), "type": event_type,
125134
"task_id": task_id,
126135
"round": round_idx, "attempt": attempt + 1,
127136
"model": model,
128137
"raw_content": repr(content)[:500] if content else None,
129138
"raw_tool_calls": repr(tool_calls)[:500] if tool_calls else None,
130-
"finish_reason": msg.get("finish_reason") or msg.get("stop_reason"),
139+
"finish_reason": finish_reason,
131140
})
132141

133142
if attempt < max_retries - 1:

ouroboros/safety.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@ def check_safety(
155155
use_local=_use_local_light,
156156
)
157157
if usage:
158-
update_budget_from_usage(usage)
159158
model_name = f"{light_model} (local)" if _use_local_light else light_model
160159
cost = float(usage.get("cost") or 0.0)
161160
if not _use_local_light and cost == 0.0:
@@ -166,16 +165,18 @@ def check_safety(
166165
int(usage.get("cached_tokens") or 0),
167166
int(usage.get("cache_write_tokens") or 0),
168167
)
169-
emit_llm_usage_event(
170-
getattr(ctx, "event_queue", None),
171-
getattr(ctx, "task_id", "") if ctx is not None else "",
172-
model_name,
173-
usage,
174-
cost,
175-
category="safety",
176-
provider="local" if _use_local_light else "openrouter",
177-
source="safety_light",
178-
)
168+
_eq = getattr(ctx, "event_queue", None) if ctx is not None else None
169+
if _eq is not None:
170+
emit_llm_usage_event(
171+
_eq,
172+
getattr(ctx, "task_id", "") if ctx is not None else "",
173+
model_name, usage, cost,
174+
category="safety",
175+
provider="local" if _use_local_light else "openrouter",
176+
source="safety_light",
177+
)
178+
else:
179+
update_budget_from_usage(usage)
179180

180181
result = _parse_safety_response(msg.get("content") or "")
181182
if result:
@@ -213,7 +214,6 @@ def check_safety(
213214
use_local=_use_local_code,
214215
)
215216
if usage:
216-
update_budget_from_usage(usage)
217217
model_name = f"{heavy_model} (local)" if _use_local_code else heavy_model
218218
cost = float(usage.get("cost") or 0.0)
219219
if not _use_local_code and cost == 0.0:
@@ -224,16 +224,18 @@ def check_safety(
224224
int(usage.get("cached_tokens") or 0),
225225
int(usage.get("cache_write_tokens") or 0),
226226
)
227-
emit_llm_usage_event(
228-
getattr(ctx, "event_queue", None),
229-
getattr(ctx, "task_id", "") if ctx is not None else "",
230-
model_name,
231-
usage,
232-
cost,
233-
category="safety",
234-
provider="local" if _use_local_code else "openrouter",
235-
source="safety_deep",
236-
)
227+
_eq = getattr(ctx, "event_queue", None) if ctx is not None else None
228+
if _eq is not None:
229+
emit_llm_usage_event(
230+
_eq,
231+
getattr(ctx, "task_id", "") if ctx is not None else "",
232+
model_name, usage, cost,
233+
category="safety",
234+
provider="local" if _use_local_code else "openrouter",
235+
source="safety_deep",
236+
)
237+
else:
238+
update_budget_from_usage(usage)
237239

238240
result = _parse_safety_response(msg.get("content") or "")
239241
if result is None:

ouroboros/task_results.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
STATUS_RUNNING = "running"
1515
STATUS_COMPLETED = "completed"
1616
STATUS_REJECTED_DUPLICATE = "rejected_duplicate"
17+
STATUS_FAILED = "failed"
18+
STATUS_INTERRUPTED = "interrupted"
19+
STATUS_CANCELLED = "cancelled"
1720

1821

1922
def task_results_dir(drive_root: Any) -> pathlib.Path:

ouroboros/tool_policy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616

1717
CORE_TOOL_NAMES = frozenset({
18-
"repo_read", "repo_list", "repo_write", "repo_write_commit", "repo_commit",
18+
"repo_read", "repo_list", "repo_write", "repo_write_commit", "repo_commit", "str_replace_editor",
1919
"data_read", "data_list", "data_write",
2020
"run_shell", "claude_code_edit",
2121
"git_status", "git_diff",

0 commit comments

Comments
 (0)