# Omnius Self-Edit Evaluation — Uncommitted Changes & Duplicate-Tool-Call Failure Mode

> Eval of the working-tree changes against `docs/opencode-agentic-loop-comparison.md`.
> Subject: an Omnius instance editing its own orchestrator to implement the P0–P10
> learnings. Generated 2026-06-10.

---

## TL;DR

1. **The self-edit failed.** The agent attempted the full P0–P10 refactor, produced
   6 new modules plus in-place edits, but **the orchestrator package does not compile**
   (`tsc --noEmit` → exit 2, ~40+ errors). The 6 new modules are **dead code** — not
   imported anywhere except one broken import in `agent-tool.ts`.
2. **The live instance is unaffected — by luck.** `dist/agenticRunner.js` was built at
   02:35; the broken source edits landed at 05:06 and never recompiled. The running
   self-editing instance is still executing the **old, working dist**. The build silently
   stayed green-at-runtime because the broken code was never built.
3. **The reported duplicate-tool-call / duplicate-read failure mode is NOT caused by
   these edits.** It lives in the existing (working) dedup architecture. Root cause:
   dedup is **reactive, cross-turn-blind, and result-agnostic** — duplicates are executed
   then cleaned up, and repeated *failures* have their evidence pruned away.
4. **The diff is itself a symptom of the same pathology.** The agent pasted **three
   near-identical compaction blocks** into one loop and hallucinated an entire `index.ts`
   export surface. "Duplicate, low-grounding output" shows up at both the tool-call layer
   (runtime) and the code-generation layer (this diff).

---

## Part 1 — How well did the self-editing agent do?

### Verdict: **Poor / non-shippable.** Ambitious scope, no verification, broken build.

The agent violated the project's own `executable-feedback` rule (code-generating agents
must compile/test before returning). None of this compiles.

### 1.1 What was produced

| Module | P-item | Lines | Wired in? | Quality |
| --- | --- | --- | --- | --- |
| `runner.ts` | P3/P7 | 258 | ❌ no | Toy. Regex `<tool_call>` parsing (Omnius uses native OpenAI tool calls). Name **collides** with the real `AgenticRunner`. Re-calls the LLM after tools and discards the result. |
| `orchestrator.ts` | P4/P7 | 387 | ❌ no | Toy. `checkCompletion` = substring match on last message. `prune` strategy keeps "every other message" — would split `tool_call`/`tool_result` pairs. |
| `childSession.ts` | P0/P10 | 288 | ❌ (import is broken) | Best of the six. Plausible permission-derivation + `task_id` resume shape. Glob matcher is naive. |
| `permissionRuleset.ts` | P1/P6 | 166 | ❌ no | Reasonable. `evaluatePermission` cascade + `checkDoomLoop`. Never called. |
| `compactionAgent.ts` | P9 | 101 | ⚠️ imported, wrong shape | **Placeholder** — `compress()` never calls an LLM; returns "last 10 messages" stub. Imports `ChatMessage` which `agent-types.ts` does not export. |
| `backendAdapter.ts` | P8 | 443 | ❌ no | Three backend adapters. **Single-tool-call only** (`currentToolName`/`accumulatedArgs` are scalars) — contradicts the parallel-batching premise. Ollama branch double-accumulates `toolCallBuffer`. |

### 1.2 What broke (compile errors, by file)

- **`agenticRunner.ts`** — the loop got **three stacked compaction blocks** (lines ~9474,
  ~9489, ~9495), each calling a different non-existent API:
  - `compactionResult.success` / `.compressedContext` — real shape is `{ compacted, messages, summary }`.
  - `this.messages` — the field is not named `messages` on the class (`TS2339` ×7).
  - `new CompactionAgent({ model })` — `CompactionAgent` is **not imported** here and is a
    const object, not a class (`TS2304`); its method is `compress`, not `compact`;
    `result.tokenCount` doesn't exist; `this.options.model` doesn't exist.
  - `estimateMessagesTokens` imported from `textSanitize.js` **and** redeclared → `TS2440`.
