blob: d24173759aa56c12fe03d7c6130d156414f50323 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_agent_facts::archive::ArchivedPlaceholder;
use maka_agent_facts::archive::outcome_projection;
use maka_agent_facts::context::CheckpointMode;
use maka_agent_facts::context::CompactOutcome;
use maka_agent_facts::context::ContextCheckpoint;
use maka_agent_facts::context::ModelPurpose;
use maka_agent_facts::context::TextSummary;
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationInput;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::LogScope;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::model::ModelStep;
use maka_agent_facts::tool_call::ToolCallIdentity;
use maka_agent_facts::tool_output::ToolSuccess;
use maka_event_log::{
EventLog, StoreError,
context::{ContextEvent, HistoryCapture, HistoryCut, LatestMainContext, ModelContextSource},
};
use serde_json::json;
#[path = "archive/context_performance.rs"]
mod context_performance;
#[path = "archive/digest.rs"]
mod digest;
#[path = "archive/fixtures.rs"]
mod fixtures;
#[path = "archive/lineage.rs"]
mod lineage;
#[path = "archive/performance.rs"]
mod performance;
#[path = "archive/streaming.rs"]
mod streaming;
use fixtures::*;
#[tokio::test]
async fn retirement_fences_new_effects_without_rejecting_accepted_results_or_exact_replay() {
use maka_agent_facts::event::CommitError;
use maka_event_log::sessions::SessionRetirement;
let directory = tempfile::tempdir().unwrap();
let log = EventLog::open(&directory.path().join("retirement.sqlite"))
.await
.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
open(&log, "run", false).await;
let accepted = request("run", "step", None);
let sequence = log.append(&accepted).await.unwrap();
let revision = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap()
.revision;
log.begin_session_removal("session", revision)
.await
.unwrap();
assert_eq!(log.append(&accepted).await.unwrap(), sequence);
let output = serde_json::from_value(json!({
"parts":[{"kind":"tool_call","call":{"id":"call","name":"Read","input":{"kind":"json","data":{"path":"file"}},"provider_executed":false}}],
"finish_reason":"tool-calls","usage":{}
})).unwrap();
log.append(&event(
"run",
Fact::ModelCompleted {
step_id: "step".into(),
output,
},
))
.await
.unwrap();
assert!(matches!(
log.append(&request("run", "next", None)).await,
Err(CommitError::Retired)
));
assert!(matches!(
log.append(&event(
"run",
Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: "step:call".into(),
call: ToolCallIdentity::provider("step".into(), "call".into()),
name: "Read".into(),
input: json!({"path":"file"}),
}
))
.await,
Err(CommitError::Retired)
));
log.append(&event(
"run",
Fact::InvocationEnded {
outcome: InvocationOutcome::Cancelled {
source: "session_removal".into(),
},
},
))
.await
.unwrap();
assert_eq!(
log.finish_session_retirement("session").await.unwrap(),
SessionRetirement::Removed
);
assert!(matches!(
log.append(&accepted).await,
Err(CommitError::Retired)
));
log.close().await.unwrap();
}
#[tokio::test]
async fn archive_atomic_retry_reopen_scope_and_source_integrity() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("archive.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "history-source", &json!({}), 1)
.await
.unwrap();
open(&log, "old", false).await;
let target = tool(&log, "old", "step", "x".repeat(12_000)).await;
let unpruned = log
.read_tool_result("session", &target.event().id)
.await
.unwrap()
.unwrap();
assert_eq!(unpruned.tool_name, "Read");
assert_eq!(
unpruned.serialized_result,
serde_json::to_string(&"x".repeat(12_000)).unwrap()
);
assert!(
log.read_tool_result("other", &target.event().id)
.await
.unwrap()
.is_none()
);
end(&log, "old").await;
let frozen = log.read_model_context("session", None).await.unwrap();
let initial_revision = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap()
.revision;
let HistoryCapture::Captured(before_archive) = log
.capture_session_history(
"session",
initial_revision,
HistoryCut::ThroughTurn("old".into()),
100,
64 * 1024,
)
.await
.unwrap()
else {
panic!("unchanged source");
};
use maka_event_log::sessions::{SessionCopy, SessionCopyResult};
assert!(matches!(
log.copy_session(
SessionCopy {
source_session_id: "session".into(),
target_session_id: "unpruned-copy".into(),
expected_source_revision: initial_revision,
purpose: maka_agent_facts::session::CopyPurpose::Branch {
turn_id: Some("old".into()),
side_conversation: false
},
},
&json!({}),
2
)
.await
.unwrap(),
SessionCopyResult::Committed(_)
));
assert_eq!(
log.resolve_read_resource(
"unpruned-copy",
maka_agent_facts::read::ResourceAddress::ToolResult("result-st".into())
)
.await
.unwrap(),
Some(maka_agent_facts::read::ResourceAddress::ToolResult(
target.event().id.clone()
))
);
open(&log, "writer", false).await;
let source = log
.read_model_context("session", Some("writer"))
.await
.unwrap();
let mut main = request("writer", "writer-main", None).event().clone();
if let Fact::ModelRequested {
source_high_water,
source_digest,
..
} = &mut main.fact
{
*source_high_water = source.source_evidence.high_water;
*source_digest = source.source_evidence.digest;
}
log.append(&EventWrite::plain(main).unwrap()).await.unwrap();
log.append(&event(
"writer",
Fact::ModelCompleted {
step_id: "writer-main".into(),
output: serde_json::from_value(
json!({"parts":[],"finish_reason":"stop","usage":{"input_tokens":10,"output_tokens":0}}),
)
.unwrap(),
},
))
.await
.unwrap();
let observed = log.subscribe_commits();
assert_eq!(
log.context_usage("session").await.unwrap().1,
Some(maka_event_log::context::ContextUsage {
tokens: 10,
approximate: false,
last_known: false,
})
);
assert!(
!observed.has_changed().unwrap(),
"usage queries must not write execution facts"
);
let write = archive("writer", &target);
let Fact::ToolResultArchived { placeholder } = &write.event().fact else {
panic!()
};
assert!(
log.read_archive("session", &placeholder.identity)
.await
.unwrap()
.is_none()
);
let mut forged_page = write.event().clone();
if let Fact::ToolResultArchived { placeholder } = &mut forged_page.fact {
placeholder.page.content.replace_range(..1, "y");
}
let commits = log.subscribe_commits();
assert!(
log.append(&EventWrite::plain(forged_page).unwrap())
.await
.is_err()
);
assert!(!commits.has_changed().unwrap());
let inspect = rusqlite::Connection::open(&path).unwrap();
inspect.execute_batch("CREATE TRIGGER reject_archive BEFORE INSERT ON event_log WHEN NEW.kind='tool_result_archived' BEGIN SELECT RAISE(ABORT,'injected archive failure'); END;").unwrap();
let commits = log.subscribe_commits();
assert!(log.append(&write).await.is_err());
assert!(!commits.has_changed().unwrap());
assert!(
log.read_archive("session", &placeholder.identity)
.await
.unwrap()
.is_none()
);
inspect
.execute_batch("DROP TRIGGER reject_archive")
.unwrap();
log.append(&write).await.unwrap();
assert_eq!(
log.context_usage("session").await.unwrap().1,
None,
"archiving invalidates the old provider baseline without inventing a subtraction"
);
assert!(
matches!(log.latest_main_context("session").await.unwrap(),LatestMainContext::Selected(selected) if !selected.projection_current)
);
let original = log
.read_frozen_model_context(&frozen.source_evidence, 100, 64 * 1024)
.await
.unwrap();
assert_eq!(
original.effective_source_digest,
frozen.effective_source_digest
);
assert!(original.tail.iter().any(
|entry| matches!(entry, ContextEvent::Canonical(entry) if entry.event.id == target.event().id)
));
assert!(
matches!(
log.read_frozen_model_context(&frozen.source_evidence, 100, 8192)
.await,
Err(StoreError::PrefixTooLarge)
),
"later archiving must not shrink a frozen source's original projection"
);
let commits = log.subscribe_commits();
log.append(&write).await.unwrap();
assert!(!commits.has_changed().unwrap());
let source = log
.read_model_context("session", Some("writer"))
.await
.unwrap();
assert!(
source
.tail
.iter()
.any(|e| matches!(e,ContextEvent::Archived(a) if a.event_id==target.event().id))
);
assert!(
log.read_archive("other", &placeholder.identity)
.await
.unwrap()
.is_none()
);
let mut forged = placeholder.identity.clone();
forged.body_sha256 = "0".repeat(64);
assert!(log.read_archive("session", &forged).await.is_err());
let expected = log
.read_archive("session", &placeholder.identity)
.await
.unwrap()
.unwrap();
assert_eq!(
log.read_tool_result("session", &target.event().id)
.await
.unwrap()
.unwrap()
.serialized_result
.as_bytes(),
expected
);
assert_eq!(expected, serde_json::to_vec(&"x".repeat(12_000)).unwrap());
end(&log, "writer").await;
let final_revision = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap()
.revision;
assert!(matches!(
log.copy_session(
SessionCopy {
source_session_id: "session".into(),
target_session_id: "archived-copy".into(),
expected_source_revision: final_revision,
purpose: maka_agent_facts::session::CopyPurpose::Branch {
turn_id: Some("old".into()),
side_conversation: false
},
},
&json!({}),
3
)
.await
.unwrap(),
SessionCopyResult::Committed(_)
));
let adopted = log.read_model_context("archived-copy", None).await.unwrap();
assert_eq!(
log.read_archive("archived-copy", &placeholder.identity)
.await
.unwrap()
.unwrap(),
expected,
"copy reads its accepted archive without borrowing source Session access"
);
assert!(
log.read_archive("unpruned-copy", &placeholder.identity)
.await
.unwrap()
.is_none(),
"a later source archive does not become an accepted archive in an older copy"
);
for session in ["archived-copy", "unpruned-copy"] {
assert_eq!(
log.read_tool_result(session, &target.event().id)
.await
.unwrap()
.unwrap()
.serialized_result
.as_bytes(),
expected
);
}
assert!(
adopted
.tail
.iter()
.any(|e| matches!(e, ContextEvent::Archived(a) if a.event_id == target.event().id))
);
assert_eq!(
log.read_frozen_model_context(&adopted.source_evidence, 100, 64 * 1024)
.await
.unwrap()
.effective_source_digest,
adopted.effective_source_digest
);
let unpruned = log.read_model_context("unpruned-copy", None).await.unwrap();
assert_eq!(
unpruned.tail.iter().find_map(|entry| match entry {
ContextEvent::Canonical(entry) if entry.event.id == target.event().id =>
Some(&entry.event),
_ => None,
}),
Some(target.event()),
"later source pruning cannot replace history already owned by a copy"
);
let copy_invocation = Invocation {
session_id: "archived-copy".into(),
turn_id: "copy-turn".into(),
run_id: "copy-run".into(),
invocation_id: "copy-inv".into(),
};
for fact in [
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
content: "continue".into(),
source_messages: Vec::new(),
request_fingerprint: None,
},
},
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
] {
log.append(&EventWrite::plain(RuntimeEvent::new(copy_invocation.clone(), fact)).unwrap())
.await
.unwrap();
}
assert_eq!(
log.context_before_run(&copy_invocation, 100, 64 * 1024)
.await
.unwrap()
.effective_source_digest,
adopted.effective_source_digest
);
let lineage = log
.scoped_prefix(
LogScope::Lineage {
session_id: "archived-copy".into(),
run_id: "copy-run".into(),
},
100,
65536,
)
.await
.unwrap();
let lineage = log
.read_frozen_model_context(
&maka_event_log::context::SourceEvidence {
scope: lineage.scope,
high_water: lineage.high_water,
digest: lineage.digest,
},
100,
64 * 1024,
)
.await
.unwrap();
assert!(
lineage
.tail
.iter()
.any(|e| matches!(e, ContextEvent::Archived(a) if a.event_id == target.event().id)),
"adopted archive writer need not belong to the new Run's lineage"
);
assert!(matches!(log.capture_session_history(
"session", initial_revision, HistoryCut::End, 100, 8192,
).await.unwrap(), HistoryCapture::SourceRevisionConflict { expected, actual }
if expected == initial_revision && actual == final_revision));
let HistoryCapture::Captured(after_archive) = log
.capture_session_history(
"session",
final_revision,
HistoryCut::ThroughTurn("old".into()),
100,
16 * 1024,
)
.await
.unwrap()
else {
panic!("unchanged source");
};
// The retained Turn is unchanged, but an already accepted later archive
// belongs to the copy's frozen rendering. It is not a copied writer Run.
assert_eq!(
after_archive.context.source_evidence.digest,
before_archive.context.source_evidence.digest
);
assert_ne!(
after_archive.context.effective_source_digest,
before_archive.context.effective_source_digest
);
assert!(after_archive.observed_through > before_archive.observed_through);
assert!(after_archive.context.tail.iter().any(
|entry| matches!(entry, ContextEvent::Archived(entry) if entry.event_id == target.event().id)
));
assert!(after_archive.context.tail.iter().all(|entry| match entry {
ContextEvent::Canonical(entry) => entry.event.invocation.invocation_id == "old",
ContextEvent::Archived(entry) => entry.invocation.invocation_id == "old",
ContextEvent::ModelItems(entry) => entry.invocation.invocation_id == "old",
}));
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
let HistoryCapture::Captured(reopened) = log
.capture_session_history(
"session",
final_revision,
HistoryCut::ThroughTurn("old".into()),
100,
16 * 1024,
)
.await
.unwrap()
else {
panic!("unchanged source after reopen");
};
assert_eq!(
reopened.context.effective_source_digest,
after_archive.context.effective_source_digest
);
log.append(&write).await.unwrap();
assert_eq!(
log.read_archive("session", &placeholder.identity)
.await
.unwrap()
.unwrap(),
expected
);
inspect.execute("UPDATE event_log SET event_json=replace(event_json,'xxxxxxxx','yyyyyyyy') WHERE event_id=?", [&target.event().id]).unwrap();
assert!(
log.read_tool_result("session", &target.event().id)
.await
.is_err()
);
assert!(
log.read_archive("archived-copy", &placeholder.identity)
.await
.is_err(),
"history membership never bypasses original evidence integrity"
);
assert!(
log.read_archive("session", &placeholder.identity)
.await
.is_err()
);
assert!(log.read_model_context("session", None).await.is_err());
log.close().await.unwrap();
}
#[tokio::test]
async fn inherited_results_prune_and_compact_without_changing_siblings_or_frozen_reads() {
use maka_event_log::sessions::{SessionCopy, SessionCopyResult};
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("copies.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "source", &json!({}), 1)
.await
.unwrap();
open(&log, "original", false).await;
let target = tool(&log, "original", "read", "evidence\n".repeat(2_000)).await;
end(&log, "original").await;
let revision = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap()
.revision;
for id in ["branch", "sibling"] {
assert!(matches!(
log.copy_session(
SessionCopy {
source_session_id: "session".into(),
target_session_id: id.into(),
expected_source_revision: revision,
purpose: maka_agent_facts::session::CopyPurpose::Branch {
turn_id: None,
side_conversation: false
},
},
&json!({}),
2
)
.await
.unwrap(),
SessionCopyResult::Committed(_)
));
}
let frozen = log.read_model_context("branch", None).await.unwrap();
// A later source archive is neither visible nor a duplicate in the copy.
open(&log, "source-prune", false).await;
log.append(&archive("source-prune", &target)).await.unwrap();
end(&log, "source-prune").await;
let candidates = log
.prepare_prune_candidates("branch", None, 10, 65536, None)
.await
.unwrap();
assert_eq!(candidates.candidates.len(), 1);
assert_eq!(candidates.candidates[0].event_id, target.event().id);
let in_branch = |write: EventWrite| {
let mut event = write.event().clone();
event.invocation.session_id = "branch".into();
EventWrite::plain(event).unwrap()
};
log.append(&in_branch(event(
"branch-prune",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
content: "continue".into(),
source_messages: Vec::new(),
request_fingerprint: None,
},
},
)))
.await
.unwrap();
let write = in_branch(archive("branch-prune", &target));
log.append(&write).await.unwrap();
log.append(&write).await.unwrap();
assert!(
log.append(&in_branch(archive("branch-prune", &target)))
.await
.is_err()
);
log.append(&in_branch(event(
"branch-prune",
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
)))
.await
.unwrap();
let pruned = log.read_model_context("branch", None).await.unwrap();
assert!(pruned.tail.iter().any(
|entry| matches!(entry, ContextEvent::Archived(a) if a.event_id == target.event().id)
));
assert!(
log.prepare_prune_candidates("branch", None, 10, 65536, None)
.await
.unwrap()
.candidates
.is_empty()
);
let sibling = log.read_model_context("sibling", None).await.unwrap();
assert_eq!(
sibling.tail.iter().find_map(|entry| match entry {
ContextEvent::Canonical(entry) if entry.event.id == target.event().id =>
Some(&entry.event),
_ => None,
}),
Some(target.event()),
"branch pruning cannot replace its sibling's original result"
);
assert_eq!(
log.read_frozen_model_context(&frozen.source_evidence, 100, 65536)
.await
.unwrap()
.effective_source_digest,
frozen.effective_source_digest
);
let Fact::ToolResultArchived { placeholder } = &write.event().fact else {
panic!()
};
let bytes = log
.read_archive("branch", &placeholder.identity)
.await
.unwrap()
.unwrap();
assert!(
log.read_archive("sibling", &placeholder.identity)
.await
.unwrap()
.is_none()
);
// Copying the copy freezes its local archive, whose writer is not the original Session.
let revision = log
.get_session::<serde_json::Value>("branch")
.await
.unwrap()
.unwrap()
.revision;
assert!(matches!(
log.copy_session(
SessionCopy {
source_session_id: "branch".into(),
target_session_id: "nested".into(),
expected_source_revision: revision,
purpose: maka_agent_facts::session::CopyPurpose::Branch {
turn_id: None,
side_conversation: false
},
},
&json!({}),
3
)
.await
.unwrap(),
SessionCopyResult::Committed(_)
));
let adopted = log.read_model_context("nested", None).await.unwrap();
assert!(adopted.tail.iter().any(
|entry| matches!(entry, ContextEvent::Archived(a) if a.event_id == target.event().id)
));
assert_eq!(
log.read_archive("nested", &placeholder.identity)
.await
.unwrap()
.unwrap(),
bytes
);
log.append(&in_branch(event(
"branch-compact",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::ContextCompact {
request_fingerprint: "branch-compact".into(),
},
},
)))
.await
.unwrap();
let source = log
.prepare_context_compaction(
"branch",
Some("branch-compact"),
&CheckpointMode::Standalone,
)
.await
.unwrap();
log.append(&in_branch(request(
"branch-compact",
"summary",
Some(&source),
)))
.await
.unwrap();
let output: ModelStep = serde_json::from_value(json!({"parts":[{"kind":"text","text_kind":"text","text":SUMMARY}],"finish_reason":"stop","usage":{}})).unwrap();
log.append(&in_branch(event(
"branch-compact",
Fact::ModelCompleted {
step_id: "summary".into(),
output: output.clone(),
},
)))
.await
.unwrap();
let checkpoint = in_branch(event(
"branch-compact",
Fact::ContextCheckpointRecorded {
checkpoint: ContextCheckpoint {
mode: CheckpointMode::Standalone,
covered_through: source.source_evidence.high_water,
source_digest: source.source_evidence.digest,
previous_checkpoint_id: None,
summary: TextSummary::from_model_step(&output).unwrap(),
summary_step_id: "summary".into(),
},
},
));
log.append_batch(&[
checkpoint.clone(),
in_branch(event(
"branch-compact",
Fact::InvocationEnded {
outcome: InvocationOutcome::ContextCompactFinished {
outcome: CompactOutcome::Compacted {
checkpoint_id: checkpoint.event().id.clone(),
},
},
},
)),
])
.await
.unwrap();
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_eq!(
log.read_model_context("branch", None)
.await
.unwrap()
.baseline
.unwrap()
.event_id,
checkpoint.event().id
);
// Nested pins still need the intermediate writer's inherited ownership and
// checkpoint evidence even after its live Session and materials are gone.
let revision = log
.get_session::<serde_json::Value>("branch")
.await
.unwrap()
.unwrap()
.revision;
log.begin_session_removal("branch", revision).await.unwrap();
assert_eq!(
log.finish_session_retirement("branch").await.unwrap(),
maka_event_log::sessions::SessionRetirement::Removed
);
assert_eq!(
log.collect_session_material(None).await.unwrap(),
maka_event_log::sessions::MaterialCollection::Retained("branch".into())
);
assert_eq!(
log.read_model_context("nested", None)
.await
.unwrap()
.effective_source_digest,
adopted.effective_source_digest
);
assert_eq!(
log.read_archive("nested", &placeholder.identity)
.await
.unwrap()
.unwrap(),
bytes
);
assert_eq!(
log.read_frozen_model_context(&frozen.source_evidence, 100, 65536)
.await
.unwrap()
.effective_source_digest,
frozen.effective_source_digest
);
// A wrong, but foreign-key-valid, pin must not silently restore raw output.
let inspect = rusqlite::Connection::open(&path).unwrap();
inspect
.execute(
"UPDATE session_history_members SET archive_sequence=sequence
WHERE session_id='nested' AND archive_sequence IS NOT NULL",
[],
)
.unwrap();
assert!(
log.read_archive("nested", &placeholder.identity)
.await
.is_err()
);
assert!(log.read_model_context("nested", None).await.is_err());
assert!(
log.read_frozen_model_context(&adopted.source_evidence, 100, 65536)
.await
.is_err()
);
}
#[tokio::test]
async fn bounded_prune_candidates_do_not_materialize_the_old_tail() {
let directory = tempfile::tempdir().unwrap();
let log = EventLog::open(&directory.path().join("large.sqlite"))
.await
.unwrap();
open(&log, "old", false).await;
let mut originals = Vec::new();
for n in 0..40 {
originals.push(tool(&log, "old", &format!("step-{n}"), "x".repeat(220_000)).await);
}
end(&log, "old").await;
open(&log, "recent", false).await;
end(&log, "recent").await;
open(&log, "current", false).await;
tool(&log, "current", "current-result", "small".into()).await;
let complete = log
.read_model_context("session", Some("current"))
.await
.unwrap();
for original in &originals {
assert_eq!(
complete.tail.iter().find_map(|entry| match entry {
ContextEvent::Canonical(entry) if entry.event.id == original.event().id =>
Some(&entry.event),
_ => None,
}),
Some(original.event()),
"the full model context must preserve every original result"
);
}
let guard = log
.prepare_prune_candidates("session", Some("current"), 0, 0, None)
.await
.unwrap();
assert!(guard.candidates.is_empty());
let first = log
.prepare_prune_candidates("session", Some("current"), 4, 1024 * 1024, None)
.await
.unwrap();
assert_eq!(first.candidates.len(), 4);
assert!(first.next.is_some());
end(&log, "current").await;
open(&log, "pruner", false).await;
let mut archived = 0;
let mut cursor = None;
loop {
let batch = log
.prepare_prune_candidates("session", Some("pruner"), 4, 1024 * 1024, cursor.as_ref())
.await
.unwrap();
if batch.candidates.is_empty() {
break;
}
for candidate in &batch.candidates {
let Some(placeholder) = ArchivedPlaceholder::prepare(
candidate.event_id.clone(),
candidate.tool_call_id.clone(),
candidate.tool_name.clone(),
&candidate.projection,
)
.unwrap() else {
continue;
};
log.append(&event("pruner", Fact::ToolResultArchived { placeholder }))
.await
.unwrap();
archived += 1;
}
cursor = batch.next;
if cursor.is_none() {
break;
}
}
assert_eq!(archived, 40);
let source = log
.read_model_context("session", Some("pruner"))
.await
.unwrap();
assert_eq!(
source
.tail
.iter()
.filter(|e| matches!(e, ContextEvent::Archived(_)))
.count(),
40
);
log.close().await.unwrap();
}
#[path = "archive/automatic.rs"]
mod automatic;
#[path = "archive/summary.rs"]
mod summary;
#[tokio::test]
async fn read_prefixes_keep_literal_archive_ids_and_session_scope() {
use maka_agent_facts::read::ResourceAddress::ToolResult;
let directory = tempfile::tempdir().unwrap();
let log = EventLog::open(&directory.path().join("prefix.sqlite"))
.await
.unwrap();
log.create_session("session", "create", &json!({}), 1)
.await
.unwrap();
open(&log, "run", false).await;
for step in ["shared", "shared-long", "literal_*?[tail", "literal-other"] {
tool(&log, "run", step, "content".into()).await;
}
let hidden = event(
"run",
Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: "hidden".into(),
call: ToolCallIdentity::standalone("hidden-call".into()),
name: "Read".into(),
input: json!({}),
},
);
log.append(&hidden).await.unwrap();
let (hidden_result, _) = EventWrite::tool_success(
"result-literal_hidden".into(),
std::time::SystemTime::now(),
hidden.event().invocation.clone(),
"hidden".into(),
ToolSuccess::from(json!("hidden")),
)
.unwrap();
log.append(&hidden_result).await.unwrap();
assert_eq!(
log.resolve_read_resource("session", ToolResult("result-literal_".into()))
.await
.unwrap(),
Some(ToolResult("result-literal_*?[tail".into()))
);
assert_eq!(
log.resolve_read_resource("session", ToolResult("result-shared".into()))
.await
.unwrap(),
Some(ToolResult("result-shared".into()))
);
assert!(matches!(
log.resolve_read_resource("session", ToolResult("result-shar".into()))
.await,
Err(StoreError::AmbiguousReadResource)
));
assert_eq!(
log.resolve_read_resource("session", ToolResult("result-literal_*?[".into()))
.await
.unwrap(),
Some(ToolResult("result-literal_*?[tail".into()))
);
for (session, prefix) in [
("other", "result-shar"),
("session", "Result-shared"),
("session", "result-shared\0x"),
] {
assert!(
log.resolve_read_resource(session, ToolResult(prefix.into()))
.await
.unwrap()
.is_none()
);
}
log.close().await.unwrap();
}
#[tokio::test]
async fn oversized_tool_result_archives_alone_and_remains_readable() {
let directory = tempfile::tempdir().unwrap();
let log = EventLog::open(&directory.path().join("large.sqlite"))
.await
.unwrap();
open(&log, "writer", false).await;
let text = "x".repeat(5 * 1024 * 1024);
let large = tool(&log, "writer", "large", text.clone()).await;
let small = tool(&log, "writer", "small", "next".into()).await;
let batch = log
.prepare_prune_candidates("session", Some("writer"), 10, 1024 * 1024, None)
.await
.unwrap();
assert_eq!(batch.candidates.len(), 1);
assert_eq!(batch.candidates[0].event_id, large.event().id);
assert_eq!(
batch.candidates[0].projection,
maka_agent_facts::tool_output::DurableToolProjection::Text { text: text.clone() }
);
let next = log
.prepare_prune_candidates(
"session",
Some("writer"),
10,
1024 * 1024,
batch.next.as_ref(),
)
.await
.unwrap();
assert_eq!(next.candidates.len(), 1);
assert_eq!(next.candidates[0].event_id, small.event().id);
let archived = archive("writer", &large);
log.append(&archived).await.unwrap();
let Fact::ToolResultArchived { placeholder } = &archived.event().fact else {
panic!()
};
let body = log
.read_archive("session", &placeholder.identity)
.await
.unwrap()
.unwrap();
assert_eq!(serde_json::from_slice::<String>(&body).unwrap(), text);
assert_eq!(placeholder.identity.original_bytes, 5 * 1024 * 1024 + 2);
assert!(placeholder.page.next.is_some());
let resource = log
.read_tool_result("session", &large.event().id)
.await
.unwrap()
.unwrap();
assert_eq!(resource.serialized_result.as_bytes(), body);
log.close().await.unwrap();
}