feat(memory): missions remember their verdicts, per repository
Until now missions wrote no memory. The chat path records every turn into the claw's .brain, but a mission's crew is minted per mission, so a brain keyed by agent would be written once and never read. What persists across missions is the repository: mission_memory keeps one .brain per repo_id, writes each judge verdict into it (reason when met, sanitized guidance when not — the operator reason may quote the acceptance text), and recalls against the next phase's task text into the brief, under a heading all three executors carry because it rides on the task. Recall is BM25 over the keyword index, no embedder; the harness asserts the brief carries the section once the repo has one judged mission behind it, and says 'first mission' rather than failing before that. OpenClaw's flush-before-compaction was the other half of this item and is moot here: the chat loop has no compaction and already remembers both halves of every turn. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
co-authored by
Claude Opus 5
parent
76ac3714f1
commit
13f7fb3aff
@@ -858,7 +858,9 @@ async fn start_pending_phases(
|
||||
m.runtime_kind, m.backend, m.target_node_id, m.team_engine,
|
||||
-- Whether a checkout exists at all. A repo-less mission's
|
||||
-- /mission/repo is scratch space, and the task text must say so.
|
||||
(m.repo_id IS NOT NULL) AS has_repo
|
||||
(m.repo_id IS NOT NULL) AS has_repo,
|
||||
-- The key of the project memory (`mission_memory`).
|
||||
m.repo_id
|
||||
FROM mission_phases mp
|
||||
JOIN missions m ON m.id = mp.mission_id
|
||||
WHERE mp.status = 'pending'
|
||||
@@ -890,6 +892,7 @@ async fn start_pending_phases(
|
||||
let target_node_id: Option<Uuid> = row.get("target_node_id");
|
||||
let team_engine: Option<String> = row.get("team_engine");
|
||||
let has_repo: bool = row.get("has_repo");
|
||||
let repo_id: Option<Uuid> = row.get("repo_id");
|
||||
|
||||
if let Err(e) = launch_phase(
|
||||
pool,
|
||||
@@ -909,6 +912,7 @@ async fn start_pending_phases(
|
||||
target_node_id,
|
||||
team_engine: team_engine.as_deref(),
|
||||
has_repo,
|
||||
repo_id,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -955,6 +959,8 @@ struct PhaseLaunch<'a> {
|
||||
/// `/mission/repo` is a git checkout or a scratch workspace whose contents
|
||||
/// are captured as artifacts.
|
||||
has_repo: bool,
|
||||
/// `missions.repo_id`, the key of the project memory a brief recalls from.
|
||||
repo_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// Which team purposes execute a phase of this kind.
|
||||
@@ -995,6 +1001,7 @@ async fn launch_phase(
|
||||
backend: _,
|
||||
target_node_id: _,
|
||||
team_engine: _,
|
||||
repo_id: _,
|
||||
} = p;
|
||||
// Which team purposes should execute this phase.
|
||||
let purposes: &[&str] = purposes_for(kind);
|
||||
@@ -1220,6 +1227,26 @@ async fn launch_phase(
|
||||
_ => task,
|
||||
};
|
||||
|
||||
// What earlier missions on this repository learned, by the judge's own
|
||||
// account, recalled against this phase's task. Every mission's crew is
|
||||
// new, so this is the only memory a mission has of the ones before it.
|
||||
// Appended to the task rather than the identity so all three executors
|
||||
// carry it: the task text is the one thing they share.
|
||||
let task = match p.repo_id {
|
||||
Some(repo) => match crate::mission_memory::section(&crate::mission_memory::recall(
|
||||
repo, &task,
|
||||
)) {
|
||||
Some(memory) => {
|
||||
eprintln!(
|
||||
"phase_runner: phase {phase_id} brief carries project memory for repo {repo}"
|
||||
);
|
||||
format!("{task}\n\n{memory}")
|
||||
}
|
||||
None => task,
|
||||
},
|
||||
None => task,
|
||||
};
|
||||
|
||||
// The container tier is deliberately NOT given this: it injects per-turn in
|
||||
// `topology_exec`, with the running node's own role, and appending here too
|
||||
// would put every crew member's skills in every turn twice.
|
||||
@@ -2537,7 +2564,7 @@ async fn evaluate_finished_phases(
|
||||
) -> Result<(), String> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT mp.id, mp.mission_id, mp.kind, mp.done_when, mp.max_iterations, mp.iteration,
|
||||
m.runtime_kind
|
||||
m.runtime_kind, m.repo_id
|
||||
FROM mission_phases mp
|
||||
JOIN missions m ON m.id = mp.mission_id
|
||||
WHERE mp.status = 'evaluating' AND m.status = 'running'
|
||||
@@ -2558,6 +2585,7 @@ async fn evaluate_finished_phases(
|
||||
let max_iterations: i32 = row.get("max_iterations");
|
||||
let iteration: i32 = row.get("iteration");
|
||||
let runtime_kind: String = row.get("runtime_kind");
|
||||
let repo_id: Option<Uuid> = row.get("repo_id");
|
||||
|
||||
// Pull the agent's work onto the host BEFORE judging it.
|
||||
//
|
||||
@@ -2603,6 +2631,11 @@ async fn evaluate_finished_phases(
|
||||
{
|
||||
eprintln!("phase_runner: recording evaluation for {phase_id} failed: {e}");
|
||||
}
|
||||
// The project remembers the verdict. Only a repo-backed mission has a
|
||||
// project to remember into; a repo-less one leaves no trace here.
|
||||
if let Some(repo) = repo_id {
|
||||
crate::mission_memory::remember_verdict(repo, mission_id, &kind, &condition, &verdict);
|
||||
}
|
||||
|
||||
// A judge that could not be REACHED has not judged. `Verdict.error` is
|
||||
// set only when the evaluator itself failed — "could not judge" as
|
||||
|
||||
Reference in New Issue
Block a user