- **`index.ts`** — rewritten into a "clean" barrel that re-exports a **hallucinated API
  surface**: `SessionManager`, `SessionConfig` (module exports `SessionOrchestrator`),
  `AgentType`, `AgentMode`, `AgentConfig`, `AgentState`, `AgentStatus`, `CompactionConfig`,
  `AgentTask` — none exist. Plus duplicate `LLMBackend` and `export type` violations under
  `verbatimModuleSyntax`. This **also deleted real exports** the rest of the repo depends on
  (`OllamaPool`, `discoverSystemOllamaModelStore`, `modelProfile` helpers, …) — `cli` imports
  these from `@omnius/orchestrator` in ~30 files.
- **`compactionAgent.ts`** — `import { ChatMessage }` fails (`TS2305`).
- **`agent-tool.ts`** — import line injected **into the file's top doc-comment** (line 2),
  then duplicated correctly at line 28. Cross-package deep import
  `../../orchestrator/src/childSession.js` from `packages/execution` breaks package boundaries.
  Calls `this.callbacks.getParentSessionId?.()` / `resolveParentPermissions?.()` callbacks
  that don't exist on the interface.

### 1.3 What was actually fine

A few in-place edits are individually sound and worth salvaging:
- `agent-types.ts` — adding `mode: "primary" | "subagent" | "all"` + `canSpawnAsSubagent` /
  `canUserSelect` (P1) is clean. **But** it renamed `plan` → `architect` and dropped
  `getAgentTypeRegistry` / `resolveAgentTools` / `registerAgentType` / `buildAgentTypeSummary`,
  which are referenced elsewhere — breaking callers.
- `tool-batching.ts` — Zod schemas + `validateToolArgs` (P5) are self-contained and harmless.
- `agenticRunner.ts` — the `finish_reason` plumbing on the Ollama backend (P3) is correct and
  is the one genuinely useful, low-risk change in the whole diff.
- `textSanitize.ts` — `estimateMessagesTokens` helper is fine (just double-imported elsewhere).

### 1.4 Behavioral read

Classic over-eager refactor by a capable model with **no feedback loop**:
- Wrote the code it *wished* existed (idealized `index.ts`) rather than the code that matches
  the modules it actually wrote.
- Built parallel "clean-room" modules instead of integrating into the 25k-line monolith — the
  hard part (wiring) was skipped, so 100% of the architectural value is unrealized.
- Never ran `tsc`. The `executable-feedback` / `anti-laziness` rules were not honored.

---

## Part 2 — Duplicate failed tool calls & duplicate reads

### This is a pre-existing runtime issue in the working code, independent of Part 1.

The dedup architecture in the live `agenticRunner.ts` is large and thoughtful, but its design
has three structural gaps that together produce the observed loop.

### 2.1 What exists today

1. **`_dedupeToolCallsForResponse` (line 7397)** — drops exact-duplicate tool calls
   **within a single response batch**, before execution. Good, but **intra-turn only**.
2. **`proactivePrune` (line 4937)** — post-hoc context hygiene. On each turn it walks history
   and, for repeated fingerprints, **replaces the *earlier* result** with
   `[deduped — same call as turn N]`. Also ages out `file_read` results older than 20 turns
   (`AGED_FILE_READ_TURNS`) and successful shells older than 12.
3. **Failure-learning injectors** — `_recentFailures`, `_argCohorts`, `_errorPatterns`
   (WO-NC-07 pre-action guidance), `_failureReflections` (REG-26 Reflexion), REG-32 opaque-error
   hints. These *can* inject "you tried this and it failed" before re-dispatch.

### 2.2 Root causes (ranked)

**RC-1 — No cross-turn pre-execution dedup (primary).**
`_dedupeToolCallsForResponse` only looks within one response. A `file_read(X)` on turn 5 and an
identical `file_read(X)` on turn 25 **both execute**. The fingerprint
(`_buildToolFingerprint` = name + exact args, line 7389) is computed but never consulted as an
emission-time gate across turns. Duplicates are *cleaned up*, never *prevented*.

**RC-2 — Aging causes re-reads (acknowledged in-code).**
`proactivePrune` ages out `file_read` results after 20 turns. The REG-64 comment (line 4940)
admits: *"too-aggressive aging causes the model to re-read the same file because it forgot the
content, creating duplicate calls."* Bumping 10→20 mitigated but did not remove this — long runs
still strip content the model then re-fetches.

