refactor: require annotated session log history

This commit is contained in:
2026-08-30 12:18:44 +09:00
parent 4ec56fe41e
commit 8493472983
28 changed files with 872 additions and 716 deletions
+4 -4
View File
@@ -631,8 +631,8 @@ fn protocol_command_status(status: WorkdirCommandStatus) -> ProtocolCommandStatu
///
/// `Worker::wire_history_persistence` is called separately to wire the
/// per-item history commit callback so every assistant / tool item
/// landing in `worker.history` becomes a singular `LogEntry::AssistantItem`
/// / `ToolResult` commit through the sync writer.
/// landing in `worker.history` becomes a singular `LogEntry::AnnotatedAssistantItem`
/// / `AnnotatedToolResult` commit through the sync writer.
pub(crate) fn wire_event_bridges_on_engine<C, St>(
worker: &mut Worker<C, St>,
event_tx: &broadcast::Sender<Event>,
@@ -1317,7 +1317,7 @@ async fn controller_loop<C, St>(
}
// Stage the run without a speculative user-message echo.
// `Worker::run` validates the input, commits
// `LogEntry::UserInput`, and the session-log sink turns that
// `LogEntry::AnnotatedUserInput`, and the session-log sink turns that
// committed entry into the live `Event::UserMessage`. That
// keeps every client ordered against `SegmentStart` replay and
// makes persisted history the single source of visible user
@@ -1343,7 +1343,7 @@ async fn controller_loop<C, St>(
Method::Notify { message, auto_run } => {
// Client-side live echo is delivered as `Event::SystemItem`
// once the interceptor commits the corresponding
// `LogEntry::SystemItem` entry — drained out of the
// `LogEntry::AnnotatedSystemItem` entry — drained out of the
// notify buffer + broadcast through the sink. No
// separate echo here.
worker.push_notify(message, auto_run);
@@ -963,9 +963,12 @@ permission = "read"
.append(
session_id,
segment_id,
&LogEntry::UserInput {
&LogEntry::AnnotatedUserInput {
ts: 1,
extensions: vec![],
history: vec![crate::session_history::test_logged_history_entry(
agen::Item::user_message("verify current Flow conditions"),
)],
segments: vec![Segment::Text {
content: "verify current Flow conditions".into(),
}],
+1 -1
View File
@@ -161,7 +161,7 @@ impl From<HookTurnEndAction> for TurnEndAction {
///
/// Hook code can use this handle only when the Worker host includes it in an
/// event-specific context. The handle queues typed requests; the host drains the
/// queue, commits each entry through `LogEntry::SystemItem`, and only then makes
/// queue, commits each entry through `LogEntry::AnnotatedSystemItem`, and only then makes
/// the matching system message visible to the model. It deliberately exposes no
/// raw `agen::Item`, history writer, event sender, `Worker`, `Engine`, or
/// notification buffer.
+5 -5
View File
@@ -539,9 +539,9 @@ mod tests {
text: "done".into(),
}],
};
let assistant_entry = LogEntry::AssistantItem {
let assistant_entry = LogEntry::AnnotatedAssistantItem {
ts: 1,
item: assistant_item.clone(),
entry: crate::session_history::test_logged_history_entry(assistant_item.clone()),
};
let in_flight_guard = in_flight.snapshot_guard();
@@ -593,9 +593,9 @@ mod tests {
text: "done".into(),
}],
};
let assistant_entry = LogEntry::AssistantItem {
let assistant_entry = LogEntry::AnnotatedAssistantItem {
ts: 1,
item: assistant_item.clone(),
entry: crate::session_history::test_logged_history_entry(assistant_item.clone()),
};
in_flight.clear_for_committed_item_then(&assistant_item, || {
@@ -608,7 +608,7 @@ mod tests {
assert!(matches!(
entries_snapshot.as_slice(),
[LogEntry::AssistantItem { item, .. }] if item == &assistant_item
[LogEntry::AnnotatedAssistantItem { entry, .. }] if entry.item == assistant_item
));
assert!(in_flight_snapshot.is_empty());
}
+3 -4
View File
@@ -60,7 +60,7 @@ pub(crate) struct WorkerInterceptor {
pending_notifies: NotifyBuffer,
/// Submit-scoped stash of resolver-produced typed system items.
/// Drained inside `on_prompt_submit`, committed as
/// `LogEntry::SystemItem` entries through `log_writer`, and
/// `LogEntry::AnnotatedSystemItem` entries through `log_writer`, and
/// returned to the worker as `Item::system_message` via
/// `PromptAction::ContinueWith`. Populated by `Worker::run`
/// immediately before handing off to the worker.
@@ -71,7 +71,7 @@ pub(crate) struct WorkerInterceptor {
/// Workspace scope associated with Prompt projection provenance.
prompt_workspace_id: Option<String>,
/// Type-erased commit handle. The interceptor uses it to commit
/// `LogEntry::SystemItem` entries directly (sync) before
/// `LogEntry::AnnotatedSystemItem` entries directly (sync) before
/// returning the corresponding `Item::system_message`s up to the
/// worker. `None` in tests / `Worker::new` paths where no writer is
/// attached.
@@ -142,7 +142,7 @@ impl WorkerInterceptor {
self
}
/// Commit each `SystemItem` as its own `LogEntry::SystemItem`
/// Commit each `SystemItem` as its own `LogEntry::AnnotatedSystemItem`
/// entry through the attached writer (no-op when no writer is
/// wired). Sync — writes complete before the matching
/// `Item::system_message`s reach the worker via
@@ -540,7 +540,6 @@ mod tests {
entry: session_store::LogEntry,
) -> Result<(), session_store::StoreError> {
let item = match entry {
session_store::LogEntry::SystemItem { item, .. } => Some(item),
session_store::LogEntry::AnnotatedSystemItem { entry, .. } => Some(entry.item),
_ => None,
};
+1 -1
View File
@@ -5,7 +5,7 @@
//! `WorkerInterceptor::pending_history_appends`, which the Engine calls
//! at the head of each turn loop iteration. The drain renders each
//! pending entry into a typed `SystemItem` (with the `notify_wrapper`
//! prompt applied), commits a `LogEntry::SystemItem` per entry through
//! prompt applied), commits a `LogEntry::AnnotatedSystemItem` per entry through
//! the session-log sink, and returns the corresponding
//! `Item::system_message`s for the worker to append to its
//! persistent history.
+6 -9
View File
@@ -29,18 +29,12 @@ pub fn subscribe_worker_protocol_session(handle: &WorkerHandle) -> WorkerProtoco
pub fn live_log_entry_event(entry: LogEntry) -> Option<Event> {
match entry {
entry @ (LogEntry::SegmentStart { .. } | LogEntry::AnnotatedSegmentStart { .. }) => {
entry @ LogEntry::AnnotatedSegmentStart { .. } => {
let session =
session_store::public_snapshot::project_current_session_snapshot(&[entry]);
Some(Event::SegmentRotated { session })
}
LogEntry::UserInput { segments, .. } | LogEntry::AnnotatedUserInput { segments, .. } => {
Some(Event::UserMessage { segments })
}
LogEntry::SystemItem { item, .. } => {
let value = serde_json::to_value(&item).expect("SystemItem is Serialize");
Some(Event::SystemItem { item: value })
}
LogEntry::AnnotatedUserInput { segments, .. } => Some(Event::UserMessage { segments }),
LogEntry::AnnotatedSystemItem { entry, .. } => {
let value = serde_json::to_value(&entry.item).expect("SystemItem is Serialize");
Some(Event::SystemItem { item: value })
@@ -89,9 +83,12 @@ mod tests {
#[test]
fn user_input_log_entry_maps_to_user_message_event() {
let segments = vec![protocol::Segment::text("hello from log")];
let event = live_log_entry_event(LogEntry::UserInput {
let event = live_log_entry_event(LogEntry::AnnotatedUserInput {
ts: session_store::segment_log::now_millis(),
extensions: vec![],
history: vec![crate::session_history::test_logged_history_entry(
agen::Item::user_message("hello from log"),
)],
segments: segments.clone(),
})
.expect("UserInput must be live-relevant");
+1 -1
View File
@@ -77,7 +77,7 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: WorkerHandle)
let mut writer = JsonLineWriter::new(writer);
// Hold the in-flight stream lock while taking the session-log mirror
// snapshot. `LogEntry::AssistantItem` is mirror-only for live clients,
// snapshot. `LogEntry::AnnotatedAssistantItem` is mirror-only for live clients,
// so a finalized assistant block must be observed either as an already
// committed entry or as the still-present in-flight block. This lock
// order matches `append_entry` (in-flight clear before sink publish) and
+31 -23
View File
@@ -50,7 +50,7 @@ struct SinkInner {
/// Broadcast channel for live entry updates. The same `Sender`
/// survives session swaps so existing subscribers keep their
/// receiver — they observe the swap as a freshly broadcast
/// `LogEntry::SegmentStart` and reset their view accordingly.
/// `LogEntry::AnnotatedSegmentStart` and reset their view accordingly.
broadcast_tx: broadcast::Sender<LogEntry>,
}
@@ -89,9 +89,9 @@ impl SegmentLogSink {
///
/// Live broadcast fires for committed session-log entries that
/// socket clients must see in log order:
/// - `LogEntry::SegmentStart` → `Event::SegmentRotated` on the wire.
/// - `LogEntry::UserInput` → `Event::UserMessage`.
/// - `LogEntry::SystemItem` → `Event::SystemItem`.
/// - `LogEntry::AnnotatedSegmentStart` → `Event::SegmentRotated` on the wire.
/// - `LogEntry::AnnotatedUserInput` → `Event::UserMessage`.
/// - `LogEntry::AnnotatedSystemItem` → `Event::SystemItem`.
/// - `LogEntry::Invoke` → `Event::InvokeStart`.
/// Everything else (AssistantItem, ToolResult, TurnEnd,
/// RunCompleted, RunErrored, PausedTurnAbandoned, LlmUsage, Extension,
@@ -120,11 +120,8 @@ impl SegmentLogSink {
fn is_live_relevant(entry: &LogEntry) -> bool {
matches!(
entry,
LogEntry::SegmentStart { .. }
| LogEntry::AnnotatedSegmentStart { .. }
| LogEntry::UserInput { .. }
LogEntry::AnnotatedSegmentStart { .. }
| LogEntry::AnnotatedUserInput { .. }
| LogEntry::SystemItem { .. }
| LogEntry::AnnotatedSystemItem { .. }
| LogEntry::Invoke { .. }
)
@@ -132,7 +129,7 @@ impl SegmentLogSink {
/// Atomically swap the mirror to `[initial]` and broadcast the new
/// session-start entry. Used during compaction / fork: the new
/// `LogEntry::SegmentStart` is the first entry of the replacement
/// `LogEntry::AnnotatedSegmentStart` is the first entry of the replacement
/// session, and existing subscribers transition by replaying it
/// like any other live entry.
///
@@ -234,7 +231,7 @@ mod tests {
use session_store::segment_log::now_millis;
fn session_start() -> LogEntry {
LogEntry::SegmentStart {
LogEntry::AnnotatedSegmentStart {
ts: now_millis(),
session_id: uuid::Uuid::nil(),
system_prompt: None,
@@ -253,9 +250,12 @@ mod tests {
}
fn user_input(text: &str) -> LogEntry {
LogEntry::UserInput {
LogEntry::AnnotatedUserInput {
ts: now_millis(),
extensions: vec![],
history: vec![crate::session_history::test_logged_history_entry(
agen::Item::user_message(text),
)],
segments: vec![protocol::Segment::Text {
content: text.to_owned(),
}],
@@ -270,7 +270,10 @@ mod tests {
let (snapshot, mut rx) = sink.subscribe_with_snapshot();
assert_eq!(snapshot.len(), 2);
assert!(matches!(snapshot[0], LogEntry::SegmentStart { .. }));
assert!(matches!(
snapshot[0],
LogEntry::AnnotatedSegmentStart { .. }
));
assert!(matches!(
snapshot[1],
LogEntry::TurnEnd { turn_count: 1, .. }
@@ -279,13 +282,15 @@ mod tests {
}
fn notification_entry(text: &str) -> LogEntry {
LogEntry::SystemItem {
LogEntry::AnnotatedSystemItem {
ts: now_millis(),
item: session_store::SystemItem::Notification {
message: text.to_owned(),
body: format!("[Notification] {text}"),
prompt_provenance: None,
},
entry: crate::session_history::test_logged_system_entry(
session_store::SystemItem::Notification {
message: text.to_owned(),
body: format!("[Notification] {text}"),
prompt_provenance: None,
},
),
}
}
@@ -305,7 +310,7 @@ mod tests {
// for Event::UserMessage.
sink.publish(user_input("hi from log"));
match rx.try_recv() {
Ok(LogEntry::UserInput { segments, .. }) => {
Ok(LogEntry::AnnotatedUserInput { segments, .. }) => {
assert_eq!(segments.len(), 1);
}
other => panic!("expected UserInput, got {other:?}"),
@@ -314,7 +319,7 @@ mod tests {
// SystemItem is live-relevant.
sink.publish(notification_entry("hi"));
match rx.try_recv() {
Ok(LogEntry::SystemItem { .. }) => {}
Ok(LogEntry::AnnotatedSystemItem { .. }) => {}
other => panic!("expected SystemItem, got {other:?}"),
}
@@ -332,7 +337,7 @@ mod tests {
assert_eq!(snapshot.len(), 1);
match rx.try_recv() {
Ok(LogEntry::SystemItem { .. }) => {}
Ok(LogEntry::AnnotatedSystemItem { .. }) => {}
other => panic!("unexpected: {other:?}"),
}
assert!(rx.try_recv().is_err());
@@ -348,13 +353,16 @@ mod tests {
sink.reset_with_initial(session_start());
match rx.try_recv() {
Ok(LogEntry::SegmentStart { .. }) => {}
Ok(LogEntry::AnnotatedSegmentStart { .. }) => {}
other => panic!("expected SegmentStart broadcast, got {other:?}"),
}
let (post_snapshot, _) = sink.subscribe_with_snapshot();
assert_eq!(post_snapshot.len(), 1);
assert!(matches!(post_snapshot[0], LogEntry::SegmentStart { .. }));
assert!(matches!(
post_snapshot[0],
LogEntry::AnnotatedSegmentStart { .. }
));
}
#[test]
+27 -37
View File
@@ -5,7 +5,6 @@
//! retained only as explicit `LegacyUnknown` entries.
use agen::{HistoryEntry, Item};
use protocol::Segment;
use session_store::{
LogEntry, LoggedHistoryDerivation, LoggedHistoryEntry, LoggedSessionHistoryEntryId,
LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin, LoggedWorkerSubject, SegmentId,
@@ -18,6 +17,32 @@ pub type WorkerHistoryProvenance = LoggedSessionHistoryOrigin;
pub type SessionHistoryDerivation = LoggedHistoryDerivation;
pub type WorkerSubjectSnapshot = LoggedWorkerSubject;
#[cfg(test)]
pub(crate) fn test_logged_history_entry(item: impl Into<Item>) -> LoggedHistoryEntry {
LoggedHistoryEntry {
item: session_store::LoggedItem::from(item.into()),
metadata: LoggedSessionHistoryMetadata {
entry_id: LoggedSessionHistoryEntryId::new(),
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
},
}
}
#[cfg(test)]
pub(crate) fn test_logged_system_entry(
item: session_store::SystemItem,
) -> session_store::LoggedSystemHistoryEntry {
session_store::LoggedSystemHistoryEntry {
item,
metadata: LoggedSessionHistoryMetadata {
entry_id: LoggedSessionHistoryEntryId::new(),
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
},
}
}
pub(crate) fn worker_subject(session_id: SessionId) -> WorkerSubjectSnapshot {
WorkerSubjectSnapshot {
workspace_id: None,
@@ -53,10 +78,6 @@ pub(crate) fn to_logged_history_entry(
}
}
fn legacy_entry(item: Item) -> HistoryEntry<SessionHistoryMetadata> {
HistoryEntry::new(item, SessionHistoryMetadata::legacy_unknown())
}
fn from_logged(entry: &LoggedHistoryEntry) -> HistoryEntry<SessionHistoryMetadata> {
HistoryEntry::new(Item::from(entry.item.clone()), entry.metadata.clone())
}
@@ -74,32 +95,15 @@ pub(crate) fn restore_history_entries(
LogEntry::AnnotatedSegmentStart { history: seed, .. } => {
history = seed.iter().map(from_logged).collect();
}
LogEntry::SegmentStart { history: seed, .. } => {
history = seed
.iter()
.cloned()
.map(Item::from)
.map(legacy_entry)
.collect();
}
LogEntry::AnnotatedUserInput { history: input, .. } => {
history.extend(input.iter().map(from_logged))
}
LogEntry::UserInput { segments, .. } => history.push(legacy_entry(Item::user_message(
Segment::flatten_to_text(segments),
))),
LogEntry::AnnotatedAssistantItem { entry, .. }
| LogEntry::AnnotatedToolResult { entry, .. } => history.push(from_logged(entry)),
LogEntry::AssistantItem { item, .. } | LogEntry::ToolResult { item, .. } => {
history.push(legacy_entry(Item::from(item.clone())));
}
LogEntry::AnnotatedSystemItem { entry, .. } => history.push(HistoryEntry::new(
entry.item.to_history_item(),
entry.metadata.clone(),
)),
LogEntry::SystemItem { item, .. } => {
history.push(legacy_entry(item.to_history_item()));
}
_ => {}
}
}
@@ -110,23 +114,9 @@ pub(crate) fn restore_history_entries(
mod tests {
use super::*;
use agen::llm_client::RequestConfig;
use protocol::Segment;
use session_store::LogEntry;
#[test]
fn legacy_user_role_is_not_inferred_as_human_authority() {
let entries = vec![LogEntry::UserInput {
ts: 1,
segments: vec![Segment::text("legacy")],
extensions: Vec::new(),
}];
let restored =
restore_history_entries(SessionId::now_v7(), SegmentId::now_v7(), &entries).unwrap();
assert!(matches!(
restored[0].annotation.origin,
WorkerHistoryProvenance::LegacyUnknown
));
}
#[test]
fn typed_flow_and_unknown_caller_input_round_trip_without_role_inference() {
let session_id = SessionId::now_v7();
+28 -15
View File
@@ -112,8 +112,12 @@ impl InternalSpawnedWorkerRecord {
fn stop_summary(&self) -> SubWorkerStopSummary {
let mut counts = BTreeMap::<String, u64>::new();
for entry in self.session.entries() {
if let session_store::LogEntry::AssistantItem {
item: LoggedItem::ToolCall { name, .. },
if let session_store::LogEntry::AnnotatedAssistantItem {
entry:
session_store::LoggedHistoryEntry {
item: LoggedItem::ToolCall { name, .. },
..
},
..
} = entry
{
@@ -1024,11 +1028,16 @@ mod tests {
&& worker.parent_session_id.as_deref() == Some("parent-session")
&& matches!(*event, Event::TextDone { ref text } if text == "answer")
));
record.session.publish_test_entry(LogEntry::UserInput {
ts: 1,
segments: vec![protocol::Segment::text("question")],
extensions: Vec::new(),
});
record
.session
.publish_test_entry(LogEntry::AnnotatedUserInput {
ts: 1,
segments: vec![protocol::Segment::text("question")],
history: vec![crate::session_history::test_logged_history_entry(
agen::Item::user_message("question"),
)],
extensions: Vec::new(),
});
let committed = tokio::time::timeout(Duration::from_secs(1), parent_rx.recv())
.await
.unwrap()
@@ -1159,14 +1168,18 @@ mod tests {
let (mut record, _events) = record("child", InternalWorkerVisibility::ParentClient).await;
record.change_tracker = Some(tracker);
for (index, name) in ["Read", "Read", "Grep"].into_iter().enumerate() {
record.session.publish_test_entry(LogEntry::AssistantItem {
ts: index as u64,
item: LoggedItem::ToolCall {
call_id: format!("call-{index}"),
name: name.to_string(),
arguments: "{}".to_string(),
},
});
record
.session
.publish_test_entry(LogEntry::AnnotatedAssistantItem {
ts: index as u64,
entry: crate::session_history::test_logged_history_entry(
LoggedItem::ToolCall {
call_id: format!("call-{index}"),
name: name.to_string(),
arguments: "{}".to_string(),
},
),
});
}
registry.start_protocol_forwarding(record.clone());
install_record(&registry, record);
+51 -42
View File
@@ -900,7 +900,6 @@ where
self.state.increment_entries();
if let Some(in_flight) = &self.in_flight {
let committed_item = match &entry {
LogEntry::AssistantItem { item, .. } => Some(item.clone()),
LogEntry::AnnotatedAssistantItem { entry, .. } => Some(entry.item.clone()),
_ => None,
};
@@ -1207,11 +1206,11 @@ pub struct Worker<C: LlmClient, St: Store> {
memory_task: Option<JoinHandle<()>>,
/// Typed user submissions in submit order. K-th entry corresponds to
/// the K-th `Item::user_message` in `worker.history()` (modulo seed
/// history loaded via `SegmentStart.history`, whose original segments
/// history loaded via `AnnotatedSegmentStart.history`, whose original segments
/// are not preserved). Populated from log on `restore_from_manifest`,
/// appended after `save_user_input` on each `run`. Pre-`Event::Snapshot`
/// this fed `WorkerSharedState.user_segments`; the new wire format
/// carries typed atoms via `LogEntry::UserInput { segments }` so
/// carries typed atoms via `LogEntry::AnnotatedUserInput { segments }` so
/// this remains purely an in-memory tracker for compact alignment.
user_segments: Vec<Vec<Segment>>,
/// Worker-side session-log mirror + broadcast sink. Populated alongside
@@ -1221,7 +1220,8 @@ pub struct Worker<C: LlmClient, St: Store> {
sink: SegmentLogSink,
/// `true` once `wire_history_persistence` has installed the
/// `Engine::on_history_append` callback that commits each appended
/// item as a singular `LogEntry::AssistantItem` / `ToolResult`
/// item as a singular `LogEntry::AnnotatedAssistantItem` /
/// `AnnotatedToolResult`
/// directly through the writer. Tests that drive `Worker::new` without
/// going through the controller leave this `false`; `persist_turn`
/// then walks the post-`history_before` slice inline so entries
@@ -1345,15 +1345,16 @@ impl<C: LlmClient + 'static, St: Store + Clone + 'static> Worker<C, St> {
}
/// Wire `Engine::on_history_append` to commit each appended item
/// directly as a singular `LogEntry::AssistantItem` / `ToolResult`
/// directly as a singular `LogEntry::AnnotatedAssistantItem` /
/// `AnnotatedToolResult`
/// through the writer. The controller calls this once per spawned
/// Worker after the worker is built; tests that drive `Worker::new` may
/// opt in to the same wiring or leave it off (in which case
/// `persist_turn`'s inline fallback writes entries at turn end).
///
/// `user_message` items are skipped because they are committed
/// up-front via `commit_entry(LogEntry::UserInput { segments })`.
/// `role:system` items are committed as typed `LogEntry::SystemItem`
/// up-front via `commit_entry(LogEntry::AnnotatedUserInput { segments })`.
/// `role:system` items are committed as typed `LogEntry::AnnotatedSystemItem`
/// entries by their producers (for example `WorkerInterceptor` and
/// interrupted-turn prep) before they reach the worker's history, so this
/// callback would otherwise double-write them.
@@ -1941,8 +1942,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
}
let input = match entries.get(target.user_input_entry_index) {
Some(LogEntry::UserInput { segments, .. })
| Some(LogEntry::AnnotatedUserInput { segments, .. }) => segments.clone(),
Some(LogEntry::AnnotatedUserInput { segments, .. }) => segments.clone(),
_ => {
return Err(RewindError::Invalid(
"rewind target is no longer a user message".into(),
@@ -2081,8 +2081,8 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
/// Cheap clone via `Option<Clone>`.
/// Snapshot of the typed user segments tracked alongside worker
/// history. The K-th entry corresponds to the K-th `Item::user_message`
/// derived from `LogEntry::UserInput` entries (post-compaction); seed
/// history loaded via `SegmentStart.history` does not contribute,
/// derived from `LogEntry::AnnotatedUserInput` entries (post-compaction); seed
/// history loaded via `AnnotatedSegmentStart.history` does not contribute,
/// which is acceptable because the original segments are unrecoverable.
pub fn user_segments(&self) -> &[Vec<Segment>] {
&self.user_segments
@@ -3533,12 +3533,13 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
// slice from `history_before` inline so the test's
// `restore`-style assertions still see entries on disk.
if !self.history_persistence_wired {
let new_items: Vec<Item> = self.session.history().entries()[history_before..]
let new_entries: Vec<_> = self.session.history().entries()[history_before..]
.iter()
.map(|entry| entry.item.clone())
.map(to_logged_history_entry)
.collect();
let ts = segment_log::now_millis();
for item in &new_items {
for history_entry in new_entries {
let item = Item::from(history_entry.item.clone());
if item.is_user_message() {
continue;
}
@@ -3551,7 +3552,7 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
) {
continue;
}
let entry = session_store::classify_history_item(item, ts);
let entry = session_store::classify_logged_history_entry(history_entry, ts);
self.commit_entry(entry)?;
}
}
@@ -6257,8 +6258,7 @@ fn build_rewind_targets(segment_id: uuid::Uuid, entries: &[LogEntry]) -> Vec<Rew
let mut targets = Vec::new();
for (entry_index, entry) in entries.iter().enumerate() {
let (segments, ts) = match entry {
LogEntry::UserInput { segments, ts, .. }
| LogEntry::AnnotatedUserInput { segments, ts, .. } => (segments, ts),
LogEntry::AnnotatedUserInput { segments, ts, .. } => (segments, ts),
_ => continue,
};
turn_index += 1;
@@ -6300,8 +6300,7 @@ fn rewind_truncate_entries(entries: &[LogEntry], user_input_entry_index: usize)
fn suffix_has_tool_side_effects(entries: &[LogEntry]) -> bool {
entries.iter().any(|entry| match entry {
LogEntry::ToolResult { .. } | LogEntry::AnnotatedToolResult { .. } => true,
LogEntry::AssistantItem { item, .. } => logged_item_is_tool_call(item),
LogEntry::AnnotatedToolResult { .. } => true,
LogEntry::AnnotatedAssistantItem { entry, .. } => logged_item_is_tool_call(&entry.item),
_ => false,
})
@@ -7636,7 +7635,7 @@ mod build_summary_prompt_tests {
);
assert!(checkpoint.is_none());
let mut replacement_entries = vec![LogEntry::SegmentStart {
let mut replacement_entries = vec![LogEntry::AnnotatedSegmentStart {
ts: segment_log::now_millis(),
session_id: uuid::Uuid::nil(),
system_prompt: None,
@@ -7964,9 +7963,12 @@ mod build_summary_prompt_tests {
);
append_test_entry(
worker,
LogEntry::UserInput {
LogEntry::AnnotatedUserInput {
ts: ts + 1,
extensions: vec![],
history: vec![crate::session_history::test_logged_history_entry(
Item::user_message(text),
)],
segments: vec![text_segment(text)],
},
);
@@ -7986,16 +7988,18 @@ mod build_summary_prompt_tests {
append_user_turn(&worker, 20, "second message");
append_test_entry(
&worker,
LogEntry::ToolResult {
LogEntry::AnnotatedToolResult {
ts: 30,
item: session_store::LoggedItem::ToolResult {
call_id: "call-1".into(),
summary: "wrote a file".into(),
content: None,
attachments: Vec::new(),
disposition: Default::default(),
is_error: false,
},
entry: crate::session_history::test_logged_history_entry(
session_store::LoggedItem::ToolResult {
call_id: "call-1".into(),
summary: "wrote a file".into(),
content: None,
attachments: Vec::new(),
disposition: Default::default(),
is_error: false,
},
),
},
);
@@ -8029,16 +8033,18 @@ mod build_summary_prompt_tests {
append_user_turn(&worker, 20, "second message");
append_test_entry(
&worker,
LogEntry::ToolResult {
LogEntry::AnnotatedToolResult {
ts: 30,
item: session_store::LoggedItem::ToolResult {
call_id: "call-1".into(),
summary: "wrote a file".into(),
content: None,
attachments: Vec::new(),
disposition: Default::default(),
is_error: false,
},
entry: crate::session_history::test_logged_history_entry(
session_store::LoggedItem::ToolResult {
call_id: "call-1".into(),
summary: "wrote a file".into(),
content: None,
attachments: Vec::new(),
disposition: Default::default(),
is_error: false,
},
),
},
);
let (head_entries, targets) = worker.list_rewind_targets().unwrap();
@@ -8456,9 +8462,9 @@ mod build_summary_prompt_tests {
worker.wire_history_persistence();
let dangling_call = Item::tool_call("call-1", "SideEffect", "{}");
worker
.commit_entry(LogEntry::AssistantItem {
.commit_entry(LogEntry::AnnotatedAssistantItem {
ts: segment_log::now_millis(),
item: dangling_call.clone().into(),
entry: crate::session_history::test_logged_history_entry(dangling_call.clone()),
})
.unwrap();
worker.set_history_for_test(vec![dangling_call]);
@@ -8863,9 +8869,12 @@ mod build_summary_prompt_tests {
);
worker.set_history_for_test(vec![evidence.clone()]);
worker
.commit_entry(LogEntry::UserInput {
.commit_entry(LogEntry::AnnotatedUserInput {
ts: segment_log::now_millis(),
extensions: vec![],
history: vec![crate::session_history::test_logged_history_entry(
evidence.clone(),
)],
segments: vec![text_segment(
"The cancellation regression must leave this evidence available for retry.",
)],
+20 -18
View File
@@ -25,6 +25,17 @@ use worker::{Worker, WorkerController};
type TestStore = CombinedStore<FsStore, FsWorkerStore>;
fn annotated(item: Item) -> session_store::LoggedHistoryEntry {
session_store::LoggedHistoryEntry {
item: session_store::LoggedItem::from(item),
metadata: session_store::LoggedSessionHistoryMetadata {
entry_id: session_store::LoggedSessionHistoryEntryId::new(),
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
},
}
}
#[derive(Clone)]
struct MockClient {
responses: Arc<Vec<Vec<LlmEvent>>>,
@@ -210,7 +221,6 @@ fn system_texts_in_sink_session_start(
.into_iter()
.map(|entry| entry.item)
.collect::<Vec<_>>(),
session_store::LogEntry::SegmentStart { history, .. } => history,
_ => continue,
};
return history
@@ -310,17 +320,14 @@ permission = "write"
// Simulate a foreign writer appending to the same segment. This bumps
// the on-disk entry count past the Worker's own append tally without
// updating the Worker's `entries_written`.
store
.append(
session_id,
source_segment_id,
&LogEntry::UserInput {
ts: 9999,
segments: vec![protocol::Segment::text("interloper")],
extensions: vec![],
},
)
.unwrap();
session_store::save_user_input(
&store,
session_id,
source_segment_id,
vec![protocol::Segment::text("interloper")],
vec![annotated(Item::user_message("interloper"))],
)
.unwrap();
// Next run triggers ensure_segment_head, which sees the drift.
worker.run_text("second").await.unwrap();
@@ -348,11 +355,6 @@ permission = "write"
session_id: seg_session,
forked_from: Some(origin),
..
}
| LogEntry::SegmentStart {
session_id: seg_session,
forked_from: Some(origin),
..
} => {
assert_eq!(*seg_session, session_id);
assert_eq!(origin.segment_id, source_segment_id);
@@ -366,7 +368,7 @@ permission = "write"
assert_eq!(source_after.len(), source_len_before + 1);
assert!(matches!(
source_after.last(),
Some(LogEntry::UserInput { .. })
Some(LogEntry::AnnotatedUserInput { .. })
));
}
+2 -16
View File
@@ -35,29 +35,16 @@ fn history_from_sink(handle: &WorkerHandle) -> Vec<Item> {
LogEntry::AnnotatedSegmentStart { history, .. } => {
items.extend(history.into_iter().map(|entry| Item::from(entry.item)));
}
LogEntry::SegmentStart { history, .. } => {
items.extend(history.into_iter().map(Item::from));
}
LogEntry::AnnotatedUserInput { history, .. } => {
items.extend(history.into_iter().map(|entry| Item::from(entry.item)));
}
LogEntry::UserInput { segments, .. } => {
let text = protocol::Segment::flatten_to_text(&segments);
items.push(Item::user_message(text));
}
LogEntry::AnnotatedAssistantItem { entry, .. }
| LogEntry::AnnotatedToolResult { entry, .. } => {
items.push(Item::from(entry.item));
}
LogEntry::AssistantItem { item, .. } | LogEntry::ToolResult { item, .. } => {
items.push(Item::from(item));
}
LogEntry::AnnotatedSystemItem { entry, .. } => {
items.push(entry.item.to_history_item());
}
LogEntry::SystemItem { item, .. } => {
items.push(item.to_history_item());
}
_ => {}
}
}
@@ -67,7 +54,6 @@ fn history_from_sink(handle: &WorkerHandle) -> Vec<Item> {
fn system_item(entry: &LogEntry) -> Option<&session_store::SystemItem> {
match entry {
LogEntry::AnnotatedSystemItem { entry, .. } => Some(&entry.item),
LogEntry::SystemItem { item, .. } => Some(item),
_ => None,
}
}
@@ -1095,7 +1081,7 @@ async fn run_with_paste_segment_inlines_content_and_emits_typed_user_message() {
// Mixed input: plain text + a paste chip + trailing text. Worker must
// flatten this into one user-message string (paste content inlined,
// no `[Clipboard ...]` label leaking to the LLM); the committed
// `LogEntry::UserInput` must carry the typed segments unchanged so
// `LogEntry::AnnotatedUserInput` must carry the typed segments unchanged so
// socket clients can derive `Event::UserMessage` and re-render the chip.
let segments = vec![
protocol::Segment::text("see "),
@@ -1130,7 +1116,7 @@ async fn run_with_paste_segment_inlines_content_and_emits_typed_user_message() {
_ => {}
},
entry = entry_rx.recv() => match entry {
Ok(session_store::LogEntry::UserInput { segments, .. } | session_store::LogEntry::AnnotatedUserInput { segments, .. }) => {
Ok(session_store::LogEntry::AnnotatedUserInput { segments, .. }) => {
user_input_segments = Some(segments);
if saw_turn_end {
break;