first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+223 -119
View File
@@ -5,7 +5,8 @@ use std::collections::{HashMap, HashSet};
use chrono::NaiveDateTime;
use diesel::prelude::*;
use serde::{Deserialize, Deserializer, Serialize};
use warp_multi_agent_api::{self as api, response_event::stream_finished};
use warp_multi_agent_api::response_event::stream_finished;
use warp_multi_agent_api::{self as api};
use super::schema::{
active_mcp_servers, agent_conversations, agent_tasks, ai_document_panes, ai_memory_panes,
@@ -14,8 +15,8 @@ use super::schema::{
generic_string_objects, ignored_suggestions, mcp_environment_variables,
mcp_server_installations, mcp_server_panes, notebook_panes, notebooks, object_actions,
object_metadata, object_permissions, pane_branches, pane_leaves, pane_nodes, panels,
project_rules, projects, server_experiments, settings_panes, tabs, team_members, team_settings,
teams, terminal_panes, user_profiles, welcome_panes, windows, workflow_panes, workflows,
project_rules, projects, server_experiments, settings_panes, tab_groups, tabs, team_members,
team_settings, teams, terminal_panes, user_profiles, windows, workflow_panes, workflows,
workspace_language_server, workspace_metadata, workspace_teams, workspaces,
};
@@ -348,6 +349,8 @@ pub struct Tab {
pub window_id: i32,
pub custom_title: Option<String>,
pub color: Option<String>,
pub tab_group_id: Option<i32>,
pub pinned: bool,
}
#[derive(Insertable)]
@@ -356,6 +359,32 @@ pub struct NewTab {
pub window_id: i32,
pub custom_title: Option<String>,
pub color: Option<String>,
pub tab_group_id: Option<i32>,
pub pinned: bool,
}
/// Persisted form of a tab group. `name` is optional — untitled groups omit
/// it and the UI falls back to a default label.
#[derive(Identifiable, Queryable, Associations)]
#[diesel(belongs_to(Window))]
#[diesel(table_name = tab_groups)]
pub struct TabGroup {
pub id: i32,
pub window_id: i32,
pub name: Option<String>,
pub color: Option<String>,
pub collapsed: bool,
pub pinned: bool,
}
#[derive(Insertable)]
#[diesel(table_name = tab_groups)]
pub struct NewTabGroup {
pub window_id: i32,
pub name: Option<String>,
pub color: Option<String>,
pub collapsed: bool,
pub pinned: bool,
}
/// The panes data model includes pane_nodes, pane_leaves and pane_branches.
@@ -466,15 +495,6 @@ pub struct SettingsPane {
pub current_page: String,
}
#[derive(Identifiable, Queryable, Selectable)]
#[diesel(table_name = welcome_panes)]
#[diesel(primary_key(id))]
pub struct WelcomePane {
pub id: i32,
pub kind: String,
pub startup_directory: Option<String>,
}
/// Maps to the `ai_memory_panes` table
/// (where table name is historical and not worth a migration to change).
#[derive(Identifiable, Queryable, Selectable)]
@@ -557,9 +577,6 @@ pub const CODE_REVIEW_PANE_KIND: &str = "code_review";
/// The [`pane_leaves::kind`] value for execution profile editor panes.
pub const EXECUTION_PROFILE_EDITOR_PANE_KIND: &str = "execution_profile_editor";
/// The [`pane_leaves::kind`] value for the welcome pane.
pub const WELCOME_PANE_KIND: &str = "welcome";
/// The [`pane_leaves::kind`] value for the get-started pane.
pub const GET_STARTED_PANE_KIND: &str = "get_started";
@@ -653,13 +670,6 @@ pub struct NewMCPServerPane {
pub id: i32,
}
#[derive(Insertable)]
#[diesel(table_name = welcome_panes)]
pub struct NewWelcomePane {
pub id: i32,
pub startup_directory: Option<String>,
}
#[derive(Identifiable, Queryable, Selectable)]
#[diesel(table_name = ambient_agent_panes)]
#[diesel(primary_key(id))]
@@ -700,7 +710,7 @@ pub struct NewBlock<'a> {
pub block_id: &'a str,
// Note that there is no pane leaf UUID foreign key relationship because there's no good way to
// enforce it: when we remove a pane and subsequently create a new snapshot, the old blocks
// will now violate the constaint. While sqlite does have deferred constraints, it doesn't
// will now violate the constraint. While sqlite does have deferred constraints, it doesn't
// work well with ON DELETE CASCADE (i.e. the cascade happens on the delete, not after the
// transaction commit).
pub pane_leaf_uuid: Vec<u8>,
@@ -942,13 +952,24 @@ impl AgentConversation {
///
/// A conversation is restorable if:
/// - It contains a single task or fewer, OR
/// - It contains multiple tasks where every task other than the root task has a parent task ID.
/// - It has exactly one parentless (root) task, OR
/// - It has multiple parentless tasks but exactly one of them has
/// non-empty `messages`. This permits restoring conversations whose
/// persisted state was corrupted by the pre-QUALITY-774 optimistic-root
/// writer bug, where a stub root row co-existed with the real server
/// root row. `AIConversation::new_restored` deterministically picks
/// the real root in that shape via its restore-side dedupe.
///
/// Non-root tasks need not be validated here: any task that does not
/// match the parentless predicate has, by construction, a non-empty
/// `parent_task_id`.
pub fn is_restorable(&self) -> bool {
if self.tasks.len() <= 1 {
return true;
}
// Find the root task(s) - tasks with no parent_task_id or empty parent_task_id
// Find parentless (root) tasks - tasks with no dependencies or with an
// empty parent_task_id.
let root_tasks: Vec<_> = self
.tasks
.iter()
@@ -960,28 +981,18 @@ impl AgentConversation {
})
.collect();
// Must have exactly one root task
if root_tasks.len() != 1 {
return false;
match root_tasks.len() {
// Malformed: no parentless task means no root to anchor restore on.
0 => false,
// Single root: the normal happy path.
1 => true,
// Multi-root: only permit the specific [stub + real] shape
// produced by the pre-QUALITY-774 optimistic-root writer bug,
// where exactly one parentless row carries the real conversation
// content. The restore-side dedupe in
// `AIConversation::new_restored` will pick that real root.
_ => root_tasks.iter().filter(|t| !t.messages.is_empty()).count() == 1,
}
// All non-root tasks must have a non-empty parent_task_id
self.tasks.iter().all(|task| {
// Root task is always valid
if task
.dependencies
.as_ref()
.map(|deps| deps.parent_task_id.is_empty())
.unwrap_or(true)
{
return true;
}
// Non-root tasks must have a non-empty parent_task_id
task.dependencies
.as_ref()
.is_some_and(|deps| !deps.parent_task_id.is_empty())
})
}
}
@@ -1005,6 +1016,10 @@ impl<'de> Deserialize<'de> for PersistedAutoexecuteMode {
})
}
}
fn is_false(value: &bool) -> bool {
!*value
}
// Serializes to `conversation_data` column in `agent_conversations`.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AgentConversationData {
@@ -1026,9 +1041,29 @@ pub struct AgentConversationData {
/// The display name for this agent, assigned by the orchestrator.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_name: Option<String>,
/// Harness type used to render the child agent's shared icon in orchestration UI.
#[serde(
default,
alias = "orchestration_avatar_id",
skip_serializing_if = "Option::is_none"
)]
pub orchestration_harness_type: Option<String>,
/// The local conversation ID of the parent conversation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_conversation_id: Option<String>,
/// True when this conversation is a parent-side placeholder for a child
/// agent executing on a remote worker.
#[serde(default, skip_serializing_if = "is_false")]
pub is_remote_child: bool,
/// Legacy marker that previously recorded whether the root task was still
/// optimistic when this conversation was persisted. Retained on the struct
/// for backward-compatible deserialization of rows written by older builds;
/// new writes always emit `None` and restore code ignores the value.
///
// TODO: Remove this field once no live local DBs still contain
// `Some(true)` rows that legacy code paths might trip over.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root_task_is_optimistic: Option<bool>,
/// The server-assigned run identifier (`ai_tasks.id`) for v2 orchestration.
/// For local agents this arrives via StreamInit; for cloud agents it will
/// come from SpawnAgentResponse once the local→cloud spawn path is wired.
@@ -1041,12 +1076,10 @@ pub struct AgentConversationData {
/// delivery without re-delivering already-processed events.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_event_sequence: Option<i64>,
/// Progressive summary of older conversation messages.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progressive_summary: Option<String>,
/// Number of messages that were summarized into progressive_summary.
#[serde(default)]
pub messages_summarized_up_to: usize,
/// Whether the user has pinned this child agent in the orchestration
/// pill bar. Orchestrator conversations always serialize as `false`.
#[serde(default, skip_serializing_if = "is_false")]
pub pinned: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -1073,6 +1106,10 @@ pub fn token_usage_category_display_name(category: &str) -> String {
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ModelTokenUsage {
/// Identifier used for both display and replay. For warp/byok rows this is the
/// server-known model id; for custom endpoint rows this is the resolved alias
/// (or fallback label) — the upstream `config_key` is translated into this
/// label once at ingestion time and is not retained separately.
pub model_id: String,
/// Alias for backward compat: old persisted data used `total_tokens` for warp usage.
#[serde(default, alias = "total_tokens")]
@@ -1080,9 +1117,13 @@ pub struct ModelTokenUsage {
#[serde(default)]
pub byok_tokens: u32,
#[serde(default)]
pub custom_endpoint_tokens: u32,
#[serde(default)]
pub warp_token_usage_by_category: HashMap<TokenUsageCategory, u32>,
#[serde(default)]
pub byok_token_usage_by_category: HashMap<TokenUsageCategory, u32>,
#[serde(default)]
pub custom_endpoint_token_usage_by_category: HashMap<TokenUsageCategory, u32>,
}
impl ModelTokenUsage {
@@ -1115,16 +1156,37 @@ impl ModelTokenUsage {
pub fn to_proto_byok_usage(&self) -> Option<(String, stream_finished::ModelTokenUsage)> {
self.to_proto_usage(self.byok_tokens, &self.byok_token_usage_by_category)
}
#[allow(deprecated)]
pub fn to_proto_custom_endpoint_usage(
&self,
) -> Option<(String, stream_finished::ModelTokenUsage)> {
if self.custom_endpoint_tokens == 0 {
return None;
}
Some((
self.model_id.clone(),
stream_finished::ModelTokenUsage {
model_id: self.model_id.clone(),
total_tokens: self.custom_endpoint_tokens,
token_usage_by_category: self
.custom_endpoint_token_usage_by_category
.iter()
.map(|(cat, tokens)| (cat.clone(), *tokens))
.collect(),
},
))
}
#[allow(deprecated)]
pub fn to_proto_combined(&self) -> stream_finished::ModelTokenUsage {
stream_finished::ModelTokenUsage {
model_id: self.model_id.clone(),
total_tokens: self.warp_tokens + self.byok_tokens,
total_tokens: self.warp_tokens + self.byok_tokens + self.custom_endpoint_tokens,
token_usage_by_category: self
.warp_token_usage_by_category
.iter()
.chain(self.byok_token_usage_by_category.iter())
.chain(self.custom_endpoint_token_usage_by_category.iter())
.fold(HashMap::new(), |mut acc, (cat, tokens)| {
*acc.entry(cat.clone()).or_insert(0) += tokens;
acc
@@ -1299,25 +1361,124 @@ impl From<&stream_finished::ToolUsageMetadata> for ToolUsageMetadata {
}
}
/// The kind of a context-window segment, mirroring the proto
/// `ContextWindowSegmentType` enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ContextWindowSegmentType {
#[default]
Unknown,
SystemPrompt,
ToolDefinitions,
ConversationHistory,
LatestInput,
Images,
Other,
}
impl ContextWindowSegmentType {
/// Snake-case identifier used for display-name lookup.
pub fn as_str(&self) -> &'static str {
match self {
ContextWindowSegmentType::Unknown => "unknown",
ContextWindowSegmentType::SystemPrompt => "system_prompt",
ContextWindowSegmentType::ToolDefinitions => "tool_definitions",
ContextWindowSegmentType::ConversationHistory => "conversation_history",
ContextWindowSegmentType::LatestInput => "latest_input",
ContextWindowSegmentType::Images => "images",
ContextWindowSegmentType::Other => "other",
}
}
}
impl From<i32> for ContextWindowSegmentType {
fn from(value: i32) -> Self {
match stream_finished::ContextWindowSegmentType::try_from(value) {
Ok(stream_finished::ContextWindowSegmentType::SystemPrompt) => Self::SystemPrompt,
Ok(stream_finished::ContextWindowSegmentType::ToolDefinitions) => Self::ToolDefinitions,
Ok(stream_finished::ContextWindowSegmentType::ConversationHistory) => {
Self::ConversationHistory
}
Ok(stream_finished::ContextWindowSegmentType::LatestInput) => Self::LatestInput,
Ok(stream_finished::ContextWindowSegmentType::Images) => Self::Images,
Ok(stream_finished::ContextWindowSegmentType::Other) => Self::Other,
// Unknown (0) and any unrecognized value map to Unknown.
_ => Self::Unknown,
}
}
}
impl From<ContextWindowSegmentType> for i32 {
fn from(value: ContextWindowSegmentType) -> Self {
match value {
ContextWindowSegmentType::Unknown => {
stream_finished::ContextWindowSegmentType::Unknown as i32
}
ContextWindowSegmentType::SystemPrompt => {
stream_finished::ContextWindowSegmentType::SystemPrompt as i32
}
ContextWindowSegmentType::ToolDefinitions => {
stream_finished::ContextWindowSegmentType::ToolDefinitions as i32
}
ContextWindowSegmentType::ConversationHistory => {
stream_finished::ContextWindowSegmentType::ConversationHistory as i32
}
ContextWindowSegmentType::LatestInput => {
stream_finished::ContextWindowSegmentType::LatestInput as i32
}
ContextWindowSegmentType::Images => {
stream_finished::ContextWindowSegmentType::Images as i32
}
ContextWindowSegmentType::Other => {
stream_finished::ContextWindowSegmentType::Other as i32
}
}
}
}
/// A single portion of the context window, described by its kind and an
/// estimated token count. Segment token counts add up to the token total
/// represented by `context_window_usage`.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ContextWindowSegment {
pub segment_type: ContextWindowSegmentType,
/// Estimated number of tokens this segment occupies in the context window.
pub token_count: u32,
}
impl From<&stream_finished::ContextWindowSegment> for ContextWindowSegment {
fn from(segment: &stream_finished::ContextWindowSegment) -> Self {
Self {
segment_type: segment.segment_type.into(),
token_count: segment.token_count,
}
}
}
impl From<&ContextWindowSegment> for stream_finished::ContextWindowSegment {
fn from(segment: &ContextWindowSegment) -> Self {
Self {
segment_type: segment.segment_type.into(),
token_count: segment.token_count,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ConversationUsageMetadata {
pub was_summarized: bool,
pub context_window_usage: f32,
pub credits_spent: f32,
#[serde(default)]
pub platform_credits_spent: f32,
#[serde(default)]
pub credits_spent_for_last_block: Option<f32>,
#[serde(default)]
pub token_usage: Vec<ModelTokenUsage>,
#[serde(default)]
pub tool_usage_metadata: ToolUsageMetadata,
#[serde(default)]
pub total_cache_read_tokens: u32,
#[serde(default)]
pub total_cache_write_tokens: u32,
#[serde(default)]
pub total_cache_miss_tokens: u32,
#[serde(default)]
pub total_cost_cents: f32,
pub context_window_segments: Vec<ContextWindowSegment>,
}
impl ConversationUsageMetadata {
@@ -1347,65 +1508,8 @@ pub struct NewMCPServerInstallation {
}
#[cfg(test)]
mod tests {
use super::AgentConversationData;
#[test]
fn agent_conversation_data_roundtrips_last_event_sequence() {
let data = AgentConversationData {
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids: None,
forked_from_server_conversation_token: None,
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
parent_conversation_id: None,
run_id: None,
autoexecute_override: None,
last_event_sequence: Some(42),
progressive_summary: None,
messages_summarized_up_to: 0,
};
let json = serde_json::to_string(&data).expect("serialize");
let roundtripped: AgentConversationData = serde_json::from_str(&json).expect("deserialize");
assert_eq!(roundtripped.last_event_sequence, Some(42));
}
#[test]
fn agent_conversation_data_deserializes_legacy_payload_without_last_event_sequence() {
// Legacy rows persisted before this feature landed omit the field
// entirely. `#[serde(default)]` must accept them as `None`.
let legacy_json = r#"{"server_conversation_token":null}"#;
let data: AgentConversationData =
serde_json::from_str(legacy_json).expect("legacy rows must deserialize");
assert_eq!(data.last_event_sequence, None);
}
#[test]
fn agent_conversation_data_skips_serializing_none_last_event_sequence() {
let data = AgentConversationData {
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids: None,
forked_from_server_conversation_token: None,
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
parent_conversation_id: None,
run_id: None,
autoexecute_override: None,
last_event_sequence: None,
progressive_summary: None,
messages_summarized_up_to: 0,
};
let json = serde_json::to_string(&data).expect("serialize");
assert!(
!json.contains("last_event_sequence"),
"None should be skipped in serialized output: {json}"
);
}
}
#[path = "model_tests.rs"]
mod tests;
#[derive(Insertable)]
#[diesel(table_name = panels)]
+315
View File
@@ -0,0 +1,315 @@
use std::collections::HashMap;
use warp_multi_agent_api as api;
use super::{AgentConversation, AgentConversationData, ModelTokenUsage};
fn parentless_task(id: &str, message_count: usize) -> api::Task {
api::Task {
id: id.to_string(),
description: String::new(),
dependencies: None,
messages: (0..message_count)
.map(|i| api::Message {
fetched_memories: vec![],
id: format!("{id}-msg-{i}"),
task_id: id.to_string(),
server_message_data: String::new(),
citations: vec![],
message: None,
request_id: String::new(),
timestamp: None,
})
.collect(),
summary: String::new(),
server_data: String::new(),
}
}
fn child_task(id: &str, parent_id: &str) -> api::Task {
api::Task {
id: id.to_string(),
description: String::new(),
dependencies: Some(api::task::Dependencies {
parent_task_id: parent_id.to_string(),
}),
messages: vec![],
summary: String::new(),
server_data: String::new(),
}
}
fn conversation_with_tasks(tasks: Vec<api::Task>) -> AgentConversation {
AgentConversation {
conversation: Default::default(),
tasks,
}
}
/// Legacy [stub + real] root shape produced by the pre-QUALITY-774
/// optimistic-root writer bug must be considered restorable so the
/// restore-side dedupe in `AIConversation::new_restored` can pick the
/// real root.
#[test]
fn is_restorable_accepts_legacy_stub_plus_real_root_shape() {
let conversation = conversation_with_tasks(vec![
parentless_task("optimistic-stub-uuid", 0),
parentless_task("server-root-id", 2),
child_task("child-1", "server-root-id"),
]);
assert!(conversation.is_restorable());
}
/// Multi-root with multiple real roots (each non-empty) is genuinely
/// ambiguous and must remain rejected — the dedupe heuristic cannot
/// disambiguate between two real roots.
#[test]
fn is_restorable_rejects_multi_root_with_multiple_real_roots() {
let conversation = conversation_with_tasks(vec![
parentless_task("root-a", 1),
parentless_task("root-b", 1),
]);
assert!(!conversation.is_restorable());
}
/// Multi-root where every candidate is empty has nothing to anchor
/// restore on and must remain rejected.
#[test]
fn is_restorable_rejects_multi_root_with_no_real_root() {
let conversation = conversation_with_tasks(vec![
parentless_task("stub-1", 0),
parentless_task("stub-2", 0),
]);
assert!(!conversation.is_restorable());
}
/// Normal happy path: a single parentless root plus well-formed child
/// tasks remains restorable.
#[test]
fn is_restorable_accepts_single_root_plus_subtasks() {
let conversation = conversation_with_tasks(vec![
parentless_task("root", 1),
child_task("child-1", "root"),
child_task("child-2", "root"),
]);
assert!(conversation.is_restorable());
}
/// Empty or single-task conversations are trivially restorable.
#[test]
fn is_restorable_accepts_empty_and_single_task_conversations() {
assert!(conversation_with_tasks(vec![]).is_restorable());
assert!(conversation_with_tasks(vec![parentless_task("root", 0)]).is_restorable());
}
#[test]
fn agent_conversation_data_roundtrips_last_event_sequence() {
let data = AgentConversationData {
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids: None,
forked_from_server_conversation_token: None,
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: Some("claude".to_string()),
parent_conversation_id: None,
is_remote_child: false,
root_task_is_optimistic: None,
run_id: None,
autoexecute_override: None,
last_event_sequence: Some(42),
pinned: false,
};
let json = serde_json::to_string(&data).expect("serialize");
let roundtripped: AgentConversationData = serde_json::from_str(&json).expect("deserialize");
assert_eq!(roundtripped.last_event_sequence, Some(42));
assert_eq!(
roundtripped.orchestration_harness_type.as_deref(),
Some("claude")
);
}
#[test]
fn agent_conversation_data_accepts_legacy_orchestration_avatar_id() {
let legacy_json = r#"{"orchestration_avatar_id":"orbit"}"#;
let data: AgentConversationData =
serde_json::from_str(legacy_json).expect("legacy rows must deserialize");
assert_eq!(data.orchestration_harness_type.as_deref(), Some("orbit"));
}
#[test]
fn agent_conversation_data_roundtrips_remote_child_marker() {
let data = AgentConversationData {
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids: None,
forked_from_server_conversation_token: None,
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: true,
root_task_is_optimistic: None,
run_id: None,
autoexecute_override: None,
last_event_sequence: None,
pinned: false,
};
let json = serde_json::to_string(&data).expect("serialize");
let roundtripped: AgentConversationData = serde_json::from_str(&json).expect("deserialize");
assert!(roundtripped.is_remote_child);
}
#[test]
fn agent_conversation_data_roundtrips_optimistic_root_marker() {
let data = AgentConversationData {
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids: None,
forked_from_server_conversation_token: None,
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
root_task_is_optimistic: Some(true),
run_id: None,
autoexecute_override: None,
last_event_sequence: None,
pinned: false,
};
let json = serde_json::to_string(&data).expect("serialize");
let roundtripped: AgentConversationData = serde_json::from_str(&json).expect("deserialize");
assert_eq!(roundtripped.root_task_is_optimistic, Some(true));
}
#[test]
fn agent_conversation_data_deserializes_legacy_payload_without_last_event_sequence() {
// Legacy rows persisted before this feature landed omit the field
// entirely. `#[serde(default)]` must accept them as `None`.
let legacy_json = r#"{"server_conversation_token":null}"#;
let data: AgentConversationData =
serde_json::from_str(legacy_json).expect("legacy rows must deserialize");
assert_eq!(data.last_event_sequence, None);
assert_eq!(data.orchestration_harness_type, None);
assert!(!data.is_remote_child);
}
#[test]
fn agent_conversation_data_skips_serializing_none_last_event_sequence() {
let data = AgentConversationData {
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids: None,
forked_from_server_conversation_token: None,
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
root_task_is_optimistic: None,
run_id: None,
autoexecute_override: None,
last_event_sequence: None,
pinned: false,
};
let json = serde_json::to_string(&data).expect("serialize");
assert!(
!json.contains("last_event_sequence"),
"None should be skipped in serialized output: {json}"
);
}
#[test]
fn agent_conversation_data_roundtrips_pinned() {
let data = AgentConversationData {
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids: None,
forked_from_server_conversation_token: None,
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
root_task_is_optimistic: None,
run_id: None,
autoexecute_override: None,
last_event_sequence: None,
pinned: true,
};
let json = serde_json::to_string(&data).expect("serialize");
let roundtripped: AgentConversationData = serde_json::from_str(&json).expect("deserialize");
assert!(roundtripped.pinned);
}
#[test]
fn agent_conversation_data_skips_serializing_unpinned() {
let data = AgentConversationData {
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids: None,
forked_from_server_conversation_token: None,
artifacts_json: None,
parent_agent_id: None,
agent_name: None,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
root_task_is_optimistic: None,
run_id: None,
autoexecute_override: None,
last_event_sequence: None,
pinned: false,
};
let json = serde_json::to_string(&data).expect("serialize");
assert!(
!json.contains("pinned"),
"Unpinned default should be skipped: {json}"
);
}
#[test]
fn agent_conversation_data_legacy_rows_default_to_unpinned() {
let legacy_json = r#"{"server_conversation_token":null}"#;
let data: AgentConversationData =
serde_json::from_str(legacy_json).expect("legacy rows must deserialize");
assert!(!data.pinned);
}
#[allow(deprecated)]
#[test]
fn model_token_usage_replays_custom_endpoint_usage_by_model_id() {
let usage = ModelTokenUsage {
model_id: "Friendly alias".to_string(),
custom_endpoint_tokens: 6,
custom_endpoint_token_usage_by_category: HashMap::from([("primary_agent".to_string(), 6)]),
..Default::default()
};
let (key, proto) = usage
.to_proto_custom_endpoint_usage()
.expect("custom endpoint usage should serialize for replay");
assert_eq!(key, "Friendly alias");
assert_eq!(proto.model_id, "Friendly alias");
assert_eq!(proto.total_tokens, 6);
assert_eq!(proto.token_usage_by_category.get("primary_agent"), Some(&6));
}
#[allow(deprecated)]
#[test]
fn model_token_usage_replay_skips_non_custom_endpoint_entries() {
let warp_only = ModelTokenUsage {
model_id: "warp-model".to_string(),
warp_tokens: 4,
..Default::default()
};
assert!(warp_only.to_proto_custom_endpoint_usage().is_none());
}
+16 -8
View File
@@ -354,12 +354,25 @@ diesel::table! {
}
}
diesel::table! {
tab_groups (id) {
id -> Integer,
window_id -> Integer,
name -> Nullable<Text>,
color -> Nullable<Text>,
collapsed -> Bool,
pinned -> Bool,
}
}
diesel::table! {
tabs (id) {
id -> Integer,
window_id -> Integer,
custom_title -> Nullable<Text>,
color -> Nullable<Text>,
tab_group_id -> Nullable<Integer>,
pinned -> Bool,
}
}
@@ -422,14 +435,6 @@ diesel::table! {
}
}
diesel::table! {
welcome_panes (id) {
id -> Integer,
kind -> Text,
startup_directory -> Nullable<Text>,
}
}
diesel::table! {
windows (id) {
id -> Integer,
@@ -509,6 +514,8 @@ diesel::joinable!(pane_branches -> pane_nodes (pane_node_id));
diesel::joinable!(pane_leaves -> pane_nodes (pane_node_id));
diesel::joinable!(pane_nodes -> tabs (tab_id));
diesel::joinable!(panels -> tabs (tab_id));
diesel::joinable!(tab_groups -> windows (window_id));
diesel::joinable!(tabs -> tab_groups (tab_group_id));
diesel::joinable!(tabs -> windows (window_id));
diesel::joinable!(team_members -> teams (team_id));
diesel::joinable!(team_settings -> teams (team_id));
@@ -521,6 +528,7 @@ diesel::allow_tables_to_appear_in_same_query!(
pane_leaves,
pane_nodes,
panels,
tab_groups,
tabs,
windows,
);