Skip to content

Completed I

Gabri Elles edited this page Aug 21, 2026 · 3 revisions

Completed, Part I

Part 1 of the Completed list of finished ROADMAP items.

Builder duplicate section bug: an edit_workspace_file call that retypes existing content while adding new material. Root caused from the live session transcript, implemented and live validated on 2026-08-17. We traced final_report.md's two near identical "Mexico City" and "Mexico City, Central Districts" sections to the exact edit_workspace_file call that caused them (BuilderFix_attempt3_reviewed, the run's last action): old_string="### Mexico City" (the bare heading), and new_string was that heading plus the heading's own pre existing bullet retyped verbatim plus a new subsection. Since the original bullet was never part of old_string, it stayed where it was, and the retyped copy landed directly in front of it. Fixed at two layers. First, BUILDER_INSTRUCTIONS/FINDINGS_WRITER_INSTRUCTIONS (src/prompts.py) now explicitly say to anchor an adding edit on the boundary of existing content, never retype it into new_string. Second, find_duplicate_report_sections/check_duplicate_report_sections (src/engine/completion.py) is a new structural, self consistency completion check comparing every h3 plus subsection of a report against every other via _content_word_overlap (threshold 0.6), registered in GROUNDING_CHECKS/_QUARANTINE_PROBLEMS/_BUILDER_FIXABLE_PROBLEMS, gated by new settings.duplicate_section_check.enabled. Live validated by running the new function directly against the real incident's final_report.md: it correctly flags exactly the one real duplicate section and nothing else, no false positives on the genuinely distinct Lisbon/Portugal sections.

