feat(decide): memory rerank and paper triage on the decision tier
mission_memory::recall is two stages when a key is set: BM25 proposes 8
candidates, one Noul per candidate ('this earlier verdict is relevant to
the task') reorders them and drops those under 0.3, three are kept. BM25
measures keyword overlap, and a verdict about MICROVM.md shares words
with every task that names a file. Without a key the BM25 order stands.
continuous_research: topic_tags was written as [] on every manifest line
since the manifest existed. triage_papers asks, per harvested paper, a
Choice over the mission's topics (readable names, the arXiv query as the
description, 'none' offered) and a four-level relevance Score; the tags
(every topic ≥ 0.3) and {score, confidence} land on the line the agents
already read. Probe on PORTICO's abstract: relevance 3.0 at 1.0; topic
'none' 0.59 / verification 0.41 — true, the topic list has no
authority/sandboxing entry. Untriaged papers write the old empty line.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2656d73def
commit
33560c7fe6
@@ -128,32 +128,161 @@ pub fn write_manifest(
|
|||||||
checkout: &std::path::Path,
|
checkout: &std::path::Path,
|
||||||
papers: &[crate::papers::Paper],
|
papers: &[crate::papers::Paper],
|
||||||
date: &str,
|
date: &str,
|
||||||
|
triage: &[PaperTriage],
|
||||||
) -> Result<std::path::PathBuf, String> {
|
) -> Result<std::path::PathBuf, String> {
|
||||||
let rel = manifest_path(date);
|
let rel = manifest_path(date);
|
||||||
let abs = checkout.join(&rel);
|
let abs = checkout.join(&rel);
|
||||||
if let Some(parent) = abs.parent() {
|
if let Some(parent) = abs.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?;
|
std::fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?;
|
||||||
}
|
}
|
||||||
let body = manifest_lines(papers, date);
|
let body = manifest_lines(papers, date, triage);
|
||||||
std::fs::write(&abs, format!("{body}\n")).map_err(|e| format!("write {}: {e}", abs.display()))?;
|
std::fs::write(&abs, format!("{body}\n")).map_err(|e| format!("write {}: {e}", abs.display()))?;
|
||||||
Ok(abs)
|
Ok(abs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What the decision model said about one harvested paper. `topic_tags`
|
||||||
|
/// was written as `[]` on every manifest line from the day the manifest
|
||||||
|
/// existed — a slot the agents were told to read and nothing filled. This
|
||||||
|
/// fills it: which of the mission's topics the paper belongs to (a Choice,
|
||||||
|
/// every topic ≥ 0.3 kept, `none` when it fits none), and how much it
|
||||||
|
/// matters to an agent platform (a four-level Score), so the ranking phase
|
||||||
|
/// starts from a calibrated number rather than from the title alone.
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, Default)]
|
||||||
|
pub struct PaperTriage {
|
||||||
|
pub topic_tags: Vec<String>,
|
||||||
|
/// 0 = unrelated … 3 = directly about what the platform does. `None`
|
||||||
|
/// when no triage ran (no key, or the call failed).
|
||||||
|
pub relevance: Option<f64>,
|
||||||
|
pub relevance_confidence: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const RELEVANCE_LEVELS: [&str; 4] = [
|
||||||
|
"Unrelated to LLM agents, agent platforms, or their evaluation",
|
||||||
|
"Adjacent: language models or systems work an agent platform might one day draw on",
|
||||||
|
"Relevant: about agents, tools, memory, skills, judging, sandboxing, or multi-agent orchestration",
|
||||||
|
"Directly applicable: a method, measurement, or failure mode a platform running autonomous coding and research agents should act on",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Triage every harvested paper in one call each. Best-effort: a missing key
|
||||||
|
/// or a failed call leaves that paper's tags empty and relevance `None`, the
|
||||||
|
/// state the manifest has always been in.
|
||||||
|
pub async fn triage_papers(
|
||||||
|
papers: &[crate::papers::Paper],
|
||||||
|
topics: &[String],
|
||||||
|
) -> Vec<PaperTriage> {
|
||||||
|
use cm_decide::{Answer, Decider as _, Question};
|
||||||
|
let Some(jev) = cm_decide::jev::Jev::from_env() else {
|
||||||
|
return vec![PaperTriage::default(); papers.len()];
|
||||||
|
};
|
||||||
|
// Topics are arXiv query strings; the option NAME the model sees is the
|
||||||
|
// readable form (`all:"agent memory" AND all:"long-term"` → `agent memory
|
||||||
|
// long-term`), the value the query itself for precision.
|
||||||
|
let mut criteria: std::collections::BTreeMap<String, Option<String>> = topics
|
||||||
|
.iter()
|
||||||
|
.map(|t| (readable_topic(t), Some(t.clone())))
|
||||||
|
.collect();
|
||||||
|
criteria.insert("none".into(), Some("Fits none of the listed topics".into()));
|
||||||
|
let questions: std::collections::BTreeMap<String, Question> = [
|
||||||
|
(
|
||||||
|
"topic".to_string(),
|
||||||
|
Question::Choice {
|
||||||
|
instructions: "Which of these research topics is this paper about?".into(),
|
||||||
|
criteria,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"relevance".to_string(),
|
||||||
|
Question::score(
|
||||||
|
"How relevant is this paper to a platform that runs autonomous LLM coding and research agents?",
|
||||||
|
RELEVANCE_LEVELS,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(papers.len());
|
||||||
|
for p in papers {
|
||||||
|
let state = format!("Title: {}\n\nAbstract: {}", p.title, p.summary);
|
||||||
|
let decided = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
jev.decide(&state, &questions),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let mut t = PaperTriage::default();
|
||||||
|
match decided {
|
||||||
|
Ok(Ok(d)) => {
|
||||||
|
if let Some(Answer::Choice { probabilities, .. }) = d.answers.get("topic") {
|
||||||
|
let mut tags: Vec<(String, f64)> = probabilities
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, p)| k.as_str() != "none" && **p >= 0.3)
|
||||||
|
.map(|(k, p)| (k.clone(), *p))
|
||||||
|
.collect();
|
||||||
|
tags.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
t.topic_tags = tags.into_iter().map(|(k, _)| k).collect();
|
||||||
|
}
|
||||||
|
if let Some(Answer::Score { score, confidence, .. }) = d.answers.get("relevance") {
|
||||||
|
t.relevance = Some((*score * 100.0).round() / 100.0);
|
||||||
|
t.relevance_confidence = Some((*confidence * 100.0).round() / 100.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => eprintln!("continuous_research: triage of {} failed: {e}", p.arxiv_id),
|
||||||
|
Err(_) => eprintln!("continuous_research: triage of {} timed out", p.arxiv_id),
|
||||||
|
}
|
||||||
|
out.push(t);
|
||||||
|
}
|
||||||
|
let tagged = out.iter().filter(|t| !t.topic_tags.is_empty()).count();
|
||||||
|
eprintln!(
|
||||||
|
"continuous_research: triaged {} paper(s) with {}: {tagged} tagged, {} scored",
|
||||||
|
papers.len(),
|
||||||
|
jev.name(),
|
||||||
|
out.iter().filter(|t| t.relevance.is_some()).count()
|
||||||
|
);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `all:"agent memory" AND all:"long-term"` → `agent memory long-term`.
|
||||||
|
fn readable_topic(query: &str) -> String {
|
||||||
|
let words: Vec<&str> = query
|
||||||
|
.split(|c: char| c == '"' || c.is_whitespace() || c == '(' || c == ')')
|
||||||
|
.filter(|w| !w.is_empty())
|
||||||
|
.filter(|w| !matches!(*w, "AND" | "OR" | "NOT"))
|
||||||
|
.map(|w| w.strip_prefix("all:").unwrap_or(w))
|
||||||
|
.map(|w| w.strip_prefix("ti:").unwrap_or(w))
|
||||||
|
.map(|w| w.strip_prefix("abs:").unwrap_or(w))
|
||||||
|
.filter(|w| !w.is_empty())
|
||||||
|
.collect();
|
||||||
|
words.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
/// The manifest lines for a set of freshly shelved papers.
|
/// The manifest lines for a set of freshly shelved papers.
|
||||||
///
|
///
|
||||||
/// Shape matches what `templates/teams/continuous_research.toml` documents:
|
/// Shape matches what `templates/teams/continuous_research.toml` documents:
|
||||||
/// `{ source, url, title, snippet, first_seen, topic_tags }`.
|
/// `{ source, url, title, snippet, first_seen, topic_tags }`, plus
|
||||||
pub fn manifest_lines(papers: &[crate::papers::Paper], first_seen: &str) -> String {
|
/// `relevance` since 2026-09-21 (see [`PaperTriage`]). `triage` is
|
||||||
|
/// positional with `papers`; shorter means the rest are untriaged.
|
||||||
|
pub fn manifest_lines(
|
||||||
|
papers: &[crate::papers::Paper],
|
||||||
|
first_seen: &str,
|
||||||
|
triage: &[PaperTriage],
|
||||||
|
) -> String {
|
||||||
papers
|
papers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| {
|
.enumerate()
|
||||||
|
.map(|(i, p)| {
|
||||||
|
let t = triage.get(i).cloned().unwrap_or_default();
|
||||||
json!({
|
json!({
|
||||||
"source": p.source_id(),
|
"source": p.source_id(),
|
||||||
"url": format!("https://arxiv.org/abs/{}", p.arxiv_id),
|
"url": format!("https://arxiv.org/abs/{}", p.arxiv_id),
|
||||||
"title": p.title,
|
"title": p.title,
|
||||||
"snippet": p.summary.chars().take(400).collect::<String>(),
|
"snippet": p.summary.chars().take(400).collect::<String>(),
|
||||||
"first_seen": first_seen,
|
"first_seen": first_seen,
|
||||||
"topic_tags": [],
|
"topic_tags": t.topic_tags,
|
||||||
|
"relevance": t.relevance.map(|r| json!({
|
||||||
|
"score": r,
|
||||||
|
"confidence": t.relevance_confidence,
|
||||||
|
"scale": "0 unrelated … 3 directly applicable",
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
.to_string()
|
.to_string()
|
||||||
})
|
})
|
||||||
@@ -198,6 +327,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arxiv_queries_become_readable_option_names() {
|
||||||
|
assert_eq!(
|
||||||
|
readable_topic(r#"all:"agent memory" AND all:"long-term""#),
|
||||||
|
"agent memory long-term"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
readable_topic(r#"all:"agentic topology" OR all:"multi-agent topology""#),
|
||||||
|
"agentic topology multi-agent topology"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_date_stamp_is_zero_padded() {
|
fn the_date_stamp_is_zero_padded() {
|
||||||
let d = today();
|
let d = today();
|
||||||
@@ -217,12 +358,25 @@ mod tests {
|
|||||||
published: "2026-08-17".into(),
|
published: "2026-08-17".into(),
|
||||||
pdf_url: "https://arxiv.org/pdf/2401.12345".into(),
|
pdf_url: "https://arxiv.org/pdf/2401.12345".into(),
|
||||||
};
|
};
|
||||||
let out = manifest_lines(std::slice::from_ref(&p), "2026-08-17");
|
let out = manifest_lines(std::slice::from_ref(&p), "2026-08-17", &[]);
|
||||||
assert_eq!(out.lines().count(), 1);
|
assert_eq!(out.lines().count(), 1);
|
||||||
let v: serde_json::Value = serde_json::from_str(&out).expect("each line is JSON");
|
let v: serde_json::Value = serde_json::from_str(&out).expect("each line is JSON");
|
||||||
for key in ["source", "url", "title", "snippet", "first_seen", "topic_tags"] {
|
for key in ["source", "url", "title", "snippet", "first_seen", "topic_tags", "relevance"] {
|
||||||
assert!(v.get(key).is_some(), "missing {key} in {v}");
|
assert!(v.get(key).is_some(), "missing {key} in {v}");
|
||||||
}
|
}
|
||||||
|
// Untriaged: the slot is there and empty, as it always was.
|
||||||
|
assert_eq!(v["topic_tags"], serde_json::json!([]));
|
||||||
|
assert!(v["relevance"].is_null());
|
||||||
|
// Triaged: the tags and the score land on the line.
|
||||||
|
let t = PaperTriage {
|
||||||
|
topic_tags: vec!["agent memory long-term".into()],
|
||||||
|
relevance: Some(2.4),
|
||||||
|
relevance_confidence: Some(0.7),
|
||||||
|
};
|
||||||
|
let out = manifest_lines(std::slice::from_ref(&p), "2026-08-17", std::slice::from_ref(&t));
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
|
||||||
|
assert_eq!(v["topic_tags"][0], "agent memory long-term");
|
||||||
|
assert_eq!(v["relevance"]["score"], 2.4);
|
||||||
assert_eq!(v["source"], "arxiv:2401.12345");
|
assert_eq!(v["source"], "arxiv:2401.12345");
|
||||||
assert!(
|
assert!(
|
||||||
v["snippet"].as_str().unwrap().chars().count() <= 400,
|
v["snippet"].as_str().unwrap().chars().count() <= 400,
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ use uuid::Uuid;
|
|||||||
/// tried" without becoming the prompt.
|
/// tried" without becoming the prompt.
|
||||||
pub const RECALL_K: usize = 3;
|
pub const RECALL_K: usize = 3;
|
||||||
|
|
||||||
|
/// How many BM25 candidates the reranker sees. Wider than `RECALL_K` so a
|
||||||
|
/// relevant verdict that keyword overlap ranked fourth can still make the
|
||||||
|
/// brief; narrow enough that one call stays one call.
|
||||||
|
pub const CANDIDATES: usize = 8;
|
||||||
|
|
||||||
|
/// Below this a candidate is dropped even if fewer than `RECALL_K` remain:
|
||||||
|
/// a brief that carries an irrelevant verdict is worse than a shorter one.
|
||||||
|
pub const RELEVANT_AT: f64 = 0.3;
|
||||||
|
|
||||||
/// The heading the recalled lines go under. Named here because the scorer and
|
/// The heading the recalled lines go under. Named here because the scorer and
|
||||||
/// the prompt-order tests read it back.
|
/// the prompt-order tests read it back.
|
||||||
pub const SECTION_HEADING: &str = "# What past missions on this repository learned";
|
pub const SECTION_HEADING: &str = "# What past missions on this repository learned";
|
||||||
@@ -112,18 +121,88 @@ fn remember_in(
|
|||||||
|
|
||||||
/// The past verdicts most relevant to `query` (the phase's task text).
|
/// The past verdicts most relevant to `query` (the phase's task text).
|
||||||
/// Empty when the repo has no brain yet, which is every repo's first mission.
|
/// Empty when the repo has no brain yet, which is every repo's first mission.
|
||||||
pub fn recall(repo_id: Uuid, query: &str) -> Vec<String> {
|
///
|
||||||
recall_in(&cm_runtime::brain::brain_dir(), repo_id, query)
|
/// Two stages when a decision model is configured: BM25 proposes
|
||||||
|
/// `CANDIDATES`, one Noul per candidate — "is this past verdict relevant
|
||||||
|
/// to the task?" — reorders them and drops the ones below `RELEVANT_AT`.
|
||||||
|
/// Keyword overlap is what BM25 measures, and a verdict about MICROVM.md
|
||||||
|
/// shares words with every task that mentions a file; the rerank is the
|
||||||
|
/// vendor's own pattern and costs one ~200 ms call. Without a key the
|
||||||
|
/// BM25 order stands, as before.
|
||||||
|
pub async fn recall(repo_id: Uuid, query: &str) -> Vec<String> {
|
||||||
|
let candidates = recall_in(&cm_runtime::brain::brain_dir(), repo_id, query, CANDIDATES);
|
||||||
|
match cm_decide::jev::Jev::from_env() {
|
||||||
|
Some(jev) if candidates.len() > 1 => rerank(&jev, query, candidates).await,
|
||||||
|
_ => candidates.into_iter().take(RECALL_K).collect(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recall_in(dir: &Path, repo_id: Uuid, query: &str) -> Vec<String> {
|
async fn rerank(jev: &cm_decide::jev::Jev, query: &str, candidates: Vec<String>) -> Vec<String> {
|
||||||
|
use cm_decide::{Answer, Decider as _, Question};
|
||||||
|
let questions: std::collections::BTreeMap<String, Question> = candidates
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, c)| {
|
||||||
|
(
|
||||||
|
format!("c{i}"),
|
||||||
|
Question::noul(format!(
|
||||||
|
"This earlier judge verdict is relevant to the task and would help an \
|
||||||
|
agent doing it: {c}"
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let decided = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
jev.decide(query, &questions),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let decision = match decided {
|
||||||
|
Ok(Ok(d)) => d,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
eprintln!("mission_memory: rerank failed ({e}); keeping the BM25 order");
|
||||||
|
return candidates.into_iter().take(RECALL_K).collect();
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("mission_memory: rerank timed out; keeping the BM25 order");
|
||||||
|
return candidates.into_iter().take(RECALL_K).collect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let scored: Vec<(String, f64)> = candidates
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, c)| {
|
||||||
|
let p = match decision.answers.get(&format!("c{i}")) {
|
||||||
|
Some(Answer::Noul { noul }) => *noul,
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
|
(c, p)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let kept: Vec<String> = cm_decide::patterns::rerank(scored)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, p)| *p >= RELEVANT_AT)
|
||||||
|
.take(RECALL_K)
|
||||||
|
.map(|(c, _)| c)
|
||||||
|
.collect();
|
||||||
|
eprintln!(
|
||||||
|
"mission_memory: reranked {} candidate(s) with {}, kept {} ({} ms)",
|
||||||
|
questions.len(),
|
||||||
|
decision.model,
|
||||||
|
kept.len(),
|
||||||
|
decision.latency.as_millis()
|
||||||
|
);
|
||||||
|
kept
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recall_in(dir: &Path, repo_id: Uuid, query: &str, k: usize) -> Vec<String> {
|
||||||
let path = brain_path(dir, repo_id);
|
let path = brain_path(dir, repo_id);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
match ClawBrain::open_or_create(&path, &format!("repo_{repo_id}")) {
|
match ClawBrain::open_or_create(&path, &format!("repo_{repo_id}")) {
|
||||||
Ok(brain) => brain
|
Ok(brain) => brain
|
||||||
.recall(query, RECALL_K)
|
.recall(query, k)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// `remember` stores "role: text"; the role is ours and not a lesson.
|
// `remember` stores "role: text"; the role is ours and not a lesson.
|
||||||
.map(|m| m.strip_prefix("judge: ").map(str::to_string).unwrap_or(m))
|
.map(|m| m.strip_prefix("judge: ").map(str::to_string).unwrap_or(m))
|
||||||
@@ -227,13 +306,13 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
remember_in(&dir, repo, Uuid::now_v7(), "benchmark", "a baseline is recorded", &v);
|
remember_in(&dir, repo, Uuid::now_v7(), "benchmark", "a baseline is recorded", &v);
|
||||||
let got = recall_in(&dir, repo, "record a performance baseline for the hot path");
|
let got = recall_in(&dir, repo, "record a performance baseline for the hot path", RECALL_K);
|
||||||
assert_eq!(got.len(), 1, "{got:?}");
|
assert_eq!(got.len(), 1, "{got:?}");
|
||||||
assert!(got[0].starts_with("MET — benchmark phase"), "{}", got[0]);
|
assert!(got[0].starts_with("MET — benchmark phase"), "{}", got[0]);
|
||||||
assert!(!got[0].starts_with("judge: "));
|
assert!(!got[0].starts_with("judge: "));
|
||||||
// A repo with no history recalls nothing and creates no file.
|
// A repo with no history recalls nothing and creates no file.
|
||||||
let other = Uuid::now_v7();
|
let other = Uuid::now_v7();
|
||||||
assert!(recall_in(&dir, other, "anything").is_empty());
|
assert!(recall_in(&dir, other, "anything", RECALL_K).is_empty());
|
||||||
assert!(!brain_path(&dir, other).exists());
|
assert!(!brain_path(&dir, other).exists());
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,12 @@ pub async fn on_launch(
|
|||||||
// rightly refuses the branch. See `write_manifest`.
|
// rightly refuses the branch. See `write_manifest`.
|
||||||
if mission.template_kind == crate::continuous_research::TEMPLATE_KIND {
|
if mission.template_kind == crate::continuous_research::TEMPLATE_KIND {
|
||||||
let date = crate::continuous_research::today();
|
let date = crate::continuous_research::today();
|
||||||
match crate::continuous_research::write_manifest(&path, &harvested, &date) {
|
// Tag and score each paper before the agents see the list;
|
||||||
|
// an untriaged manifest (no key) is the old, empty-tags one.
|
||||||
|
let topics = crate::continuous_research::topics_for(&mission.config);
|
||||||
|
let triage =
|
||||||
|
crate::continuous_research::triage_papers(&harvested, &topics).await;
|
||||||
|
match crate::continuous_research::write_manifest(&path, &harvested, &date, &triage) {
|
||||||
Ok(at) => eprintln!(
|
Ok(at) => eprintln!(
|
||||||
"mission_orchestrator: wrote {} paper(s) to {}",
|
"mission_orchestrator: wrote {} paper(s) to {}",
|
||||||
harvested.len(),
|
harvested.len(),
|
||||||
|
|||||||
@@ -1254,9 +1254,9 @@ async fn launch_phase(
|
|||||||
// Appended to the task rather than the identity so all three executors
|
// Appended to the task rather than the identity so all three executors
|
||||||
// carry it: the task text is the one thing they share.
|
// carry it: the task text is the one thing they share.
|
||||||
let task = match p.repo_id {
|
let task = match p.repo_id {
|
||||||
Some(repo) => match crate::mission_memory::section(&crate::mission_memory::recall(
|
Some(repo) => match crate::mission_memory::section(
|
||||||
repo, &task,
|
&crate::mission_memory::recall(repo, &task).await,
|
||||||
)) {
|
) {
|
||||||
Some(memory) => {
|
Some(memory) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"phase_runner: phase {phase_id} brief carries project memory for repo {repo}"
|
"phase_runner: phase {phase_id} brief carries project memory for repo {repo}"
|
||||||
|
|||||||
Reference in New Issue
Block a user