fix: restore console run accounting from persisted logs
This commit is contained in:
@@ -83,6 +83,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
cache_read_input_tokens,
|
||||
..
|
||||
} => {
|
||||
println!(
|
||||
"[usage] in={} (cache_read={}) out={}",
|
||||
|
||||
@@ -178,7 +178,10 @@ impl WorkerHandle {
|
||||
(entries, entry_rx, in_flight)
|
||||
};
|
||||
let mut session =
|
||||
session_store::public_snapshot::project_current_session_snapshot(&entries);
|
||||
session_store::public_snapshot::project_current_session_snapshot_with_accounting(
|
||||
self.artifact_store.as_ref(),
|
||||
&entries,
|
||||
);
|
||||
session.pending_submissions = self
|
||||
.pending_activations
|
||||
.lock()
|
||||
@@ -1286,14 +1289,8 @@ pub(crate) fn wire_event_bridges_on_engine<C, St>(
|
||||
});
|
||||
});
|
||||
|
||||
let tx = working_event_tx.clone();
|
||||
worker.on_usage(move |event| {
|
||||
let _ = tx.send(Event::Usage {
|
||||
input_tokens: event.input_tokens,
|
||||
output_tokens: event.output_tokens,
|
||||
cache_read_input_tokens: event.cache_read_input_tokens,
|
||||
});
|
||||
});
|
||||
// Usage is published from committed LlmUsage records through the session-log
|
||||
// lane. Snapshot and live usage therefore share one exact, gap-free boundary.
|
||||
|
||||
let tx = working_event_tx.clone();
|
||||
worker.on_error(move |event| {
|
||||
|
||||
@@ -494,7 +494,11 @@ impl InternalWorkerSessionHandle {
|
||||
(entries, snapshot_from_guard(&guard))
|
||||
};
|
||||
InternalWorkerSessionSnapshot {
|
||||
session: session_store::public_snapshot::project_current_session_snapshot(&entries),
|
||||
session:
|
||||
session_store::public_snapshot::project_current_session_snapshot_with_accounting(
|
||||
&self.store,
|
||||
&entries,
|
||||
),
|
||||
greeting: self.greeting.as_ref().map(|greeting| {
|
||||
greeting
|
||||
.read()
|
||||
|
||||
@@ -96,7 +96,22 @@ pub fn live_log_entry_event(entry: LogEntry) -> Option<Event> {
|
||||
item: value,
|
||||
})
|
||||
}
|
||||
LogEntry::Invoke { trigger, .. } => Some(Event::InvokeStart { kind: trigger }),
|
||||
LogEntry::Invoke { ts, trigger } => Some(Event::InvokeStart {
|
||||
kind: trigger,
|
||||
timestamp_ms: Some(ts),
|
||||
}),
|
||||
LogEntry::LlmUsage {
|
||||
ts,
|
||||
input_total_tokens,
|
||||
cache_read_tokens,
|
||||
output_tokens,
|
||||
..
|
||||
} => Some(Event::Usage {
|
||||
timestamp_ms: Some(ts),
|
||||
input_tokens: Some(input_total_tokens),
|
||||
cache_read_input_tokens: Some(cache_read_tokens),
|
||||
output_tokens: Some(output_tokens),
|
||||
}),
|
||||
other => {
|
||||
// `SegmentLogSink::is_live_relevant` keeps non-live-relevant
|
||||
// variants off the broadcast lane; reaching here means the two are
|
||||
|
||||
@@ -124,6 +124,7 @@ impl SegmentLogSink {
|
||||
| LogEntry::ToolResultCorrected { .. }
|
||||
| LogEntry::AnnotatedSystemItem { .. }
|
||||
| LogEntry::Invoke { .. }
|
||||
| LogEntry::LlmUsage { .. }
|
||||
| LogEntry::RunYielded { .. }
|
||||
| LogEntry::RunResumed { .. }
|
||||
| LogEntry::RunCancelled { .. }
|
||||
@@ -357,6 +358,54 @@ mod tests {
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_usage_is_split_once_between_snapshot_and_live_even_with_equal_timestamps() {
|
||||
let sink = SegmentLogSink::new();
|
||||
sink.publish(LogEntry::Invoke {
|
||||
ts: 100,
|
||||
trigger: protocol::InvokeKind::UserSend,
|
||||
});
|
||||
let usage = LogEntry::LlmUsage {
|
||||
ts: 200,
|
||||
history_len: 2,
|
||||
input_total_tokens: 2000,
|
||||
cache_read_tokens: 1000,
|
||||
cache_write_tokens: 0,
|
||||
output_tokens: 100,
|
||||
};
|
||||
sink.publish(usage.clone());
|
||||
let (snapshot, mut rx) = sink.subscribe_with_snapshot();
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.iter()
|
||||
.filter(|e| matches!(e, LogEntry::LlmUsage { .. }))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(rx.try_recv().is_err());
|
||||
sink.publish(usage);
|
||||
let event =
|
||||
crate::ipc::protocol_session::live_log_entry_event(rx.try_recv().unwrap()).unwrap();
|
||||
assert!(matches!(
|
||||
event,
|
||||
protocol::Event::Usage {
|
||||
timestamp_ms: Some(200),
|
||||
input_tokens: Some(2000),
|
||||
..
|
||||
}
|
||||
));
|
||||
assert!(rx.try_recv().is_err());
|
||||
let (reconnected, mut next_rx) = sink.subscribe_with_snapshot();
|
||||
assert_eq!(
|
||||
reconnected
|
||||
.iter()
|
||||
.filter(|e| matches!(e, LogEntry::LlmUsage { .. }))
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(next_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_with_initial_clears_and_broadcasts() {
|
||||
let sink = SegmentLogSink::new();
|
||||
|
||||
@@ -294,6 +294,9 @@ impl SessionCapture {
|
||||
// Observation deliberately excludes system items and
|
||||
// controller errors from model-visible session evidence.
|
||||
SessionSnapshotEntryData::SystemItem { .. }
|
||||
| SessionSnapshotEntryData::Invoke { .. }
|
||||
| SessionSnapshotEntryData::Usage { .. }
|
||||
| SessionSnapshotEntryData::RunStats { .. }
|
||||
| SessionSnapshotEntryData::RunYielded { .. }
|
||||
| SessionSnapshotEntryData::RunResumed { .. }
|
||||
| SessionSnapshotEntryData::RunCancelled
|
||||
|
||||
@@ -1556,6 +1556,70 @@ async fn snapshot_includes_user_input_for_in_flight_turn() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnect_snapshot_restores_live_run_accounting_from_engine_events() {
|
||||
let client = MockClient::new(vec![
|
||||
LlmEvent::tool_use_start(0, "hold-call", "Hold"),
|
||||
LlmEvent::tool_input_delta(0, "{}"),
|
||||
LlmEvent::tool_use_stop(0),
|
||||
LlmEvent::Usage(UsageEvent {
|
||||
input_tokens: Some(25_000),
|
||||
output_tokens: Some(300),
|
||||
cache_read_input_tokens: Some(20_000),
|
||||
..Default::default()
|
||||
}),
|
||||
LlmEvent::Status(StatusEvent {
|
||||
status: ResponseStatus::Completed,
|
||||
}),
|
||||
]);
|
||||
let mut worker = make_worker(client).await;
|
||||
worker
|
||||
.engine_mut()
|
||||
.register_tool(hanging_tool_definition("Hold"));
|
||||
let handle = spawn_controller(worker).await;
|
||||
let mut events = handle.sink.subscribe_with_snapshot().1;
|
||||
handle
|
||||
.send(Method::submit_text(
|
||||
protocol::new_submission_request_id(),
|
||||
"keep running",
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let observed = tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||
loop {
|
||||
if let LogEntry::LlmUsage {
|
||||
ts,
|
||||
input_total_tokens,
|
||||
cache_read_tokens,
|
||||
output_tokens,
|
||||
..
|
||||
} = events.recv().await.unwrap()
|
||||
{
|
||||
break (ts, input_total_tokens, cache_read_tokens, output_tokens);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("live accounting");
|
||||
assert_eq!((observed.1, observed.2, observed.3), (25_000, 20_000, 300));
|
||||
for _ in 0..2 {
|
||||
let Event::Snapshot { session, .. } = handle.snapshot_event() else {
|
||||
panic!("expected snapshot")
|
||||
};
|
||||
let usage: Vec<_> = session
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| matches!(e.data, protocol::SessionSnapshotEntryData::Usage { .. }))
|
||||
.collect();
|
||||
assert_eq!(usage.len(), 1);
|
||||
assert_eq!(usage[0].timestamp, observed.0);
|
||||
assert!(session.entries.iter().any(|e| matches!(
|
||||
e.data,
|
||||
protocol::SessionSnapshotEntryData::Invoke { .. }
|
||||
) && e.timestamp > 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn attach_snapshot_includes_current_status() {
|
||||
let client = MockClient::sequential(vec![MockResponse::Hang(simple_text_events())]);
|
||||
|
||||
Reference in New Issue
Block a user