Citation misattribution: a real, correctly fetched claim gets attached to the wrong cited URL. Implemented and live verified against the real incident data on 2026-08-17. Found via a manual claim by claim audit of the ablation smoke test's own final_report.md, after the user asked "did you evaluate the report properly?" since the automated structural score had missed it entirely. Four real, accurately worded Mexico visa figures (the "300 days" income formula, the "$53" fee, a sworn translator requirement, and the MiConsulado interview) were all traced verbatim to themexicohandbook.com's fetched content but cited to esimcard.com instead. Both URLs were genuinely fetched this run, so the hard grounding gate passed, and claim_grounding_problem's term overlap gate passed too, on nothing more than a coincidentally shared bare year, since both sources were 2026 dated visa guides. extract_salient_terms's own number regex is narrow enough (period grouped decimals, bare 4 digit years, percentages only) that it never extracted "300" or "53" as a checkable term at all. This is the same failure shape find_unsupported_regulation_ids already catches for regulation identifiers, generalized to the broader class of small numeric claims via a new sibling function, find_unsupported_specific_figures (src/utils/grounding.py): line scoped, matches dollar or currency figures and day or month count claims, and flags one that's verbatim absent from its own cited source. Wired into cheap_grounding_problems (new settings.grounding_check.specific_figure_check, default true) and a new check_specific_figure_unsupported completion check. A real implementation bug got caught and fixed during live validation, not just synthetic tests: the first version stripped commas from the claim's digits but not from the source content before searching, so a genuinely supported figure whose source also formats it with a thousands separator comma (source says "$1,200," search target was bare "1200") false positived as unsupported. Fixed by normalizing thousands separator commas out of the source content too, then re validated against the same real report: the false positives resolved, and the true incident plus two more previously unnoticed real grounding failures in the same report (a fabricated Lisbon rent range cited to a property purchase price page with no rental data at all, and the model's own unstated USD approximation absent from every fetched source) were all correctly caught.

Output and export feature gaps, plus an optional HTTP API and web UI. Implemented and live verified on 2026-08-02. Scoped from a purpose versus built feature gap check rather than a code audit: research inputs were web only, output was markdown only, no programmatic API existed, and research_output/ folder names collided for long queries sharing a prefix. Several things shipped together. Folder name collisions got fixed: _slugify_run_dir_name (engine/tui.py) now appends a 6 hex sha1 hash of the full query, not just its truncated slug. A references.bib output got added, built from what final_report.md actually cites, not the raw fetched URL list, since a bibliography from "everything fetched" would list sources the report never used, a gap caught and corrected before implementation. --seed-doc//seed-doc added local document ingestion (PDF, DOCX, XLSX, PPTX, txt, md) mirroring the existing --seed-url pattern, deliberately exposing no new tool to the model, since the engine just loads the file into the workspace and read_workspace_file already covers it. settings.pdf_engine added an optional final_report.pdf via pandoc, weasyprint, or system LaTeX. A real bug got found and fixed here too: pandoc's --pdf-engine=weasyprint shells out to the weasyprint CLI, which lives in the venv's bin/, not the system PATH when invoked as ~/.venvs/deepdelve/bin/python directly, fixed by prepending the executable's directory to the subprocess env's PATH.

The bigger piece was src/api.py plus src/static/index.html, an optional FastAPI HTTP API and web UI. A key finding before writing any code: orchestrator.py's _session and tui.py's session log state are module level globals, not contextvars, so two truly concurrent runs in one process would corrupt each other's state. Fixed by design with one in process FIFO job queue and one worker coroutine, always single flight, deliberately a third copy of run_cli/run_agent's orchestration shape rather than a fourth shared abstraction. New endpoints included POST /research, GET /research/{id}/status|stream|report|bib|pdf, POST /research/{id}/resume|followup|cancel, GET /runs, and GET/POST /settings. A follow up design correction got caught before implementation: the original plan leaned on orchestrator_module._session surviving in memory between a run and its follow up, but the shared queue can run an unrelated job in between and silently clobber it. Fixed by persisting session.to_dict() to <run_dir>/_agent_session.json per run instead of trusting the global. A real bug found live, not just written and assumed correct: /followup's first version ran the full completion check and artifact rewrite pipeline against a follow up question, the same as a fresh run, which rewrote findings.md repeatedly and never touched final_report.md. Root cause was missing that tui.py's own run_agent/is_followup branch deliberately sets skip_completion_check = True once the required artifact already exists, fixed by mirroring that condition in api.py::_run_research. Two more real bugs surfaced during the frontend pass: _artifact_path gated report, bib, and pdf routes on run_id in _jobs, which is in memory and this process lifetime only, so any run from a prior server process 404'd with a misleading "Unknown run_id" despite having real artifacts on disk, fixed to check the workspace directory directly. And FastAPI doesn't auto add HEAD support to a @app.get route, so every "does this artifact exist" check in the frontend, which used fetch(..., {method:"HEAD"}) specifically to avoid downloading full content just to check presence, silently 405'd, meaning no Report, Bibliography, or PDF button ever appeared, fixed via @app.api_route(..., methods=["GET","HEAD"]). A new settings.api_password option, unset by default to match the loopback only posture, got added specifically because full settings CRUD (API keys included) plus user requested LAN or phone access together raised the stakes enough to warrant it, layered on top of the existing --i-understand-the-risk non loopback bind guard. The web UI itself has no CDN or build step, uses a small dependency free markdown to HTML renderer for the report viewer rather than a general CommonMark library, since this project's own report dialect is narrow and predictable, renders settings as a real generated form from the config's own shape instead of a raw JSON textarea, and uses a signal lamp status indicator as the one deliberate animated element, with reduced motion respected. Status text shows the user's actual query, never the internal run id slug, caught live after a resume plus cancel left that slug as the only visible status. The mobile responsive layout was built to spec but not visually verified this session, no browser automation tool was available, so it needs a check on an actual phone before being treated as done.

Also folded in the same session, all 5 findings from an earlier 3 agent audit covering security, correctness, and production readiness: an SSRF guard on outbound fetches (tools/web.py, an httpx event hook checking every request's real resolved target, including mid redirect chain hops, against loopback, link local, private, and reserved ranges), a no_urls_count resume carryover gap, traceback leakage trimmed across tools/core.py/web.py/fs.py (exception type and message only, no local filesystem paths handed to the model), enable_session_persistence's _DEFAULTS fallback fixed to match config_template.yaml's real default, and a new test_tools.py (same flat assert based convention as test_structural_checks.py, wired into CI) pinning the SSRF guard plus several previously untested pure logic functions.

Completion check starvation bug class: 8 real instances found and fixed, then replaced with one shared structural mechanism. Implemented and live verified on 2026-07-31. This started as an Ornith 1.0 9B re test (see the Model Bake off page), but the same night's investigation surfaced the identical bug shape repeatedly on gpt-oss, the project's own default, too: a COMPLETION_CHECKS/GROUNDING_CHECKS check that isn't Builder or FindingsWriter fixable and never caps its own firing wins first match on every attempt for as long as its condition holds, permanently starving every check below it. Six live incident patches went in one night, covering consecutive counter duplication across check_task_verification_flagged and run_completion_check's own force_whole_rebuild, final verdict salvage's hardcoded problem name tuple widened three times, check_task_verification_flagged/check_thin_coverage left uncapped, and a hand written lambda c: A(c) or B(c) starvation guard for report_underuses_findings/_evidence that was live confirmed dead code, since or's short circuit meant the already winning check got tried first. At that point the user explicitly asked to stop patching incident by incident: "we're having too much issues with starvation we need to create a defined structure, we're guessing."

The structural fix, in src/engine/completion.py: _consecutive_occurrences gained an optional skip_problems set, becoming the one canonical counting definition, previously duplicated three times with drifting copies of the same fix. CONSECUTIVE_SAME_PROBLEM_ESCALATION_THRESHOLD got hoisted to module level, since two separate copies of "3" had already silently disagreed once mid session, caught by the test suite. A new _capped(ctx, problem, verdict, skip_problems) became the required call for any non self resolving check. A new _STARVATION_YIELD_TARGETS dict plus _apply_starvation_yield replaced the buggy lambda with a declarative mechanism that can't repeat the ordering bug by construction. Final verdict salvage's hardcoded tuple got replaced with an unconditional call, since the function's own 200 character gate was always the real safety check. A systematic audit, not another live incident, found 2 more previously unknown real instances, check_propagated_ungrounded_content and check_report_underuses_evidence, both missing a cap, both found by checking every entry in both lists against the invariant before they ever caused a failure. A new standing test in test_structural_checks.py asserts every non self resolving check in COMPLETION_CHECKS + GROUNDING_CHECKS calls _capped, which is the actual "defined structure" payoff, a future check that skips it fails the suite immediately. The design is grounded in real prior art: OS scheduler aging, the circuit breaker pattern (Nygard, Release It!), Chain of Responsibility (GoF), and a 2026-07-14 industry piece applying the same retry/circuit-breaker/fallback-chain shape to production LLM agent systems, confirming this is a known missing layer, not a codebase specific invention. Live verified post refactor: a fresh gpt-oss run against the standing sales forecasting benchmark converged cleanly through the full check chain, no starvation, no stuck pattern, and produced a real, honestly caveated, correctly grounded final_report.md. Full detail is in ARCHITECTURE.md section 1. One honest caveat this fix does NOT resolve: that same live verification run's report, while structurally sound, didn't actually meet the query's own expectations, no "top 5 heuristic algorithms" list, no Colombia cultural pattern integration, despite check_report_underuses_findings/_evidence correctly flagging both gaps every attempt. The pipeline is now working as designed, real convergence, no fabrication, but the model's own tendency to abandon the harder half of a multi facet query under repeated correction is a genuine, separate, still open capability gap. See the Model Bake off page's gpt-oss:20b entry.

check_task_verification_flagged escalation streak fragmentation plus a static warning text bug. Implemented and live verified on 2026-07-29, found while live smoke testing the two fixes above. Two live runs of the standing two facet benchmark query, one fresh, one --resume-run, both ended Report: NOT WRITTEN, with task_verification_flagged cycling for the entire retry budget and never reaching findings.md/final_report.md at all. Root caused via _run_state.json directly, not assumed: check_task_verification_flagged's escalation from "delegate tasks again" to "acknowledge the gap," and eventually to force_whole_rebuild, counts a consecutive streak of the same problem, but check_untracked_delegation firing once in between, itself a direct symptom of the model failing to comply with this check's own "stop redelegating, reuse the exact task_name" directive, confirmed live, reset that streak to zero. So a run stuck on the identical underlying problem for its whole budget kept getting the same "redo" directive instead of ever escalating. Separately, the check's own .warning field, the human or log facing message distinct from .inject, the model facing directive, was a static string always claiming "Pushing agent to redo them specifically" regardless of which of the three branches (fresh redo, acknowledge after repeat, quota exhausted stop) actually fired, which made diagnosing this exact incident from _run_state.json alone harder than it should have been. Fixed: check_task_verification_flagged's own prior_same counting now treats untracked_delegation as a continuation, not a break, scoped specifically to that one symptom, since a genuinely different interrupting problem still correctly resets it. .warning now has three distinct messages matching the three directives. Live reverified directly against both failed runs' actual saved _run_state.json sequences: replaying the exact attempt history now correctly produces "acknowledge the gap" instead of "delegate tasks again" at the point the real run got stuck.

check_report_underuses_evidence, a Builder stage per task coverage check. Implemented and live verified on 2026-07-29. check_findings_underuses_evidence (2026-07-26) already guarantees every covered top level task has at least one real URL surviving into findings.md, but nothing then guaranteed Builder's own selection from findings.md represented every task either. check_report_underuses_findings's flat citation ratio can pass while every surviving citation comes from a single task, confirmed live on 2026-07-28 (part 4 of the Literature Review): gpt-oss:20b dropped a heuristic algorithms task entirely for an off topic citation, but still cleared the 50 percent ratio threshold on Colombia's larger raw source count. This one is literature grounded (the NAACL 2025 "Coverage based Fairness in Multi document Summarization" paper, arXiv:2412.08795, validates a binary per cluster presence check as a legitimate, cheap proxy for their more expensive NLI based "Equal Coverage" metric). It's a new sibling check with the same per task binary presence design as its upstream sibling, wired into GROUNDING_CHECKS and the existing starvation guard. Live smoke tested (real gpt-oss:20b, a 48 minute run, the standing two facet benchmark query): it correctly stayed silent on a report that unevenly but genuinely covered both tasks, no false positive.

Check priority shadowing, or cross fix regression blindness. Implemented and live verified on 2026-07-29. The same live smoke test above surfaced a second, independent, more serious bug once the user insisted the actual report content be read, not just checked for crashes: check_uncited_claims never got a turn across 3 completion check attempts because check_stub_source kept winning. Traced to real_grounding_problem's own internal ordered if chain (utils/grounding.py) returning only its first hit, so every GROUNDING_CHECKS function keyed off the single shared ctx.grounding_problem string can be permanently shadowed by a persistently recurring higher priority one, and re running a sibling check against the same ctx can never reveal a second problem, since the fact was never computed. The terminal "retry budget exhausted" message reported only stub_source, even though uncited_claims, 6 figure bearing lines with no citation, confirmed by calling find_uncited_claim_lines directly against the run's actual saved output, was independently, simultaneously true the whole time. This one is also literature grounded ("Regression Accumulation in Multi Turn LLM Programming Conversations," arXiv:2607.01855, finds 55.7 percent of multi turn regressions are a later fix breaking an earlier requirement through incompatibility, not forgetting, and their validated fix, "Verification Gate," is full re verification every turn with every failing constraint made visible). Fixed at the correct layer: utils/grounding.py::cheap_grounding_problems extracts the pure string and regex sub checks (stub_source, non_url_citation, regulation_unsupported, quote_paraphrased, claim_unsupported, uncited_claims, deliberately excluding NLI and reranker model inference checks to avoid multiplying that cost every attempt) into a function that returns every hit, not just the first. real_grounding_problem itself is unchanged in behavior, verified against 18 other call sites. completion.py gained _other_grounding_problems/_with_other_grounding_addendum for the correct layer plus _collect_other_active_problems/_with_other_problems_addendum for COMPLETION_CHECKS, whose checks are genuinely independent of each other, unlike most of GROUNDING_CHECKS, confirmed via direct testing after the first implementation attempt targeted GROUNDING_CHECKS itself and was proven dead code by feeding it this session's own real incident data. This deliberately does not change which single Verdict is "the" recorded problem, the escalation or bonus counters, or the verdict matrix tests, only the addendum text shown to the model and the terminal user facing message gain the extra visibility, preserving the one primary directive per turn design. Live reverified directly against this session's own captured incident: feeding the real saved final_report.md into the fixed code now correctly surfaces uncited_claims alongside stub_source.

FindingsWriter evidence base quality, two fixes. Implemented and live verified on 2026-07-21, confirmed working at their own layer, though the deeper root behavior they were meant to help with stayed open (see the Pending section of ROADMAP.md). Root caused via the actual session transcript, not guessed: a live run's FindingsWriter dispatch abandoned a complete, in budget, 30 real finding evidence base ("we don't see them here," its own recorded reasoning) and went to re read raw source files by hand instead, writing findings.md from only 1 of 33 real findings. This was researched against real prior art before building (RAG noise robustness literature, DeepResearch Slice arXiv:2601.03261, read in full, whose diagnosis of "distracted by spurious passages" as one root cause of exactly this retrieval utilization gap directly supported fix 1 below; LLMxMapReduce arXiv:2410.09342, read in full, informing a not yet built chunked dispatch alternative). Fix 1 filtered relevance flagged findings out of the citable list (src/engine/completion.py::_is_citable_finding): a finding confirmed off topic by orchestrator.py's scope relevance check was still rendered as an ordinary citable entry, indistinguishable from genuinely useful findings around it, live case was a Colombia holidays task that fetched a New Zealand page. Scoped specifically to the relevance marker, not the two verification warning variants, since those are a narrower citation mismatch that may coexist with real content. Fix 2 threaded the already extracted page title through to the evidence base: the evidence base rendered ### Source: {url} with no title while FINDINGS_WRITER_INSTRUCTIONS requires the model's own output to be ### [Title](url), a real format mismatch forcing the model to invent a title for every entry before it could start. A real title is already extracted at fetch time but was only ever written into the saved file's own header, never threaded to run_state.data["fetched_urls"]. Now it is. A live re test confirmed both fixes fire correctly, the one relevance flagged finding this run was correctly excluded, and most fetched URLs got a real title in the correct format, but FindingsWriter's first action was again reading raw source files directly, bypassing the now cleaner, better formatted evidence base entirely, only 2 of 44 real findings made it into findings.md. Two independent live runs now confirm the model's raw file exploration habit is independent of evidence base noise or format, real progress on evidence base quality, but not the fix for the actual behavior.

FindingsWriter evidence abandonment, root cause closed on 2026-07-22, moved here from Pending on 2026-07-24 after a documentation sync gap, since the fix shipped the day after the entry above but Pending was never updated to reflect it. The two contributing factor fixes above held at their own layer but didn't change the headline behavior. Two further changes, same session (commit f1562f7, "Fix FindingsWriter evidence-abandonment: structural write-first gate + dedupe multi-URL fan-out"), actually closed it. First, writer_gate_ctx (src/tools/core.py, a new contextvar plus check_writer_gate, wired into with_quota's sync and async wrappers) blocks read_workspace_file/grep_workspace_file until the armed gate's write_workspace_file call has happened, armed only for FindingsWriter, never Builder, whose own instructions correctly require reading findings.md first. A prompt only reorder of FINDINGS_WRITER_INSTRUCTIONS' workflow was tried first and live disconfirmed, the model still called read_workspace_file first, before escalating to this structural gate, which is what actually held. Second, _collapse_multi_url_task_findings (completion.py) was found while investigating why even a gate forced first write stayed thin. orchestrator.py::_run_single_task's add_finding call fires once per fetched URL but attaches the same task level summary every time, so a task fetching N URLs produced N near identical "findings," inflating the raw count and letting whichever task fetched most URLs dominate the position bias reorder's favorable edges by fetch count alone. This groups citable findings by task name and summary before rendering, keeping the body text once per group while every real URL is still individually named for citation. Live confirmed closed on a third smoke test run: the evidence base collapsed to 5 real distinct clusters from a raw 15 citable findings, and findings.md wrote all 5 of 5, with the first FindingsWriter dispatch calling write_workspace_file directly with zero blocked reads, the gate never even needed to fire. Only 2 completion check retries total this time, versus 4 and 6 in the two prior live tested runs, the fastest, cleanest convergence of the three. final_report.md ended up thin but honest, 3 of 5 sources genuinely had nothing usable, and Builder said so instead of fabricating filler. Two smaller items surfaced during this investigation but weren't covered by this fix and got tracked separately: one Searcher sub agent fired around 14 near duplicate web_search calls on one angle without diversifying into a sibling angle its own task line implied, and a cosmetic mojibake filename showed up in one findings.md heading, diagnosed as the model degenerating into token repetition on a genuinely empty source rather than a code bug.

Shared quota starvation, angles (a) and (c). Implemented and live verified on 2026-07-21, found via a live benchmark run, the standing sales forecasting and heuristic algorithms query, right after this session's 4 synthesis fixes shipped, that failed outright: Report: NOT WRITTEN, thin_coverage 4 times consecutively, retry budget exhausted. Root caused with hard evidence, not assumption: one sibling task produced 14 finding entries and 26 tool call log lines, while its four siblings got exactly 1 log line each and the model's own narration said outright "could not be retrieved due to exhausted search quota." Angle (a), in src/tools/core.py::check_quota: the existing per task rescue required task_fetched_urls_ctx to be non empty as proof a task had already fetched something, but web_search never populates that context var, only fetch_url_to_workspace does, so a task blocked on its own first web_search call could never qualify, exactly backwards from what a starved task needs. Dropped the requirement, every task now gets one guaranteed grace top up the first time it hits the wall, proven progress or not, still bounded to once per task_id. Angle (c), in src/engine/orchestrator.py, added a new _reserve_batch_quota_headroom at delegate_tasks's call site: with max_concurrent_tasks small, often 1, tasks in one delegate_tasks batch run roughly in listed order, sequentially draining the same shared pool, so a heavily active sibling can structurally starve later listed ones even across completion check topups. This pre reserves enough total headroom for the whole batch before any task in it starts, removing the structural first mover advantage, though it's not true round robin scheduling, which would need cooperative mid turn interleaving, a much bigger change, it just bounds the damage instead of eliminating dispatch order entirely. A live re test on the same query confirmed the fix: zero thin_coverage attempts versus 4 before, 25 findings recorded across 9 balanced task angles versus one task dominating 14 of 28, and a real report converged in 4 attempts. Quota starvation itself is closed.

A second, distinct bug turned up in that same re test and also got fixed: findings.md carried an AUTO-RECOVERED DRAFT banner, FindingsWriter had narrated instead of calling write_workspace_file, and the narration itself got cut short by its own generation budget partway, capturing only 3 of 25 real findings. Root cause: SUBAGENT_BUDGET_NUDGE (orchestrator.py) is a single, role blind nudge shared across every dispatched role, and its text, "do NOT call any more tools, return as your final message," is correct for Searcher and Analyzer, whose final message is their findings, but directly instructs a writer role, whose entire success criterion is calling write_workspace_file, to do the exact opposite of its job the moment it hits budget pressure. A new _select_budget_nudge (pure, testable) plus SUBAGENT_BUDGET_NUDGE_WRITER route Builder and FindingsWriter to a write the file now nudge instead. This was the user's own catch, not found via self audit, findable by reading the code without needing a live failure, folded into the standing "check all surfaces" lesson. A follow up live re test confirmed this specific bug was fixed, but surfaced a deeper, still open one: no AUTO-RECOVERED banner this time, findings.md written via a real write_workspace_file call, faster convergence, but findings.md contained exactly 1 of 33 real findings recorded in _run_state.json, since the model chose to write a single well formatted, fully cited entry rather than all 33, and no existing check verifies "findings.md reflects everything found," only "findings.md exists and is formatted correctly." The final report was grounded, zero fabrication, but entirely about an unrelated topic, missing every Colombia and heuristic algorithm angle despite most fetched sources containing real Colombia content. Net effect: this converted a total loss failure into a partial loss failure, real progress, not a full fix. The underlying pressure, that context_budget_chars's single roughly 50,000 character ceiling is too tight for FindingsWriter to consolidate a large finding set in one dispatch, narrating or writing, stayed open and tracked as its own item.

Four literature backed synthesis and reliability candidates. Implemented on 2026-07-22, planned and executed the same session. All four extend src/engine/completion.py's GROUNDING_CHECKS/_dispatch_writer_review_fix/run_completion_check. The first, findings ordering, drew on "Lost in the Middle" (arXiv:2307.03172) and PING's "Anchor Effect" (arXiv:2601.22984): _reorder_findings_for_position_bias, right before _build_findings_source_material, is a pure positional zigzag or sandwich reorder, since no value or importance signal exists anywhere in the data model to rank by, splitting chronological entries in half and interleaving front forward with back reversed so every finding lands within one hop of either edge of the assembled context instead of drifting toward the middle as a run accumulates more findings. The second, a propagation aware hallucination check drawing on the PING taxonomy, got narrowed from the paper's full DAG plus NLI entailment design, since DeepDelve has no claim dependency structure and the paper's own released code doesn't actually implement that mechanism either, down to the specific, already documented split brain pattern: a citable finding whose content substantially term overlaps an uncited or cutoff sibling for the same task name. A new _find_propagated_bad_content plus check_propagated_ungrounded_content only fires if the flagged content also appears inside the report itself, not merely in findings.md. The third, forcing reasoning at synthesis time, drew on PIVOT (arXiv:2605.11225), and corrected the original candidate's premise: BUILDER_INSTRUCTIONS/FINDINGS_WRITER_INSTRUCTIONS already instruct think_tool use before finalizing, confirmed by reading both directly during planning. So this shipped as verification rather than a new prompt: _dispatch_writer_review_fix now snapshots think_tool's quota usage before and after the Write dispatch, reusing the same reads before and after pattern already proven there for PeerReviewer's read_workspace_file check, deliberately not a hard gate since a writer skipping think_tool makes no false claim, unlike a fabricated "REVIEW: CLEAN." The fourth, full artifact rebuild repetition escalation, drew on an ACM CAIS '26 planning horizon paper and also corrected the original candidate's premise: the real current behavior of CONSECUTIVE_SAME_PROBLEM_ESCALATION_THRESHOLD was "give up early" on 3 consecutive identical problems, not a "narrower nudge" as originally described, so there was no existing gentler nudge to sharpen. The user decided on "the more complete one," a full rebuild rather than a wording only reframe, matching the paper's actual full horizon replan mechanism: on the third consecutive occurrence, the system grants exactly one extra attempt, bounded to once per problem type, that dispatches a genuine full rebuild instead of the usual targeted fix instructions. Three more literature leads found via fresh search, plus three RAG repos the user asked about, LightRAG, RAG-Anything, and GraphRAG, were also researched this session but none were adopted, either reviewed and not applicable or too weak an evidence base on a full read.

Engine driven iterative deepening, from ROADMAP item 10 and the dzhng/deep-research candidate. Implemented on 2026-07-19, found still mis listed as an untried candidate during the 2026-07-21 status audit. This is a structural refine loop: each round's Searcher findings carry a FOLLOW-UP DIRECTIONS section, stored per finding. When check_thin_coverage fires, _select_deepening_tasks picks real, unconsumed directions and geometrically narrows them, using the exact newBreadth = ceil(breadth/2) formula from the source project, deduplicated against already consumed directions so a retry never redispatches the same lead twice. _dispatch_deepening_round then dispatches them directly, bypassing the Planner entirely, the same mechanism _dispatch_writer_review_fix already used for Builder and FindingsWriter retries, so the engine drives the extra research round rather than hoping the Planner model chooses to loop.

A non generative routing classifier for delegate_tasks's agent_id. Implemented and live verified on 2026-07-20, merged from the SOTA literature review: small and mid sized models fail disproportionately at structured serialization, schema valid output but wrong content, not semantic understanding, in a way a 6,000 sample SFT run can't fix, per the constraint tax papers, confirmed live in this project's own data at roughly 4.9 percent of real delegate_tasks calls using a hallucinated but well formed agent_id. Routing got pulled out of free generation into a frozen all-MiniLM-L6-v2 embedding plus a LogisticRegression(class_weight="balanced"), with a decided policy of reject and nudge, not silent override, not advisory only. A new extraction script, run against 101 real session logs, found 1,096 valid pairs and 57 hallucinated, about 4.9 percent, matching an earlier ad hoc count almost exactly, now from a reproducible script. After near duplicate removal, 814 valid pairs remained, with a class distribution of DocumentAnalyzer 331, WebSearcher 330, DataAnalyzer 99, and AcademicSearcher 54, split 651 train and 163 held out. A new training script got real held out per class results: DocumentAnalyzer at 0.89 precision and 0.88 recall, WebSearcher at 0.89/0.85, DataAnalyzer at 0.68/0.65, and AcademicSearcher, the weakest since it's the smallest class, at 0.44/0.64. Overall accuracy came out to 0.82. A new src/utils/agent_routing.py module is a lazy singleton that fails open, and a real design gap got found and fixed before shipping: the classifier's 4 known classes span two different delegation levels, so predict_agent_id takes a candidate_classes param restricting prediction to the intersection of the caller's own real roster and the classifier's known classes, never a blind global argmax. New logic in src/engine/orchestrator.py rejects a delegate_tasks call when the declared agent_id isn't real for this caller, or when the classifier disagrees above min_confidence, silently no opping on the common case. The config default shipped as enabled: false, a conservative rollout matching this project's own standing caution about untested features.

The first live end to end test, same day, found a real regression and reverted the config back to enabled: false. With the feature flipped on, a real headless query exercising both delegation levels worked correctly at the nested level, but the Planner level AcademicSearcher dispatch was wrongly rejected 8 consecutive times, every single retry of "find peer reviewed papers on borrow-checker soundness" got rejected as "looks like a WebSearcher task" at 0.67 to 0.75 confidence, above the 0.6 threshold, across roughly 2 real minutes and 8 wasted Planner turns, until the Planner gave up on the angle entirely. The concrete, measured harm: the final report contained zero content on the academic angle, half the user's actual question never got researched, a real content coverage failure directly caused by this feature. Root cause: AcademicSearcher is the classifier's weakest, smallest class, and a flat min_confidence=0.6 threshold treats a confidence number as equally trustworthy regardless of which class produced it, but this run showed the model can be confidently wrong specifically for the class it's worst at. This was a structural policy flaw, not just a data problem: reject and nudge conflated two different risk profiles under one policy, rejecting a declared role that isn't real for this caller at all versus rejecting a declared role that is valid but the classifier disagrees with.

A same day data quality fix found a real, root cause bug in the training data itself. Auditing the 54 AcademicSearcher examples by eye surfaced 6 that were obviously market sizing or business research tasks, not literature searches, tracing further to 13 total from one session, a near identical task template repeated with a different sector noun each time, sitting alongside a differently phrased sibling task in the same session correctly labeled WebSearcher. Two general purpose automated detectors were tried and rejected here, worth keeping the reasoning for: plain text similarity never caught this since different sector nouns keep character overlap below the dedup threshold, and embedding cosine similarity couldn't cleanly separate "same underlying ask, inconsistently routed" from "different ask, same topic domain, correctly routed differently," since this project's own real historical benchmark queries cluster so heavily on one topic that a loose enough threshold also flagged hundreds of genuinely correct pairs. The team landed on a direct, documented exclusion of the one manually verified session instead of forcing an unreliable general heuristic. After retraining on the cleaned data, the AcademicSearcher class shrank from 54 to 50 examples, but held out recall rose from 0.64 to 0.80 at the same 0.44 precision, fewer false negatives, directly the failure direction that caused the live regression. All 3 real instruction variants that failed live now correctly predict AcademicSearcher. A second, same day live re test of the exact query that failed before confirmed the fix held in the real pipeline: zero classifier rejections anywhere in the run, and the AcademicSearcher task was accepted on the first try. A second, unrelated bug surfaced in that same run and got recorded separately: final_report.md dropped the entire academic section despite findings.md having it correctly, root caused to the run hitting 3 completion check remediation cycles that each burned read_workspace_file quota, exhausting it by the third BuilderFix pass, which then said outright it couldn't re read the source file and cited only 2 of roughly 7 real sources. This wasn't fixed in this session, just newly found.

Sub agent dispatches had no wall clock deadline at all. A real, live confirmed gap, fixed and live verified. engine/tui.py's run_cli already raced each stream update against settings.max_run_minutes via asyncio.wait_for instead of a plain async for (a 2026-07-12 fix, because a plain async for only checks a deadline once it actually receives an update, invisible to a stream that goes silent for a long time). That fix never propagated to engine/orchestrator.py::_run_single_task, the code path every Searcher, Analyzer, Builder, FindingsWriter, and PeerReviewer dispatch goes through, which used a bare async for update in stream: with zero deadline, relying entirely on the raw OpenAI SDK HTTP client's blunt roughly 600 second default connection timeout, which then discards the whole in progress response and raises a generic error instead of degrading gracefully. Root caused live on 2026-07-14 during Phase 4 smoke testing, after the user pushed back on accepting repeated live run timeouts as "just model slowness" without further investigation, correctly, since the real cause turned out to be structural: cross referencing Ollama's own logs against the exact failure window found two requests that returned HTTP 500 after hanging exactly 10m0s and 6m24s. The 10 minute one wasn't actually stuck, Ollama's own timing log showed it continuously, validly decoding tokens the entire time up to nearly 20,000 tokens when the connection was force closed, matching almost exactly what 600 seconds at that model's real token rate would produce. A single sub agent turn had run away into a very long generation with no early cutoff mechanism watching it at all. A first fix attempt was itself incomplete, caught by live verification, not just the unit suite: it gave _run_single_task the same max_run_minutes deadline as run_cli's own top level guard, anchored to the same run start clock. Live testing with a tight 2 minute budget showed a cutoff did fire, but the persisted session log showed the new inner marker text never actually appeared, since the outer guard's cancellation had propagated down and pre empted the inner one before its own deadline check ever got a chance to run on its own terms, meaning in realistic configs the new code was effectively dead. The real fix was a new, independent settings.sub_agent_timeout_minutes (default 10), a fresh per dispatch deadline computed at the start of each _run_single_task call, not shared with the run wide clock. The client's own SDK timeout also got bumped to run comfortably past both budgets, otherwise the SDK's own blunt default kept winning the race against either graceful cutoff. Live verified directly: with a generous 45 minute run budget and a tight 1 minute sub agent budget, a sub agent dispatch was cut short within a minute, and the Planner's own next turn literally said "The task timed out, but the capital of Italy is a well-known fact (Rome)... I stop delegation immediately," proving the graceful marker text reached the Planner and it correctly adapted instead of hanging, retrying blindly, or crashing. This also retroactively explained several earlier "timeout" observations from that same day's smoke tests that had been attributed to model slowness or complexity, since this structural gap was the common thread making any of those failure modes catastrophic, total loss of the turn, no graceful degrade, instead of just slow.

Clone this wiki locally