Skip to content

feat: implement shared workspace for agent teams (#62)#74

Open
Deepak-negi11 wants to merge 3 commits into
mofa-org:mainfrom
Deepak-negi11:feat/shared-workspace-62
Open

feat: implement shared workspace for agent teams (#62)#74
Deepak-negi11 wants to merge 3 commits into
mofa-org:mainfrom
Deepak-negi11:feat/shared-workspace-62

Conversation

@Deepak-negi11
Copy link
Copy Markdown
Contributor

Summary

Implements a shared, persistent workspace where multiple agents can collaborate on common artifacts, share project knowledge, and maintain consistency across a team. This is the foundation for multi-agent coordination in mofaclaw.

Closes #62
Parent: #58 (Multi-Agent Collaboration System)


Problem

When multiple agents work on related tasks, they operate in isolation. There is no shared location for intermediate artifacts, no way to track who changed what, no conflict detection when two agents modify the same resource, and no shared understanding of project decisions or constraints.


Solution

A new core::workspace module that provides a complete shared workspace backed by the filesystem at ~/.mofaclaw/workspace/. The workspace is structured into four subsystems: Artifact Management, Context Sharing, Locking, and Change History, all unified behind a SharedWorkspace façade and integrated into the agent runtime loop.


Architecture

Directory Structure

~/.mofaclaw/workspace/
├── artifacts/              # Shared artifacts organized by type
│   ├── designs/            # Architecture documents
│   ├── code/               # Code changes in progress
│   ├── reviews/            # Review feedback
│   ├── tests/              # Test results
│   └── other/              # Generic artifacts
├── context/                # Shared project knowledge
│   ├── decisions.json      # Architecture decision log
│   ├── constraints.json    # Project constraints registry
│   └── glossary.json       # Domain terminology definitions
├── state/                  # Runtime state
│   ├── locks/              # Per-artifact exclusive lock files
│   └── active_tasks.json   # Currently active agent tasks
└── history/                # Audit trail
    └── changes.log         # Append-only NDJSON change log

Module Overview

Module File Purpose
types workspace/types.rs All shared data structures
artifact workspace/artifact.rs Artifact CRUD with versioning
context workspace/context.rs Shared knowledge store
lock workspace/lock.rs Exclusive access locks
history workspace/history.rs Append-only audit log
fs workspace/fs.rs Low-level filesystem safety primitives
mod workspace/mod.rs SharedWorkspace façade

Detailed Feature Breakdown

1. Artifact Management (artifact.rs)

Each artifact is a typed, versioned, named binary blob owned by an agent.

Data model:

pub struct Artifact {
    pub id: Uuid,
    pub name: String,
    pub artifact_type: ArtifactType,  // Design | Code | Review | Test | Other(String)
    pub owner: AgentId,
    pub version: u32,
    pub content: Vec<u8>,             // base64-encoded in JSON via custom serde
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

Operations:

  • Create — Assigns a new UUID, version 1, persists metadata + content + version snapshot
  • Get — Searches all type subdirectories by UUID
  • Update — Bumps version, persists new content, saves version snapshot; supports optimistic concurrency (see Conflict Detection below)
  • Delete — Removes metadata, content, and all version snapshot files
  • List — Scans subdirectories with optional filtering by artifact_type, owner, or name_contains (case-insensitive substring match); returns results sorted newest-first

Storage format:

  • <id>.json — Full artifact metadata (including base64-encoded content)
  • <id>.content — Raw content bytes
  • <id>.v<N>.json — Immutable version snapshot for rollback

Version History & Rollback:

  • Every create/update saves an immutable <id>.v<N>.json snapshot
  • get_versions(id) returns all historical snapshots sorted oldest → newest
  • rollback(id, target_version, agent) restores a previous version's content as a new head version (creating a new version number, not rewriting history)

2. Conflict Detection & Resolution (artifact.rs + types.rs)

Supports optimistic concurrency control via expected version checks:

pub enum ConflictStrategy {
    Reject,     // Return VersionConflict error if stale
    Overwrite,  // Accept update even if caller's view is stale
}
  • update_artifact_if_version(id, content, agent, expected_version) — Rejects the update if the artifact's current version doesn't match expected_version
  • update_artifact_with_strategy(id, content, agent, expected_version, strategy) — Full control: pass Overwrite to force through a stale update
  • All conflicts (rejected + overwritten) are recorded in the change history with ChangeAction::Conflict

Error types:

WorkspaceError::VersionConflict { artifact_id, expected, actual }
WorkspaceError::ArtifactLocked { artifact_id, held_by }

3. Exclusive Locking (lock.rs)

Per-artifact locks for exclusive access, using atomic create_new file creation for race-free acquisition:

  • Acquire — Creates <artifact-id>.lock.json atomically; if the file already exists, checks if the same agent owns it (idempotent re-lock) or rejects with ArtifactLocked error
  • Release — Only the owning agent can release; returns LockNotOwned error if a different agent tries
  • Force Release — Admin-level release regardless of owner
  • Is Locked — Check lock status and owner
  • List Locks — Enumerate all active locks

Lock enforcement is integrated into the SharedWorkspace façade — update_artifact, delete_artifact, and rollback_artifact all check locks before proceeding. If a different agent holds the lock, the operation is rejected.

4. Context Sharing (context.rs)

Three JSON files maintain shared project knowledge:

Decisions (context/decisions.json):

pub struct Decision {
    pub id: Uuid,
    pub title: String,
    pub rationale: String,
    pub decided_by: AgentId,
    pub decided_at: DateTime<Utc>,
}

Operations: list_decisions(), add_decision(title, rationale, agent), remove_decision(id)

Constraints (context/constraints.json):

pub struct Constraint {
    pub id: Uuid,
    pub name: String,
    pub description: String,
    pub added_by: AgentId,
    pub added_at: DateTime<Utc>,
}

Operations: list_constraints(), add_constraint(name, description, agent), remove_constraint(id)

Glossary (context/glossary.json):

pub struct GlossaryEntry {
    pub term: String,
    pub definition: String,
    pub added_by: AgentId,
    pub added_at: DateTime<Utc>,
}

Operations: list_glossary(), add_glossary_entry(term, definition, agent), remove_glossary_entry(term)

All context mutations go through ExclusiveLock to prevent concurrent corruption.

5. Change History (history.rs)

Append-only NDJSON (newline-delimited JSON) log at history/changes.log:

pub struct ChangeRecord {
    pub timestamp: DateTime<Utc>,
    pub agent: AgentId,
    pub action: ChangeAction,
    pub artifact_id: Option<Uuid>,
    pub description: String,
}

pub enum ChangeAction {
    Create, Update, Delete, Lock, Unlock,
    ContextUpdate, TaskUpdate, Conflict,
}
  • Every workspace mutation (artifact CRUD, lock/unlock, context changes, task updates, conflicts) is automatically recorded through the SharedWorkspace façade
  • read_all() returns full history oldest-first
  • read_recent(n) returns the last N entries newest-first

6. Task Tracking (mod.rs)

Active task management stored in state/active_tasks.json:

pub struct ActiveTask {
    pub id: Uuid,
    pub agent: AgentId,
    pub description: String,
    pub status: TaskStatus,  // Pending | InProgress | Completed | Failed
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

Operations: add_task(), add_task_with_status(), update_task_status(), remove_task(), list_tasks()

All task mutations are serialized via ExclusiveLock and recorded in change history.

7. Dashboard (mod.rs)

pub struct WorkspaceDashboard {
    pub artifact_count: usize,
    pub active_tasks: Vec<ActiveTask>,   // Only Pending + InProgress
    pub locks: Vec<LockInfo>,
    pub recent_changes: Vec<ChangeRecord>,
}

dashboard(recent_changes) returns an aggregated view of the workspace state.

8. Filesystem Safety (fs.rs)

Two low-level primitives used throughout the module:

ExclusiveLock — Cross-process file lock using create_new (atomic file creation):

  • 5-second timeout with 10ms retry polling
  • Auto-cleanup on Drop (RAII pattern)
  • Used to serialize all mutations to prevent data corruption

Atomic Writesatomic_write() and atomic_write_json():

  • Write to a temporary file with a UUID-suffixed name
  • rename() to the target path (atomic on most filesystems)
  • Prevents partial/corrupt writes on crash

9. Agent Loop Integration (agent/loop_.rs)

SharedWorkspace is integrated into the core agent runtime:

  • Field: workspace: Arc<SharedWorkspace> on AgentLoop
  • Initialization: Created in both with_agent() and with_agent_and_tools() constructors
  • Subagent Tracking: When spawn_subagent() is called, a task is automatically created with InProgress status. On completion, it's updated to Completed; on failure, to Failed.
  • Accessor: workspace() returns &Arc<SharedWorkspace> for use by tools and handlers

Error Handling

New error types in core/src/error.rs:

pub enum WorkspaceError {
    ArtifactNotFound(Uuid),
    VersionConflict { artifact_id: Uuid, expected: u32, actual: u32 },
    ArtifactLocked { artifact_id: Uuid, held_by: AgentId },
    LockNotOwned { artifact_id: Uuid, held_by: AgentId, requester: AgentId },
    VersionNotFound(Uuid, u32),
    Busy(String),
}

Integrated into MofaclawError via #[from]: MofaclawError::Workspace(WorkspaceError).


Files Changed

New (8 files, +2258 lines)

File Lines Purpose
core/src/workspace/types.rs ~243 All data types + base64 serde helper
core/src/workspace/artifact.rs ~350 Artifact store with CRUD + versioning
core/src/workspace/context.rs ~161 Context store (decisions/constraints/glossary)
core/src/workspace/lock.rs ~130 Per-artifact exclusive locking
core/src/workspace/history.rs ~90 Append-only NDJSON change log
core/src/workspace/fs.rs ~80 ExclusiveLock + atomic write primitives
core/src/workspace/mod.rs ~562 SharedWorkspace façade
core/tests/workspace_test.rs ~600 23 integration tests

Modified (3 files)

File Change
core/src/error.rs Added WorkspaceError enum (6 variants) + Workspace variant in MofaclawError
core/src/lib.rs Added pub mod workspace; + pub use workspace::SharedWorkspace;
core/src/agent/loop_.rs Added workspace field, initialization in constructors, subagent task tracking, accessor method

Testing

23 integration tests in core/tests/workspace_test.rs:

Test What it verifies
test_workspace_creates_directory_tree All directories and initial files created
test_artifact_create_and_get Create artifact, retrieve by ID, verify all fields
test_artifact_update_bumps_version Update increments version, preserves name, changes content
test_artifact_delete Delete removes artifact, subsequent get returns None
test_artifact_list_with_filters Filtering by type, owner, name substring
test_version_history_and_rollback Multiple updates create snapshots, rollback restores content
test_lock_and_unlock Acquire lock, verify locked, release, verify unlocked
test_lock_idempotent_same_agent Same agent can re-lock without error
test_lock_nonexistent_artifact_fails Locking non-existent artifact returns ArtifactNotFound
test_unlock_wrong_agent_fails Different agent can't release another's lock
test_decisions_crud Add/list/remove decisions
test_constraints_crud Add/list/remove constraints
test_glossary_crud Add/list/remove glossary entries
test_history_records_operations Create + update + delete all appear in history
test_task_lifecycle Add task → update status → remove task
test_reopen_preserves_data Close and reopen workspace, data persists
test_artifact_serde_roundtrip Serialize-deserialize preserves all fields including binary content
test_version_conflict_rejected Stale version with Reject strategy returns VersionConflict
test_overwrite_strategy_accepts_stale Stale version with Overwrite strategy succeeds
test_delete_blocked_by_lock Delete by non-owner agent returns ArtifactLocked
test_rollback_through_facade Rollback via SharedWorkspace facade works end-to-end
test_context_via_facade_records_history Context changes through facade appear in history
test_dashboard Dashboard aggregates artifacts, tasks, locks, history correctly
test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Acceptance Criteria Checklist

  • Workspace directory structure
  • Artifact CRUD operations
  • Version history tracking
  • Lock mechanism for exclusive access
  • Conflict detection
  • Context sharing API
  • Integration with workflow engine

- Add workspace module with artifact store, context store, lock manager,
  change history, and filesystem helpers (atomic writes, exclusive locks)
- Artifact CRUD with version history, rollback, and conflict detection
- Exclusive per-artifact locking with agent ownership
- Shared context API (decisions, constraints, glossary)
- Task tracking with status lifecycle and dashboard
- Integrate SharedWorkspace into AgentLoop with subagent task tracking
- 23 integration tests
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a new core::workspace module that provides a filesystem-backed shared workspace (artifacts, shared context, locking, history, and active task tracking) and integrates it into the agent runtime to support multi-agent collaboration.

Changes:

  • Added core/src/workspace/* implementing artifact CRUD/versioning, context store, lock manager, history log, atomic write + exclusive lock helpers, and a SharedWorkspace façade.
  • Integrated SharedWorkspace into AgentLoop and added automatic task tracking for spawned subagents.
  • Added integration tests covering workspace lifecycle, concurrency controls, history, and dashboard aggregation.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
core/src/workspace/types.rs Defines workspace data types (artifacts, locks, history, tasks, dashboard) and base64 serde for bytes.
core/src/workspace/mod.rs Implements SharedWorkspace façade tying together artifact/context/locks/history/tasks and recording history.
core/src/workspace/artifact.rs Artifact persistence with CRUD, snapshots, listing filters, rollback, and per-artifact mutation locking.
core/src/workspace/context.rs JSON-backed shared context stores with exclusive mutation locks and atomic writes.
core/src/workspace/lock.rs Per-artifact lock files with acquire/release/force-release/status/list operations.
core/src/workspace/history.rs Append-only NDJSON change log with read-all and read-recent helpers.
core/src/workspace/fs.rs Exclusive lock primitive and “atomic write” helpers used by workspace subsystems.
core/tests/workspace_test.rs Integration test suite validating workspace behavior end-to-end.
core/src/agent/loop_.rs Adds workspace: Arc<SharedWorkspace> and subagent task lifecycle updates.
core/src/error.rs Adds WorkspaceError and plumbs it into MofaclawError.
core/src/lib.rs Exposes the workspace module and re-exports SharedWorkspace.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread core/src/workspace/fs.rs Outdated
Comment on lines +64 to +68
file.flush().await?;
drop(file);

fs::rename(&temp_path, path).await?;
Ok(())
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

atomic_write uses tokio::fs::rename to move the temp file onto an existing target. On Windows, rename fails if the destination already exists, so updates to JSON files (tasks/context/artifacts metadata) can error in CI. Consider removing/replacing the destination first while holding a lock, or using a Windows-safe atomic replace (e.g., platform-specific replace, or a crate that wraps ReplaceFileW).

Copilot uses AI. Check for mistakes.
Comment thread core/src/workspace/fs.rs Outdated
Comment on lines +31 to +35
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
if Instant::now() >= deadline {
return Err(WorkspaceError::Busy(path.display().to_string()).into());
}
sleep(Duration::from_millis(10)).await;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExclusiveLock can leave stale .lock files behind on process crash (Drop won't run), which will cause all future callers to spin until the 5s deadline and then permanently fail with Busy. Consider writing metadata (pid/timestamp) into the lock file and supporting stale-lock eviction/TTL, or using an OS-level lock that is released automatically when the process exits.

Copilot uses AI. Check for mistakes.
Comment thread core/src/workspace/artifact.rs Outdated
Comment on lines +64 to +66
// Write metadata (without bulky content duplicated – content lives in the .content file)
atomic_write_json(&meta, artifact).await?;
atomic_write(&content, &artifact.content).await?;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says metadata is written "without bulky content duplicated", but atomic_write_json(&meta, artifact) serializes the full Artifact including content (base64), and .content is also written. Additionally, get() only reads the JSON, so the .content file isn't used for reads. Consider either removing .content entirely or changing the JSON format to exclude content and load it from the .content file.

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +208
let subdirs: Vec<&str> = if let Some(ref at) = filter.artifact_type {
vec![match at {
ArtifactType::Design => "designs",
ArtifactType::Code => "code",
ArtifactType::Review => "reviews",
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list() treats filter.artifact_type only as a directory selector. For ArtifactType::Other("..."), this will return all artifacts in artifacts/other/ (regardless of the inner string), and it also never verifies that artifact.artifact_type == filter.artifact_type after deserialization. Consider applying an explicit artifact.artifact_type equality check when filter.artifact_type is set (especially for Other).

Copilot uses AI. Check for mistakes.
Comment thread core/src/workspace/mod.rs Outdated
Comment on lines +131 to +144
// Check lock – if locked by someone else, reject
if let Some(lock) = self.locks.is_locked(id).await? {
if lock.agent != agent {
return Err(crate::error::WorkspaceError::ArtifactLocked {
artifact_id: id,
held_by: lock.agent,
}
.into());
}
}

let artifact = match self
.artifacts
.update(id, content, agent.clone(), expected_version, strategy)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock enforcement is subject to a TOCTOU race: the code checks locks.is_locked() and then performs the mutation in artifacts.update(...). Another process can acquire the lock between those steps, allowing an update/delete/rollback to proceed while locked. Consider performing the lock check under the same per-artifact mutation lock used by ArtifactStore (or making LockManager::acquire/release also participate in that lock) so the check+mutate sequence is atomic.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +52
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_path)
.await?;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChangeHistory::record appends without any cross-process serialization. Multiple writers can interleave writes to changes.log and corrupt NDJSON lines (especially since write_all may perform multiple underlying writes). Consider guarding appends with ExclusiveLock (similar to tasks/context) or another file-locking mechanism so each record is written atomically.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Shared Workspace for Agent Teams

2 participants