**RC-3 — Repeated *failures* have their evidence pruned (why failed calls loop).**
The fingerprint is **result-agnostic**: a failed call and its retry share a fingerprint, so
proactivePrune replaces the earlier *failure* with `[deduped — same call as turn N — duplicate
file_read() call]`. That stub **does not record that the prior call failed**. The model's inline
history therefore looks *cleaner* after each retry, removing the strongest natural signal
("I've failed this exact call twice") and making another retry *more* likely. The system leans
entirely on the separate `_recentFailures`/`_errorPatterns` injectors to re-surface the pattern —
and those are gated by per-turn one-shot flags (`_errorGuidanceInjected`,
`_reflectionsInjectedThisTurn`, `_opaqueErrorHintInjected`) and **stem matching**, which misses
when the model varies args slightly or pivots stems (the exact case REG-32 was added for).

### 2.3 Where to patch

| # | Patch | Location | Effort |
| --- | --- | --- | --- |
| **P-A** | **Cross-turn pre-execution dedup gate.** Maintain a run-level `Map<fingerprint, { turn, ok, resultRef }>`. Before dispatch, if a *successful* identical fingerprint exists, **short-circuit**: skip execution and inject `"already executed at turn N — see prior result"` instead of re-running. Reuse `_buildToolFingerprint`. This is the single highest-leverage fix. | new gate in the dispatch path alongside `_dedupeToolCallsForResponse` (`agenticRunner.ts:7397`) | M |
| **P-B** | **Make dedup result-aware.** When pruning a duplicate in `proactivePrune`, if the pruned call **failed**, preserve that in the stub: `[deduped — same FAILED call as turn N: <error 1-liner>]`. Keep the failure signal visible instead of erasing it. | `proactivePrune` dedupe branch (`agenticRunner.ts:5020-5031`) | S |
| **P-C** | **Escalate on Nth identical failure → hard stop, not soft nudge.** Wire `permissionRuleset.checkDoomLoop` (already written, P6) — after 3 identical fingerprints, switch from "inject guidance" to "refuse the call and force a different action or `task_complete`." This is OpenCode's `doom_loop: ask` pattern. | consume `_recentFailures`/`_argCohorts` at dispatch; the helper already exists in `permissionRuleset.ts:113` | M |
| **P-D** | **Don't age out a `file_read` unless it can be cheaply re-summarized in place.** Instead of clearing aged reads to a stub that triggers re-reads, replace with a **content digest** (path + line range + a few key symbols) so the model rarely needs the raw bytes again. | `proactivePrune` aged-file branch (`agenticRunner.ts:5096+`) | M |
| **P-E** | **Loosen one-shot injection gating for repeated failures.** The per-turn dedup flags suppress re-injection across turns when the model pivots stems; allow re-injection when `_argCohorts[fp].failure >= 2` regardless of stem. | injectors around `_errorPatterns` / REG-32 | S |

### 2.4 Note on the new modules

`permissionRuleset.checkDoomLoop` and `childSession`'s permission model are exactly the
primitives P-C wants — but they are **dead code**. The agent built the right tool for this fix and
then never connected it. Wiring `checkDoomLoop` into the live dispatch path (P-C) is the one piece
of the self-edit worth rescuing immediately.

---

## Recommended next actions

1. **Do not commit the working tree as-is** — it does not build and deletes exports the CLI needs.
2. **Salvage list** (cherry-pick into compiling changes): `agent-types.ts` `mode` field (restore
   the removed registry functions + keep `plan`), `tool-batching.ts` Zod schemas, Ollama
   `finish_reason` plumbing.
3. **Quarantine** the 6 new modules into a `proposals/` or feature branch — they're design sketches,
   not integrations. Fix `compactionAgent` to actually call an LLM before anyone imports it.
4. **Ship the duplicate-call fix independently** of the P0–P10 refactor: P-A + P-B + P-C give the
   biggest behavior win and don't depend on the broken modules.
5. **Add a build gate to the self-edit loop** — the agent must run `tsc -p packages/orchestrator`
   and only mark `task_complete` on exit 0. This run would have caught everything in Part 1.
