Skip to main content

WorkspaceManager

Struct WorkspaceManager 

Source
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

Source

pub fn new(db: PgPool) -> Self

Create a new, empty workspace manager backed by NoOpCache.

Source

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.

Source

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).

Source

pub fn cache(&self) -> &dyn WorkspaceCache

Return a reference to the underlying cache implementation.

Source

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.

Source

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.

Source

pub fn get_workspace( &self, session_id: &SessionId, ) -> Option<Ref<'_, SessionId, SessionWorkspace>>

Get an immutable reference to a workspace.

Source

pub fn get_workspace_mut( &self, session_id: &SessionId, ) -> Option<RefMut<'_, SessionId, SessionWorkspace>>

Get a mutable reference to a workspace.

Source

pub fn destroy_workspace( &self, session_id: &SessionId, ) -> Option<SessionWorkspace>

Remove and drop a workspace.

Source

pub fn active_count(&self, repo_id: RepoId) -> usize

Count active workspaces for a specific repository.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn total_active(&self) -> usize

Total number of active workspaces across all repos.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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_session must exist.
  • abandoned_at must be NULL (not already abandoned).
  • Changeset state must not be terminal (merged/rejected/closed).
  • stranded_at must be non-NULL (workspace is actually stranded).
  • agent_id on the row must match the caller’s agent_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.

Source

pub fn list_sessions(&self, repo_id: RepoId) -> Vec<SessionInfo>

List all active sessions for a given repository.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert 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>

Convert 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)

Convert &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)

Convert &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
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Fruit for T
where T: Send + Downcast,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more