Skip to content

Commit 7ec3cae

Browse files
authored
Merge pull request #1168 from org2AI/chloe/modularize-org2-root
refactor(org2): split the crate root and agent-session modules
2 parents 8358521 + 3593a7e commit 7ec3cae

31 files changed

Lines changed: 4595 additions & 4177 deletions

src-tauri/src/agent_sessions/cli/persistence/session_crud.rs

Lines changed: 37 additions & 1126 deletions
Large diffs are not rendered by default.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! Insert path for new CLI code-session rows, including the wire-typo
2+
//! guards and the frozen transcript-source decision.
3+
4+
use rusqlite::{params, Result as SqliteResult};
5+
6+
use agent_core::session::AgentExecMode;
7+
use database::db::get_connection;
8+
9+
use crate::agent_sessions::cli::native_transcript;
10+
use crate::agent_sessions::cli::persistence::types::{CodeSession, CreateCodeSessionParams};
11+
use crate::agent_sessions::cli::types::{
12+
session_defaults, KeySource, SessionRunner, SessionStatus, DEFAULT_CODE_SESSION_FLOW,
13+
PERSONAL_ORG_ID,
14+
};
15+
16+
use super::read::get_session;
17+
use super::shared::{now_iso, sync_orgtrack_mirror};
18+
19+
/// Create a new code session. Returns the session ID.
20+
pub fn create_session(
21+
session_id: &str,
22+
params: &CreateCodeSessionParams,
23+
) -> SqliteResult<CodeSession> {
24+
let conn = get_connection()?;
25+
let ts = now_iso();
26+
let name = params
27+
.name
28+
.clone()
29+
.unwrap_or_else(|| session_defaults::CODE_SESSION_NAME.to_string());
30+
let flow = params
31+
.flow
32+
.clone()
33+
.unwrap_or_else(|| DEFAULT_CODE_SESSION_FLOW.to_string());
34+
// Wire-typo guard: `runner` is read back via `SessionRunner::parse`
35+
// (typed enum) at every read site. If the caller passes a typo'd
36+
// string here, the row would be persisted as garbage and every
37+
// subsequent `row_to_session` would reject it as a
38+
// `FromSqlConversionFailure` — i.e. the session would be created
39+
// but unloadable. Reject at the entry point instead.
40+
let runner = match params.runner.as_deref().filter(|s| !s.is_empty()) {
41+
Some(raw) => SessionRunner::parse(raw)
42+
.ok_or_else(|| {
43+
rusqlite::Error::ToSqlConversionFailure(
44+
format!("unknown SessionRunner value: {raw:?}").into(),
45+
)
46+
})?
47+
.to_string(),
48+
None => SessionRunner::Local.to_string(),
49+
};
50+
51+
let background = params.background.unwrap_or(false);
52+
53+
// Wire-typo guard for `key_source` — same reasoning as `runner`.
54+
// `row_to_session` will fail-closed on an unknown column value, so
55+
// accepting an unvalidated string here would create an unloadable
56+
// session row (the frontend would see a created session that can
57+
// never be opened). Validate at the write boundary.
58+
let key_source_str = match params.key_source.as_deref().filter(|s| !s.is_empty()) {
59+
Some(raw) => KeySource::parse(raw)
60+
.ok_or_else(|| {
61+
rusqlite::Error::ToSqlConversionFailure(
62+
format!("unknown KeySource value: {raw:?}").into(),
63+
)
64+
})?
65+
.to_string(),
66+
None => KeySource::default().to_string(),
67+
};
68+
69+
let org_id = params
70+
.org_id
71+
.clone()
72+
.filter(|value| !value.trim().is_empty())
73+
.unwrap_or_else(|| PERSONAL_ORG_ID.to_string());
74+
75+
let additional_dirs_json: Option<String> = params
76+
.additional_directories
77+
.as_ref()
78+
.filter(|v| !v.is_empty())
79+
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "[]".to_string()));
80+
let product_mode = if params.work_item_id.is_some() {
81+
"project".to_string()
82+
} else {
83+
params
84+
.product_mode
85+
.clone()
86+
.filter(|mode| matches!(mode.as_str(), "build" | "plan" | "ask" | "project"))
87+
.unwrap_or_else(|| "build".to_string())
88+
};
89+
90+
// Native-transcript capability is decided once at creation and frozen:
91+
// a later capability flip must never re-route an existing session's
92+
// replay away from where its turns were actually persisted.
93+
let transcript_source = key_vault::key_store::ModelType::from_str(&params.cli_agent_type)
94+
.filter(native_transcript::native_transcript_enabled)
95+
.map(|_| native_transcript::TRANSCRIPT_SOURCE_NATIVE)
96+
.unwrap_or(native_transcript::TRANSCRIPT_SOURCE_CHUNKS);
97+
98+
conn.execute(
99+
"INSERT INTO code_sessions
100+
(session_id, name, status, flow, runner, cli_agent_type, model, tier,
101+
account_id, repo_path, branch, proxy_token, proxy_url, hosted_token,
102+
proxy_session_id, background, key_source, additional_directories,
103+
parent_session_id, org_member_id, org_id, project_id, project_name,
104+
project_slug, work_item_id, agent_role, created_at, updated_at,
105+
transcript_source, product_mode, agent_exec_mode)
106+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31)",
107+
params![
108+
session_id, name, SessionStatus::Pending.as_ref(), flow, runner, params.cli_agent_type,
109+
params.model, params.tier, params.account_id,
110+
params.repo_path, params.branch, params.proxy_token, params.proxy_url,
111+
params.hosted_token, params.proxy_session_id, background, key_source_str,
112+
additional_dirs_json, params.parent_session_id, params.org_member_id,
113+
org_id, params.project_id, params.project_name, params.project_slug,
114+
params.work_item_id, params.agent_role, ts, ts, transcript_source,
115+
product_mode, AgentExecMode::Build.as_str(),
116+
],
117+
)?;
118+
119+
let session = get_session(session_id)?.ok_or(rusqlite::Error::QueryReturnedNoRows)?;
120+
sync_orgtrack_mirror(session_id);
121+
Ok(session)
122+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//! Session deletion: shell-replay guard, non-CASCADE usage-table cleanup,
2+
//! orgtrack mirror removal, and hosted-Codex profile teardown.
3+
4+
use rusqlite::Result as SqliteResult;
5+
6+
use database::db::get_connection;
7+
8+
/// Delete a session and all its chunks (CASCADE) + per-round token usage records.
9+
pub fn delete_session(session_id: &str) -> SqliteResult<bool> {
10+
if let Err(error) =
11+
agent_core::tools::impls::coding::exec::shell_replay::ensure_session_replays_deletable(
12+
session_id,
13+
)
14+
{
15+
return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
16+
std::io::Error::other(error),
17+
)));
18+
}
19+
agent_core::tools::impls::coding::exec::shell_replay::queue_session_replay_cleanup(session_id)
20+
.map_err(|error| {
21+
rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::other(error)))
22+
})?;
23+
let conn = get_connection()?;
24+
conn.execute(
25+
"DELETE FROM code_session_chunks WHERE session_id = ?1",
26+
[session_id],
27+
)?;
28+
// Clean up per-round token usage records
29+
conn.execute(
30+
"DELETE FROM session_token_usage WHERE session_id = ?1",
31+
[session_id],
32+
)?;
33+
// Per-LLM-call telemetry lives in the shared usage tables (not under the
34+
// code_session_chunks CASCADE), so it needs its own cleanup here.
35+
conn.execute(
36+
"DELETE FROM session_llm_usage_spans WHERE session_id = ?1",
37+
[session_id],
38+
)?;
39+
conn.execute(
40+
"DELETE FROM session_tool_usage WHERE session_id = ?1",
41+
[session_id],
42+
)?;
43+
let affected = conn.execute(
44+
"DELETE FROM code_sessions WHERE session_id = ?1",
45+
[session_id],
46+
)?;
47+
if affected > 0 {
48+
if let Err(err) =
49+
agent_core::tools::impls::coding::exec::shell_replay::remove_session_replays(session_id)
50+
{
51+
tracing::warn!(session_id, error = %err, "[cli-persistence] shell replay delete failed");
52+
}
53+
if let Err(err) =
54+
crate::agent_sessions::session_directory::orgtrack_adapter::remove_mirrored_session(
55+
session_id,
56+
)
57+
{
58+
tracing::warn!(session_id, error = %err, "[cli-persistence] orgtrack delete mirror failed");
59+
}
60+
let hosted_codex_profile = app_paths::codex_hosted_cli_profile_dir(session_id);
61+
if hosted_codex_profile.exists() {
62+
if let Err(err) = std::fs::remove_dir_all(&hosted_codex_profile) {
63+
tracing::warn!(
64+
session_id,
65+
path = %hosted_codex_profile.display(),
66+
error = %err,
67+
"[cli-persistence] hosted Codex profile delete failed"
68+
);
69+
}
70+
}
71+
}
72+
Ok(affected > 0)
73+
}

0 commit comments

Comments
 (0)