pub struct WorkspaceManager { /* private fields */ }Expand description
Central registry of all active session workspaces.
Thread-safe via DashMap; every public method is either &self or
returns a scoped reference guard.
The optional cache field holds an Arc-wrapped WorkspaceCache
implementation. In single-pod deployments the default NoOpCache is
used. Multi-pod deployments can supply a ValkeyCache (or any other
implementation) via WorkspaceManager::with_cache.
Implementations§
Source§impl WorkspaceManager
impl WorkspaceManager
Sourcepub fn with_cache(db: PgPool, cache: Arc<dyn WorkspaceCache>) -> Self
pub fn with_cache(db: PgPool, cache: Arc<dyn WorkspaceCache>) -> Self
Create a workspace manager with an explicit cache implementation.
Use this constructor when a ValkeyCache or other L2 cache is
available. Pass Arc::new(NoOpCache) to opt-out of caching.
Sourcepub fn with_deps(
db: PgPool,
cache: Arc<dyn WorkspaceCache>,
claim_tracker: Arc<dyn ClaimTracker>,
events: Arc<dyn EventPublisher>,
) -> Self
pub fn with_deps( db: PgPool, cache: Arc<dyn WorkspaceCache>, claim_tracker: Arc<dyn ClaimTracker>, events: Arc<dyn EventPublisher>, ) -> Self
Create a workspace manager with full dependency injection.
Use this constructor when wiring a real ClaimTracker (e.g. Valkey-backed)
and/or a real EventPublisher (e.g. the protocol event bus).
Sourcepub fn cache(&self) -> &dyn WorkspaceCache
pub fn cache(&self) -> &dyn WorkspaceCache
Return a reference to the underlying cache implementation.
Sourcepub fn next_agent_name(&self, repo_id: &Uuid) -> String
pub fn next_agent_name(&self, repo_id: &Uuid) -> String
Auto-assign the next agent name for a repository.
Returns “agent-1”, “agent-2”, etc. incrementing per repo.
Sourcepub async fn create_workspace(
&self,
session_id: SessionId,
repo_id: RepoId,
agent_id: AgentId,
changeset_id: Uuid,
intent: String,
base_commit: String,
mode: WorkspaceMode,
agent_name: String,
) -> Result<SessionId>
pub async fn create_workspace( &self, session_id: SessionId, repo_id: RepoId, agent_id: AgentId, changeset_id: Uuid, intent: String, base_commit: String, mode: WorkspaceMode, agent_name: String, ) -> Result<SessionId>
Create a new workspace for a session and register it.
Sourcepub fn get_workspace(
&self,
session_id: &SessionId,
) -> Option<Ref<'_, SessionId, SessionWorkspace>>
pub fn get_workspace( &self, session_id: &SessionId, ) -> Option<Ref<'_, SessionId, SessionWorkspace>>
Get an immutable reference to a workspace.
Sourcepub fn get_workspace_mut(
&self,
session_id: &SessionId,
) -> Option<RefMut<'_, SessionId, SessionWorkspace>>
pub fn get_workspace_mut( &self, session_id: &SessionId, ) -> Option<RefMut<'_, SessionId, SessionWorkspace>>
Get a mutable reference to a workspace.
Sourcepub fn destroy_workspace(
&self,
session_id: &SessionId,
) -> Option<SessionWorkspace>
pub fn destroy_workspace( &self, session_id: &SessionId, ) -> Option<SessionWorkspace>
Remove and drop a workspace.
Sourcepub fn active_count(&self, repo_id: RepoId) -> usize
pub fn active_count(&self, repo_id: RepoId) -> usize
Count active workspaces for a specific repository.
Sourcepub fn active_sessions_for_repo(
&self,
repo_id: RepoId,
exclude_session: Option<SessionId>,
) -> Vec<SessionId> ⓘ
pub fn active_sessions_for_repo( &self, repo_id: RepoId, exclude_session: Option<SessionId>, ) -> Vec<SessionId> ⓘ
Return session IDs of all active workspaces for a repo, optionally excluding one session.
Sourcepub fn gc_expired(&self) -> Vec<SessionId> ⓘ
pub fn gc_expired(&self) -> Vec<SessionId> ⓘ
Garbage-collect expired persistent workspaces.
Ephemeral workspaces are not GC’d here — they are destroyed when
the session disconnects. This only handles persistent workspaces
whose expires_at deadline has passed.
Sourcepub fn cleanup_disconnected(&self, active_session_ids: &[Uuid])
pub fn cleanup_disconnected(&self, active_session_ids: &[Uuid])
Destroy workspaces for sessions that no longer exist.
Pin-aware: non-terminal workspaces are stranded instead of evicted
when DKOD_PIN_NONTERMINAL is enabled (default: on). Terminal
workspaces are evicted as before. Callers without a Tokio runtime
fall back to legacy (immediate eviction, no pin guard).
Sourcepub async fn cleanup_disconnected_async(&self, active_session_ids: &[Uuid])
pub async fn cleanup_disconnected_async(&self, active_session_ids: &[Uuid])
Async pin-aware implementation of cleanup_disconnected.
Candidates = sessions in-memory but NOT in the active_session_ids list.
If pinned (flag-on + non-terminal): skip.
Else if non-terminal (flag-off): strand with CleanupDisconnected.
Else (terminal): evict.
Sourcepub fn gc_expired_sessions(
&self,
idle_ttl: Duration,
max_ttl: Duration,
) -> Vec<SessionId> ⓘ
pub fn gc_expired_sessions( &self, idle_ttl: Duration, max_ttl: Duration, ) -> Vec<SessionId> ⓘ
Remove workspaces that are idle beyond idle_ttl or alive beyond max_ttl.
Returns the list of expired session IDs. This complements [gc_expired]
(which handles persistent workspace deadlines) by enforcing activity-based
and hard-maximum lifetime limits on all workspaces.
Pin-aware: non-terminal workspaces survive (they remain in memory) when
DKOD_PIN_NONTERMINAL is enabled (default: on). Terminal workspaces are
evicted as before. With DKOD_PIN_NONTERMINAL=0, legacy behavior: no pinning.
Sourcepub async fn gc_expired_sessions_async(
&self,
idle_ttl: Duration,
max_ttl: Duration,
) -> Vec<SessionId> ⓘ
pub async fn gc_expired_sessions_async( &self, idle_ttl: Duration, max_ttl: Duration, ) -> Vec<SessionId> ⓘ
Activity-based GC with Epic B pin guard. Non-terminal workspaces survive
(they remain in memory). Terminal workspaces are evicted as before.
With DKOD_PIN_NONTERMINAL=0, legacy behavior: no pinning.
Sourcepub async fn startup_reconcile(&self) -> Result<usize>
pub async fn startup_reconcile(&self) -> Result<usize>
One-shot sweep at server boot: find orphaned non-terminal workspaces (rows with no live in-memory workspace, changeset non-terminal, stranded_at IS NULL, abandoned_at IS NULL) and mark them stranded so callers surface SESSION_STRANDED and can resume. Returns count stranded.
Sourcepub fn total_active(&self) -> usize
pub fn total_active(&self) -> usize
Total number of active workspaces across all repos.
Sourcepub async fn strand(
&self,
session_id: &SessionId,
reason: StrandReason,
) -> Result<()>
pub async fn strand( &self, session_id: &SessionId, reason: StrandReason, ) -> Result<()>
Mark a workspace as stranded. Idempotent: a second call on an already-
stranded row does not change stranded_at. Drops the in-memory entry,
releases all symbol locks held by the session, and emits a
session.stranded lifecycle event.
Sourcepub async fn should_pin(&self, session_id: &SessionId) -> bool
pub async fn should_pin(&self, session_id: &SessionId) -> bool
Return true when this workspace’s changeset is in a non-terminal state and the workspace should NOT be evicted by GC. See Epic B spec §Pin rule.
Uses a single indexed query on (session_id) → (changeset_id); returns false on missing workspace/changeset so the caller falls through to the existing eviction path.
Sourcepub fn describe_other_modifiers(
&self,
file_path: &str,
repo_id: RepoId,
exclude_session: SessionId,
) -> String
pub fn describe_other_modifiers( &self, file_path: &str, repo_id: RepoId, exclude_session: SessionId, ) -> String
Describe which other sessions have modified a given file.
Returns a formatted string like "fn create_task modified by agent-2"
or "modified by agent-2, agent-3". Returns an empty string if no
other session has touched the file.
Sourcepub async fn abandon_stranded(
&self,
session_id: &SessionId,
reason: AbandonReason,
) -> Result<()>
pub async fn abandon_stranded( &self, session_id: &SessionId, reason: AbandonReason, ) -> Result<()>
Terminal cleanup for a stranded workspace: mark the changeset rejected, delete overlay rows, tombstone the workspace row, emit session.abandoned, release any residual locks. Idempotent.
Sourcepub async fn sweep_stranded(&self, ttl: Duration) -> Result<usize>
pub async fn sweep_stranded(&self, ttl: Duration) -> Result<usize>
Auto-abandon stranded workspaces past their TTL. Returns the number of rows abandoned. Called from the engine’s periodic GC loop.
Sourcepub async fn resume(
&self,
dead_session: &SessionId,
new_session: SessionId,
agent_id: &str,
) -> Result<ResumeResult>
pub async fn resume( &self, dead_session: &SessionId, new_session: SessionId, agent_id: &str, ) -> Result<ResumeResult>
Transactionally rehydrate a stranded workspace under a new session id.
Preconditions (checked inside a SELECT FOR UPDATE transaction):
- Row with
session_id = dead_sessionmust exist. abandoned_atmust be NULL (not already abandoned).- Changeset state must not be terminal (merged/rejected/closed).
stranded_atmust be non-NULL (workspace is actually stranded).agent_idon the row must match the caller’sagent_id.
On success: rotates session_id to new_session, clears stranded_at,
sets superseded_by = new_session (stores the new session UUID), rehydrates
the in-memory overlay from DB, and inserts the workspace into the active map.
Returns [ResumeResult::Ok(new_session_id)]. Use
WorkspaceManager::get_workspace to borrow the resumed workspace.
§Note on superseded_by
The migration 016 declares superseded_by UUID REFERENCES session_workspaces(id),
but this method stores the new session_id UUID (not the workspace id PK) in
that column. The FK semantics are intentionally relaxed here; a future migration
will correct the reference target or change the column’s purpose.
Sourcepub fn list_sessions(&self, repo_id: RepoId) -> Vec<SessionInfo>
pub fn list_sessions(&self, repo_id: RepoId) -> Vec<SessionInfo>
List all active sessions for a given repository.
Auto Trait Implementations§
impl !RefUnwindSafe for WorkspaceManager
impl !UnwindSafe for WorkspaceManager
impl Freeze for WorkspaceManager
impl Send for WorkspaceManager
impl Sync for WorkspaceManager
impl Unpin for WorkspaceManager
impl UnsafeUnpin for WorkspaceManager
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<T> Fruit for T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more