fix(research): the manifest belongs to the mission, and a brittle phrase must not mean silence
deploy / test (push) Successful in 4m28s
deploy / build (push) Successful in 5m21s

Two defects from the first on-topic run.

**1. The manifest could never be updated twice in a day.** It was written into
the VAULT at a per-DATE path, but it is per-RUN data. A second mission the same
day rewrites a file that already exists, and `auto_merge` correctly refused the
whole branch:

    diff is not additive (1 non-add change(s), first:
    M ContinuousResearch/2026-08-18/harvest.jsonl); left for a human

So `main` kept the FIRST run's manifest, the next mission cloned it, and the
agents analysed yesterday's papers while every log line reported a successful
harvest. The merge policy was right; the placement was wrong. The manifest now
goes into the mission's own checkout after `ensure_checkout`, which keeps the
vault additive and gives each mission exactly its own papers. The agents commit
it alongside their analysis through the normal delivery path.

**2. A quoted phrase that matches nothing looked like a quiet day.** Phrase
search is precise and brittle: "hybrid retrieval BM25 dense" is a reasonable
topic and appears verbatim in no paper on arXiv — measured, 0 hits — while the
same four terms unquoted return exactly the hybrid-retrieval evaluations the
topic asked for. Harvesting zero because of adjacency is indistinguishable from
a genuinely quiet field, which is the distinction `Harvest::healthy()` vs
`added_anything()` exists to preserve. `search` now retries unquoted when the
phrase finds nothing, and says so in the log.

Proven in one run, all three behaviours at once:

    "approximate nearest neighbor search" -> 5 candidates, 5 already held, 0 shelved
    "hybrid retrieval BM25 dense"         -> no exact phrase match, retrying broad
                                          -> 5 candidates, 0 already held, 5 shelved
    "LLM as a judge evaluation"           -> 5 candidates, 5 already held, 0 shelved
    wrote 5 paper(s) to .../ContinuousResearch/2026-08-18/harvest.jsonl

The seen-set suppressing 10 of 15 is the whole point of a recurring mission, and
the 5 that landed are on topic for the first time: RAG architecture evaluation,
agent-controlled search over chat logs, compute-aware retrieval and reranking,
hybrid retrieval in hyperbolic space, sparse-dense fusion limits.

353 tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-18 07:18:11 -07:00
co-authored by Claude Opus 5
parent 2cd0872e50
commit 850f11838b
4 changed files with 137 additions and 41 deletions
+35 -7
View File
@@ -75,6 +75,7 @@ pub async fn on_launch(
// because the phase is what reports whether today was quiet or broken, and
// those must stay distinguishable. What is never acceptable is silence, so
// both outcomes are logged with their counts.
let mut harvested: Vec<crate::papers::Paper> = Vec::new();
if mission.template_kind == crate::continuous_research::TEMPLATE_KIND {
match blobs.as_ref() {
Some(b) => {
@@ -89,9 +90,13 @@ pub async fn on_launch(
)
.await
{
Ok(n) => eprintln!(
"mission_orchestrator: continuous research harvest shelved {n} paper(s) for mission {mission_id}"
),
Ok(papers) => {
eprintln!(
"mission_orchestrator: continuous research harvest shelved {} paper(s) for mission {mission_id}",
papers.len()
);
harvested = papers;
}
Err(e) => eprintln!(
"mission_orchestrator: continuous research harvest FAILED for {mission_id} (phases still start, and will report an empty day): {e}"
),
@@ -113,10 +118,33 @@ pub async fn on_launch(
// repo checkout even though its team was minted on the first
// launch. Non-fatal — logs and continues on failure.
match crate::mission_workspace::ensure_checkout(pool, workspace_id, mission_id).await {
Ok(Some(path)) => eprintln!(
"mission_orchestrator: repo checked out at {} for mission {mission_id}",
path.display()
),
Ok(Some(path)) => {
eprintln!(
"mission_orchestrator: repo checked out at {} for mission {mission_id}",
path.display()
);
// The manifest goes in the CHECKOUT, not the vault: it is this run's
// input, and the vault path is per-date and shared, so a second run
// the same day rewrites a file that already exists and auto_merge
// rightly refuses the branch. See `write_manifest`.
if mission.template_kind == crate::continuous_research::TEMPLATE_KIND {
let date = crate::continuous_research::today();
match crate::continuous_research::write_manifest(&path, &harvested, &date) {
Ok(at) => eprintln!(
"mission_orchestrator: wrote {} paper(s) to {}",
harvested.len(),
at.display()
),
// Loud: the reader phase would find no manifest and, being
// resourceful, go and search arXiv itself — which corrupts
// the seen-set. Better to see why here.
Err(e) => eprintln!(
"mission_orchestrator: could NOT write the harvest manifest for \
{mission_id} — the reader phase will see no papers: {e}"
),
}
}
}
Ok(None) => eprintln!(
"mission_orchestrator: mission {mission_id} has no repo bound, skipping checkout"
),