diff --git a/mobile/index.html b/mobile/index.html
index 0f1e0a5..eb20bfb 100644
--- a/mobile/index.html
+++ b/mobile/index.html
@@ -1318,7 +1318,7 @@ function tabBody(b, d) {
if (S.tab === "audit") {
const a = S.audit[b.id];
if (a === undefined) { loadAudit(b.id); return `
載入中…
`; }
- if (!a.turns.length) return `現在沒有進行中的工作。稽核時間軸記的是**這回合**做了什麼:每次問模型、每個工具的參數與結果、你插入的話、停止。回合結束後只留最近的,聊天記錄本身才是長久的。
`;
+ if (!a.turns.length) return `現在沒有進行中的工作。稽核時間軸記的是**這回合**做了什麼:每次問模型、每個工具的參數與結果、你插入的話、停止。這裡記的是最近 30 天的每一回合;更早的故事在聊天記錄裡。
`;
const OUT = {completed: "已結束", aborted: "被你停止", failed: "失敗"};
const ST = {done: "完成", running: "進行中…", waiting: "等核准", denied: "被拒絕", failed: "失敗"};
const TI = {computer: "zap", menu_click: "zap", fill_form: "zap", wait_for: "clock", run_cmd: "zap", web_fetch: "globe", browser: "globe"};
diff --git a/src/store.rs b/src/store.rs
index 0d764cf..f0395b0 100644
--- a/src/store.rs
+++ b/src/store.rs
@@ -32,7 +32,11 @@ fn path() -> PathBuf {
/// Open (create if needed) the data folder's audit database. Failure means "no
/// history", never "no bot".
fn open() -> Option {
- let c = Connection::open(path()).ok()?;
+ let p = path();
+ if let Some(d) = p.parent() {
+ let _ = std::fs::create_dir_all(d); // (the data folder may not exist yet on first run)
+ }
+ let c = Connection::open(p).ok()?;
c.pragma_update(None, "journal_mode", "WAL").ok()?;
c.pragma_update(None, "synchronous", "NORMAL").ok()?;
c.execute_batch(
@@ -122,6 +126,7 @@ pub fn record(bot: u64, r: &Record) {
/// The phone's 稽核時間軸: finished turns stay here, so the page answers for
/// KEEP_DAYS, not just for the turn running now. Newest turn first; the last 20 turns.
pub fn turns(bot: u64) -> serde_json::Value {
+ let bot = bot as i64;
let Some(s) = store() else { return serde_json::json!({"turns": []}) };
let Ok(c) = s.conn.lock() else { return serde_json::json!({"turns": []}) };
let cutoff = crate::model::now_ms() - KEEP_DAYS * 86_400_000;
@@ -208,18 +213,21 @@ mod tests {
#[test]
fn the_trail_keeps_the_story_after_the_turn_log_cleans_up() {
- // (Tests share the temp dir of this process; one database, one bot id.)
+ // (Tests share the temp dir of this process; one database, one bot id.
+ // Times ride on "now" — the trail prunes anything older than a month.)
let bot = 4242;
- record(bot, &Record::OperationStarted { op: "op1".into(), at: 100, source_leaf: 0, prompt: Some(Prompt { at: 99, text: "幫我叫車".into() }) });
+ let t0 = crate::model::now_ms() - 60_000;
+ let at = |d| t0 + d;
+ record(bot, &Record::OperationStarted { op: "op1".into(), at: at(0), source_leaf: 0, prompt: Some(Prompt { at: at(0), text: "幫我叫車".into() }) });
record(bot, &Record::StepAttempt { op: "op1".into(), step: "assistant".into(), attempt: 1 });
record(bot, &Record::StepAttempt { op: "op1".into(), step: "assistant".into(), attempt: 2 });
- record(bot, &Record::ToolStarted { op: "op1".into(), call_id: "c1".into(), name: "computer".into(), args: "{\"action\":\"click\"}".into(), replay: Replay::Never, at: 110 });
- record(bot, &Record::ToolFinished { op: "op1".into(), call_id: "c1".into(), status: "done".into(), at: 120 });
- record(bot, &Record::QueueEnqueued { op: Some("op1".into()), id: "q1".into(), text: "快一點".into(), at: 111 });
+ record(bot, &Record::ToolStarted { op: "op1".into(), call_id: "c1".into(), name: "computer".into(), args: "{\"action\":\"click\"}".into(), replay: Replay::Never, at: at(10) });
+ record(bot, &Record::ToolFinished { op: "op1".into(), call_id: "c1".into(), status: "done".into(), at: at(20) });
+ record(bot, &Record::QueueEnqueued { op: Some("op1".into()), id: "q1".into(), text: "快一點".into(), at: at(11) });
record(bot, &Record::QueueConsumed { id: "q1".into() });
record(bot, &Record::OperationFinished { op: "op1".into(), outcome: Outcome::Completed });
- record(bot, &Record::OperationStarted { op: "op2".into(), at: 200, source_leaf: 100, prompt: None });
- record(bot, &Record::ToolStarted { op: "op2".into(), call_id: "c9".into(), name: "run_cmd".into(), args: "{}".into(), replay: Replay::Safe, at: 210 });
+ record(bot, &Record::OperationStarted { op: "op2".into(), at: at(100), source_leaf: 100, prompt: None });
+ record(bot, &Record::ToolStarted { op: "op2".into(), call_id: "c9".into(), name: "run_cmd".into(), args: "{}".into(), replay: Replay::Safe, at: at(110) });
let v = turns(bot);
let ts = v["turns"].as_array().unwrap();