-
Notifications
You must be signed in to change notification settings - Fork 0
Completed
Finished ROADMAP items, moved out of ROADMAP.md to keep that file focused on what's still open.
-
Builder duplicate-section bug: an
edit_workspace_filecall that retypes existing content while adding new material — root-caused from the live session transcript, IMPLEMENTED and live-validated, 2026-08-17. Tracedfinal_report.md's two near-identical "Mexico City" / "Mexico City – Central Districts" sections to the exactedit_workspace_filecall that caused them (BuilderFix_attempt3_reviewed, the run's last action):old_string="### Mexico City"(the bare heading),new_string=heading + the heading's own PRE-EXISTING bullet retyped verbatim + a new subsection — since the original bullet was never part ofold_string, it stayed where it was, and the retyped copy landed directly in front of it. Fixed at both layers: (1)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 intonew_string; (2)find_duplicate_report_sections/check_duplicate_report_sections(src/engine/completion.py) — a new structural, self-consistency completion-check comparing every h3+ subsection of a report against every other via_content_word_overlap(threshold 0.6), registered inGROUNDING_CHECKS/_QUARANTINE_PROBLEMS/_BUILDER_FIXABLE_PROBLEMS, gated by newsettings.duplicate_section_check.enabled. Live-validated by running the new function directly against the real incident'sfinal_report.md: correctly flags exactly the one real duplicate section and nothing else (no false positives on the genuinely-distinct Lisbon/Portugal sections). SeeROADMAP.md's Pending "UNIFIED LIST" entry (item B.4, now resolved) andsession_status/CURRENT.mdfor the full incident writeup. -
Citation misattribution: a real, correctly-fetched claim gets attached to the wrong cited URL — IMPLEMENTED and live-verified against the real incident data, 2026-08-17. Found via a manual claim-by-claim audit of the ablation smoke-test's own
final_report.md(user asked "did you evaluate the report properly?" — the automated structural score had missed it entirely): four real, accurately-worded Mexico-visa figures ("300 days" income formula, "$53" fee, sworn- translator requirement, MiConsulado interview) were all traced verbatim tothemexicohandbook.com's fetched content but cited toesimcard.cominstead — both URLs were genuinely fetched this run, so the hard grounding gate passed, andclaim_grounding_problem's term-overlap gate passed too, on nothing more than a coincidentally shared bare year (both sources being 2026-dated visa guides) —extract_salient_terms's own number regex is narrow enough (period-grouped decimals, bare 4-digit years, percentages only) to never extract "300" or "53" as a checkable term at all. Same failure shapefind_unsupported_regulation_idsalready catches for regulation identifiers ("the URL-presence gate passed... and the zero-overlap content check passed (shared generic terms)... so a misattributed law number sailed through both," live 2026-07-11) — 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/currency figures and day/month-count claims, flags one verbatim-absent from its own cited source. Wired intocheap_grounding_problems(newsettings.grounding_check.specific_figure_ check, default true) and a newcheck_specific_figure_unsupportedcompletion-check (src/engine/completion.py), registered inGROUNDING_CHECKS,_QUARANTINE_PROBLEMS, and_BUILDER_FIXABLE_PROBLEMS. A real implementation bug 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 (e.g. 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 before the presence check, then re-validated against the same real report: false positives (€1,000, $2,050×3) resolved, while the true incident (300 days, $53) AND two more previously-unnoticed real grounding failures in the SAME report (a fabricated "€1,000–€1,800" Lisbon rent range cited to a property-purchase-price page with no rental data at all; the model's own unstated "$1,200–$1,400" USD approximation, absent from every fetched source) were all correctly caught. Fulltest_structural_checks.pycoverage: a directfind_unsupported_specific_figuresunit scenario (including a regression test for the comma-formatting bug) plus a new verdict-matrix row. Seesession_status/CURRENT.mdandROADMAP.md's Pending "UNIFIED LIST" entry (item A.1, now resolved) for the full incident writeup. -
Output/export feature gaps + optional HTTP API/web UI — IMPLEMENTED and live-verified, 2026-08-02. Scoped from a purpose-vs-built-feature gap check (not 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. Full plan in~/.claude/plans/cosmic-growing-canyon.md; session-by-session detail insession_status/2026-08-02*.md(archived).-
Folder-name collisions —
_slugify_run_dir_name(engine/tui.py) now appends a 6-hex sha1 hash of the FULL query, not just its truncated slug. -
references.bib—utils/run_state.py::build_bibliography, built from whatfinal_report.mdACTUALLY cites (grounding.py's ownextract_cited_urls/parse_academic_references— the same logic every grounding check already trusts), not the raw fetched-URL list. A bibliography from "everything fetched" would list sources the report never used — caught and corrected before implementation, not after. -
--seed-doc//seed-doc— local document ingestion (PDF/DOCX/XLSX/PPTX/txt/md viautils.parsers.convert_to_markdown, the same extractortools/web.py's PDF-fetch path already uses), mirrors the existing--seed-urlpattern exactly. Deliberately no new tool exposed to the model — the engine loads the file into the workspace,read_workspace_file(already exists) is all a sub-agent needs. -
settings.pdf_engine— optionalfinal_report.pdfviapandoc(external binary, not pip-installable),weasyprintor system LaTeX as the engine. Real bug found and fixed: pandoc's--pdf-engine=weasyprintshells out to theweasyprintCLI, which lives in the venv'sbin/, not the system PATH when invoked as~/.venvs/deepdelve/bin/pythondirectly — fixed by prependingos.path.dirname(sys.executable)to the subprocess env's PATH. -
src/api.py+src/static/index.html— optional FastAPI HTTP API + web UI (Research/ Runs/Settings),pip install -e ".[api]",deepdelve-apiconsole script. Key finding before writing any code:orchestrator.py's_sessionandtui.py's session-log state are module-level globals, not contextvars — two truly concurrent runs in one process would corrupt each other's state. Fixed by design: one in-process FIFO job queue, one worker coroutine, always single-flight. Deliberately a THIRD copy ofrun_cli/run_agent's orchestration shape (reusing the same lower-level primitives), not a fourth shared abstraction — extracting one now would mean touching the two already-shipped entry points as a side effect of an unrelated change (same reasoning as therun_cli/BasicTuiAgentunification item below, which stays its own separate task).- Endpoints:
POST /research(multipart, file uploads reuse--seed-doc's_ingest_local_doc),GET /research/{id}/status|stream|report|bib|pdf,POST /research/{id}/resume|followup|cancel,GET /runs,GET/POST /settings. -
Follow-up design correction, caught before implementation: originally planned to lean
on
orchestrator_module._sessionsurviving in memory between a run and its follow-up — but the shared queue can run an unrelated job in between, silently clobbering it via that job's own session reset. Fixed by persistingsession.to_dict()to<run_dir>/_agent_session.jsonper-run (reusing the existingAgentSession.to_dict()/from_dict()mechanism--resume <session_id>already uses for a different purpose) instead of trusting the global. -
Real bug found live, not just written and assumed correct:
/followup's first version ran the FULL completion-check/artifact-rewrite pipeline against a follow-up question, same as a fresh run — confirmed this rewritesfindings.mdrepeatedly (3 rejected attempts) and never touchesfinal_report.md, because the pipeline doesn't understand a follow-up is a narrower ask than the original query. Root cause: missed thattui.py's ownrun_agent/is_followupbranch deliberately setsskip_completion_check = Trueonce the required artifact already exists (a follow-up is Q&A over existing research, not a report rewrite) — fixed by mirroring that exact condition inapi.py::_run_research. -
Two more real bugs found live during the frontend pass: (1)
_artifact_pathgated report/bib/pdf routes onrun_id in _jobs(in-memory, this-process-lifetime only) — any run from a prior server process, or never touched by this API instance at all (started via TUI/CLI), 404'd with a misleading "Unknown run_id" despite having real artifacts on disk; fixed to check the workspace directory directly, same fix already applied to/followup. (2) FastAPI doesn't auto-add HEAD support to a@app.getroute — every "does this artifact exist" check in the frontend usedfetch(..., {method:"HEAD"})specifically to avoid downloading full content just to check presence, and all of them silently 405'd, so no Report/Bibliography/PDF button ever appeared; fixed via@app.api_route(..., methods=["GET","HEAD"]). -
settings.api_password— unset by default (no auth, matches the loopback-only-by- default posture); added specifically because full settings CRUD (API keys included) + user-requested LAN/phone access together raised the stakes enough to warrant it. Layered on top of, not instead of, the--i-understand-the-risknon-loopback bind guard. -
Web UI: no CDN/build step (a small dependency-free markdown→HTML renderer for the report
viewer, not a general CommonMark library — this project's own report dialect is narrow and
predictable), settings rendered as a real generated form from the config's own shape (not a
raw JSON textarea), a signal-lamp status indicator as the one deliberate animated element
(reduced-motion respected). Status text shows the user's actual query, never the internal
run-id slug (
i_want_documentation_on_heuristic_..._20260731_140130is not something a user recognizes — caught live after a resume+cancel left it as the only visible status). Mobile-responsive layout built to spec but not visually verified in this session (no browser automation tool available) — check on an actual phone before treating that part as done.
- Endpoints:
- Also folded in, same session: all 5 findings from an earlier 3-agent audit (security/
correctness/production-readiness) — SSRF guard on outbound fetches (
tools/web.py, anhttpxevent hook checking every request's real resolved target, including mid-redirect- chain hops, against loopback/link-local/private/reserved ranges),no_urls_countresume- carryover gap, traceback-leakage trim acrosstools/core.py/web.py/fs.py(exception type+message only, no local filesystem paths handed to the model),enable_session_ persistence's_DEFAULTSfallback fixed to matchconfig_template.yaml's real default, and a newtest_tools.py(same flat assert-based convention astest_structural_checks.py, wired into CI) pinning the SSRF guard plus several previously-untested pure-logic functions.
-
Folder-name collisions —
-
Completion-check starvation bug class: 8 real instances found and fixed, then replaced with one shared structural mechanism — IMPLEMENTED and live-verified 2026-07-31. Started as an Ornith- 1.0-9B re-test (see MODELS.md's Ornith entry), 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_CHECKScheck that isn't Builder/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 in one night (consecutive-counter duplication acrosscheck_task_verification_flaggedandrun_completion_check's ownforce_whole_rebuild; final- verdict salvage's hardcoded problem-name tuple widened three times;check_task_verification_ flagged/check_thin_coverageuncapped; a hand-writtenlambda c: A(c) or B(c)starvation guard forreport_underuses_findings/_evidencethat was live-confirmed dead code,or's short-circuit meaning the already-winning check got tried first) — 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."-
Structural fix (
src/engine/completion.py):_consecutive_occurrencesgained an optionalskip_problemsset, becoming the one canonical counting definition (previously duplicated 3 times with drifting copies of the same fix).CONSECUTIVE_SAME_PROBLEM_ESCALATION_THRESHOLDhoisted to module level (two separate copies of "3" had already silently disagreed once mid-session, caught by the test suite). New_capped(ctx, problem, verdict, skip_problems)— the required call for any non-self-resolving check. New_STARVATION_YIELD_TARGETSdict +_apply_starvation_yield, replacing the buggy lambda with a declarative mechanism that cannot repeat the ordering bug by construction. Final-verdict salvage's hardcoded tuple replaced with an unconditional call (the function's own 200-char gate was always the real safety check). -
Systematic audit (not another live incident) found 2 MORE, previously-unknown real
instances:
check_propagated_ungrounded_contentandcheck_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. -
New standing test (
test_structural_checks.py): asserts every non-self-resolving check inCOMPLETION_CHECKS + GROUNDING_CHECKScalls_capped. This is the actual "defined structure" payoff — a future check that skips it fails the suite immediately. - Prior art grounding the design: 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 — confirmed 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),
produced a real, honestly-caveated, correctly-grounded
final_report.md. Full detail inARCHITECTURE.md§1. -
Honest caveat, NOT resolved by this fix: that same live-verification run's report, while
structurally sound, did not actually meet the query's own expectations — no "top 5 heuristic
algorithms" list, no Colombia cultural-pattern integration, despite
check_report_underuses_ findings/_evidencecorrectly flagging both gaps every attempt. The pipeline is now working as designed (real convergence, no fabrication); 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 MODELS.md'sgpt-oss:20bentry and the new Pending item below.
-
Structural fix (
-
check_task_verification_flaggedescalation-streak fragmentation + static warning-text bug — IMPLEMENTED and live-verified 2026-07-29, found while live-smoke-testing the two fixes above. Two live runs of the standing 2-facet benchmark query (one fresh, one--resume-run) both endedReport: NOT WRITTEN—task_verification_flaggedcycling for the ENTIRE retry budget, never reachingfindings.md/final_report.mdat all. Root-caused via_run_state.jsondirectly, not assumed:check_task_verification_flagged's escalation from "delegate_tasks again" to "acknowledge the gap" (and eventually toforce_whole_rebuild) counts a CONSECUTIVE streak of the same problem — butcheck_untracked_delegationfiring 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.warningfield (the human/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 — made diagnosing this exact incident from_run_state.jsonalone harder than it should have been. Fixed:check_task_verification_flagged's ownprior_samecounting now treatsuntracked_delegationas a continuation, not a break (scoped to that one specific symptom — a genuinely different interrupting problem still correctly resets it);.warningnow has three distinct messages matching the three directives. Live-reverified directly against both failed runs' actual saved_run_state.jsonsequences: replaying the exact attempt history now correctly produces "acknowledge the gap" instead of "delegate_tasks again" at the point the real run got stuck. Regression test intest_structural_checks.py(4 cases: interruption doesn't reset, unrelated problem still does, warning text matches each of the 3 branches). -
check_report_underuses_evidence— Builder-stage per-task coverage check, IMPLEMENTED and live-verified 2026-07-29.check_findings_underuses_evidence(2026-07-26) already guarantees every covered top-level task has ≥1 real URL surviving intofindings.md, but nothing then guaranteed Builder's OWN selection fromfindings.mdrepresented every task either —check_report_underuses_findings's flat citation ratio can pass while every surviving citation comes from a single task (confirmed live 2026-07-28,RESEARCH.md§14h:gpt-oss:20bdropped a heuristic-algorithms task entirely for an off-topic citation, but still cleared the 50% ratio threshold on Colombia's larger raw source count). Literature-grounded (NAACL 2025 "Coverage-based Fairness in Multi-document Summarization", arXiv:2412.08795 — validates a binary per-cluster presence check as a legitimate, cheap proxy for their more expensive NLI-based "Equal Coverage" metric). New sibling check (src/engine/completion.py), same per-task binary-presence design as its upstream sibling; wired intoGROUNDING_CHECKSand the existing starvation-guard (_yield_to_starved_check); newreport_evidence_checkconfig section (config_template.yaml). 5-case regression suite intest_structural_checks.py. Live-smoke- tested (realgpt-oss:20b, 48-minute run, standing 2-facet benchmark query): correctly stayed silent on a report that unevenly but genuinely covered both tasks — no false positive. -
Check-priority-shadowing / cross-fix regression blindness — IMPLEMENTED and live-verified 2026-07-29. The same live smoke test above surfaced a second, independent, more serious bug when the user insisted the actual report content be read, not just checked for crashes:
check_uncited_claimsnever got a turn across 3 completion-check attempts becausecheck_stub_sourcekept winning — traced toreal_grounding_problem's own internal ordered if-chain (utils/grounding.py) returning only its FIRST hit, so everyGROUNDING_CHECKSfunction keyed off the single sharedctx.grounding_problemstring can be permanently shadowed by a persistently-recurring higher-priority one; re-running a sibling check against the samectxcan never reveal a second problem, since the fact was never computed. The terminal "retry budget exhausted" message reported onlystub_source, even thoughuncited_claims(6 figure-bearing lines with no citation, confirmed by callingfind_uncited_claim_linesdirectly against the run's actual saved output) was independently, simultaneously true the whole time. Literature-grounded ("Regression Accumulation in Multi-Turn LLM Programming Conversations", arXiv:2607.01855 — 55.7% of multi-turn regressions are a later fix breaking an earlier requirement through incompatibility, not forgetting; 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_problemsextracts the pure string/regex sub-checks (stub_source, non_url_citation, regulation_unsupported, quote_paraphrased, claim_unsupported, uncited_claims — deliberately excluding NLI/reranker model-inference checks, to avoid multiplying that cost every attempt) into a function that returns EVERY hit, not just the first;real_grounding_problemitself is unchanged in behavior (verified: existing test suite covering it, including 18 other call sites, passed unmodified).completion.pygained_other_grounding_problems/_with_other_grounding_addendum(correct-layer) plus_collect_other_active_problems/_with_other_problems_addendum(forCOMPLETION_CHECKS, whose checks ARE genuinely independent of each other, unlike most ofGROUNDING_CHECKS— confirmed via direct testing, not assumed, after the first implementation attempt targetedGROUNDING_CHECKSitself and was proven dead code by feeding it this session's own real incident data). Deliberately does NOT change which single Verdict is "the" recorded problem, the escalation/bonus counters, or the verdict-matrix tests — only the addendum text shown to the model (.inject) 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 savedfinal_report.mdinto the fixed code now correctly surfacesuncited_claimsalongsidestub_source. Regression test intest_structural_checks.pyreproduces the same shape end-to-end (a report with both a stub citation and 6 uncited-claims lines in a separate section). -
FindingsWriter evidence-base quality, two fixes — IMPLEMENTED and live-verified 2026-07-21 (confirmed working at their own layer; the deeper root behavior they were meant to help with is still open, see Pending). Root-caused via the actual session transcript (
~/.deepdelve/sessions/, 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, writingfindings.mdfrom only 1 of 33 real findings. Researched real prior art before building (RAG noise-robustness literature, DeepResearch-Slice arXiv:2601.03261 read in full — its diagnosis of "distracted by spurious passages" as one root cause of exactly this retrieval-utilization gap directly supported fix 1 below; LLM×MapReduce arXiv:2410.09342 read in full, informing the not-yet-built chunked-dispatch alternative in Pending).-
Fix 1 — filter relevance-flagged findings out of the citable list
(
src/engine/completion.py::_is_citable_finding): a finding confirmed off-topic byorchestrator.py's scope-relevance check ([SYSTEM RELEVANCE WARNING...]) 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 (confirmed off-topic, zero value regardless of other content), not the two VERIFICATION-warning variants (a narrower citation-mismatch that may coexist with real content). Since_is_citable_findingis the shared predicate already reused by_build_findings_source_material,_uncited_task_names, and_find_propagated_bad_content, this one change correctly propagates everywhere. -
Fix 2 — thread the already-extracted page title through to the evidence base
(
src/utils/run_state.py::record_fetched_url,src/tools/web.py::_save_fetched,completion.py::_build_findings_source_material): the evidence base rendered### Source: {url}(no title) whileFINDINGS_WRITER_INSTRUCTIONSrequires 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 (_extract_html_metadata) but was only ever written into the saved file's own header, never threaded torun_state.data["fetched_urls"]. Now is (same "absent key when not present" convention asstub) —_build_findings_source_materialrenders### [{title}]({url})when available, falling back to the plain shape when not (non-HTML fetches, extraction failures). Confirmed safe first:extract_cited_urls(the actual URL-extraction workhorse every grounding check uses) is a bare URL regex, completely format-agnostic — changing the heading shape risked nothing downstream. -
Live re-test #4 confirmed both fixes fire correctly: the one relevance-flagged finding
this run was correctly excluded; 29/33 fetched URLs got a real title, 36/40 assembled entries
used the correct
[Title](url)format. But FindingsWriter's first action was again reading raw source files directly, bypassing the (now cleaner, better-formatted) evidence base entirely — 2 of 44 real findings made it intofindings.md, both generic definitional content, no domain-specific or Colombia content. Two independent live runs now confirm the model's raw-file-exploration habit is independent of evidence-base noise/format — real progress on evidence-base quality, but not the fix for the actual behavior. See Pending for the next candidate (workflow-order fix). -
test_structural_checks.pyextended for both (_is_citable_finding's relevance-exclusion plus a VERIFICATION-warning control case;record_fetched_url's title storage with/without a title;_build_findings_source_material's title-vs-fallback rendering). Full suite green.
-
Fix 1 — filter relevance-flagged findings out of the citable list
(
-
FindingsWriter evidence-abandonment ROOT CAUSE — CLOSED 2026-07-22, moved here from Pending 2026-07-24 (documentation-sync gap: 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 (Live re-test #4: FindingsWriter still hand-read raw source files first, 2/44 real findings made it into
findings.md). Two further changes, same session (f1562f7, "Fix FindingsWriter evidence-abandonment: structural write-first gate + dedupe multi-URL fan-out"), actually closed it:-
writer_gate_ctx(src/tools/core.py, new contextvar +check_writer_gate, wired intowith_quota's sync/async wrappers): blocksread_workspace_file/grep_workspace_fileuntil the armed gate'swrite_workspace_filecall has happened. Armed only for FindingsWriter (never Builder, whose own instructions correctly require readingfindings.mdfirst), in both the Write and corrective Fix dispatches inside_dispatch_writer_review_fix(src/engine/completion.py). A prompt-only reorder ofFINDINGS_WRITER_INSTRUCTIONS' workflow was tried FIRST and live-disconfirmed (model still calledread_workspace_filefirst) before escalating to this structural gate — the gate is what actually held. -
_collapse_multi_url_task_findings(completion.py): found while investigating why even a gate-forced first write stayed thin (2/29) —orchestrator.py::_run_single_task'sadd_findingcall 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_reorder_findings_for_position_bias's favorable edges by fetch count alone. Groups citable findings by(task_name, summary)before rendering — body text kept once per group, every real URL still individually named for citation (preserves the one-entry-per-URL contract).
-
Live-confirmed closed, 3rd smoke-test run
(
i_want_documentation_on_heuristic_algoritms_for_de_20260722_213120): evidence base collapsed to 5 real distinct clusters (from a raw 15 citable findings);findings.mdwrote all 5 of 5, first FindingsWriter dispatch calledwrite_workspace_filedirectly with zero blocked reads (gate never even needed to fire). Only 2 completion-check retries total (vs. 4 and 6 in the two prior live-tested runs) — fastest, cleanest convergence of the three.final_report.mdended up thin but HONEST (3 of 5 sources genuinely had nothing usable, and Builder said so instead of fabricating filler). Full detail and the two prior comparison runs:session_status/2026-07-22.md. -
Two smaller items surfaced during this investigation, NOT covered by this fix, tracked
separately: (a) one Searcher sub-agent fired ~14 near-duplicate
web_searchcalls on one angle without diversifying into a sibling angle its own task line implied — this looks like the same shape as the query-diversity/rabbit-holing issue independently fixed the same week viaPLANNER_INSTRUCTIONS' single-facet-per-slot guidance (seesession_status/CURRENT.md's "Closed this session, believed done" section) but was never explicitly cross-confirmed against THIS specific run; (b) a cosmetic mojibake filename in onefindings.mdheading, not investigated (seesession_status/CURRENT.md, diagnosed as the model degenerating into token repetition on a genuinely empty source, not a code bug).
-
-
Shared-quota starvation, angles (a) and (c) — IMPLEMENTED and live-verified 2026-07-21, found via a live benchmark run (the standing sales-forecasting/heuristic-algorithms query, right after this session's 4 synthesis fixes shipped) that failed outright:
Report: NOT WRITTEN,thin_coverage4x consecutive, retry budget exhausted. Root-caused with hard evidence, not assumption — one sibling task (comparison_GA) produced 14 finding entries and 26 tool-call log lines; its four siblings (comparison_PSO/SA/ACO/BO) 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) (
src/tools/core.py::check_quota): the existing per-task rescue requiredtask_fetched_urls_ctxto be non-empty (proof a task had already fetched something), butweb_searchnever populates that context var — onlyfetch_url_to_workspacedoes — so a task blocked on its own FIRSTweb_searchcall 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) (
src/engine/orchestrator.py, new_reserve_batch_quota_headroom+delegate_tasks's call site): withmax_concurrent_taskssmall (often 1), tasks in onedelegate_tasksbatch run roughly in listed order, sequentially draining the same shared pool — a heavily-active sibling can structurally starve later-listed ones even across completion-check topups. Pre-reserves enough total headroom for the WHOLE batch (2 calls/task forweb_search, 1 forfetch_url_to_workspace) before any task in it starts, removing the structural first-mover advantage. Not true round-robin scheduling (would need cooperative mid-turn interleaving, a much bigger change) — bounds the damage instead of eliminating dispatch order entirely. -
Live re-test #2, same query, confirmed the fix: zero
thin_coverageattempts (vs. 4), 25 findings recorded across 9 balanced task angles (vs. one task dominating 14/28), a real report converged in 4 attempts. Quota starvation itself is closed. -
A second, distinct bug found in that same re-test, also fixed:
findings.mdcarried anAUTO-RECOVERED DRAFTbanner — FindingsWriter narrated instead of callingwrite_workspace_file, and the narration was itself cut short by its own generation budget mid-way, 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/Analyzer (their final message IS their findings) but directly instructs a WRITER role (Builder/FindingsWriter, whose entire success criterion is callingwrite_workspace_file) to do the exact opposite of its job the moment it hits budget pressure. New_select_budget_nudge(pure, testable) +SUBAGENT_BUDGET_NUDGE_WRITERroute Builder/FindingsWriter to a write-the-file-NOW nudge instead. User's own catch, not found via self-audit: this was findable by reading the code (a shared mechanism used across roles with different real requirements) without needing a live failure — folded into the standing "check all surfaces" lesson. -
Live re-test #3 confirmed this specific bug is fixed, but surfaced a DEEPER, still-open one:
no
AUTO-RECOVEREDbanner this time,findings.mdwritten via a realwrite_workspace_filecall, faster convergence (2 real problems, not 4), 26 sources, 0 web_search failures. Butfindings.mdcontained exactly 1 of 33 real findings recorded in_run_state.json— 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 (a University of Rochester class project on Corning Inc.'s revenue), missing every Colombia/heuristic-algorithm angle despite 9/26 fetched sources containing real Colombia content. Net: converted a total-loss failure (unverified narration, 3/25 kept) into a partial-loss failure (verified real file, 1/33 kept) — real progress, not a full fix. The underlying pressure (context_budget_chars's single ~50000-char ceiling is too tight for FindingsWriter to consolidate a large finding set in one dispatch, narrating or writing) is still open — see Pending, tracked as its own item, actively being hunted. - All new logic pulled into pure, directly-testable functions (
_reserve_batch_quota_headroom,_select_budget_nudge), same established pattern as_agent_routing_rejection_reason.test_structural_checks.pyextended for all three; full suite green.
-
Angle (a) (
-
Four literature-backed synthesis/reliability candidates — IMPLEMENTED 2026-07-22, planned in
~/.claude/plans/vast-singing-creek.mdand executed the same session. All four extendsrc/engine/completion.py'sGROUNDING_CHECKS/_dispatch_writer_review_fix/run_completion_check;test_structural_checks.pyextended for each, full suite green throughout. Two documentation errors found and corrected during planning (noted per-item below).-
Findings-ordering (Lost in the Middle, arXiv:2307.03172 + PING's "Anchor Effect",
arXiv:2601.22984):
_reorder_findings_for_position_bias(completion.py, right before_build_findings_source_material) — a pure positional zigzag/sandwich reorder (no value/importance signal exists anywhere in the data model to rank by), splitting chronologicalentriesin 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. Called right before the existing budget-truncation scan, which is untouched. Tested via a 6-item zigzag assertion plus length/set invariants across edge cases (0/1/2/5/7 items). -
Propagation-aware hallucination check (PING taxonomy, arXiv:2601.22984): narrowed from the
paper's full DAG+NLI-entailment design (confirmed DeepDelve has no claim-dependency structure,
and confirmed the paper's OWN released code doesn't actually implement that mechanism either —
see the History entry on this) to the specific, already-documented split-brain pattern: a
citable finding whose content substantially term-overlaps (
extract_salient_terms, no NLI model) an uncited/cutoff sibling for the SAMEtask_name. New_find_propagated_bad_content-
check_propagated_ungrounded_content(added toGROUNDING_CHECKS, before the genericcheck_not_groundedcatch-all) — only fires if the flagged content also appears inside the report itself, not merely in findings.md. Refactored_build_findings_source_material's dedup/citability logic into 3 shared helpers (_dedupe_findings,_uncited_task_names,_is_citable_finding) so the check and the findings-assembly function can't drift on the definition of "citable." New verdict-matrix row (propagated_ungrounded) plus a standalone unit test with a control (no-overlap) case.
-
-
Force reasoning at synthesis time (PIVOT, arXiv:2605.11225): correcting the original
candidate's premise —
BUILDER_INSTRUCTIONS/FINDINGS_WRITER_INSTRUCTIONS(src/prompts.py) already instructthink_tooluse before finalizing (a "" block), confirmed by reading both directly during planning. So this shipped as VERIFICATION, not a new prompt:_dispatch_writer_review_fix(completion.py) now snapshotsthink_tool's quota usage before/after the Write dispatch, reusing the exact reads-before/reads-after pattern already proven there for PeerReviewer'sread_workspace_filecheck. Deliberately NOT a hard gate (a writer skippingthink_toolmakes no false claim, unlike a fabricated "REVIEW: CLEAN") — folded into the Fix-pass instructions only when PeerReviewer separately flags real issues; anotify()-only note when PeerReviewer says CLEAN. 3-case test (issues-found+skip, clean+skip, issues-found+used — the control). -
Full-artifact-rebuild repetition-escalation (ACM CAIS '26 planning-horizon paper):
correcting the original candidate's premise — re-verified
CONSECUTIVE_SAME_PROBLEM_ ESCALATION_THRESHOLD(actually atcompletion.py'srun_completion_check, not thecompletion.py:979-1005location first cited, which wascheck_missing_artifact's docstring merely mentioning it) — its real current behavior was "give up early" (attempt = max_attemptson 3 consecutive identical problems), not a "narrower nudge" as originally described; there was no existing gentler nudge to sharpen. Decided (user: "the more complete one" — full rebuild, not a wording-only reframe, matching the paper's actual full-horizon-replan mechanism): on the 3rd consecutive occurrence, grant exactly ONE extra attempt (run_state.data[ "whole_approach_retry_used_for"], keyed by problem, bounds it to once per problem type) that dispatches a genuine full rebuild (Builder: "rewrite completely from scratch... reconsidering your whole approach," not the targeted-fix wording; FindingsWriter: same reframe on itswrite_directive) instead of the usual targeted-fix instructions. Falls through to the pre-existing early-exit unchanged once that one retry is spent. The classic inject-into-Planner path (nodispatch_task, or a non-writer-fixable problem) gets the closest equivalent: a reworded, strongerverdict.inject. Two PRE-EXISTING tests (missing_artifact/thin_coverageescalation scenarios) updated for the new two-phase behavior since they encoded the old immediate-stop assumption; one new test confirms the full-rebuild instruction shape actually reaches the Builder dispatch (not just the classic-path text). - **Three more literature leads found via fresh search (PING's own comparison, a citation-
hallucination/
urlhealthtool, VMAO) and three RAG repos (LightRAG, RAG-Anything, GraphRAG, user-requested) were also researched this session — see History andRESEARCH.md§1/§8 for the full writeups; none were adopted (either reviewed-and-not-applicable, or too weak an evidence base on full read), so nothing further to implement from those.
-
Findings-ordering (Lost in the Middle, arXiv:2307.03172 + PING's "Anchor Effect",
arXiv:2601.22984):
-
Engine-driven iterative deepening (ROADMAP item 10, from
dzhng/deep-research) — IMPLEMENTED 2026-07-19, found still mis-listed as an untried candidate during the 2026-07-21 status audit. A STRUCTURAL refine loop: each round's Searcher findings carry aFOLLOW-UP DIRECTIONSsection (_extract_follow_up_directions,src/engine/orchestrator.py), stored per-finding (RunState.add_finding'sfollow_up_directionsfield,src/utils/run_state.py). Whencheck_thin_coveragefires,_select_deepening_tasks(src/engine/completion.py:1002) picks real, unconsumed directions and geometrically narrows them —math.ceil(coverage["total"] / 2), the exactnewBreadth = ceil(breadth/2)formula from the source project — deduplicated againstrun_state.data["consumed_directions"]so a retry never redispatches the same lead twice._dispatch_deepening_round(completion.py:1040) then dispatches them directly viadispatch_task, bypassing the Planner entirely (same mechanism_dispatch_writer_review_fixalready used for Builder/FindingsWriter retries) — the ENGINE drives the extra research round, not a hope that the Planner model chooses to loop. Wired live intorun_completion_check(completion.py:1277). Covered bytest_structural_checks.py's_deepening_round_scenario. -
Non-generative routing classifier for
delegate_tasks'sagent_id— IMPLEMENTED and live-verified 2026-07-20. Merged from the SOTA literature review (RESEARCH.md§6): small/mid models fail disproportionately at STRUCTURED SERIALIZATION (schema-valid output, wrong content), not semantic understanding, in a way a 6,000-sample SFT run can't fix (constraint-tax papers, arXiv:2606.25605 + arXiv:2605.26128) — confirmed live in this project's own data (~4.9% of realdelegate_taskscalls used a hallucinated but well-formedagent_id). Pulled routing out of free generation into a frozenall-MiniLM-L6-v2embedding +LogisticRegression(class_weight= "balanced"), decided policy reject-and-nudge (not silent override, not advisory-only).-
New:
finetune/extract_agent_routing_dataset.py. Real bug caught and fixed while building it: an initial version filtered session-log events tosource == "Agent"(the Planner's own turn) only, silently missing 100% ofDocumentAnalyzer/DataAnalyzerexamples — those only ever appear as targets of a NESTEDdelegate_taskscall made byWebSearcher/AcademicSearcher's own dispatch (source == "SubAgent_<task_name>", persrc/app.py'ssub_agents=[document_analyzer, data_analyzer]on both searcher roles), not the Planner directly. Fixed by dropping the source filter entirely. Real extraction, run against 101 session logs: 1,096 valid pairs + 57 hallucinated (4.9%) — matches the literature review's earlier ad hoc count (1,153 total, ~4.9%) almost exactly, now from a reproducible script instead of a one-off pass. After near-duplicate removal: 814 valid pairs, class distributionDocumentAnalyzer=331, WebSearcher=330, DataAnalyzer=99, AcademicSearcher=54, stratified 651 train / 163 held-out. -
New:
finetune/train_agent_routing_classifier.py. Real held-out per-class results:DocumentAnalyzer0.89 precision / 0.88 recall,WebSearcher0.89/0.85,DataAnalyzer0.68/0.65,AcademicSearcher0.44/0.64 (weakest — smallest class, 54 real examples, matches the literature review's own "imbalanced but workable" caveat). Overall accuracy 0.82. Real regression-check finding: every"searcher"(lowercase) hallucination correctly routes toWebSearcherwith real confidence (0.44–0.83);IndustrySearcher/BookSearcher/BusinessNewsSearchercorrectly route toAcademicSearcher(0.62–0.73); allPeerReviewerhallucinations get LOW confidence (0.30–0.46, all below the chosen 0.6 threshold) — the classifier correctly abstains on a genuinely out-of-scope role rather than forcing a guess, validating themin_confidencedesign. -
New:
src/utils/agent_routing.py— lazy singleton, fails open (mirrorsgrounding.py::_get_nli_model). Real design gap found and fixed before shipping: the classifier's 4 known classes (KNOWN_AGENT_IDS) span TWO different delegation levels — Planner→{WebSearcher,AcademicSearcher,PeerReviewer,Builder,FindingsWriter}(only 2 of 5 are classifier classes) and Searcher→{DocumentAnalyzer,DataAnalyzer}(a disjoint pair) — sopredict_agent_idtakes acandidate_classesparam, restricting the prediction to the INTERSECTION of the caller's own real roster and the classifier's known classes, never a blind global argmax that could suggest a role the caller doesn't even have available. -
src/engine/orchestrator.py: new pure function_agent_routing_rejection_reason(decision logic only, directly testable without the async tool closure) + a new check insidedelegate_tasks's per-task validation loop, before the existingif errors:gate. Rejects (adds to the same error-accumulation path every otherdelegate_tasksvalidation already uses) when the declaredagent_idisn't real for this caller, or the classifier disagrees abovemin_confidence— silently no-ops on the common case (declared role agrees, or classifier abstains). -
Config: new
settings.agent_routing_classifierblock inconfig_template.yamlAND the live~/.deepdelve/config.yaml(per this project's own standing rule), defaultenabled: false— new, not yet exercised against live mixed Planner/Searcher traffic end-to-end, a conservative rollout matching this project's own standing caution about untested features. -
New dependency:
scikit-learn>=1.7.0,<2.0.0inpyproject.toml(already installed transitively via the dev venv, no fresh install needed). -
Tests: new
_agent_routing_rejection_scenariointest_structural_checks.py(5 cases: no-prediction no-op, unknown-role rejection, strong-disagreement rejection, agreement no-op, low-confidence-abstention no-op) +src/utils/agent_routing.py's own__main__self-test (fail-open behavior, empty/out-of-scope candidate-class handling). Full suite +ruff checkclean across all changed/new files. -
Live sanity check: 5 real instruction strings run through the loaded artifact directly.
4/5 correct (Rust version → WebSearcher 0.74; peer-reviewed GOA papers → AcademicSearcher
0.83; numeric population table → DataAnalyzer 0.78; a PeerReviewer critique task correctly
got low-confidence 0.12, would abstain/reject on the unknown-role path). One real, honestly-
reported misclassification: a DocumentAnalyzer-shaped "read a file and extract findings"
instruction predicted
DataAnalyzer(0.68 confidence) — consistent with the measured 0.68 precision for that class, not a bug, a genuine limitation of a 0.82-accuracy classifier on ambiguous wording between two semantically close roles. -
LIVE END-TO-END TEST RUN, 2026-07-20 (same day) — FOUND A REAL REGRESSION, REVERTED TO
enabled: false. Flipped the live config on, ran a real headless query exercising both delegation levels ("What is the current stable version of the Rust programming language, and what does peer-reviewed academic research say about the soundness of Rust's borrow checker?"). The nested WebSearcher→DocumentAnalyzer level worked correctly, no false rejections. The Planner-level AcademicSearcher dispatch was wrongly rejected 8 CONSECUTIVE times — every single retry of "find peer-reviewed papers on borrow-checker soundness" was rejected with "looks like a WebSearcher task" at 0.67-0.75 confidence (above the 0.6 threshold), across ~2 real minutes / 8 wasted Planner turns, until the Planner gave up on the angle entirely. Concrete, measured harm: the final report contains ZERO content on the academic angle — half the user's actual question was never researched, a real content-coverage failure directly caused by this feature, not a pre-existing bug. Root cause:AcademicSearcheris the classifier's weakest, smallest class (54 real examples, 0.44 held-out precision, by far the worst of the 4) — a flatmin_confidence=0.6threshold 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. 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 (safe, unambiguous, no valid alternative exists) vs. rejecting a declared role that IS valid because the classifier disagrees (risky, since the classifier can be confidently wrong, as just demonstrated). The live test showed the second case caused active harm this session. Config reverted:~/.deepdelve/config.yaml'sagent_routing_classifier.enabledset back tofalseimmediately after this finding. -
DATA-QUALITY FIX, 2026-07-20 (same day, direction (a) above) — a real, root-cause bug found in
the training data itself, not just a volume problem. Auditing the 54
AcademicSearcherexamples by eye surfaced 6 that were obviously market-sizing/business-research tasks ("Evaluate the X market in Colombia... market size, current state, gaps, competition"), not literature searches — none used any academic-search language at all. Tracing further: all 13 came 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 labeledWebSearcher. This is one real historical Planner routing mistake (or drift across a repeated batch) baked into the training data as if it were correct ground truth — a genuine root cause, not just "not enough data."- Two general-purpose automated detectors tried and rejected, worth keeping the reasoning for: (1) SequenceMatcher literal-text similarity never caught this at all (different sector nouns keep character overlap below the dedup threshold despite an identical template). (2) Embedding (all-MiniLM-L6-v2) cosine similarity, even restricted to same-routing-level pairs, couldn't cleanly separate "same underlying ask, inconsistently routed" from "different ask, same topic domain, correctly routed differently" — this project's own real historical benchmark queries cluster so heavily on one topic (heuristic algorithms / sales forecasting / Colombia) that a threshold loose enough to catch the real conflict also flagged hundreds of genuinely correct, differently-labeled pairs. Landed on a direct, documented exclusion of the one manually-verified session instead of forcing an unreliable general heuristic — honest engineering given the alternative was trading a small, well-evidenced problem for a much larger, noisier one.
-
Real result after retraining on the cleaned data:
AcademicSearcherclass shrank 54→50 (the wrong examples excluded, not replaced), but held-out RECALL rose 0.64→0.80 at the same 0.44 precision — fewer false negatives, directly the failure direction that caused the live regression (the classifier was calling real academic tasksWebSearcher). Directly re-tested against the exact 3 real instruction variants that failed live — all 3 now correctly predictAcademicSearcher(0.63-0.66 confidence, wasWebSearcherbefore). Regression-checked the other direction too: 4 genuineWebSearcher-shaped market/factual tasks (including ones structurally identical to the now-excluded bad examples) still correctly predictWebSearcher, several at HIGHER confidence than before (0.72-0.89) — the fix sharpened the boundary in both directions, not just patched the one failing case.AcademicSearcherprecision (0.44) is still the weakest of the 4 classes — volume/diversity (candidate direction (a), still the smallest class by far) remains the deeper fix, not yet done; this closes the specific, confirmed data-contamination bug, not the whole gap. -
New
finetune/extract_agent_routing_dataset.pyoutput:agent_routing_conflicting.jsonl(13 excluded examples, kept for inspection, not silently discarded). -
SECOND LIVE RE-TEST, 2026-07-20, same day, same exact query that failed before
("What is the current stable version of the Rust programming language, and what does
peer-reviewed academic research say about the soundness of Rust's borrow checker?") —
~/.deepdelve/config.yaml'sagent_routing_classifier.enabledre-flipped totrue, run end-to-end viapython src/app.py --auto-approve. Confirmed fixed: zero classifier rejections anywhere in the run (checked both the raw log and the session'sui_events). TheAcademicSearchertask (borrow_checker_soundness) was accepted on the first try, found the real paper (arXiv:2404.02680, "Sound Borrow-Checking for Rust via Symbolic Semantics"), andfindings.mdcorrectly contains both the Rust-version findings AND the academic section — directly reproduces the first test's setup and confirms the data-quality fix holds in the real pipeline, not just isolated classifier calls.-
A second, unrelated bug surfaced in the same run, worth recording separately:
final_report.mddropped the entire academic section despitefindings.mdhaving it correctly. Root cause is NOT the routing classifier — this run hit 3 completion-check remediation cycles (missing two-pass discipline onfindings.md, missingfinal_report.md, an unsupported-claim flag on the Rust-docs source), each dispatching a corrective sub-agent (FindingsWriterFix,BuilderFixx3,ReviewFixx3) that itself callsread_workspace_file/write_workspace_file. By the thirdBuilderFixpass, theread_workspace_filequota (limit 30,config_template.yaml) was exhausted, and the final report says so outright: "Due to workspace tool quota limits, I was unable to re-read the source file during this session... only claims that can be directly traced to specific lines in findings.md are included" — then cites only 2 of ~7 real sources. Not yet fixed; new finding, not scoped into this session's work. Candidate fixes: raiseread_workspace_file's quota specifically forBuilderFix/ReviewFixremediation sub-agents, or give the completion-check remediation loop its own separate quota pool so legitimate first-pass work doesn't starve retries (and vice versa).
-
A second, unrelated bug surfaced in the same run, worth recording separately:
-
New:
-
Sub-agent dispatches had NO wall-clock deadline at all — a real, live-confirmed gap, fixed and live-verified.
engine/tui.py'srun_clialready races each stream update againstsettings.max_run_minutesviaasyncio.wait_for(stream_iter.__anext__(), timeout=remaining)instead of a plainasync for(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 was never propagated toengine/orchestrator.py::_run_single_task, the code path EVERY Searcher/Analyzer/Builder/FindingsWriter/PeerReviewer dispatch goes through — it used a bareasync for update in stream:with zero deadline, relying entirely on the raw openai-SDK HTTP client's blunt ~600s default connection timeout, which then discards the whole in-progress response and raises a generic error instead of degrading gracefully.-
Root-caused live (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-referenced
journalctl -u ollamaagainst the exact failure window and found two requests that returned HTTP 500 after hanging exactly10m0sand6m24s. The10m0sone was NOT stuck —ollama's ownprint_timinglog showed it continuously, validly decoding tokens the entire time (steady ~33 tok/s, no gaps) up to 19,908+ tokens when the connection was force-closed —600s × ~33 tok/s ≈ 19,800 tokens, matching almost exactly. A single sub-agent turn ran away into a very long generation with no early-cutoff mechanism watching it at all. -
First fix attempt was itself incomplete, caught by live verification, not just the unit
suite: an initial version gave
_run_single_taskthe SAMEmax_run_minutesdeadline asrun_cli's own top-level guard, anchored to the same run-start clock. Live-tested with a tightmax_run_minutes: 2— a cutoff DID fire, but checking the persisted~/.deepdelve/sessions/session_<id>.jsonUI-event log showed the new inner marker text never actually appeared; the OUTER guard's cancellation had propagated down throughasyncio.wait_forand pre-empted the inner one before its own deadline check ever got a chance to run on its own terms — meaning in realistic configs (max_run_minutes=45) the new code was effectively dead, providing no protection against ONE runaway call among MANY quick ones early in a long run. -
Real fix: new, INDEPENDENT
settings.sub_agent_timeout_minutes(default 10), a fresh per-DISPATCH deadline computed at the start of each_run_single_taskcall (not shared with the run-wide clock or any other dispatch) — closes the actual gap (one runaway call) instead of just duplicating the whole-run ceiling. Also bumped_build_client'sAsyncOpenAItimeout=to run comfortably past bothmax_run_minutesandsub_agent_timeout_minutes— otherwise the SDK's own blunt ~600s default keeps winning the race against either of our graceful cutoffs whenever a configured budget exceeds 10 minutes (both template defaults do), making them dead code in any realistic config, only ever exercised by an artificially short override. -
Live-verified the corrected fix directly: ran with
max_run_minutes: 45(generous, so the outer guard has no reason to fire) andsub_agent_timeout_minutes: 1(tight) — 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. - Verified the core
asyncio.wait_for-racing mechanism in isolation too (a stream that hangs 999s against a 1s deadline is cut off at ~1.00s, not 999s). Full suite +ruff check .pass. -
_run_budget_deadlineshared acrossrun_cli/run_agent(both callcreate_local_agentonce per run) — no separate TUI-specific change needed to close this gap on both surfaces. -
This also retroactively explains several "timeout" observations during today's earlier Phase
2-4 live smoke tests that had been attributed to model slowness/complexity — those diagnoses
weren't necessarily wrong (Gemma4's genuine slowness and a separate
qwen3:4btool-repetition pattern were both independently confirmed too, see "Findings from live testing" below), but this structural gap was the common thread making ANY of those failure modes catastrophic (total loss of the turn, no graceful degrade, occasionally a doomed retry into the same pattern) instead of just slow.
-
Root-caused live (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-referenced
-
3-tier domain-specialized architecture:
Planner -> {WebSearcher, AcademicSearcher, PeerReviewer} -> {DocumentAnalyzer, DataAnalyzer}.PeerRevieweris a Planner-tier delegate (independent critique, findings.md or, in report mode, final_report.md), not part of the Searcher→Analyzer chain. (2026-07-13: aBuilderPlanner-tier delegate was added — see the "Builder sub-agent + Build→Review→Fix loop" entry below. 2026-07-14: aFindingsWriterPlanner-tier delegate was added the same way, one artifact earlier — see "Planner now only plans and delegates" below. Five Planner-tier delegates total now: WebSearcher, AcademicSearcher, PeerReviewer, Builder, FindingsWriter.) -
Planner now only plans and delegates — it cannot write ANY file. (2026-07-14, user-driven design question: "the planner should only plan and delegate... giving the planner the job of writing the findings will poison context.") Previously the Planner wrote
findings.mditself (the only artifact-writing job it still had after Builder was split out forfinal_report.md) — a real, inconsistent gap: afindings_ungrounded/missing_findingsretry grew the PLANNER'S OWN conversation exactly the way Builder was invented to prevent forfinal_report.md. Confirmed live the same day, independent of this fix: a benchmark run hit 4 consecutivefindings_ungroundedretries and exhausted its budget with nothing ever written. Fix: newFindingsWriterPlanner-tier delegate (src/prompts.py::FINDINGS_WRITER_INSTRUCTIONS,src/app.py::findings_writer_agent), dispatched exclusively byengine/completion.py's generalized Write→Review→Fix loop (renamed from Build→Review→Fix —_dispatch_build_review_fix→_dispatch_writer_review_fix, now shared by both Builder and FindingsWriter;_ensure_builder_write_quota_headroom→_ensure_writer_quota_headroomfor the same reason) whenmissing_findings/findings_ungroundedfires. FindingsWriter never sees the Planner's conversation — its dispatch instructions are built entirely fromRunState's structureddata["findings"]({source_url, summary}per dispatched task, populated automatically by every Searcher/Analyzer call — see_build_findings_source_material), plusread_workspace_file/grep_workspace_fileaccess to go deeper into a raw fetched source if a summary isn't detailed enough. The Planner'swrite_workspace_filetool was removed entirely (src/app.py) — it is now structurally incapable of writing any file, the same way it's already structurally incapable of researching.PLANNER_INSTRUCTIONSrewritten accordingly (job ends at delegation;PeerReviewer/Builder/FindingsWriterall removed from its Delegation Routing, since none are ever Planner-dispatched anymore). Fallback verdict text for both problems rewritten to never instruct awrite_workspace_filecall the Planner can't make. New test coverage:_findings_writer_dispatch_scenariointest_structural_checks.py, mirroring_builder_dispatch_scenario(CLEAN review, ISSUES FOUND review with corrective re-dispatch, malformed-sentinel conservative handling, missing-registration fallback that doesn't reference the removed tool).-
Live-verified end-to-end, two real runs, same day: architecture confirmed working correctly —
FindingsWriter/Builderboth dispatch via their own independent Write→Review→Fix loops,PeerReviewercatches real issues in both (confirmed: flagged a genuinefindings.mdproblem, triggering a real corrective FindingsWriter re-dispatch; separately flagged aclaim_unsupportedcitation infinal_report.md), andcurrent_inputstays provably unchanged across dispatches (no context growth) exactly as designed. Traced one flagged citation to its root cause to confirm layer independence, not just redundancy:findings.mdcorrectly recorded thatpython.org's landing page has NO biographical content about Python's creator (a truthful negative finding); Builder then citedpython.orgin support of a creator claim anyway on its own initiative — a downstream synthesis error introduced AFTER FindingsWriter's own review, caught by a completely separate check, not something that leaked through from badfindings.mdcontent. -
New gap surfaced by this same live testing, not a flaw in the dispatch mechanism itself: on a run where Builder repeatedly re-committed the identical
claim_unsupportedmistake across separate corrective attempts (never actually fixing it, just re-flagged next cycle), the Planner — resuming each time withcurrent_inputunchanged and no signal that a Write→Review→Fix cycle just ran — kept deciding to delegate MORE research rather than recognizing the problem was a downstream citation/authoring error, not a research gap. Becausedelegate_tasks's own quota is independent ofmax_completion_check_attempts, this let a trivially simple factual query ("who created Python") run 25 minutes and fetch 35 URLs before finally exhausting its completion-check budget. The system's OWN safety net still worked correctly at the end —_restore_quarantined_draftproduced a real, honestly-labeled, mostly-correct report instead of a silent failure or a hang — so this is an efficiency/looping gap, not a correctness or reliability regression. -
Fixed the same day:
run_completion_check(src/engine/completion.py) now wraps its retry loop inwhile True:, and the two successful-dispatch paths (Builder,FindingsWriter)continuestraight into the next completion-check iteration instead ofreturning control to the Planner. A persistently-failing chain (e.g. theclaim_unsupportedloop above) now burns its retries entirely inside onerun_completion_checkcall — landing on the same quarantine-restore/salvage outcome, but without the wasted Planner-driven "more research" turns in between. Bounded by the sameattempt < max_attemptsceiling as before, no new infinite-loop risk (traced:current_inputis never mutated in place, so the "unchanged on success" invariant that makes the whole Write→Review→Fix mechanism safe holds automatically across any number of internalcontinuecycles).test_structural_checks.py's_builder_dispatch_scenario/_findings_writer_dispatch_scenarioreworked with stateful mocks that genuinely write to the fake workspace (a canned string list can no longer stand in for a dispatch — the chained re-check would just exhaust it and fall through), including a new primary FindingsWriter→Builder chained case and a narrow variant pinning the classic-path fallback when a needed writer role isn't registered.
-
Live-verified end-to-end, two real runs, same day: architecture confirmed working correctly —
-
context_budget_charsblind spot for the classic inject-into-Planner path, fixed.run_stream_chars(src/engine/tui.py'srun_cli) only ever counted chars from the Planner's own streamed generation — a completion-check nudge appended tocurrent_inputoutside that stream loop (now onlynot_delegatedon the normal path with both writer pairs registered; still any ofmissing_findings/findings_ungrounded/missing_artifacttoo if a writer role isn't registered) was invisible to the budget guard, so it could in principle grow the Planner's context unboundedly on repeat with zero accounting. Fixed by measuring the char length of whateverrun_completion_checkactually appended tocurrent_input(diffed by list length before/after the call) and adding it torun_stream_charsright after the call.context_budget_charsremains deliberately headless-only ("TUI Planner exempt") — no TUI-side change needed, matching that existing, documented design choice. -
Structural reliability fixes: per-attempt quota top-up, artifact quarantine-before-nudge, structured run-state (
_run_state.json, now including populatedfindings[]), a real URL-presence + content-level grounding check (utils/grounding.py), and history-scanning salvage for a narrated-but-never-written report (fixes the old single-turn-lookback bug that discarded good content when the final retry produced empty text — verified against a real saved session log). -
Upstream verification: each Searcher specialist's summary is grounding-checked before it reaches the Planner, not just the final report (
settings.grounding_check.verify_specialist_output). -
replan_actiontool: the Planner's replanning decision is a structured, checkable call (add_slot/verify_conflict/finalize_report) alongside itsthink_toolreasoning, not free text only. (Deleted in the 2026-07-11 ponytail audit, 929b987 — unused in practice.) -
Persona-brainstorming step: before planning non-trivial queries, the Planner briefly reasons from 2-3 relevant expert perspectives to widen slot coverage.
-
HTML boilerplate stripped on the primary fetch path: previously only the BeautifulSoup fallback stripped nav/footer/script; the primary markitdown path passed raw chrome straight through.
-
DDGS per-call client instead of a shared singleton (concurrent specialist searches no longer share one client instance).
-
extract_structured_datatool: real tool-level distinction betweenDataAnalyzerandDocumentAnalyzer, not just prompt-driven. -
Wiki index (
settings.workspace.wiki_index): deterministic, engine-maintained cross-runindex.md, independent of session isolation. (Deleted in the 2026-07-11 ponytail audit, 929b987 — cross-run state poisons benchmarks, same reasoning as the rejected knowledge cache.) -
Heavy search mode (
settings.search_mode: heavy): searches deeper and auto-fetches more top results per call, instead of fabricating fake query-variant strings. -
Human-in-the-loop gate (
settings.human_in_the_loop): reuses the existingApprovalWidget/tool-approval infrastructure to gate the Planner'swrite_todos. -
MCP tool loader (
settings.mcp_servers,tools/mcp_loader.py): generic loader foragent_framework's nativeMCPStdioTool/MCPStreamableHTTPTool, connected per-task viaAsyncExitStack. Nothing enabled by default;config_template.yamldocuments two researched, ready-to-uncomment servers (Semantic Scholar MCP, Brave Search MCP) instead of guessed ones. -
Readable run-folder names:
<slugified-query>_<timestamp>instead of a bare unix timestamp. -
TUI click-to-copy fix: tries direct system clipboard (
xclip/wl-copy) before falling back to Textual's OSC52 escape sequence, which silently no-ops in terminals that don't support it. -
TUI paste fixes: Textual's base
Input._on_pastesilently keeps only the first line of a paste and drops the rest — the query box now flattens a multi-line paste into one line instead. Also debounces a same/prefix paste redelivered within 0.5s, found live: a large pasted prompt showed up with a truncated repeat of its own opening appended, consistent with the terminal re-sending an interrupted paste. -
Model choice re-tested against this project's actual nested
delegate_tasksschema — see README "Model choice".mistral-nemo:12bis the default;devstral:24b,hermes3:8b,qwen2.5-coder:14b-instruct,llama3-groq-tool-use:8b, andmistral:7b-instruct-v0.3-q5_K_Mwere all tried and rejected for the Planner role. Re-confirmed on a real demanding query (2026-07-10):devstral:24b, despite being ~2x the parameter count, made zero realdelegate_taskstool calls across a full 8-attempt run — it narrated perfectly-formatted JSON in a markdown code block instead of emitting a real structured call, every attempt. Bigger is not better on this schema; the failure is a structured-output habit, not a reasoning/capacity limit. (Superseded 2026-07-11: the 13-run Colombia B2B benchmark madedeepdelve-gpt-ossthe default — best 7/10, only model in the "usable with verification" band; nemo passes the schema test but ceilings at 2/10 on the full rubric.) -
Per-task fetch-tracking race condition, fixed: the "URLs fetched by this task" delta used to be computed via a before/after length check on the single run-wide shared fetched-URL list, which races under concurrent
delegate_tasksdispatch — confirmed live (3-task run produced 9 cross-attributed findings instead of 3). Fixed with a proper per-task-scoped contextvar; verified at 10-task concurrency with zero duplication afterward. -
Delegation-scope relevance check (
settings.grounding_check.verify_scope_relevance): flags a specialist's summary when nothing it actually fetched mentions the entity (e.g. a country) its own delegation instructions required. Depended on the race-condition fix above to attribute fetches to the right task at all. -
no_urlsgets its own distinct completion-check message with escalating language (hands back the exact fetched-URL list on repeat failures) instead of reusing wrong-citation wording that didn't fit a report with zero citations.max_completion_check_attemptsis now configurable instead of hardcoded to 3. -
Explicit re-delegation directive: when a grounding-check failure repeats with no new fetches since the last completion-check attempt, the nudge now forces
delegate_tasksagain instead of implicitly assuming enough real findings already exist. Confirmed live to detect the exact failure it targets (a 9-attempt run that never delegated a second time, fetched_url_count stuck at 2 the whole way). -
delegate_tasksrejects unresolved placeholder tasks and same-batch cross-task dependencies before dispatching — e.g. "sector 1" / "sector X" instead of a real name, or "for each identified sector" bundled in the same batch as the discovery task it depends on. Both patterns were confirmed live (garbageweb_searchqueries like"market size of sector 1 in colombia") and both checks verified against real observed strings with no false positives on legitimate task names from working runs. -
Live test battery: 15+ live runs across factual lookups, comparative queries, academic paper + related-work queries, current-event queries outside training data, a TUI session, and multiple real market-research queries at varying scope (5, 6, 10, 12, 14 sectors) — see "Findings from live testing" below for what these surfaced.
-
Full strict code audit, 2 real bugs found and fixed: (1)
eval/evaluate.py'sfind_latest_sessionstill filtered on arun_*prefix left over from before the "Readable run-folder names" rename above — no current run folder has ever matchedrun_*, so every eval harness run since that rename silently scored raw stdout instead of the actualfinal_report.mdartifact. Fixed to take whatever directory exists in the run's isolated workspace, verified against the real slugified-timestamp naming. (2)remove_workspace_file's own docstring claims it "mandates human oversight," butconfig_template.yamlshipped with nosettings.permissionsentry gating it, so that claim was unenforced by default (moot today since no agent is actually wired to this tool yet, but misleading if one ever is). Fixed by addingsettings.permissions.remove_workspace_file: require_approvalto the default config. Also declaredpydanticas an explicit direct dependency inpyproject.toml— it's imported directly inengine/sdk.pybut was only ever installed transitively viaagent-framework. -
delegate_tasks's placeholder-detector false-positive, found and fixed via a real bad-output diagnosis. Traced a live "neglected markets in Colombia" run (research_output/do_a_market_research_of_neglected_markets_in_colom_20260710_223455) where every single cited source turned out to be fabricated. Root cause: the Planner's real, well-formed 12-task batch (each with a genuine specific topic ininstructions, e.g."Assess the needs of logistics and supply chain management in Colombia...") was rejected wholesale by the placeholder detector becausetask_nameused an ordinary numbered label ("Analyze market 1: ...") — the detector checkedtask_nameandinstructionstogether, so a harmless numbered label falsely tripped the same check meant for a task with no real name anywhere. Facing a full-batch rejection, the model gave up delegating those 12 sectors entirely and fabricated all offindings.mdfrom memory instead (including a repeated fake source domain used identically across every unrelated sector) — nothing catches this because the grounding check only runs onfinal_report.md, never onfindings.md. Fixed by checking onlyinstructionsfor the placeholder pattern, since that's the field that actually becomes a Searcher's query, nottask_name; verified against both the real rejected 12-task batch (now passes) and the original documented true-positive case (still correctly rejected).-
Not yet fixed, same diagnosis:
findings.md(Pass 1's "verbatim extraction") is never grounding-checked at all — onlyfinal_report.mdis. A Planner that abandons real delegation partway through a run (as happened here) can fabricatefindings.mdwholesale and nothing structural catches it before Pass 2 treats it as ground truth. Also reproduced live in the same run: hard exclusion rules still don't hold (SESSION_STATUS.md's tracked #2 item) — 4 explicitly-excluded sectors (Agritech, HealthTech, EdTech, VR/AR-Education) were researched and included anyway.
-
Not yet fixed, same diagnosis:
-
Non-URL pseudo-citations now caught (
settings.grounding_check.non_url_citation_check,utils/grounding.py::find_non_url_citations) — closes the #1-ranked open item above. Re-tested the placeholder-detector fix on a smaller 4-6 sector scope of the same query and confirmed the fix worked (real Colombia-specific sources were actually delegated and fetched this time, no wholesale batch rejection) — but the run still ended in unverified salvage because the model attributed a claim to a bare(DANE, 2020)-style parenthetical instead of a real hyperlink, whichextract_cited_urlsnever even saw since it only recognizeshttps?://. Added a line-scoped check for aSource:-labeled or(Org, Year)-shaped attribution with no URL on the same line; runs as a hard gate alongside the existing URL-presence check, on both the final report and each specialist's summary (sharedreal_grounding_problem, soorchestrator.py's upstream check picks it up for free). Verified against the real fabricated line from that run (caught), a well-formed- **[Title](URL)**-only report (not flagged), and the exact mixed case SESSION_STATUS.md documented — real URL citations elsewhere in the report plus one bareSource: Expert opinion from...line (caught, without disturbing the real citations). Also excludes the engine's own injected[SYSTEM ... WARNING: ...]nudge text from the check, found necessary because a salvaged report can carry one of those across a turn boundary and its own use of the word "source" would otherwise self-flag. -
Headless/headed-browser fetch fallback (
settings.fetch.headless_fallback, optionalplaywright+pyvirtualdisplayextra —pip install deepdelve[browser] && playwright install chromium) for pages that bot-wall a plainhttpxGET. Motivated by a live-reported bug: real, citable papers on Springer, ScienceDirect, and MDPI were all getting flagged as fake/stub sources. Root-cause investigation (2026-07-14) found three distinct bot-wall signatures, reproduced directly against the reported URLs: Springer served a stripped shell to the plain fetch (200 OK, title only, no body), ScienceDirect returned a Cloudflare Turnstile challenge (initially masked by an additional UA-sniffing "browser is outdated" block, HTTP 400, on the pre-fix stale UA), and MDPI served an Akamai Bot Manager block._stub_reason(src/tools/web.py) was correctly flagging all three — the actual gap was one level upstream, no way to get past the wall at all. Fix (src/tools/web.py::_fetch_raw,src/utils/browser_fetch.py): when the plain fetch looks like a stub, retry once via a real browser before giving up, reusing the exact same boilerplate-strip/markdown pipeline on the browser-rendered HTML. Also bumped the plain-fetch UA string off a stale 2021 Chrome build. Soft dependency, fails open with zero latency cost if Playwright isn't installed — mirrorsutils/parsers.py's markitdown soft-import pattern.-
Headed beats headless, confirmed live: MDPI's block turned out to be a headless-specific fingerprint check — it fires at the network/edge level before any JS/DOM loads, so JS-side stealth tweaks (webdriver-flag override, custom UA/plugins/locale,
--disable-blink-features=AutomationControlled) made zero difference underheadless=True, but a genuinely headed (non-headless) Chromium sailed straight through, both against a real X session and a freshly-started virtual one (Xvfb viapyvirtualdisplay,DISPLAYunset beforehand to rule out riding the real desktop's session). Shipped behavior: try headed first (real display, or auto-started Xvfb on Linux) and fall back to headless only when no display is available at all — recovers MDPI in addition to Springer. -
Found and fixed one more real bug along the way:
_strip_boilerplate_html's boilerplate-class regex (cookie|consent|advert|sidebar|...) is a substring match, so it was deleting Springer's actual 221K-char article-body container because its CSS class (eds-l-with-sidebar, a layout hint) happened to contain "sidebar" — silently leaving only the cookie-consent banner behind, which then passed the stub check on its own prose mass. Pre-existing bug, invisible until headless/headed fetch started returning real content to trigger it. Fixed with a size guard (elements over 3000 chars are left alone — real chrome is never that large). -
ScienceDirect confirmed NOT fixable this way — root cause pinned down precisely, not just "still blocked." It's gated by Cloudflare Turnstile, not Akamai. Live-tested exhaustively (2026-07-14): the challenge iframe never resolves even after 60s of patient polling with a real headed browser, clicking any checkbox that appears (none ever did — the widget cycles in and out of the DOM every ~6-12s, retrying itself indefinitely). Isolated the cause: Playwright's Chromium exposes
navigator.webdriver: trueby default (confirmed:page.evaluate("() => navigator.webdriver")→True); spoofing it toundefinedviaadd_init_scriptchanged the JS-visible value but the challenge still never resolved after another 45s of patient polling — so the detection is deeper than any single JS flag, almost certainly Cloudflare fingerprinting the CDP (Chrome DevTools Protocol) connection Playwright itself requires to drive the browser, a much harder thing to hide thannavigator.webdriver(the reason dedicated "undetected browser" tooling exists as its own arms race, with a poor track record against Cloudflare specifically). Directly confirmed the same exact URL, same machine/IP, loads cleanly in the user's real (non-automated) Firefox — ruling out IP-reputation/rate-limiting as the cause; it's specifically automation-fingerprint detection. Deliberately not pursued further: defeating this would mean building and maintaining real anti-detection/stealth-patching tooling aimed specifically at circumventing a publisher's bot controls, which doesn't belong in DeepDelve's shipped default behavior even though the underlying paper is legitimately citable — same reasoning as declining to add CAPTCHA-solving. Left as a permanent, honestly-flagged residual gap, not a bug to keep chasing. -
Checked whether "just fetch the abstract instead of the full PDF" routes around this — it
doesn't, for ScienceDirect specifically. The abstract/landing page (
/science/article/pii/...) IS the same URL already being tested; Turnstile gates that whole page, not specifically a PDF download action. Also checked Crossref's metadata API for two real DOIs from the pdfdownload review above — no abstracts returned; Elsevier generally doesn't submit them to Crossref. The legitimate alternative is Elsevier's own developer API (dev.elsevier.com, registered API key, often free for text-mining/research use) — noted as a real future option, not started (no key exists yet; the tool would need one as a required config value). Worth stating plainly what this finding does NOT change: DeepDelve's existing fetch behavior already does the right thing for every other publisher —fetch_url_to_workspacenever tries to get past a paywall to reach a full PDF specifically, it just fetches whatever's at the URL, so a typical journal with an open abstract page and a separately-paywalled PDF link already naturally grounds on the abstract with no special-casing needed. ScienceDirect is the unlucky case where the wall sits in front of the abstract too, not a sign anything needs to change generally. - TUI/CLI parity:
/toggle_headless_fetchslash command + status-bar/banner indicator on both surfaces (session-only, not persisted, same asreport_style).
-
Headed beats headless, confirmed live: MDPI's block turned out to be a headless-specific fingerprint check — it fires at the network/edge level before any JS/DOM loads, so JS-side stealth tweaks (webdriver-flag override, custom UA/plugins/locale,
-
The three remaining structural fabrication gaps, closed in one pass (2026-07-11, first Windows-side session). (1)
findings.mdwholesale-fabrication gate:run_completion_checknow flags a Pass-1 file with zero cited URLs or where not one cited URL matches a real fetch (utils/grounding.py::fully_ungrounded,settings.grounding_check.check_findings), quarantines it and forces re-delegation — deliberately laxer than the strict per-URL final-report check, since Pass-1 notes legitimately mention unfetched snippet URLs. (2) Structural exclusion enforcement:delegate_tasksextracts explicit exclusions from the original query (_extract_excluded_topics— only unambiguous cues like "excluding"/"except", NOT "avoid", which appears inside legitimate topics) and skips matching tasks individually rather than rejecting the batch, because the placeholder-detector incident showed wholesale rejection makes the model abandon delegation and fabricate. (3) Unresolved-referent rejection: a live delegated task "Summarize its headline feature." searched the web with no idea what "its" meant and returned Microsoft Research patent statistics as Python's headline feature — short instructions leaning on a bare pronoun with no proper-noun/digit/quote anchor are now rejected with guidance to restate the subject (_lacks_concrete_subject, kept deliberately conservative given the placeholder false-positive history). All three covered bytest_structural_checks.py; verified live that none false-positive on a clean run. -
Windows migration (dual-boot, same NTFS drive). Ollama model store shared via
OLLAMA_MODELS=D:\Projects\AI shit\Models; cp1252 UnicodeEncodeError in headless mode fixed with a UTF-8 reconfigure guard inapp.py;markitdown[all]is unresolvable on Windows/py3.14 (silently downgrades to a 0.0.2 stub) sopyproject.tomlnow pins the doc extras actually used. Full pipeline re-verified live on Windows (ROCm on an RX 9060 XT 16GB). -
Production batch (2026-07-11, commits
2ef3f46..ee63b0d), all validated live during the 13-run benchmark day:findings.mdexistence gate (missing_findings— runs 10/11's exact failure, Pass 1 now structurally required beforefinal_report.mdis accepted);--resume-run(reattaches an interrupted run: same workspace, fetched URLs restored into the grounding check, engine-built resume briefing); TUI intake clarifier (clarify_before_research, fail-open);settings.max_run_minutesrun budget;--depth quick|standard|deeppresets; repeatable--seed-url; finish-line summary +--list-runs; TUI follow-up continuity (Q&A mode on an existing report);regulation_id_check(a law number cited to a genuinely-fetched source that never mentions that number — run 12's "Ley 1906 de 2021" failure class, caught live on first deployment in run 13); quarantined-draft restore at final verdict (runs 11/13 ended with a real draft in.rejected_attempt_Nwhile salvage delivered meta-narration — the draft now wins, loudly labeled). -
Completion-check refactor (2026-07-12): the ~250-line if/elif verdict chain in
tui.py— which shipped the swallowed-elif bug twice (bd307f4, run 13) — is now a data-driven check list insrc/engine/completion.py(check_<problem>(ctx) -> Verdict|None, first verdict wins, no elif headers to swallow), pinned by a 10-row verdict matrix intest_structural_checks.py(mutation-verified) and a CLAUDE.md suite-before-commit rule. -
_get_safe_pathWindows workspace escape, fixed (2026-07-12):os.path.join(base, "C:\evil")discards the base entirely, so drive-qualified/drive-relative filenames escaped the workspace (Planner haswrite_workspace_file). Drive-lettered names now rejected outright + abspath containment check on disk workspaces. External review #2's one HIGH finding. -
Documentation update pass (2026-07-12,
a4d8380): config template default flipped todeepdelve-gpt-oss, README model-verdict table + new CLI flags + headless failure semantics +sources/provenance; ROADMAP synced. -
Context-budget endgame guard (
98ef24a): ROADMAP candidate from Tongyi'sreact_agent.py— local models run atnum_ctx ~16384with no context accounting, so on overflow Ollama silently truncates from the TOP (eating the system prompt mid-run, indistinguishable from model collapse).settings.context_budget_chars(template default 50000) counts text + tool args + results per agent stream; on overshoot the turn is cut and the agent gets one forced wrap-up turn (sub-agents return findings immediately; at the time this shipped the headless Planner wrotefinal_report.mddirectly on overshoot too — since 2026-07-13 that's Builder's job, and the Planner's own wrap-up now only affectsfindings.md), a second overshoot forces the completion check's final verdict. TUI Planner exempt. Verified live with a 3000-char budget: honest "budget exhausted" report, no silent truncation. -
Grounding-layer hardening batch (2026-07-12 evening,
7f0782f..5c24607), every fix validated live in run 15: stub-fetch detection (soft-404/paywall shells recorded asstubinfetched_urls, refused by all grounding checks, ownstub_sourceverdict — closes run 14's invented-URL hole; 10/21 run-15 fetches flagged, zero false positives); Source-URL header self-grounding fix (the injected line-1 header's URL slug no longer counts as source content); charset fix (HTML decoded by real encoding — strict UTF-8 → header → meta → cp1252; stale meta tags scrubbed so markitdown can't re-mojibake; Spanish accents verified intact live); citation-format enforcement (uncited_claims: ≥3 figure-bearing lines with no citation in an h1-h3 section without URLs — run 14's table + detached "Source URLs" shape; section-scoped after run 15 caught the per-niche#### Sourcesfalse positive); URL prefix-boundary fix (fetched.../articleno longer grounds fabricated.../article-fake-2024);grounding_check.enabledmaster switch actually honored; platform-independent drive-letter guard (splitdrive silently stopped rejectingC:\evilafter the Linux migration). -
Repo governance + CI (2026-07-12), triggered by an external audit's one genuinely real finding (the repo is public with no LICENSE):
LICENSE(MIT),.github/workflows/ci.yml(install +ruff check+test_structural_checks.pyon push/PR to main, verified green in a clean throwaway venv before ever touching GitHub), a pragmatic[tool.ruff]config (pyflakes-only —E/Igenerated 189 line-length/import-sort hits that were pure style noise against this codebase's established dense-comment/lazy-import conventions; narrowed toF, which found 21 real issues: dead imports, an unused variable left over from this session's own_fetch_rawrewrite, one f-string-without-placeholders). Floor+ceiling dependency pins (agent-framework,httpx,textual,beautifulsoup4,PyYAML,ddgs,markitdown,pydantic— E12, previously onlymarkitdownwas pinned) +requirements.lock(192-packagepip freezesnapshot from a clean install). The rest of that audit's "critical" findings (no iterative loop, no token budgeting, TUI blocks on LLM calls, needs a DI rewrite, roadmap "contradictions") were checked directly against the code and found false or already solved — see the session's plan file for the full point-by-point rebuttal; not reproduced here since none of it required a code change. -
Academic / literature-review output mode (2026-07-12), triggered by a real gap: a live sales-forecasting query got a properly-structured literature-review paper from DeepSeek (
eval/reference/sales_forecasting_deepseek.md,(Author, Year)citations + numbered References) whiledeepdelve-mistral-nemocollapsed on the same query through DeepDelve (eval/sales_forecasting_benchmark.md— 9 completion-check attempts, no accepted artifact).settings.report_style/--style standard|academic(orthogonal to--depth, which only changes tool budgets): academic style rewritesPLANNER_INSTRUCTIONS' Report Structure step to a literature-review shape (Abstract, Introduction, thematic sections, Cross-Cutting Synthesis, Quantitative Benchmarking Summary, Challenges & Future Directions, Conclusion, References) — modeled onimbad0202/academic-research-skills'literature_review_template.md(see README References) — and swaps the citation-format instructions to(Author, Year)in-text + a numbered References list, instead of the default inline- **[Title](URL)**. Also carries that repo's Anti-Leakage Protocol ("Knowledge Isolation Directive": preferfindings.mdover parametric memory, write "Not covered by this run's research" instead of inventing a section).utils/grounding.pygainedparse_academic_references(maps(surname, year)keys to the URL on that References entry — an entry with no real URL stays unresolvable, same failure mode as a fabricated inline citation) and every line-scoped check (find_non_url_citations/find_uncited_claim_lines/claim_grounding_problem/find_unsupported_regulation_ids) now resolves academic citations through it alongside the existing inline-URL format — same grounding guarantees, second citation dialect. A real bug was caught building the test coverage: a line with TWO(Author, Year)citations only had its FIRST one checked (regex.search()vs.finditer()), so a real citation earlier on a line could mask a fabricated one later on the same line — fixed, pinned by a dedicated test row. A fresh audit pass then caught a HIGHER-severity bug in the same feature before any live run: the citation detector required every token before the comma to start with an ASCII capital, so it silently failed to even DETECT "et al."/"&"/"and"/accented-surname citations at all — exactly the forms the feature's own prompt tells the model to use — breaking grounding in both directions (a fabricated multi-author citation went undetected; a well-formed one got wrongly quarantined). Fixed, plus a related mis-keying bug (a reference entry's own title could shadow its real author/year) and 5 more regression rows. 13 total assertions intest_structural_checks.py. Live-validated 2026-07-12 (deepdelve-gpt-oss --style academicagainsteval/sales_forecasting_benchmark.md, 21.5 min,research_output/i_want_documentation_on_heuristic_algoritms_for_de_20260712_144216): the literature-review shape was produced correctly end to end (Abstract, Introduction, thematic sections with tables, Cross-Cutting Synthesis, Challenges & Future Directions, Conclusion, numbered References), org-style(Wikipedia, 2026)/(Papaya Global, 2026)citations all resolved with zero false positives from the citation-format work. The run's one real failure — quarantined atnot_grounded(unverified_urls:https://en.wikipedia.org/wiki/Heuristic, cited without its_(computer_science)disambiguator, vs. the actually-fetched.../wiki/Heuristic_(computer_science)) — is the pre-existing hard URL-presence gate correctly catching a genuine citation-accuracy slip, not a defect in academic mode. The subsequent 8-attemptmissing_artifactstall (model never rewrote after quarantine) reproduces the already-documented gpt-oss endgame-collapse weakness (runs 11/13); the quarantined-draft-restore fix delivered the real, mostly-correct draft with a loud warning banner instead of losing it to salvage narration, exactly as designed. -
Checkmark-on-error TUI bug fixed (2026-07-12,
ad07a5f):ToolCallWidget.set_resultalways rendered a green checkmark regardless of the result text — a real run showed aread_workspace_filecall marked complete despite returning an error. New_looks_like_tool_error()(matches "Error:"/"CRITICAL TOOL EXECUTION ERROR"/"forcefully aborted") drives both the TUI glyph and a newRunState.record_tool_errorcounter/sample log. -
Fuzzy-filename fallback for
read_workspace_file/grep_workspace_file(2026-07-12): traced root cause of a run that gathered substantial research (33 fetches, 38 findings) but never produced a report — 16% of workspace-read calls used a garbled/truncated filename reconstructed from memory by a sub-agent one hop removed from the original fetch (e.g.sources/nixtaverse_nixta?), each failure burning a turn and a quota unit, cascading intoQuotaAbortExceptionaborts.resolve_fuzzy_filename()insrc/tools/fs.py(difflib.SequenceMatcher, conservative single-best-match threshold) now auto-resolves these instead of erroring. -
Structured
_run_state.jsonlogging (2026-07-12): full completion-check verdict detail (not just the problem label) now persisted per attempt;RunState.record_tool_error(count + samples);RunState.next_subagent_labeldisambiguates repeat dispatches of the same task name (SubAgent_x→SubAgent_x#2, with a collision-avoidance guard against a task literally named to collide with the auto-generated suffix) so post-hoc elapsed-time analysis on sub-agents is trustworthy without hand-parsing the raw session log. Live-validated end-to-end in the answer-mode smoke test below. -
/resume-runadded to the TUI (2026-07-12): was CLI-only for a full prior session unnoticed — the exact scenario it exists for (a quarantined run with real work already on disk) happened and had no TUI path. New no-argument slash command with a picker, reusing the existing headlessload_resume_state/build_resume_inputlogic. Prompted two new CLAUDE.md rules: mandatory TUI/CLI feature parity checks, and tracing a change's blast radius across sibling surfaces before calling it done. -
Answer mode (2026-07-12), from the
dzhng/deep-researchcandidate below: thirdreport_styleoption (standard/academic/answer) — a short 1-3 sentence direct answer, no section headings, inline(Source: [Title](URL))citation instead of a References list. Live-validated ondeepdelve-gpt-oss: first attempt hit a realclaim_unsupportedquarantine (model's citation format deviated from spec, no square brackets around the title); the completion-check cycle correctly caught it and nudged a rewrite; attempt 2 passed with a clean short answer — confirmsextract_cited_urlstolerates the format deviation and that the quarantine/nudge cycle generalizes to a third report style, not just the original two. -
TUI
ProcessingWidgettimer leak fixed (2026-07-12,e24ecd8): caught live — a run's final turn (model's response after tool quotas were exhausted, with nothing left to say) streamed zero content, soProcessingWidget.stop()— gated on the turn's first content token — never fired. Itsset_intervalanimation kept climbing the elapsed-seconds counter indefinitely, well past the point the run had already reached its quarantine-restore final verdict, making a genuinely finished run look stuck. Same UI-implies-false-run-state bug class as the checkmark-on-error fix earlier this session. Fixed with unconditional cleanup once the stream is guaranteed exhausted, not just the reactive first-token path. Checkedrun_cli(no equivalent — plain stdout writes, no stateful timer widget there). -
NIM cross-model benchmark (2026-07-12/13): the standing heuristics-algorithms/sales-forecasting benchmark query run against
deepseek-ai/deepseek-v4-pro,nvidia/llama-3.3-nemotron-super-49b-v1.5, andopenai/gpt-oss-20b, all via NVIDIA NIM. deepseek-v4-pro crashed on an uncaught 429 mid-run (real progress lost, not a quality issue); nemotron-super-49b made zero realdelegate_taskscalls and fabricated 100% placeholderexample.comcitations in its wrap-up; gpt-oss-20b was the only one to reach a clean pass, but the report was thin and its one real citation had a wrong paper title (the exact failure class Track 1 below now catches). None beat localgpt-oss:20b— confirms a single general-purpose LLM handling research+synthesis+verification end-to-end has a real ceiling here, not just a local-model weakness, directly motivating the two tracks below. -
Two tracks of "specialized non-LLM component instead of another LLM call" (2026-07-12/13), informed by FactScore's decompose-then-verify pattern and HALT-RAG's combine-lexical-and-NLI finding (don't replace term-overlap with NLI, layer it on top), plus independent confirmation that Anthropic's own multi-agent research system beats a single agent by 90.2% specifically on deep research — validating DeepDelve's existing Planner→Searchers→Analyzers shape, not just the new work here:
-
Track 1, NLI-based grounding verification (
dc977a6):nli_unsupported_probleminutils/grounding.py— a smallcross-encoder/nli-deberta-v3-smallentailment classifier (86M params, CPU-only, lazy singleton, fails open on any load error) runs only on claim lines that already passed the cheap term-overlap check, scored against the source's own best-matching paragraph window, flagging only on contradiction (never neutral). Catches a citation with the right source and shared terms but a wrong specific detail — e.g. a paper title quoted with one word swapped ("Dual Causal Network" vs. the source's real "Dual Correlation Network," the NIM benchmark's exact failure above).settings.grounding_check.nli_verify(defaulttrue, fail-open). First ML/NLP dependency this project has taken on; caught and fixed a real footprint issue (pip install sentence-transformerspulls the full CUDA torch build, ~6GB, even though nothing here touches a GPU — switched to the ~200MB CPU-only wheel). -
Track 2, fetch-time metadata extraction (
05b175b):_extract_html_metadataintools/web.pypulls title/author/published-date from the same BeautifulSoup parse_strip_boilerplate_htmlalready builds, written asTitle:/Authors:/Published:header lines alongsideSource-URL:. Eliminates the "Extract title/authors/abstract from [paper]" sub-agent dispatch pattern that fired 13 times identically in one day's logs — the single most repeated mechanical delegation observed. -
Live-verified end-to-end on local gpt-oss (not just mocked tests): confirmed real
Title:/Authors:headers on fetched sources and confirmed the old mechanical metadata sub-agent pattern never fired once in the verification run.
-
Track 1, NLI-based grounding verification (
-
Uncaught crash on malformed-tool-call retry exhaustion, fixed + TUI parity added (
f5dd1af): a hugewrite_workspace_fileargument got truncated mid-JSON by the model; the existing 2-retry recovery correctly retried twice, but the 3rd consecutive occurrence hit a bareraisethat killed the whole run with an uncaught 500 — at attempt 8/8, after 18 real sources already fetched and 5 report attempts already written to disk.run_clinow degrades to the same final-verdict path used formax_run_minutes/context_budgetexhaustion instead of crashing;run_agent(TUI) gained the identical retry-then-degrade logic it previously had none of at all for this failure class (CLAUDE.md TUI/CLI parity rule). -
Tool-call validation-error visibility gap found and fixed (
5eb8fbc): a full-day log cross-reference found"Error: Argument parsing failed."was the single most common error signature of the day (41 occurrences) — and every one had its actual cause silently stripped, becauseagent-framework'sinclude_detailed_errorsconfig was never enabled. Enabled on the shared client (helps the model self-correct on retry too, not just diagnostics). Two of the 41's concrete root causes fixed the same commit:grep_workspace_filewas missingpatternin 13 occurrences (the model was using it to check file existence, not search — docstring now says so explicitly) andfetch_url_to_workspacewas missingfilenamein 5 (made optional with an auto-derived default, since a missing REQUIRED field is rejected by schema validation before the function body ever runs and can't be caught defensively inside it). ~15 moreweb_searchmulti-item-query failures investigated but inconclusive offline — will be diagnosable live now that detailed errors are on. -
Second live-confirmed completion-check stall,
missing_findings, fixed (66fae56): a verification run produced literally zero content (no tool call, no text) in response to this nudge for 6 consecutive attempts, then genuinely self-corrected with real content on the 7th — a different failure shape frommissing_artifact's (which never self-corrected without help). Wording escalates after the first occurrence and, from the second on, hands the model its own actual fetched URLs verbatim as proof real material exists — deliberately WITHOUTmissing_artifact's aggressive early-cutoff, since that would have killed this exact run's real recovery at attempt 3, before its genuine success at attempt 7. Superseded for report-authoring problems by the Builder Build→Review→Fix loop below, butmissing_findingsitself stays Planner-escalated (see that entry) since it means Pass 1 was skipped, not that the report is bad. -
Builder sub-agent + Build→Review→Fix loop (2026-07-13) — the direct fix for the context-growth risk above. User's diagnosis: the Planner's own conversation only ever grows across a run (no compaction exists in the underlying
agent-frameworksession; every completion-check retry historically meant appending another nudge and re-showing the model its own rejected drafts) — "context poisoning," a documented failure mode where an agent's own accumulated context degrades its attention well before any hard token limit. Maps onto the established "Plan-and-Execute" agentic pattern (see README References) and reuses the existingdelegate_tasks/_run_single_taskmechanism, which already gives every dispatched sub-agent a genuinely fresh, isolated context — the fix is routing report-writing retries through that mechanism instead of the Planner's own conversation, not inventing a new one.- New Builder role (
src/prompts.py,src/app.py) — writes/rewritesfinal_report.mdfromfindings.md. The Planner no longer writes or delegates the report at all; its own instructions end at Pass 1 (findings.md, optionally reviewed byPeerReviewer). -
src/engine/completion.pyclassifies completion-check problems into Builder-fixable (missing_artifact,not_grounded,claim_unsupported,non_url_citation,regulation_unsupported,stub_source,nli_unsupported,uncited_claims— all fixable by rewriting the report from the SAMEfindings.md, no new research needed) vs. Planner-escalated (missing_findings,findings_ungrounded,not_delegated— genuinely need more/different research, which only the Planner can decide to delegate). - For Builder-fixable problems,
run_completion_checkdispatches a Build → Review → Fix sequence directly — Builder rewrites the artifact, a freshPeerReviewerdispatch reviews the result (generalized to review eitherfindings.mdorfinal_report.md, with a requiredREVIEW: CLEAN/REVIEW: ISSUES FOUND:opening line so the caller can branch without another LLM call — a malformed/missing sentinel is treated conservatively as ISSUES FOUND), and Builder gets exactly one corrective re-dispatch if flagged. None of this touches the Planner's owncurrent_input—run_completion_checkreturns it byte-for-byte unchanged on this path, which is the actual regressiontest_structural_checks.py's new scenario pins. Reuses the existing attempt budget/escalation threshold and quarantine/salvage machinery unchanged (all already filesystem-only, no conversation-state coupling). -
Live-validated end-to-end, two runs (
deepdelve-gpt-oss, 2026-07-13):-
Simple factual query ("current stable Python version + headline feature"): hit
missing_artifacton attempt 1, dispatched Builder (wrote the report), dispatched PeerReviewer (REVIEW: CLEAN, no corrective pass needed), completed cleanly in 691s with the Planner's own conversation untouched by any of it (_run_state.jsonshows exactly one Builder-fixable cycle,Noneon the next check). Confirms the clean-pass path works end-to-end exactly as designed. -
The standing heuristics-algorithms sales-forecasting benchmark (the same 3-way-AND query
already documented above as having no source satisfying all three criteria — genuinely hard,
not a fluke): the loop DID fire correctly 3 times on real
not_groundedproblems (a fabricated arXiv URL), each time dispatching Builder then PeerReviewer without touching the Planner's conversation. New finding, not previously possible to observe: on attempts 4-6, Builder itself hit the SAME "narrate instead of write" failure the Planner used to be prone to — because Builder shares the run's singlewrite_workspace_filequota pool with the Planner and every prior Builder dispatch, and by attempt 5 that pool was exhausted; Builder's own text even says so explicitly ("I'm unable to create new files because thewrite_workspace_filequota has been exhausted"). The pre-existing quarantine-restore fallback caught this correctly at the final verdict — restored the best surviving draft (from the attempt-3 quarantine) with its loud unresolved-check banner, an honest labeled recovery rather than a silent failure or a lost draft. Total run time (1448.7s) was longer than this exact query's earlier pre-Builder baseline (1174.4s, ended in unlabeled "retry budget exhausted" instead) — extra wall-clock from the added Build/Review dispatch turns, on a query the architecture was never going to make suddenly satisfiable. Net assessment: the Build→Review→Fix mechanism itself works as designed (dispatch routing, sentinel parsing, current_input staying untouched, all confirmed); it does not (and isn't meant to) rescue a query where the source material genuinely doesn't exist, and it surfaced a new, real quota-sharing constraint under heavy retry load — see "Carried forward" below.
-
Simple factual query ("current stable Python version + headline feature"): hit
- Deferred, documented as a known residual gap rather than blocking this change:
context_budget_chars/stream_content_chars()still doesn't count text injected outside a stream's own generation loop, so the 3 Planner-escalated problems can still in principle grow the Planner'scurrent_inputunboundedly on repeat — lower priority since those problems are rarer/more terminal (a stuck Planner, not an oscillating-on-polish loop).
- New Builder role (
-
Phase 1 of the approved 6-phase plan: claim-level grounding upgrade (atomic-claim decomposition + per-claim evidence binding). (Found 2026-07-13, informed by FActScore's decompose-then-verify pattern (arXiv:2305.14251, already cited in README for the NLI check) and Rasheed et al.'s claim-evidence provenance framing (arXiv:2602.13855 — UPDATE 2026-08-17: read in full for the first time as part of a broader literature-completeness audit; the real title is "From Fluent to Verifiable: Claim-Level Auditability for Deep Research Agents," not "Claim-Evidence Provenance in Grounded Generation" as this file and README.md had it — a citation title error, author/substance were correct, now fixed in both places. It's an unreviewed perspective paper, not empirical validation, but its claim-node/typed-edge formalization matches what's cited here).) The prior
claim_grounding_problemcompared a WHOLE LINE's terms against the UNION of every source cited anywhere on that line — a real gap when a line carries two distinct claims each with its own citation (e.g. "Sector A grew 12% gov, while Sector B declined 3% news"): a shared generic term between claim A and claim B's source could mark BOTH claims "supported" even though claim B's own citation didn't actually back it. Newutils/grounding.py::decompose_claim_segmentssplits a line into atomic segments at each citation boundary (mechanical regex-token splitting, no NLP, no new dependency — matches the "the decomposition step only splits propositions, it doesn't decide what's true" design goal);claim_grounding_problemnow checks each segment only against its OWN bound citation's source, closing the citation-sharing/drift gap. A line with zero or one citation decomposes to itself unchanged, so this is a strict refinement — every previously-passing single-citation-per-line test is unaffected (verified: full suite passes with zero pre-existing assertion changes needed). New tests:decompose_claim_segmentsunit assertions (single-citation invariance, 2-citation split, trailing-uncited-text handling) plus a live-shaped same-line scenario intest_structural_checks.py(a genuinely-supported cacao claim and a fabricated software claim sharing one line, each with its own distinct citation — correctly flags only the fabricated one, by its own citation, not the supported one's). Residual note — CLOSED 2026-07-14, commitfa2e562:nli_unsupported_problem/topical_relevance_problem(both driven by the shared_grounded_claim_pairs) had the same latent whole-line term-overlap gap this pass fixed forclaim_grounding_problem— now ported toutils/grounding.py::_grounded_claim_pairs, iteratingdecompose_claim_segments(line)the same way. New test:_grounded_claim_pairs_scenario(pure function, no NLI model load needed) pins a same-line two-claim case yielding two correctly segment-scoped pairs. -
check_excluded_topic— report-write-time enforcement of query exclusions, closing the gap in the "Hard exclusion rules" finding below.delegate_tasksalready skipped DISPATCHING a task whose own topic matched an explicit query exclusion (_extract_excluded_topics), but did nothing to stop that topic showing up as its own section in the final report anyway, recalled from a sibling task's tangential findings. Newengine/completion.py::check_excluded_topic(aGROUNDING_CHECKSentry,_BUILDER_FIXABLE_PROBLEMSmember) reuses the exact same_extract_excluded_topicsparser, now applied tofinal_report.md's own h1-h3 heading sections (utils/grounding.py::split_into_heading_sections, extracted from the existingfind_uncited_claim_linessection-scoping logic so both share one implementation) — deliberately heading-scoped rather than whole-document substring matching, so a topic mentioned once in passing prose doesn't false-positive the way a bare match would. New verdict-matrix row intest_structural_checks.py(query exclusion + a report with its own "## Sector Agritech" section). -
Phase 2 of the approved 6-phase plan: cross-source contradiction detection (FEVER-style, Thorne et al., NAACL 2018,
fever.ai). Depends on Phase 1's claim segmentation (decompose_claim_segments). Newutils/grounding.py::find_cross_source_contradictions: builds a (subject_phrase, figure) index of every OTHER fetched source's own claims (_extract_figure_claims, each subject paired with its NEAREST same-line figure by character distance, not a full cross-product — avoids cross-contaminating unrelated subjects sharing a line), then for each report claim segment, checks whether a DIFFERENT fetched source (one not cited on that segment) reports a same-kind (_figure_kind— never a year against a percentage) but numerically different figure for the same subject, unmentioned anywhere else in the report. Distinct fromclaim_unsupported: the cited source really does support the claim — this instead catches the report silently picking a side of a real disagreement between two fetched sources without saying so. Newengine/completion.py::check_cross_source_contradiction(GROUNDING_CHECKSentry,_BUILDER_FIXABLE_PROBLEMSmember — Builder is told to surface both figures rather than pick one). New verdict-matrix row (two fetched sources reporting 12% vs 18% for "Sector Fintech", report cites only the 12% one) plus isolated pure-function sanity checks during development that caught and fixed a real bug before it shipped: an early version paired every subject with every number on a line regardless of kind, so a line naming both a year and an unrelated percentage spuriously "contradicted" any other source's differing percentage for a totally unrelated reason — fixed by the same-kind guard and nearest-figure pairing.-
Second real bug, found live 2026-07-14 during Phase 6's TUI smoke test, fixed and
live-verified. A citation attribution (an organization name appearing ONLY inside a
- Source: [Title - Statistics Iceland](url)line, and dozens of times across a long fetched Wikipedia article as bare source attribution / image captions / reference-list entries, never as the subject of an actual claim) got treated by_extract_figure_claimsas a genuine claim subject, paired with an unrelated nearby year by the nearest-figure heuristic — firingcross_source_contradictionon the exact same phantom issue after every single Builder rewrite, a structurally unfixable, non-converging retry loop (Builder can't satisfy a check based on a false premise). Caught only because the user pushed back on accepting the loop at face value ("you're not analyzing the run properly") rather than assuming it was Phase 6's stream-handling. Fixed with newutils/grounding.py::_is_citation_only_line— a line is bibliographic, not a claim, if fewer than 8 letters of real text remain after stripping markdown links and a leading bullet/number/"Source:" marker;_extract_figure_claimsnow skips citation-only lines entirely, on both the report's own prose and each fetched source's raw content. Verified two ways: (1) pure reproduction against the real saved report + Wikipedia source from the killed run, confirmingfind_cross_source_contradictionswent from a real hit to[]; (2) a fresh live re-run of the identical query converged in 1 Builder cycle and 307.2s (vs. 5+ cycles and never converging before). New regression test_cross_source_citation_line_scenariointest_structural_checks.py, confirmed not to weaken the existing genuine-contradiction verdict-matrix row.
-
Second real bug, found live 2026-07-14 during Phase 6's TUI smoke test, fixed and
live-verified. A citation attribution (an organization name appearing ONLY inside a
-
Phase 3 of the approved 6-phase plan: xQuAD-style search-result diversity reranking (Santos, Peng, Macdonald, Ounis, Explicit Search Result Diversification through Sub-Queries, ECIR 2010). DDGS already ranks by its own relevance signal, but several near-duplicate results for the same angle commonly dominate the top of that ranking — addresses the already-documented "scaling down scope did not improve grounding rate" finding (a 5-source run still only surfaced ~5 genuinely distinct sources, thin discovery even at small scope). New
tools/web.py::_diversity_rerank: greedily reordersweb_search's results by MARGINAL new aspect-term coverage instead of raw rank — DDGS's own #1 always stays first (preserving its relevance judgment for the single best result), then each subsequent pick is whichever remaining result adds the most new aspect terms (_result_aspect_terms, a deliberately looser local term extractor thanutils.grounding.extract_salient_terms— a short snippet needs single-word distinguishing terms, not just 2+-word capitalized phrases, same reasoningorchestrator.py's_extract_scope_entitiesalready documents for not reusingextract_salient_termseither). Pure reranking, no LLM call, no new dependency. Single integration point (web_search, right after search-health recording, before the auto-fetch slice) improves both consumers downstream — the auto-fetch selection and the returned snippet ordering — without touching either consumer directly. New tests intest_structural_checks.py: a near-duplicate-heavy case (3 near-identical fintech results + 1 genuinely distinct agritech result — the distinct one gets promoted to position 2), empty/single- result edge cases, an already-diverse case (order preserved), and direct_result_aspect_termsstopword/length-filter assertions. -
Phase 4 of the approved 6-phase plan: topical-relevance cross-encoder reranker. Third-stage grounding check, layered after
claim_grounding_problem(term-overlap) andnli_unsupported_problem(entailment) — reuses the exact same evidence set as the NLI check (extracted into a new shared_grounded_claim_pairshelper, factored out of both functions) but asks a different question: is the cited source actually about the SAME SUBJECT as the claim, not just lexically overlapping and non-contradictory? Fixes the GOA-algorithm-vs-Goa-state acronym collision above — 'GOA'/'Goa' term-overlap passes and an EV-policy sentence about Goa doesn't contradict an algorithm claim (it's just unrelated), so neither upstream layer can catch it. Newutils/grounding.py::_get_topical_relevance_model/topical_relevance_problem: a secondsentence-transformersCrossEncodercheckpoint,BAAI/bge-reranker-v2-m3— not a new pip dependency,sentence-transformersis already installed for the NLI check, this just loads a second checkpoint through the same library. Constructed with an explicitSigmoidactivation so.predict()returns a 0-1 relevance probability directly. Newengine/completion.py::check_topical_mismatch(GROUNDING_CHECKS/_QUARANTINE_PROBLEMS/_BUILDER_FIXABLE_PROBLEMSmember, mirrorscheck_nli_unsupported's string-prefix-matching pattern exactly). New config keyssettings.grounding_check.topical_relevance_check/topical_relevance_threshold(default 0.1), documented inconfig_template.yaml. Verified against the REAL checkpoint, not just the mocked test (test_structural_checks.py's_topical_relevance_scenariomocks the model the same way_nli_verify_scenariodoes, to keep the suite fast/offline): loaded the real model standalone and scored the exact GOA/Goa pair — the irrelevant (Goa-state) pair scored 0.023, the relevant (GOA-algorithm) pair scored 0.997, a huge margin either side of the 0.1 threshold, confirming the Sigmoid-activation design assumption was correct before it ever reached a live run. Real bug caught and fixed during this same pass: the new check's config gate wasn't included in the test suite's existingnli_verify: Falseguards (4 call sites), so the first full-suite run after wiring it in silently loaded the REAL, unmocked bge-reranker-v2-m3 model — the exact anti-pattern that guard was built to prevent, now closed at all 4 sites plus a 5th (_nli_verify_scenarioitself, which neededtopical_relevance_check: falseadded since it deliberately leavesnli_verifyon). -
Phase 5 of the approved 6-phase plan: coverage accounting / ResearchMap. Distinct from every other completion check: those all verify content that ALREADY EXISTS is properly grounded/cited; this instead asks whether the Planner's own top-level delegated research plan actually paid off — a report can be perfectly grounded yet still be thin because most of the Planner's own delegated angles came back with nothing usable and got silently dropped rather than surfaced or retried. Deliberately built entirely from already-reliable, model-independent structural data instead of a new Planner-authored schema (investigated first and explicitly ruled out:
_todos.mdis free text with only a prompted, zero-code-validated convention — exactly the kind of compliance-dependent signal this project's own established philosophy avoids, given repeated live failures of small local models following new structured-output requirements). Newutils/run_state.py::RunState.coverage()reuses two ALREADY-existing, engine-populated primitives —delegation_depth_ctx(depth==1 = a task the Planner itself dispatched viadelegate_tasks; depth>1 = a nested Analyzer-tier sub-call, excluded from coverage since it's expected to reuse already-fetched content with no new URL of its own) and per-task fetch attribution (task_fetched_urls_ctx, from the 2026-07-12 race-condition fix) — to compute{total, covered, ratio, uncovered_task_names}over distinct top-level task names.RunState.add_findinggained optionaltask_name/depthparams (both defaultNone, fully backward compatible) soorchestrator.py::_run_single_task's existing two call sites can tag each finding with what produced it. Newengine/completion.py::check_thin_coverage(COMPLETION_CHECKSentry, right aftercheck_not_delegated— same category, "did research happen adequately," not grounding). Not Builder/FindingsWriter-fixable (fixing thin coverage needs NEW delegation, which only the Planner can decide, same asnot_delegated) — falls through to the classic inject-into-Planner path by design. Conservative by construction: fires only when a MAJORITY of top-level tasks came back with no real source (threshold, default 0.5) AND there are enough of them for that ratio to mean anything (min_tasks, default 2) — a single-task query (the common case for a simple factual lookup) that succeeded is never affected regardless of "breadth." New config:settings.coverage_check.{enabled,threshold,min_tasks}. New tests: a pureRunState.coverage()unit-test block (empty run, single covered task, nested-Analyzer exclusion, 1-of-3 thin case) plus acheck_thin_coveragewiring scenario (fires with the correct injected task names + ratio text, stays silent on a successful single-task query, stays silent exactly AT the threshold — confirms "below," not "at or below"). TUI/CLI parity confirmed by construction: bothrun_cliandrun_agentcall the same sharedrun_completion_check, and_run_single_taskis shared engine code, so no surface-specific wiring was needed. Full suite +ruff check .pass. Committed2a70d01.-
Live verification (same day) found 2 more real bugs, both fixed and live-confirmed — the
standing-rule smoke test for this phase took 4 attempts; the first 3 timed out for reasons NOT
in Phase 5's own code, and root-causing each timeout (per the "don't hand-wave as model
slowness" standing rule —
journalctl -u ollama,~/.deepdelve/sessions/session_<id>.json,ollama ps) surfaced two separate, previously-invisible bugs:-
settings.sub_agent_timeout_minutes(the Phase-4-era sub-agent deadline fix,d72772c/9962a22) was never actually live — it exists inconfig_template.yamlbut nothing back-fills an existing user's real~/.deepdelve/config.yaml, so it was silently0(disabled) the whole time; every earlier "live-verified" confirmation of that fix was only true because the key had been temporarily test-added to the config and reverted afterward along with unrelated per-test overrides. Not a code bug — fixed by adding the key directly to the live config. New standing memory: newsettings.*keys must be grepped in the LIVE config, not just the template, before a dependent fix counts as verified. -
_dispatch_writer_review_fix's corrective Fix pass had no evidence base of its own (src/engine/completion.py). Its second dispatch (fixing PeerReviewer-flagged issues) is a fresh sub-agent with zero memory of the first Write dispatch;fix_instructionssaid to use "the real source material you were given" but never actually included it. Harmless for Builder (its source,findings.md, is a real re-readable file) but fatal for FindingsWriter, whose source material (_build_findings_source_material) only ever existed as a string in the first dispatch's prompt. Confirmed live: aFindingsWriterFix_..._revieweddispatch burned its entire turn huntingread_workspace_filefor guessed, nonexistent filenames (task_results.json,research_results.json,instructions.md) instead of writing a fix. Fixed by re-appending the originalwrite_instructionstofix_instructions, keeping the function writer-role-agnostic. Live-confirmed fixed on the very next run. -
check_thin_coverageitself false-positived on the project's own internal Write→Review→Fix dispatches (src/engine/orchestrator.py).Builder/FindingsWriter/PeerReviewerare dispatched directly from the Planner's own top-level context (viarun_completion_check, notdelegate_tasks), so they land atdelegation_depth_ctx==1exactly like a genuine top-level research task — structurally indistinguishable by depth alone. Confirmed live: coverage counted'FindingsWriterFix_attempt1','ReviewFix_attempt1','FindingsWriterFix_attempt1_reviewed'as 3 of 5 "delegated research tasks" that produced no source. Fixed with a new_NON_RESEARCH_DISPATCH_ROLES = frozenset({"Builder", "FindingsWriter", "PeerReviewer"})constant;add_findingnow skips recording entirely for those roles. Pinned by a regression test asserting the exact role set. Live-confirmed fixed: the final clean run's_run_state.jsonshowed only real task names infindings, coverage 2/2 (ratio 1.0), nothin_coverageentry.
-
Final clean end-to-end run
(
compare_the_population_of_canada_and_australia_20260714_170629, 1018.5s):findings.mdandfinal_report.mdboth written, PeerReviewer passed clean on thefinal_report.mdre-check, real grounded citations (ABS + Wikipedia). Full suite +ruff check .pass throughout. Committed3dd349a.
-
-
Live verification (same day) found 2 more real bugs, both fixed and live-confirmed — the
standing-rule smoke test for this phase took 4 attempts; the first 3 timed out for reasons NOT
in Phase 5's own code, and root-causing each timeout (per the "don't hand-wave as model
slowness" standing rule —
-
Phase 6 / B4: unify
run_cli/run_agent's stream-iteration + retry logic — DONE. The two genuinely duplicated pieces between headless (run_cli) and TUI (run_agent) extracted into shared helpers inengine/orchestrator.py:iter_agent_stream(stream, deadline)(async generator racing each update against an optional wall-clock deadline viaasyncio.wait_for, replacingrun_cli's inline manualstream_iter/while Trueloop;deadline=None, the TUI's case with no wall-clock limit by design, is behavior-identical to a plainasync forperasyncio.wait_for's own documented semantics, sorun_agentgets the same iteration mechanics for free with zero behavior change) andclassify_malformed_retry(...)(pure decision logic for the malformed-tool-call retry pattern, previously copy-pasted between both call sites and once found missing fromrun_agententirely — callers keep their own stdout/widget notification, only the retry decision itself is shared). CI green, both CLI and TUI live-verified this session (same smoke test that caught the_is_citation_only_linecross-source-contradiction bug above). Committed2e4758f. This was the last open phase of the 2026-07-14 6-phase plan — all 6 phases now done. -
claim_grounding_problem/_grounded_claim_pairsfalse-positive on citation-only sub-bullets — FIXED 2026-07-14, commit061c10a. Root-caused a live Eiffel Tower smoke-test failure that burned its entire 8-attempt retry budget onclaim_unsupported: both flagged claims ("2 years, 2 months and 5 days", "assembly began July 1, 1887, completed twenty-two months later") were verbatim in the fetched source — a genuine false positive, not model fabrication. This project's own Builder output shape puts a claim on one line and its citation on a SEPARATE- Source: [Title](url).sub-bullet; the bare sub-bullet was being processed as its own claim segment, andextract_salient_termspulled "Official Eiffel Tower" out of the citation's own editorialized anchor text as if it were a checkable fact — then failed it because that exact phrase (the writer's own paraphrase) doesn't appear verbatim in the source._is_citation_only_linealready existed for exactly this line shape (built for_extract_figure_claims/cross-source-contradiction, 2026-07-14 earlier this same day) but was never applied here. Now guarded in both functions. New test:_citation_only_subbullet_scenario, reproducing the real failing report/source directly rather than a synthetic case. Live end-to-end re-verification: the exact same query re-run end-to-end produced zeroclaim_unsupportedoccurrences (vs. 6 consecutive + retry-budget- exhausted before), converging cleanly by attempt 4 in 490.0s vs. the prior run's 1017.9s wasted grinding on the false positive. -
_dispatch_writer_review_fiximmediate narration salvage — IMPLEMENTED 2026-07-18, live verification in progress. Targets the "writer role Finishes its turn without ever callingwrite_workspace_file" failure class (Bonsai-8B,qwen2.5:3b-instruct) at its root: the project already had_salvage_narrated_reportfor a model that narrates a complete report as chat text instead of calling the tool, but it only ran as a LAST-RESORT at final-verdict time, and only formissing_artifact—missing_findings(the FindingsWriter case, the one that actually burnedqwen2.5:3b-instruct's full 8-attempt budget) had no equivalent path at all. Now checked immediately after every Write dispatch inside_dispatch_writer_review_fixitself (shared by both Builder and FindingsWriter): ifreq_artifactis still missing but the dispatch returned ≥200 chars of real text, that text is persisted as the artifact right away, clearly flaggedAUTO-RECOVERED DRAFT, and flows into the same PeerReviewer/Fix cycle and grounding checks a genuine write would — instead of looping blind on a file that will never appear on its own. New test coverage:_immediate_narration_salvage_scenario(salvage fires, correctly flagged, converges in 2 dispatches) and a negative-case scenario confirming a real write is never clobbered by salvage logic. Does not touchCOMPLETION_CHECKS/GROUNDING_CHECKS/anyVerdict—test_structural_checks.pyrun and passing regardless per project rule. Live re-test against the exact case that motivated it (qwen2.5:3b-instruct, same sales-forecasting benchmark) surfaced a more precise root cause than assumed: this model'sFindingsWriterFixdispatches return a genuinely empty response (confirmed via the persisted session log: zero events, no tool call, no text at all — not a narrated report), the exact same symptom already documented for Bonsai-8B, not the "narrates full content instead of calling the tool" pattern the fix targets. Salvage correctly declines to act (nothing above the 200-char floor to recover) rather than fabricating content from nothing. Conclusion: the fix is verified correct and safe, but doesn't rescue THIS specific model's failure shape — it would still help a model that genuinely narrates substantial content instead of calling the tool (the originally-documented pattern, seen in the reference project this was forked from too). Whetherqwen2.5:3b-instructis rescuable at all needs a different angle (e.g. option 4 below, keep it out of the writer role entirely) if pursued further.-
Full re-run completed 2026-07-18, confirms the diagnosis:
Report: NOT WRITTEN, stillmissing_findings, 8/8 attempts, 3524.3s (58.7 min — vs. 254.6s on the original run) with the live defaultsub_agent_timeout_minutes: 10in effect.findings.mdstill never existed on disk at any point across all 9 dispatches (0 through the final_reviewedpass) — confirmed via the run folder listing (_run_state.json/sources/only) and the persisted session log (zeroFindingsWriterFix*-sourced events across the ENTIRE run, meaning every single dispatch, not just attempt 1, returned nothing usable). The 14x wall-clock increase traces to one specific event, confirmed live viajournalctl -u ollama's ownprint_timingoutput (not assumed): the final corrective pass (FindingsWriterFix_attempt8_reviewed) decoded 45,000+ tokens continuously at ~80 tok/s, blew past its own 16K context window once (forcing acontext shift, n_discard = 8189), and was still running when checked — a second, independent confirmation of the "runaway generation with no natural stop point" failure class this project already fixed the missing GUARD for (README: "Independent per-dispatch wall-clock deadline," originally found via a Gemma4 19,908-token case) —sub_agent_timeout_minutescorrectly cut it off rather than hanging forever, but whatever text existed at cutoff still wasn't real content (0 events recorded), so nothing was salvageable even from that dispatch. Final verdict:qwen2.5:3b-instructgenuinely has no recoverable content to give in the FindingsWriter role, empty responses and runaway non-answers alike — this is a harder failure than "narrates instead of writing," and no structural salvage can rescue a dispatch that produces nothing at all. Confirms option 1 (structural fix) is exhausted for this specific candidate; option 2 (keep it out of the writer role, use it only for Searcher/Analyzer-tier work where it has shown real capability — 3 sources fetched cleanly, 0 search failures, both runs) is the next thing worth trying if this model is revisited.
-
Full re-run completed 2026-07-18, confirms the diagnosis:
-
Shared quota-pool starvation — FIXED 2026-07-18 (
src/tools/core.py::check_quota). Ring- fences a task's remaining quota once it's shown real fetch activity this dispatch (task_fetched_urls_ctxnon-empty, already per-task/race-free): the first time a tool call would exceed the shared cumulative limit for a task that's already fetched something real, grants one small one-time top-up (+2) instead of hard-blocking, bounded by a_rescuedflag on the pool entry so it can only fire once per tool per run — not an unbounded loophole. Directly targets the documented failure below (a dispatch that fetched 2 real sources, then hit a bare "Quota reached" wall before ever synthesizing them) — addresses option (b) from that finding's own candidate list (ring-fencing a task's remaining quota once it's shown real progress). Does NOT fully cover every angle that finding raised: a REDISPATCH's owntask_fetched_urls_ctxstarts empty again, so a task that gets cut off before fetching anything on a later retry still isn't rescued — options (a)/(c) from that finding remain open if that gap resurfaces. Verified with 3 direct unit scenarios (rescue fires once, normal enforcement resumes after, no rescue without real fetch activity) plus a live headless smoke test with no regressions. Doesn't touchCOMPLETION_CHECKS/GROUNDING_CHECKS/anyVerdict, so the verdict-matrix test requirement doesn't formally apply, thoughtest_structural_checks.pywas still run and passes. -
Brave Search MCP
countryparameter rejecting real countries (e.g. Colombia) — FIXED 2026-07-18 (src/tools/mcp_loader.py::_wrap_brave_search_tool/_BRAVE_SEARCH_COUNTRY_ENUM).@brave/brave-search-mcp-server'scountryparam is a fixed 37-code zod enum that does not includeCO(confirmed by reading the installed package's own schema source) — broke every Colombia-targeted search outright (MCP error -32602). WrapsMCPTool.call_tool(confirmed viaagent_framework's own source that every model-invoked call to any function this MCP server advertises funnels through this one method) to strip an out-of-enumcountryvalue before it reaches the subprocess, falling back to an unscoped search instead of a hard rejection. Scoped only to specs whose server name contains "brave", so it can't affect any other MCP server. Unit-verified directly against a fakecall_toolbefore the live smoke test confirmed no regressions. -
Completion-check remediation loop exhausting
read_workspace_file's quota before the final Builder pass gets to read what it needs — FIXED. Found live 2026-07-20 during the routing classifier's second re-test run: a run hitting multiple completion-check remediation cycles (missing two-pass discipline, missing artifact, unsupported-claim flag) dispatches a corrective sub-agent per cycle (FindingsWriterFix,BuilderFix,ReviewFix), each burningread_workspace_filecalls against the SAME shared quota (limit 30) as normal first-pass work — confirmed live, 3 remediation cycles in one run exhausted the quota, and the finalBuilderFixpass silently dropped an entire correctly-researched section (findings.mdhad it,final_report.mddidn't) rather than erroring loudly. Fixed 2026-07-21 (commite1ba577):_ensure_reader_quota_headroom(src/engine/completion.py:734), mirroring the existing writer-side_ensure_writer_quota_headroom, tops up the quota at both Builder/FindingsWriter remediation dispatch sites.test_structural_checks.pyextended, passes. -
TUI QoE improvements, two shipped items (moved from Pending during the 2026-07-21 status audit; the rest of that backlog item is still open, see Pending):
-
AgentMessageWidgetclick-to-copy — DONE 2026-07-14, commit577fd53. MirrorsUserMessageWidget's existingon_click→_copy_to_system_clipboard/OSC52 fallback pattern exactly — one-click copy on the agent's actual answers/reports, not just the user's own prompt. -
Right-click paste — DONE 2026-07-14, commit
577fd53. Newengine/tui.py::_paste_from_system_clipboard(read-side mirror of_copy_to_system_clipboard:wl-paste --no-newline/xclip -o -selection clipboard, no OSC52 equivalent since that escape sequence is write-only) wired intoPromptInput.on_clickonbutton == 3(right-click, confirmed against this project's installedtextual/_xterm_parser.py's SGR mouse-button mapping), inserting at cursor / replacing the current selection. Live-verified: a real_copy_to_system_clipboard→_paste_from_system_clipboardround trip returned the exact original text. Required installingwl-clipboardon the dev machine — neither it norxclipwas present beforehand, so this had never actually worked via either mechanism (copy silently fell back to OSC52, unverified; paste had no fallback at all). Worth checking for on any fresh setup — without one of these two tools, paste always shows a "clipboard paste failed" warning instead of pasting.
-
History
Model Research
Reviews & Audits
Reference