pathway_memory: Mem0 versioning + deletion (upsert/revise/retire/history)
Per J 2026-04-25: pathway_memory was append-only — every agent run added
a new trace, bad/failed runs polluted the matrix forever, no notion of
"this is the canonical evolved playbook." Ported playbook_memory's
Phase 25/27 patterns into pathway_memory so the agent loop's matrix
converges on best-known approaches per task class instead of bloating.
Fields added to PathwayTrace (all #[serde(default)] for back-compat):
- trace_uid: stable UUID per individual trace within a bucket
- version: u32 default 1
- parent_trace_uid, superseded_at, superseded_by_trace_uid
- retirement_reason (paired with existing retired:bool)
Methods added to PathwayMemory:
- upsert(trace) → PathwayUpsertOutcome {Added|Updated|Noop}
Workflow-fingerprint dedup: ladder_attempts + final_verdict hash.
Identical workflow → bumps existing replay_count instead of duplicating.
- revise(parent_uid, new_trace) → PathwayReviseOutcome
Chains versions; rejects retired or already-superseded parents.
- retire(trace_uid, reason) → bool
Marks specific trace retired with reason. Idempotent.
- history(trace_uid) → Vec<PathwayTrace>
Walks parent_trace_uid back to root, then superseded_by forward to tip.
Cycle-safe via visited set.
Retrieval gates updated:
- query_hot_swap skips superseded_at.is_some()
- bug_fingerprints_for skips both retired AND superseded
HTTP endpoints in service.rs:
- POST /vectors/pathway/upsert
- POST /vectors/pathway/retire
- POST /vectors/pathway/revise
- GET /vectors/pathway/history/{trace_uid}
scripts/seal_agent_playbook.ts switched insert→upsert + accepts SESSION_DIR
arg so it can seal any archived session, not just iter4.
Verified live (4/4 ops):
- UPSERT first run: Added trace_uid 542ae53f
- UPSERT identical: Updated, replay_count bumped 0→1 (no duplicate)
- REVISE 542ae53f→87a70a61: parent stamped superseded_at, v2 created
- HISTORY of v2: chain_len=2, v1 superseded, v2 tip
- RETIRE iter-6 broken trace: retired=true, retirement_reason preserved
- pathway_memory.stats: total=79, retired=1, reuse_rate=0.0127
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,11 @@ pub fn router(state: VectorState) -> Router {
|
||||
.route("/pathway/stats", get(pathway_stats))
|
||||
// ADR-021 Phase C: pre-review bug-fingerprint retrieval.
|
||||
.route("/pathway/bug_fingerprints", post(pathway_bug_fingerprints))
|
||||
// Mem0 ops (J 2026-04-25): upsert/retire/revise/history.
|
||||
.route("/pathway/upsert", post(pathway_upsert))
|
||||
.route("/pathway/retire", post(pathway_retire))
|
||||
.route("/pathway/revise", post(pathway_revise))
|
||||
.route("/pathway/history/{trace_uid}", get(pathway_history))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -2904,6 +2909,58 @@ async fn pathway_bug_fingerprints(
|
||||
Json(json!({ "fingerprints": fps }))
|
||||
}
|
||||
|
||||
// ─── Mem0 ops endpoints (J 2026-04-25) ───
|
||||
|
||||
async fn pathway_upsert(
|
||||
State(state): State<VectorState>,
|
||||
Json(trace): Json<pathway_memory::PathwayTrace>,
|
||||
) -> impl IntoResponse {
|
||||
match state.pathway_memory.upsert(trace).await {
|
||||
Ok(outcome) => Ok(Json(json!({"ok": true, "outcome": outcome}))),
|
||||
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PathwayRetireRequest {
|
||||
trace_uid: String,
|
||||
reason: String,
|
||||
}
|
||||
|
||||
async fn pathway_retire(
|
||||
State(state): State<VectorState>,
|
||||
Json(req): Json<PathwayRetireRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match state.pathway_memory.retire(&req.trace_uid, &req.reason).await {
|
||||
Ok(touched) => Ok(Json(json!({"ok": true, "retired": touched}))),
|
||||
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PathwayReviseRequest {
|
||||
parent_trace_uid: String,
|
||||
new_trace: pathway_memory::PathwayTrace,
|
||||
}
|
||||
|
||||
async fn pathway_revise(
|
||||
State(state): State<VectorState>,
|
||||
Json(req): Json<PathwayReviseRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match state.pathway_memory.revise(&req.parent_trace_uid, req.new_trace).await {
|
||||
Ok(outcome) => Ok(Json(json!({"ok": true, "outcome": outcome}))),
|
||||
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn pathway_history(
|
||||
State(state): State<VectorState>,
|
||||
axum::extract::Path(trace_uid): axum::extract::Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let chain = state.pathway_memory.history(&trace_uid).await;
|
||||
Json(json!({"trace_uid": trace_uid, "chain_len": chain.len(), "chain": chain}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod extractor_tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user