- 1
//! The trash (docs/plans/data-architecture-plan.md, M0): a session moved to - 2
//! the trash is hidden from every list, every search a person or the model - 3
//! can run, its transcript and export, and the digest, and comes back whole - 4
//! when restored. - 5
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 6
- 7
use std::path::Path; - 8
- 9
use vak_core::Core; - 10
use vak_session::types::{FrozenContract, SessionHeader}; - 11
use vak_session::{SessionLog, SessionPath}; - 12
use vak_tools::Tool; - 13
- 14
fn header_for(id: &str, cwd: &Path) -> SessionHeader { - 15
SessionHeader { - 16
agent: None, - 17
session_id: id.to_string(), - 18
created_at: chrono::Utc::now(), - 19
cwd: cwd.to_path_buf(), - 20
parent_session_id: None, - 21
contract_id: None, - 22
work_item_id: None, - 23
conversation: None, - 24
contract: FrozenContract { - 25
app_version: "test".into(), - 26
provider: "scripted".into(), - 27
model: "m".into(), - 28
route_ladder: Vec::new(), - 29
route_objective: String::new(), - 30
route_annotations: Vec::new(), - 31
system_prompt: String::new(), - 32
permission_mode: "workspace-write".into(), - 33
capabilities: Vec::new(), - 34
prompt_layers: Vec::new(), - 35
}, - 36
} - 37
} - 38
- 39
/// Written where the built-in Agent keeps its ledgers, as a real run would. - 40
fn write_session(home: &Path, cwd: &Path, id: &str, text: &str) { - 41
let agent_home = vak_config::paths::agent_home_at(home, "vak"); - 42
let path = SessionPath::new_session_file(&agent_home, cwd, id); - 43
let mut header = header_for(id, cwd); - 44
header.agent = Some(vak_core::vak_agent_identity()); - 45
let mut log = SessionLog::create(path, header).unwrap(); - 46
log.append_message(vak_session::types::MessageRecord { - 47
message: vak_llm::Message { - 48
role: vak_llm::Role::User, - 49
content: vec![vak_llm::types::ContentBlock::text(text)], - 50
}, - 51
meta: None, - 52
}) - 53
.unwrap(); - 54
} - 55
- 56
fn client_with(token: &str) -> reqwest::Client { - 57
reqwest::ClientBuilder::new() - 58
.default_headers({ - 59
let mut h = reqwest::header::HeaderMap::new(); - 60
h.insert( - 61
reqwest::header::AUTHORIZATION, - 62
format!("Bearer {token}").parse().unwrap(), - 63
); - 64
h - 65
}) - 66
.build() - 67
.unwrap() - 68
} - 69
- 70
fn mentions(body: &serde_json::Value, id: &str) -> bool { - 71
body.to_string().contains(id) - 72
} - 73
- 74
#[tokio::test(flavor = "multi_thread", worker_threads = 4)] - 75
async fn trashed_session_absent_from_every_search() { - 76
let dir = tempfile::tempdir().unwrap(); - 77
let home = dir.path().join("home"); - 78
let cwd = dir.path().join("work"); - 79
std::fs::create_dir_all(&cwd).unwrap(); - 80
let cwd = cwd.canonicalize().unwrap(); - 81
let kept = uuid::Uuid::now_v7().to_string(); - 82
let gone = uuid::Uuid::now_v7().to_string(); - 83
write_session(&home, &cwd, &kept, "the zanzibar itinerary is in the notes"); - 84
write_session( - 85
&home, - 86
&cwd, - 87
&gone, - 88
"the zanzibar passport number is private", - 89
); - 90
let now = chrono::Utc::now().to_rfc3339(); - 91
let cost_rows: String = [&kept, &gone] - 92
.iter() - 93
.map(|sid| { - 94
format!( - 95
"{}\n", - 96
serde_json::json!({ - 97
"ts": now, "model": "m", "provider": "p", - 98
"input_tokens": 10, "output_tokens": 5, "usd": 0.01, - 99
"source": "estimated", "session_id": sid, - 100
}) - 101
) - 102
}) - 103
.collect(); - 104
std::fs::write(home.join("cost-log.jsonl"), cost_rows).unwrap(); - 105
- 106
vak_config::paths::isolate_home_for_tests(); - 107
let core = Core::new(cwd.clone()).unwrap(); - 108
core.set_sessions_home(home.clone()); - 109
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - 110
let addr = listener.local_addr().unwrap(); - 111
let (app, token) = vak_server::secured_router(core); - 112
tokio::spawn(async move { - 113
axum::serve(listener, app).await.unwrap(); - 114
}); - 115
let base = format!("http://{addr}"); - 116
let client = client_with(&token); - 117
let get = |path: String| { - 118
let client = client.clone(); - 119
let base = base.clone(); - 120
async move { - 121
let res = client.get(format!("{base}{path}")).send().await.unwrap(); - 122
let status = res.status().as_u16(); - 123
let body: serde_json::Value = res.json().await.unwrap_or(serde_json::Value::Null); - 124
(status, body) - 125
} - 126
}; - 127
- 128
let res = client - 129
.post(format!("{base}/admin/api/store/rebuild")) - 130
.send() - 131
.await - 132
.unwrap(); - 133
assert_eq!(res.status(), 200); - 134
let search_tool = vak_core::session_search::SessionSearchTool { - 135
sessions_home: home.clone(), - 136
trash_home: home.clone(), - 137
cwd: cwd.clone(), - 138
exclude_session_id: String::new(), - 139
agent_id: None, - 140
audience_id: None, - 141
}; - 142
let ctx = vak_tools::ToolContext { - 143
cwd: cwd.clone(), - 144
cancel: tokio_util::sync::CancellationToken::new(), - 145
sandbox: None, - 146
sandbox_sink: None, - 147
agent_id: None, - 148
new_documents: Vec::new(), - 149
}; - 150
let tool_search = || async { - 151
search_tool - 152
.execute(&serde_json::json!({"query": "zanzibar"}), &ctx) - 153
.await - 154
.content - 155
}; - 156
- 157
// Every reader sees the session before it is trashed, so an absence - 158
// afterwards is the trash at work and not a reader that never saw it. - 159
let readers = [ - 160
"/sessions".to_string(), - 161
"/search?q=zanzibar".to_string(), - 162
"/search?q=zanzibar&all=true".to_string(), - 163
"/admin/api/sessions".to_string(), - 164
"/admin/api/search?q=zanzibar".to_string(), - 165
"/digest".to_string(), - 166
]; - 167
for reader in &readers { - 168
let (status, body) = get(reader.clone()).await; - 169
assert_eq!(status, 200, "{reader}: {body}"); - 170
assert!(mentions(&body, &gone), "{reader} before trash: {body}"); - 171
assert!(mentions(&body, &kept), "{reader} before trash: {body}"); - 172
} - 173
assert!(tool_search().await.contains(&gone)); - 174
- 175
// Only an archived session can go to the trash. - 176
let res = client - 177
.delete(format!("{base}/sessions/{gone}")) - 178
.send() - 179
.await - 180
.unwrap(); - 181
assert_eq!(res.status(), 400); - 182
let res = client - 183
.post(format!("{base}/sessions/{gone}/archive")) - 184
.json(&serde_json::json!({ "archived": true })) - 185
.send() - 186
.await - 187
.unwrap(); - 188
assert_eq!(res.status(), 200); - 189
let res = client - 190
.delete(format!("{base}/sessions/{gone}")) - 191
.send() - 192
.await - 193
.unwrap(); - 194
assert_eq!(res.status(), 200); - 195
let body: serde_json::Value = res.json().await.unwrap(); - 196
assert_eq!(body["trashed"], gone.as_str()); - 197
- 198
for reader in &readers { - 199
let (status, body) = get(reader.clone()).await; - 200
assert_eq!(status, 200, "{reader}: {body}"); - 201
assert!(!mentions(&body, &gone), "{reader} after trash: {body}"); - 202
assert!(mentions(&body, &kept), "{reader} after trash: {body}"); - 203
} - 204
let found = tool_search().await; - 205
assert!(!found.contains(&gone), "session_search: {found}"); - 206
assert!(found.contains(&kept), "session_search: {found}"); - 207
- 208
for path in [ - 209
format!("/sessions/{gone}/transcript"), - 210
format!("/sessions/{gone}/transcript.md"), - 211
] { - 212
let (status, _) = get(path.clone()).await; - 213
assert_eq!(status, 404, "{path}"); - 214
} - 215
let (_, body) = get(format!("/admin/api/sessions/{gone}/transcript")).await; - 216
assert!(body.get("entries").is_none(), "admin transcript: {body}"); - 217
let res = client - 218
.post(format!("{base}/sessions/{gone}/attach")) - 219
.json(&serde_json::json!({ "session_id": gone })) - 220
.send() - 221
.await - 222
.unwrap(); - 223
assert_eq!(res.status(), 404, "a trashed session cannot be reopened"); - 224
- 225
// The trash itself lists it, and restoring brings it back archived. - 226
let (_, trash) = get("/sessions?trash=true".to_string()).await; - 227
assert!(mentions(&trash, &gone), "{trash}"); - 228
assert!(!mentions(&trash, &kept), "{trash}"); - 229
let res = client - 230
.post(format!("{base}/sessions/{gone}/restore")) - 231
.send() - 232
.await - 233
.unwrap(); - 234
assert_eq!(res.status(), 200); - 235
let (_, listed) = get("/sessions".to_string()).await; - 236
let restored = listed["sessions"] - 237
.as_array() - 238
.unwrap() - 239
.iter() - 240
.find(|s| s["session_id"] == gone.as_str()) - 241
.expect("restored session is listed again"); - 242
assert_eq!(restored["archived"], true); - 243
let (_, found) = get("/search?q=zanzibar".to_string()).await; - 244
assert!(mentions(&found, &gone), "{found}"); - 245
} - 246
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.