fix(session): preflight migration and retain timestamps
This commit is contained in:
@@ -394,6 +394,8 @@ pub struct SessionSnapshotEntry {
|
|||||||
/// Stable identity from durable history metadata, or a deterministic
|
/// Stable identity from durable history metadata, or a deterministic
|
||||||
/// identity derived from the legacy segment and log position.
|
/// identity derived from the legacy segment and log position.
|
||||||
pub entry_id: String,
|
pub entry_id: String,
|
||||||
|
/// Timestamp copied from the durable log record that commits this entry.
|
||||||
|
pub timestamp: u64,
|
||||||
pub provenance: SessionEntryProvenance,
|
pub provenance: SessionEntryProvenance,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub derived_from: Vec<String>,
|
pub derived_from: Vec<String>,
|
||||||
@@ -1536,6 +1538,7 @@ mod tests {
|
|||||||
session: SessionSnapshot {
|
session: SessionSnapshot {
|
||||||
entries: vec![SessionSnapshotEntry {
|
entries: vec![SessionSnapshotEntry {
|
||||||
entry_id: "entry-1".into(),
|
entry_id: "entry-1".into(),
|
||||||
|
timestamp: 1,
|
||||||
provenance: SessionEntryProvenance::HumanInput,
|
provenance: SessionEntryProvenance::HumanInput,
|
||||||
derived_from: Vec::new(),
|
derived_from: Vec::new(),
|
||||||
data: SessionSnapshotEntryData::UserInput {
|
data: SessionSnapshotEntryData::UserInput {
|
||||||
@@ -1565,6 +1568,7 @@ mod tests {
|
|||||||
parsed["data"]["session"]["entries"][0]["kind"],
|
parsed["data"]["session"]["entries"][0]["kind"],
|
||||||
"user_input"
|
"user_input"
|
||||||
);
|
);
|
||||||
|
assert_eq!(parsed["data"]["session"]["entries"][0]["timestamp"], 1);
|
||||||
assert_eq!(parsed["data"]["greeting"]["worker_name"], "test");
|
assert_eq!(parsed["data"]["greeting"]["worker_name"], "test");
|
||||||
assert_eq!(parsed["data"]["greeting"]["tools"][0], "Read");
|
assert_eq!(parsed["data"]["greeting"]["tools"][0], "Read");
|
||||||
assert_eq!(parsed["data"]["greeting"]["context_window"], 200_000);
|
assert_eq!(parsed["data"]["greeting"]["context_window"], 200_000);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub fn project_session_snapshot(session_id: SessionId, log: &[LogEntry]) -> Sess
|
|||||||
for (log_index, record) in log.iter().enumerate() {
|
for (log_index, record) in log.iter().enumerate() {
|
||||||
match record {
|
match record {
|
||||||
LogEntry::SegmentStart {
|
LogEntry::SegmentStart {
|
||||||
|
ts,
|
||||||
session_id,
|
session_id,
|
||||||
history,
|
history,
|
||||||
..
|
..
|
||||||
@@ -41,57 +42,65 @@ pub fn project_session_snapshot(session_id: SessionId, log: &[LogEntry]) -> Sess
|
|||||||
entries.clear();
|
entries.clear();
|
||||||
for (item_index, item) in history.iter().enumerate() {
|
for (item_index, item) in history.iter().enumerate() {
|
||||||
if let Some(data) = project_item(item) {
|
if let Some(data) = project_item(item) {
|
||||||
entries.push(legacy_entry(&session_key, log_index, item_index, data));
|
entries.push(legacy_entry(&session_key, log_index, item_index, *ts, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogEntry::AnnotatedSegmentStart {
|
LogEntry::AnnotatedSegmentStart {
|
||||||
|
ts,
|
||||||
session_id,
|
session_id,
|
||||||
history,
|
history,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
session_key = *session_id;
|
session_key = *session_id;
|
||||||
entries.clear();
|
entries.clear();
|
||||||
extend_history(&mut entries, history, None);
|
extend_history(&mut entries, history, None, *ts);
|
||||||
}
|
}
|
||||||
LogEntry::UserInput { segments, .. } => entries.push(legacy_entry(
|
LogEntry::UserInput { ts, segments, .. } => entries.push(legacy_entry(
|
||||||
&session_key,
|
&session_key,
|
||||||
log_index,
|
log_index,
|
||||||
0,
|
0,
|
||||||
|
*ts,
|
||||||
SessionSnapshotEntryData::UserInput {
|
SessionSnapshotEntryData::UserInput {
|
||||||
segments: segments.clone(),
|
segments: segments.clone(),
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
LogEntry::AnnotatedUserInput {
|
LogEntry::AnnotatedUserInput {
|
||||||
segments, history, ..
|
ts,
|
||||||
} => extend_history(&mut entries, history, Some(segments)),
|
segments,
|
||||||
LogEntry::AssistantItem { item, .. } | LogEntry::ToolResult { item, .. } => {
|
history,
|
||||||
|
..
|
||||||
|
} => extend_history(&mut entries, history, Some(segments), *ts),
|
||||||
|
LogEntry::AssistantItem { ts, item } | LogEntry::ToolResult { ts, item } => {
|
||||||
if let Some(data) = project_item(item) {
|
if let Some(data) = project_item(item) {
|
||||||
entries.push(legacy_entry(&session_key, log_index, 0, data));
|
entries.push(legacy_entry(&session_key, log_index, 0, *ts, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogEntry::AnnotatedAssistantItem { entry, .. }
|
LogEntry::AnnotatedAssistantItem { ts, entry }
|
||||||
| LogEntry::AnnotatedToolResult { entry, .. } => {
|
| LogEntry::AnnotatedToolResult { ts, entry } => {
|
||||||
if let Some(data) = project_item(&entry.item) {
|
if let Some(data) = project_item(&entry.item) {
|
||||||
entries.push(history_entry(entry, data));
|
entries.push(history_entry(entry, *ts, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogEntry::SystemItem { item, .. } => entries.push(system_entry(
|
LogEntry::SystemItem { ts, item } => entries.push(system_entry(
|
||||||
item,
|
item,
|
||||||
legacy_entry_id(&session_key, log_index, 0),
|
legacy_entry_id(&session_key, log_index, 0),
|
||||||
|
*ts,
|
||||||
SessionEntryProvenance::LegacyUnknown,
|
SessionEntryProvenance::LegacyUnknown,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
)),
|
)),
|
||||||
LogEntry::AnnotatedSystemItem { entry, .. } => entries.push(system_entry(
|
LogEntry::AnnotatedSystemItem { ts, entry } => entries.push(system_entry(
|
||||||
&entry.item,
|
&entry.item,
|
||||||
entry.metadata.entry_id.0.clone(),
|
entry.metadata.entry_id.0.clone(),
|
||||||
|
*ts,
|
||||||
provenance(&entry.metadata.origin),
|
provenance(&entry.metadata.origin),
|
||||||
derivation_ids(entry),
|
derivation_ids(entry),
|
||||||
)),
|
)),
|
||||||
LogEntry::RunErrored { message, .. } => entries.push(legacy_entry(
|
LogEntry::RunErrored { ts, message, .. } => entries.push(legacy_entry(
|
||||||
&session_key,
|
&session_key,
|
||||||
log_index,
|
log_index,
|
||||||
0,
|
0,
|
||||||
|
*ts,
|
||||||
SessionSnapshotEntryData::RunError {
|
SessionSnapshotEntryData::RunError {
|
||||||
message: message.clone(),
|
message: message.clone(),
|
||||||
},
|
},
|
||||||
@@ -116,6 +125,7 @@ fn extend_history(
|
|||||||
output: &mut Vec<SessionSnapshotEntry>,
|
output: &mut Vec<SessionSnapshotEntry>,
|
||||||
history: &[LoggedHistoryEntry],
|
history: &[LoggedHistoryEntry],
|
||||||
input_segments: Option<&Vec<Segment>>,
|
input_segments: Option<&Vec<Segment>>,
|
||||||
|
timestamp: u64,
|
||||||
) {
|
) {
|
||||||
let mut attached_segments = false;
|
let mut attached_segments = false;
|
||||||
for entry in history {
|
for entry in history {
|
||||||
@@ -136,16 +146,18 @@ fn extend_history(
|
|||||||
};
|
};
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
output.push(history_entry(entry, data));
|
output.push(history_entry(entry, timestamp, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn history_entry(
|
fn history_entry(
|
||||||
entry: &LoggedHistoryEntry,
|
entry: &LoggedHistoryEntry,
|
||||||
|
timestamp: u64,
|
||||||
data: SessionSnapshotEntryData,
|
data: SessionSnapshotEntryData,
|
||||||
) -> SessionSnapshotEntry {
|
) -> SessionSnapshotEntry {
|
||||||
SessionSnapshotEntry {
|
SessionSnapshotEntry {
|
||||||
entry_id: entry.metadata.entry_id.0.clone(),
|
entry_id: entry.metadata.entry_id.0.clone(),
|
||||||
|
timestamp,
|
||||||
provenance: provenance(&entry.metadata.origin),
|
provenance: provenance(&entry.metadata.origin),
|
||||||
derived_from: entry
|
derived_from: entry
|
||||||
.metadata
|
.metadata
|
||||||
@@ -182,10 +194,12 @@ fn legacy_entry(
|
|||||||
session_key: &SessionId,
|
session_key: &SessionId,
|
||||||
log_index: usize,
|
log_index: usize,
|
||||||
item_index: usize,
|
item_index: usize,
|
||||||
|
timestamp: u64,
|
||||||
data: SessionSnapshotEntryData,
|
data: SessionSnapshotEntryData,
|
||||||
) -> SessionSnapshotEntry {
|
) -> SessionSnapshotEntry {
|
||||||
SessionSnapshotEntry {
|
SessionSnapshotEntry {
|
||||||
entry_id: legacy_entry_id(session_key, log_index, item_index),
|
entry_id: legacy_entry_id(session_key, log_index, item_index),
|
||||||
|
timestamp,
|
||||||
provenance: SessionEntryProvenance::LegacyUnknown,
|
provenance: SessionEntryProvenance::LegacyUnknown,
|
||||||
derived_from: Vec::new(),
|
derived_from: Vec::new(),
|
||||||
data,
|
data,
|
||||||
@@ -283,6 +297,7 @@ fn project_item(item: &LoggedItem) -> Option<SessionSnapshotEntryData> {
|
|||||||
fn system_entry(
|
fn system_entry(
|
||||||
item: &SystemItem,
|
item: &SystemItem,
|
||||||
entry_id: String,
|
entry_id: String,
|
||||||
|
timestamp: u64,
|
||||||
provenance: SessionEntryProvenance,
|
provenance: SessionEntryProvenance,
|
||||||
derived_from: Vec<String>,
|
derived_from: Vec<String>,
|
||||||
) -> SessionSnapshotEntry {
|
) -> SessionSnapshotEntry {
|
||||||
@@ -298,6 +313,7 @@ fn system_entry(
|
|||||||
.to_owned();
|
.to_owned();
|
||||||
SessionSnapshotEntry {
|
SessionSnapshotEntry {
|
||||||
entry_id,
|
entry_id,
|
||||||
|
timestamp,
|
||||||
provenance,
|
provenance,
|
||||||
derived_from,
|
derived_from,
|
||||||
data: SessionSnapshotEntryData::SystemItem {
|
data: SessionSnapshotEntryData::SystemItem {
|
||||||
@@ -351,6 +367,7 @@ mod tests {
|
|||||||
let second = project_session_snapshot(session_id, &log);
|
let second = project_session_snapshot(session_id, &log);
|
||||||
assert_eq!(first, second);
|
assert_eq!(first, second);
|
||||||
assert_eq!(first.entries.len(), 1);
|
assert_eq!(first.entries.len(), 1);
|
||||||
|
assert_eq!(first.entries[0].timestamp, 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
first.entries[0].provenance,
|
first.entries[0].provenance,
|
||||||
SessionEntryProvenance::LegacyUnknown
|
SessionEntryProvenance::LegacyUnknown
|
||||||
|
|||||||
@@ -381,6 +381,15 @@ fn segment_log_paths(root: &Path) -> Result<Vec<(SegmentId, PathBuf)>, StoreErro
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn migrate_segment_logs_to_v3(root: &Path, session_id: SessionId) -> Result<(), StoreError> {
|
fn migrate_segment_logs_to_v3(root: &Path, session_id: SessionId) -> Result<(), StoreError> {
|
||||||
|
struct MigrationPlan {
|
||||||
|
path: PathBuf,
|
||||||
|
source: Vec<u8>,
|
||||||
|
output: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1 is strictly read-only. Every segment must parse and canonicalize
|
||||||
|
// successfully before the first authoritative byte is replaced.
|
||||||
|
let mut plans = Vec::new();
|
||||||
for (segment_id, path) in segment_log_paths(root)? {
|
for (segment_id, path) in segment_log_paths(root)? {
|
||||||
let source = fs::read(&path)?;
|
let source = fs::read(&path)?;
|
||||||
let entries: Vec<LogEntry> = parse_jsonl(&source).map_err(|error| StoreError::Corrupt {
|
let entries: Vec<LogEntry> = parse_jsonl(&source).map_err(|error| StoreError::Corrupt {
|
||||||
@@ -403,19 +412,30 @@ fn migrate_segment_logs_to_v3(root: &Path, session_id: SessionId) -> Result<(),
|
|||||||
serde_json::to_writer(&mut output, &entry)?;
|
serde_json::to_writer(&mut output, &entry)?;
|
||||||
output.push(b'\n');
|
output.push(b'\n');
|
||||||
}
|
}
|
||||||
|
plans.push(MigrationPlan {
|
||||||
|
path,
|
||||||
|
source,
|
||||||
|
output,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Opening a Session is the exclusive restore boundary, but retain an
|
// Fence the complete preflight snapshot before starting phase 2. Session
|
||||||
// unchanged-source fence so a racing writer cannot be silently lost.
|
// open is the exclusive restore boundary; this additionally fails closed
|
||||||
if fs::read(&path)? != source {
|
// if an unexpected writer raced the preflight.
|
||||||
|
for plan in &plans {
|
||||||
|
if fs::read(&plan.path)? != plan.source {
|
||||||
return Err(StoreError::Corrupt {
|
return Err(StoreError::Corrupt {
|
||||||
line: 0,
|
line: 0,
|
||||||
message: format!(
|
message: format!(
|
||||||
"Worker Session segment changed during migration: {}",
|
"Worker Session segment changed during migration: {}",
|
||||||
path.display()
|
plan.path.display()
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
atomic_write_bytes(&path, &output)?;
|
}
|
||||||
|
|
||||||
|
for plan in plans {
|
||||||
|
atomic_write_bytes(&plan.path, &plan.output)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -774,12 +794,73 @@ mod tests {
|
|||||||
&reopened.read_all(session_id, segment_id).unwrap(),
|
&reopened.read_all(session_id, segment_id).unwrap(),
|
||||||
);
|
);
|
||||||
assert_eq!(snapshot.entries.len(), 3);
|
assert_eq!(snapshot.entries.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.timestamp)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![1, 2, 3]
|
||||||
|
);
|
||||||
assert!(snapshot.entries.iter().all(|entry| {
|
assert!(snapshot.entries.iter().all(|entry| {
|
||||||
entry.provenance == protocol::SessionEntryProvenance::LegacyUnknown
|
entry.provenance == protocol::SessionEntryProvenance::LegacyUnknown
|
||||||
&& entry.entry_id.len() <= 64
|
&& entry.entry_id.len() <= 64
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schema_v2_preflight_keeps_earlier_segments_unchanged_when_later_is_corrupt() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let session_id = new_session_id();
|
||||||
|
let valid_segment = uuid::Uuid::from_u128(1);
|
||||||
|
let corrupt_segment = uuid::Uuid::from_u128(2);
|
||||||
|
fs::create_dir_all(root.path().join(SEGMENTS_DIR)).unwrap();
|
||||||
|
atomic_write_json(
|
||||||
|
&root.path().join(SESSION_FILE),
|
||||||
|
&SessionManifest {
|
||||||
|
schema_version: PREVIOUS_SESSION_SCHEMA_VERSION,
|
||||||
|
session_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let manifest_before = fs::read(root.path().join(SESSION_FILE)).unwrap();
|
||||||
|
|
||||||
|
let valid_path = root
|
||||||
|
.path()
|
||||||
|
.join(SEGMENTS_DIR)
|
||||||
|
.join(format!("{valid_segment}.jsonl"));
|
||||||
|
let valid_entry = LogEntry::SegmentStart {
|
||||||
|
ts: 1,
|
||||||
|
session_id,
|
||||||
|
system_prompt: None,
|
||||||
|
config: agen::llm_client::RequestConfig::default(),
|
||||||
|
history: vec![LoggedItem::from(agen::Item::assistant_message("prior"))],
|
||||||
|
forked_from: None,
|
||||||
|
compacted_from: None,
|
||||||
|
};
|
||||||
|
let mut valid_bytes = serde_json::to_vec(&valid_entry).unwrap();
|
||||||
|
valid_bytes.push(b'\n');
|
||||||
|
fs::write(&valid_path, &valid_bytes).unwrap();
|
||||||
|
let corrupt_path = root
|
||||||
|
.path()
|
||||||
|
.join(SEGMENTS_DIR)
|
||||||
|
.join(format!("{corrupt_segment}.jsonl"));
|
||||||
|
fs::write(&corrupt_path, b"{not-json}\n").unwrap();
|
||||||
|
let corrupt_before = fs::read(&corrupt_path).unwrap();
|
||||||
|
|
||||||
|
let error = match WorkerSessionStore::new(root.path()) {
|
||||||
|
Ok(_) => panic!("later corrupt segment must fail migration preflight"),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
assert!(matches!(error, StoreError::Corrupt { .. }));
|
||||||
|
assert_eq!(fs::read(&valid_path).unwrap(), valid_bytes);
|
||||||
|
assert_eq!(fs::read(&corrupt_path).unwrap(), corrupt_before);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(root.path().join(SESSION_FILE)).unwrap(),
|
||||||
|
manifest_before
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schema_v3_rejects_legacy_records_and_new_writes_are_canonical() {
|
fn schema_v3_rejects_legacy_records_and_new_writes_are_canonical() {
|
||||||
let root = tempfile::tempdir().unwrap();
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -4588,6 +4588,7 @@ mod tests {
|
|||||||
session: protocol::SessionSnapshot {
|
session: protocol::SessionSnapshot {
|
||||||
entries: vec![protocol::SessionSnapshotEntry {
|
entries: vec![protocol::SessionSnapshotEntry {
|
||||||
entry_id: "restored-log-entry".to_owned(),
|
entry_id: "restored-log-entry".to_owned(),
|
||||||
|
timestamp: 1,
|
||||||
provenance: protocol::SessionEntryProvenance::LegacyUnknown,
|
provenance: protocol::SessionEntryProvenance::LegacyUnknown,
|
||||||
derived_from: Vec::new(),
|
derived_from: Vec::new(),
|
||||||
data: protocol::SessionSnapshotEntryData::RunError {
|
data: protocol::SessionSnapshotEntryData::RunError {
|
||||||
|
|||||||
@@ -185,12 +185,9 @@ impl Tool for StageMemoryCandidateTool {
|
|||||||
})?);
|
})?);
|
||||||
}
|
}
|
||||||
if matches!(params.kind, CandidateKind::Preference)
|
if matches!(params.kind, CandidateKind::Preference)
|
||||||
&& entries.iter().any(|entry| {
|
&& entries
|
||||||
!matches!(
|
.iter()
|
||||||
entry.origin,
|
.any(|entry| !matches!(entry.origin, protocol::SessionEntryProvenance::HumanInput))
|
||||||
crate::WorkerHistoryProvenance::HumanInput { .. }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
return Err(ToolError::InvalidArgument(
|
return Err(ToolError::InvalidArgument(
|
||||||
"preference candidates require exclusively HumanInput evidence; model, Worker, Flow, backend, derived, and legacy-unknown origins are not preference authority"
|
"preference candidates require exclusively HumanInput evidence; model, Worker, Flow, backend, derived, and legacy-unknown origins are not preference authority"
|
||||||
@@ -324,10 +321,23 @@ fn evidence_kind(entry: &SessionEntryEvidence) -> EvidenceKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evidence_origin(origin: &crate::WorkerHistoryProvenance) -> EvidenceOrigin {
|
fn evidence_origin(origin: &protocol::SessionEntryProvenance) -> EvidenceOrigin {
|
||||||
use crate::WorkerHistoryProvenance as Origin;
|
use protocol::SessionEntryProvenance as Origin;
|
||||||
let mut evidence = EvidenceOrigin {
|
let kind = match origin {
|
||||||
kind: EvidenceOriginKind::LegacyUnknown,
|
Origin::HumanInput => EvidenceOriginKind::HumanInput,
|
||||||
|
Origin::WorkerInput => EvidenceOriginKind::WorkerInput,
|
||||||
|
Origin::FlowInstruction => EvidenceOriginKind::FlowInstruction,
|
||||||
|
Origin::BackendInstruction => EvidenceOriginKind::BackendInstruction,
|
||||||
|
Origin::ModelOutput => EvidenceOriginKind::ModelOutput,
|
||||||
|
Origin::ToolOutput => EvidenceOriginKind::ToolOutput,
|
||||||
|
Origin::DerivedSummary => EvidenceOriginKind::DerivedSummary,
|
||||||
|
Origin::LegacyUnknown => EvidenceOriginKind::LegacyUnknown,
|
||||||
|
};
|
||||||
|
EvidenceOrigin {
|
||||||
|
kind,
|
||||||
|
// The public SessionSnapshot intentionally excludes account, Worker,
|
||||||
|
// Runtime, and Flow internals. Preserve the authenticated origin class
|
||||||
|
// without inventing missing control-plane identity fields.
|
||||||
account_id: None,
|
account_id: None,
|
||||||
workspace_id: None,
|
workspace_id: None,
|
||||||
runtime_id: None,
|
runtime_id: None,
|
||||||
@@ -335,46 +345,7 @@ fn evidence_origin(origin: &crate::WorkerHistoryProvenance) -> EvidenceOrigin {
|
|||||||
flow_selector: None,
|
flow_selector: None,
|
||||||
flow_definition_id: None,
|
flow_definition_id: None,
|
||||||
flow_definition_revision: None,
|
flow_definition_revision: None,
|
||||||
};
|
|
||||||
match origin {
|
|
||||||
Origin::HumanInput { account_id } => {
|
|
||||||
evidence.kind = EvidenceOriginKind::HumanInput;
|
|
||||||
evidence.account_id = Some(account_id.clone());
|
|
||||||
}
|
|
||||||
Origin::WorkerInput { actor } => {
|
|
||||||
evidence.kind = EvidenceOriginKind::WorkerInput;
|
|
||||||
evidence.workspace_id = actor.workspace_id.clone();
|
|
||||||
evidence.runtime_id = actor.runtime_id.clone();
|
|
||||||
evidence.worker_id = Some(actor.worker_id.clone());
|
|
||||||
}
|
|
||||||
Origin::FlowInstruction {
|
|
||||||
selector,
|
|
||||||
definition_id,
|
|
||||||
definition_revision,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
evidence.kind = EvidenceOriginKind::FlowInstruction;
|
|
||||||
evidence.flow_selector = Some(selector.clone());
|
|
||||||
evidence.flow_definition_id = Some(definition_id.clone());
|
|
||||||
evidence.flow_definition_revision = Some(*definition_revision);
|
|
||||||
}
|
|
||||||
Origin::BackendInstruction { .. } => evidence.kind = EvidenceOriginKind::BackendInstruction,
|
|
||||||
Origin::ModelOutput { worker } => {
|
|
||||||
evidence.kind = EvidenceOriginKind::ModelOutput;
|
|
||||||
evidence.workspace_id = worker.workspace_id.clone();
|
|
||||||
evidence.runtime_id = worker.runtime_id.clone();
|
|
||||||
evidence.worker_id = Some(worker.worker_id.clone());
|
|
||||||
}
|
|
||||||
Origin::ToolOutput { worker } => {
|
|
||||||
evidence.kind = EvidenceOriginKind::ToolOutput;
|
|
||||||
evidence.workspace_id = worker.workspace_id.clone();
|
|
||||||
evidence.runtime_id = worker.runtime_id.clone();
|
|
||||||
evidence.worker_id = Some(worker.worker_id.clone());
|
|
||||||
}
|
|
||||||
Origin::DerivedSummary => evidence.kind = EvidenceOriginKind::DerivedSummary,
|
|
||||||
Origin::LegacyUnknown => evidence.kind = EvidenceOriginKind::LegacyUnknown,
|
|
||||||
}
|
}
|
||||||
evidence
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn staging_evidence(entry: &SessionEntryEvidence) -> StagingEvidence {
|
fn staging_evidence(entry: &SessionEntryEvidence) -> StagingEvidence {
|
||||||
@@ -502,12 +473,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn human_origin_projects_account_authority_into_evidence() {
|
fn public_human_origin_preserves_class_without_inventing_account_authority() {
|
||||||
let origin = evidence_origin(&crate::WorkerHistoryProvenance::HumanInput {
|
let origin = evidence_origin(&protocol::SessionEntryProvenance::HumanInput);
|
||||||
account_id: "account-1".into(),
|
|
||||||
});
|
|
||||||
assert_eq!(origin.kind, EvidenceOriginKind::HumanInput);
|
assert_eq!(origin.kind, EvidenceOriginKind::HumanInput);
|
||||||
assert_eq!(origin.account_id.as_deref(), Some("account-1"));
|
assert_eq!(origin.account_id, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -794,6 +794,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
Some(protocol::SessionSnapshotEntry {
|
Some(protocol::SessionSnapshotEntry {
|
||||||
entry_id: format!("fake-{index:08}"),
|
entry_id: format!("fake-{index:08}"),
|
||||||
|
timestamp: index as u64,
|
||||||
provenance: protocol::SessionEntryProvenance::LegacyUnknown,
|
provenance: protocol::SessionEntryProvenance::LegacyUnknown,
|
||||||
derived_from: Vec::new(),
|
derived_from: Vec::new(),
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ impl ToolPart {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct OverviewItem {
|
pub(crate) struct OverviewItem {
|
||||||
pub id: SessionEntryRef,
|
pub id: SessionEntryRef,
|
||||||
pub origin: WorkerHistoryProvenance,
|
pub origin: SessionEntryProvenance,
|
||||||
pub entry_range: [u64; 2],
|
pub entry_range: [u64; 2],
|
||||||
pub kind: ReferenceKind,
|
pub kind: ReferenceKind,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
@@ -120,7 +120,7 @@ pub(crate) struct OverviewItem {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ReferenceEntry {
|
pub(crate) struct ReferenceEntry {
|
||||||
pub id: SessionEntryRef,
|
pub id: SessionEntryRef,
|
||||||
pub origin: WorkerHistoryProvenance,
|
pub origin: SessionEntryProvenance,
|
||||||
pub entry_range: [u64; 2],
|
pub entry_range: [u64; 2],
|
||||||
pub kind: ReferenceKind,
|
pub kind: ReferenceKind,
|
||||||
pub tool_part: Option<ToolPart>,
|
pub tool_part: Option<ToolPart>,
|
||||||
@@ -146,7 +146,7 @@ pub(crate) struct SearchOptions {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct SearchHit {
|
pub(crate) struct SearchHit {
|
||||||
pub id: SessionEntryRef,
|
pub id: SessionEntryRef,
|
||||||
pub origin: WorkerHistoryProvenance,
|
pub origin: SessionEntryProvenance,
|
||||||
pub kind: ReferenceKind,
|
pub kind: ReferenceKind,
|
||||||
pub tool_part: Option<ToolPart>,
|
pub tool_part: Option<ToolPart>,
|
||||||
pub tool_name: Option<String>,
|
pub tool_name: Option<String>,
|
||||||
@@ -192,7 +192,7 @@ impl Default for ReadOptions {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ReadEntry {
|
pub(crate) struct ReadEntry {
|
||||||
pub id: SessionEntryRef,
|
pub id: SessionEntryRef,
|
||||||
pub origin: WorkerHistoryProvenance,
|
pub origin: SessionEntryProvenance,
|
||||||
pub kind: ReferenceKind,
|
pub kind: ReferenceKind,
|
||||||
pub tool_part: Option<ToolPart>,
|
pub tool_part: Option<ToolPart>,
|
||||||
pub tool_name: Option<String>,
|
pub tool_name: Option<String>,
|
||||||
@@ -211,7 +211,7 @@ pub(crate) struct ReadResult {
|
|||||||
pub(crate) struct SessionEntryEvidence {
|
pub(crate) struct SessionEntryEvidence {
|
||||||
pub segment_id: String,
|
pub segment_id: String,
|
||||||
pub entry_ref: SessionEntryRef,
|
pub entry_ref: SessionEntryRef,
|
||||||
pub origin: WorkerHistoryProvenance,
|
pub origin: SessionEntryProvenance,
|
||||||
pub entry_range: [u64; 2],
|
pub entry_range: [u64; 2],
|
||||||
pub kind: ReferenceKind,
|
pub kind: ReferenceKind,
|
||||||
pub tool_part: Option<ToolPart>,
|
pub tool_part: Option<ToolPart>,
|
||||||
@@ -220,10 +220,17 @@ pub(crate) struct SessionEntryEvidence {
|
|||||||
pub excerpt: String,
|
pub excerpt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct CapturedHistoryEntry {
|
||||||
|
item: Item,
|
||||||
|
entry_id: session_store::LoggedSessionHistoryEntryId,
|
||||||
|
origin: SessionEntryProvenance,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct SessionCapture {
|
pub(crate) struct SessionCapture {
|
||||||
segment_id: String,
|
segment_id: String,
|
||||||
entries: Arc<Vec<HistoryEntry<SessionHistoryMetadata>>>,
|
entries: Arc<Vec<CapturedHistoryEntry>>,
|
||||||
overview: Vec<OverviewItem>,
|
overview: Vec<OverviewItem>,
|
||||||
index: Vec<ReferenceEntry>,
|
index: Vec<ReferenceEntry>,
|
||||||
}
|
}
|
||||||
@@ -280,35 +287,49 @@ impl SessionCapture {
|
|||||||
SessionSnapshotEntryData::SystemItem { .. }
|
SessionSnapshotEntryData::SystemItem { .. }
|
||||||
| SessionSnapshotEntryData::RunError { .. } => return None,
|
| SessionSnapshotEntryData::RunError { .. } => return None,
|
||||||
};
|
};
|
||||||
Some(HistoryEntry::new(
|
Some(CapturedHistoryEntry {
|
||||||
item,
|
item,
|
||||||
public_snapshot_metadata(entry.entry_id, entry.provenance),
|
entry_id: session_store::LoggedSessionHistoryEntryId(entry.entry_id),
|
||||||
))
|
origin: entry.provenance,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Self::from_history_entries(segment_id, entries)
|
Self::from_captured_entries(segment_id, entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(segment_id: impl Into<String>, items: Vec<Item>) -> Self {
|
pub(crate) fn new(segment_id: impl Into<String>, items: Vec<Item>) -> Self {
|
||||||
let entries = items
|
let entries = items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, item)| {
|
.map(|(index, item)| CapturedHistoryEntry {
|
||||||
let mut metadata = SessionHistoryMetadata::legacy_unknown();
|
item,
|
||||||
metadata.entry_id =
|
entry_id: session_store::LoggedSessionHistoryEntryId(format!("{index:08}")),
|
||||||
session_store::LoggedSessionHistoryEntryId(format!("{index:08}"));
|
origin: SessionEntryProvenance::LegacyUnknown,
|
||||||
HistoryEntry::new(item, metadata)
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Self::from_history_entries(segment_id, entries)
|
Self::from_captured_entries(segment_id, entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_history_entries(
|
pub(crate) fn from_history_entries(
|
||||||
segment_id: impl Into<String>,
|
segment_id: impl Into<String>,
|
||||||
entries: Vec<HistoryEntry<SessionHistoryMetadata>>,
|
entries: Vec<HistoryEntry<SessionHistoryMetadata>>,
|
||||||
|
) -> Self {
|
||||||
|
let entries = entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| CapturedHistoryEntry {
|
||||||
|
item: entry.item,
|
||||||
|
entry_id: entry.annotation.entry_id,
|
||||||
|
origin: public_provenance(&entry.annotation.origin),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self::from_captured_entries(segment_id, entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_captured_entries(
|
||||||
|
segment_id: impl Into<String>,
|
||||||
|
entries: Vec<CapturedHistoryEntry>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let segment_id = segment_id.into();
|
let segment_id = segment_id.into();
|
||||||
let entries = Arc::new(entries);
|
|
||||||
let mut overview = Vec::new();
|
let mut overview = Vec::new();
|
||||||
let mut index = Vec::new();
|
let mut index = Vec::new();
|
||||||
|
|
||||||
@@ -317,7 +338,7 @@ impl SessionCapture {
|
|||||||
let entry_range = [idx as u64, idx as u64];
|
let entry_range = [idx as u64, idx as u64];
|
||||||
match item {
|
match item {
|
||||||
Item::Message { role, content, .. } => {
|
Item::Message { role, content, .. } => {
|
||||||
let Some(kind) = message_reference_kind(&entry.annotation.origin, role) else {
|
let Some(kind) = message_reference_kind(&entry.origin, role) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let text = content
|
let text = content
|
||||||
@@ -327,10 +348,10 @@ impl SessionCapture {
|
|||||||
.join("");
|
.join("");
|
||||||
let label = format!("{} message", kind.as_str());
|
let label = format!("{} message", kind.as_str());
|
||||||
let summary = truncate_chars(&text, 240);
|
let summary = truncate_chars(&text, 240);
|
||||||
let id = SessionEntryRef::from_history_entry_id(&entry.annotation.entry_id);
|
let id = SessionEntryRef::from_history_entry_id(&entry.entry_id);
|
||||||
index.push(ReferenceEntry {
|
index.push(ReferenceEntry {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
origin: entry.annotation.origin.clone(),
|
origin: entry.origin.clone(),
|
||||||
entry_range,
|
entry_range,
|
||||||
kind,
|
kind,
|
||||||
tool_part: None,
|
tool_part: None,
|
||||||
@@ -342,7 +363,7 @@ impl SessionCapture {
|
|||||||
if matches!(kind, ReferenceKind::User | ReferenceKind::Assistant) {
|
if matches!(kind, ReferenceKind::User | ReferenceKind::Assistant) {
|
||||||
overview.push(OverviewItem {
|
overview.push(OverviewItem {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
origin: entry.annotation.origin.clone(),
|
origin: entry.origin.clone(),
|
||||||
entry_range,
|
entry_range,
|
||||||
kind,
|
kind,
|
||||||
label,
|
label,
|
||||||
@@ -356,8 +377,8 @@ impl SessionCapture {
|
|||||||
} => {
|
} => {
|
||||||
let text = format!("{name}\n{arguments}");
|
let text = format!("{name}\n{arguments}");
|
||||||
index.push(ReferenceEntry {
|
index.push(ReferenceEntry {
|
||||||
id: SessionEntryRef::from_history_entry_id(&entry.annotation.entry_id),
|
id: SessionEntryRef::from_history_entry_id(&entry.entry_id),
|
||||||
origin: entry.annotation.origin.clone(),
|
origin: entry.origin.clone(),
|
||||||
entry_range,
|
entry_range,
|
||||||
kind: ReferenceKind::Tool,
|
kind: ReferenceKind::Tool,
|
||||||
tool_part: Some(ToolPart::Input),
|
tool_part: Some(ToolPart::Input),
|
||||||
@@ -383,8 +404,8 @@ impl SessionCapture {
|
|||||||
content.as_deref().unwrap_or_default(),
|
content.as_deref().unwrap_or_default(),
|
||||||
);
|
);
|
||||||
index.push(ReferenceEntry {
|
index.push(ReferenceEntry {
|
||||||
id: SessionEntryRef::from_history_entry_id(&entry.annotation.entry_id),
|
id: SessionEntryRef::from_history_entry_id(&entry.entry_id),
|
||||||
origin: entry.annotation.origin.clone(),
|
origin: entry.origin.clone(),
|
||||||
entry_range,
|
entry_range,
|
||||||
kind: ReferenceKind::Tool,
|
kind: ReferenceKind::Tool,
|
||||||
tool_part: Some(ToolPart::Output),
|
tool_part: Some(ToolPart::Output),
|
||||||
@@ -424,7 +445,7 @@ impl SessionCapture {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
segment_id,
|
segment_id,
|
||||||
entries,
|
entries: Arc::new(entries),
|
||||||
overview,
|
overview,
|
||||||
index,
|
index,
|
||||||
}
|
}
|
||||||
@@ -607,65 +628,41 @@ impl SessionCapture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn public_snapshot_metadata(
|
fn public_provenance(origin: &WorkerHistoryProvenance) -> SessionEntryProvenance {
|
||||||
entry_id: String,
|
match origin {
|
||||||
provenance: SessionEntryProvenance,
|
WorkerHistoryProvenance::HumanInput { .. } => SessionEntryProvenance::HumanInput,
|
||||||
) -> SessionHistoryMetadata {
|
WorkerHistoryProvenance::WorkerInput { .. } => SessionEntryProvenance::WorkerInput,
|
||||||
let worker = session_store::LoggedWorkerSubject {
|
WorkerHistoryProvenance::FlowInstruction { .. } => SessionEntryProvenance::FlowInstruction,
|
||||||
workspace_id: None,
|
WorkerHistoryProvenance::BackendInstruction { .. } => {
|
||||||
runtime_id: None,
|
SessionEntryProvenance::BackendInstruction
|
||||||
worker_id: "public-session-snapshot".to_owned(),
|
|
||||||
};
|
|
||||||
let origin = match provenance {
|
|
||||||
SessionEntryProvenance::HumanInput => WorkerHistoryProvenance::HumanInput {
|
|
||||||
account_id: "public-session-snapshot".to_owned(),
|
|
||||||
},
|
|
||||||
SessionEntryProvenance::WorkerInput => WorkerHistoryProvenance::WorkerInput {
|
|
||||||
actor: worker.clone(),
|
|
||||||
},
|
|
||||||
SessionEntryProvenance::FlowInstruction => WorkerHistoryProvenance::FlowInstruction {
|
|
||||||
selector: "public-session-snapshot".to_owned(),
|
|
||||||
definition_id: "public-session-snapshot".to_owned(),
|
|
||||||
definition_revision: 0,
|
|
||||||
instance_id: "public-session-snapshot".to_owned(),
|
|
||||||
state_id: "public-session-snapshot".to_owned(),
|
|
||||||
},
|
|
||||||
SessionEntryProvenance::BackendInstruction => {
|
|
||||||
WorkerHistoryProvenance::BackendInstruction { operation_id: None }
|
|
||||||
}
|
}
|
||||||
SessionEntryProvenance::ModelOutput => WorkerHistoryProvenance::ModelOutput {
|
WorkerHistoryProvenance::ModelOutput { .. } => SessionEntryProvenance::ModelOutput,
|
||||||
worker: worker.clone(),
|
WorkerHistoryProvenance::ToolOutput { .. } => SessionEntryProvenance::ToolOutput,
|
||||||
},
|
WorkerHistoryProvenance::DerivedSummary => SessionEntryProvenance::DerivedSummary,
|
||||||
SessionEntryProvenance::ToolOutput => WorkerHistoryProvenance::ToolOutput { worker },
|
WorkerHistoryProvenance::LegacyUnknown => SessionEntryProvenance::LegacyUnknown,
|
||||||
SessionEntryProvenance::DerivedSummary => WorkerHistoryProvenance::DerivedSummary,
|
|
||||||
SessionEntryProvenance::LegacyUnknown => WorkerHistoryProvenance::LegacyUnknown,
|
|
||||||
};
|
|
||||||
SessionHistoryMetadata {
|
|
||||||
entry_id: session_store::LoggedSessionHistoryEntryId(entry_id),
|
|
||||||
origin,
|
|
||||||
derivation: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message_reference_kind(
|
fn message_reference_kind(
|
||||||
origin: &WorkerHistoryProvenance,
|
origin: &SessionEntryProvenance,
|
||||||
provider_role: &Role,
|
provider_role: &Role,
|
||||||
) -> Option<ReferenceKind> {
|
) -> Option<ReferenceKind> {
|
||||||
match origin {
|
match origin {
|
||||||
WorkerHistoryProvenance::HumanInput { .. }
|
SessionEntryProvenance::HumanInput | SessionEntryProvenance::WorkerInput => {
|
||||||
| WorkerHistoryProvenance::WorkerInput { .. } => Some(ReferenceKind::User),
|
Some(ReferenceKind::User)
|
||||||
WorkerHistoryProvenance::ModelOutput { .. } => Some(ReferenceKind::Assistant),
|
}
|
||||||
WorkerHistoryProvenance::ToolOutput { .. } => Some(ReferenceKind::Tool),
|
SessionEntryProvenance::ModelOutput => Some(ReferenceKind::Assistant),
|
||||||
WorkerHistoryProvenance::LegacyUnknown => match provider_role {
|
SessionEntryProvenance::ToolOutput => Some(ReferenceKind::Tool),
|
||||||
|
SessionEntryProvenance::LegacyUnknown => match provider_role {
|
||||||
Role::User => Some(ReferenceKind::User),
|
Role::User => Some(ReferenceKind::User),
|
||||||
Role::Assistant => Some(ReferenceKind::Assistant),
|
Role::Assistant => Some(ReferenceKind::Assistant),
|
||||||
Role::System => None,
|
Role::System => None,
|
||||||
},
|
},
|
||||||
// Flow/backend/system content remains out of the observation surface
|
// Flow/backend/system content remains out of the observation surface
|
||||||
// even when represented with a provider user/system role.
|
// even when represented with a provider user/system role.
|
||||||
WorkerHistoryProvenance::FlowInstruction { .. }
|
SessionEntryProvenance::FlowInstruction
|
||||||
| WorkerHistoryProvenance::BackendInstruction { .. }
|
| SessionEntryProvenance::BackendInstruction
|
||||||
| WorkerHistoryProvenance::DerivedSummary => None,
|
| SessionEntryProvenance::DerivedSummary => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,13 +759,13 @@ mod tests {
|
|||||||
assert_eq!(overview.len(), 1);
|
assert_eq!(overview.len(), 1);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
overview[0].origin,
|
overview[0].origin,
|
||||||
WorkerHistoryProvenance::HumanInput { .. }
|
SessionEntryProvenance::HumanInput
|
||||||
));
|
));
|
||||||
let evidence = capture.evidence_for(overview[0].id.as_str()).unwrap();
|
let evidence = capture.evidence_for(overview[0].id.as_str()).unwrap();
|
||||||
assert!(evidence.excerpt.ends_with("remember my preference"));
|
assert!(evidence.excerpt.ends_with("remember my preference"));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
evidence.origin,
|
evidence.origin,
|
||||||
WorkerHistoryProvenance::HumanInput { .. }
|
SessionEntryProvenance::HumanInput
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ mod tests {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, value)| protocol::SessionSnapshotEntry {
|
.map(|(index, value)| protocol::SessionSnapshotEntry {
|
||||||
entry_id: format!("test-{index}"),
|
entry_id: format!("test-{index}"),
|
||||||
|
timestamp: index as u64,
|
||||||
provenance: protocol::SessionEntryProvenance::LegacyUnknown,
|
provenance: protocol::SessionEntryProvenance::LegacyUnknown,
|
||||||
derived_from: Vec::new(),
|
derived_from: Vec::new(),
|
||||||
data: protocol::SessionSnapshotEntryData::RunError {
|
data: protocol::SessionSnapshotEntryData::RunError {
|
||||||
|
|||||||
@@ -93,7 +93,11 @@ export type SessionSnapshotEntry = {
|
|||||||
* Stable identity from durable history metadata, or a deterministic
|
* Stable identity from durable history metadata, or a deterministic
|
||||||
* identity derived from the legacy segment and log position.
|
* identity derived from the legacy segment and log position.
|
||||||
*/
|
*/
|
||||||
entry_id: string, provenance: SessionEntryProvenance, derived_from?: Array<string>, } & ({ "kind": "user_input", segments: Array<Segment>, } | { "kind": "message", role: SessionMessageRole, content: Array<SessionContentPart>, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array<SessionToolAttachment>, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_error", message: string, });
|
entry_id: string,
|
||||||
|
/**
|
||||||
|
* Timestamp copied from the durable log record that commits this entry.
|
||||||
|
*/
|
||||||
|
timestamp: number, provenance: SessionEntryProvenance, derived_from?: Array<string>, } & ({ "kind": "user_input", segments: Array<Segment>, } | { "kind": "message", role: SessionMessageRole, content: Array<SessionContentPart>, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array<SessionToolAttachment>, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_error", message: string, });
|
||||||
|
|
||||||
export type SessionSnapshot = { entries: Array<SessionSnapshotEntry>, };
|
export type SessionSnapshot = { entries: Array<SessionSnapshotEntry>, };
|
||||||
|
|
||||||
|
|||||||
@@ -2108,6 +2108,7 @@ Deno.test("snapshot restores TaskStore state from system history", () => {
|
|||||||
event.data.session = {
|
event.data.session = {
|
||||||
entries: [{
|
entries: [{
|
||||||
entry_id: "task-reminder-1",
|
entry_id: "task-reminder-1",
|
||||||
|
timestamp: 1,
|
||||||
provenance: "backend_instruction",
|
provenance: "backend_instruction",
|
||||||
kind: "system_item",
|
kind: "system_item",
|
||||||
item_kind: "task_reminder",
|
item_kind: "task_reminder",
|
||||||
|
|||||||
Reference in New Issue
Block a user