feat(code): CodeSession + agent loop + Matrix room bridge

phase 2 server core:
- CodeSession: create/resume sessions, Matrix room per project,
  Mistral conversation lifecycle, tool dispatch loop
- agent loop: user input → Mistral → tool calls → route (client
  via gRPC / server via ToolRegistry) → collect results → respond
- Matrix bridge: all messages posted to project room, accessible
  from any Matrix client
- code_sessions SQLite table (Postgres-compatible schema)
- coding mode context injection (project path, git info, prompt.md)
This commit is contained in:
2026-03-23 11:46:22 +00:00
parent 35b6246fa7
commit abfad337c5
4 changed files with 638 additions and 66 deletions

View File

@@ -83,6 +83,19 @@ impl Store {
PRIMARY KEY (localpart, service)
);
CREATE TABLE IF NOT EXISTS code_sessions (
session_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
room_id TEXT NOT NULL,
conversation_id TEXT,
project_path TEXT NOT NULL,
project_name TEXT NOT NULL,
model TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_active TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS research_sessions (
session_id TEXT PRIMARY KEY,
room_id TEXT NOT NULL,
@@ -244,6 +257,120 @@ impl Store {
}
}
// =========================================================================
// Code Sessions (sunbeam code)
// =========================================================================
/// Find an active code session for a user + project.
pub fn find_code_session(
&self,
user_id: &str,
project_name: &str,
) -> Option<(String, String, String)> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT session_id, room_id, conversation_id FROM code_sessions
WHERE user_id = ?1 AND project_name = ?2 AND status = 'active'
ORDER BY last_active DESC LIMIT 1",
params![user_id, project_name],
|row| Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
)),
)
.ok()
}
/// Create a new code session.
pub fn create_code_session(
&self,
session_id: &str,
user_id: &str,
room_id: &str,
project_path: &str,
project_name: &str,
model: &str,
) {
let conn = self.conn.lock().unwrap();
if let Err(e) = conn.execute(
"INSERT INTO code_sessions (session_id, user_id, room_id, project_path, project_name, model)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![session_id, user_id, room_id, project_path, project_name, model],
) {
warn!("Failed to create code session: {e}");
}
}
/// Update the conversation_id for a code session.
pub fn set_code_session_conversation(
&self,
session_id: &str,
conversation_id: &str,
) {
let conn = self.conn.lock().unwrap();
if let Err(e) = conn.execute(
"UPDATE code_sessions SET conversation_id = ?1, last_active = datetime('now')
WHERE session_id = ?2",
params![conversation_id, session_id],
) {
warn!("Failed to update code session conversation: {e}");
}
}
/// Touch the last_active timestamp.
pub fn touch_code_session(&self, session_id: &str) {
let conn = self.conn.lock().unwrap();
if let Err(e) = conn.execute(
"UPDATE code_sessions SET last_active = datetime('now') WHERE session_id = ?1",
params![session_id],
) {
warn!("Failed to touch code session: {e}");
}
}
/// End a code session.
pub fn end_code_session(&self, session_id: &str) {
let conn = self.conn.lock().unwrap();
if let Err(e) = conn.execute(
"UPDATE code_sessions SET status = 'ended' WHERE session_id = ?1",
params![session_id],
) {
warn!("Failed to end code session: {e}");
}
}
/// Check if a room is a code session room.
pub fn is_code_room(&self, room_id: &str) -> bool {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT 1 FROM code_sessions WHERE room_id = ?1 AND status = 'active' LIMIT 1",
params![room_id],
|_| Ok(()),
)
.is_ok()
}
/// Get project context for a code room.
pub fn get_code_room_context(
&self,
room_id: &str,
) -> Option<(String, String, String)> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT project_name, project_path, model FROM code_sessions
WHERE room_id = ?1 AND status = 'active'
ORDER BY last_active DESC LIMIT 1",
params![room_id],
|row| Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
)),
)
.ok()
}
// =========================================================================
// Service Users (OIDC → service username mapping)
// =========================================================================