Back to Dyad

Concurrent Local Agent Writers

plans/concurrent-local-agent-writers.md

1.12.026.3 KB
Original Source

Concurrent Local Agent Writers

Generated by swarm planning session on 2026-08-22

Summary

Allow root Local Agents and Implementer/Sidekick sub-agents to edit the same app concurrently, across different chats and within one chat, using the existing shared working tree. Replace the app-wide exclusive writer lease with owner-scoped lifecycle tracking so cancellation and finalization remain correct without blocking unrelated work.

The governing invariant is: concurrent agent mutation, serialized unsafe primitives. Ordinary file and tool mutations do not exclude other agents; only intrinsically unsafe Git, package-manager, provider, runtime, and destructive app operations retain their existing narrow coordination.

Problem Statement

Dyad currently keys writer ownership by appId. An Implementer, active mutation tool, or root finalization in one chat can therefore reject mutations from every other chat attached to the same app with errors such as “Another agent is currently editing this app.” This prevents users from treating chats and Sidekicks as parallel workstreams.

The intended product contract is shared-tree collaboration. Users may run several writable roots and Implementers at once, including agents with overlapping scopes. Changes are not isolated: agents can observe or overwrite one another's edits, a commit may contain work from several turns, and a deployment may reflect the shared tree visible when it runs.

Scope

In Scope (MVP)

  • Permit multiple root Local Agent turns and Implementers to mutate one app concurrently, across or within chats.
  • Replace app-wide lease, mutation admission, quarantine, and finalization state with in-memory identities scoped to a root turn, child execution, and individual activity token.
  • Keep Implementer scopes required, normalized, and advisory; allow overlapping scopes.
  • Make cancellation close and drain only the targeted child execution.
  • Make root finalization join and seal only work owned by that root turn.
  • Track queued child runs so finalization cannot race a late spawn or follow-up.
  • Track direct, MCP, and sandbox-hosted mutations exactly once.
  • Serialize each complete Local Agent Git checkpoint transaction and each same-app Supabase function reconciliation batch.
  • Remove app-writer conflict messages from normal Local Agent concurrency paths.
  • Preserve legacy hydration support for durable waiting_for_writer rows while no longer producing that status.
  • Add adversarial unit, integration, and E2E coverage for concurrent writers and lifecycle races.

Out of Scope (Follow-up)

  • Per-chat worktrees, branches, selective staging, merge UI, or file/hunk attribution.
  • File locks, conflict detection, or prevention of same-line overwrites.
  • Stable filesystem snapshots for tests, reviews, commits, or deployments.
  • A concurrency setting, blocking confirmation, persistent banner, or global activity dashboard.
  • Automatic Reviewer reruns, new filesystem watching, or snapshot machinery for review staleness.
  • Relaxing app deletion/rename, runtime, provider-transition, repository, or other resource-specific coordinators.
  • Guarantees that a commit or deployment contains changes from only one chat or turn.

User Stories

  • As a power user, I want several chats to edit one app concurrently so I can parallelize independent tasks.
  • As a root Local Agent, I want to delegate to several Implementers without unrelated writers rejecting their work.
  • As a user cancelling one stalled Implementer, I want every other chat and agent to continue normally.
  • As a root turn, I want to wait only for the child executions that belong to my turn before finalizing.
  • As a user, I accept that commits and deployments use the current shared tree so I can favor throughput over isolation.
  • As a Reviewer user, I want review to inspect a captured target without waiting for the entire app to become idle.

UX Design

User Flow

  1. The user starts writable work in Chat A.
  2. The user starts another root or Implementer in Chat B, or another Implementer in Chat A.
  3. All work begins normally without a writer-conflict prompt, wait, or setting.
  4. Each chat displays and controls its own root and sub-agent activity.
  5. Each root waits for only its owned children and mutations, then finalizes the current shared app state.
  6. A concurrent finalizer that finds the combined tree already committed treats “nothing to commit” as success.

Key States

  • Concurrent work: Existing chat and Agent Team cards show per-turn activity; there is no app-global locked state.
  • Owned child wait: Name the task, for example, “Waiting for Implementer: Update auth flow…”
  • Finalizing: Use neutral shared-tree language such as “Finalizing current app changes…”
  • Narrow resource wait: If perceptible, identify the resource, such as “Waiting to create a Git commit…” Never describe the app as being edited by another agent.
  • Stopping: Show “Stopping…” while the targeted activity drains.
  • Abort-ignoring operation: Show “Stop requested · tool still finishing.” Do not claim cancellation has completed while the tool is still active.
  • Finalization timeout: Fail only the owning turn with a targeted message; explicitly allow other chats to continue.
  • Reviewer drift: Retain the report, use the existing target-hash comparison to mark it outdated, and offer a manual rerun. Do not block writers or automatically rerun.

Interaction Details

  • Do not add a concurrency toggle or confirmation.
  • Preserve all existing tool-consent and destructive-action approval gates.
  • Stop/cancel affects one durable thread's current execution, not sibling agents or other chats.
  • Cancellation stops future work but does not roll back completed edits; retain the existing partial-edits warning.
  • Commit copy must say “Committed current app changes,” not “committed this chat's changes.”
  • Reviewer success must not claim coverage of a later combined commit unless the reviewed target still matches.

Accessibility

  • Keep task-specific accessible names on Stop and Retry actions.
  • Announce running, stopping, cancelled, failed, and outdated status changes through the existing live-status mechanism.
  • Pair warning/error color with text and icons.
  • Keep focus stable after Stop, Retry, and Review Again actions.

Technical Design

Architecture

Replace mutation_lease.ts with an in-memory activity tracker whose identity hierarchy is:

ts
interface MutationActivityOwner {
  turnId: string; // `local-agent-turn:${placeholderMessageId}`
  chatId: number;
  actorRunId: string; // fresh for each root or child execution
  threadId?: string;
  persona?: SubagentPersona;
}

threadId identifies the durable sub-agent conversation. actorRunId identifies one execution or follow-up generation of that thread. turnId identifies the root response that owns the execution. The assistant placeholder message ID is a stable, database-global turn key and requires no migration.

The tracker records:

  • An open or finalizing phase per turn.
  • Open or closed admission per actor run.
  • Opaque activity tokens for queued/running child executions and mutation tools.
  • Closed, abort-ignoring actors until their exact tokens really settle.

Suggested API:

ts
reserveSubagentRun(owner): ActivityHandle;
withTrackedMutation(ctx, operation): Promise<T>;
closeMutationActor(actorRunId): void;
waitForMutationActorDrain(actorRunId, timeoutMs): Promise<boolean>;
tryBeginTurnFinalization(turnId): boolean;
describeTurnActivity(turnId): string | null;
endTurnFinalization(turnId): void;

Every check-and-register operation must be synchronous before the next async gap. Opaque handles use token-checked, idempotent settlement so late cleanup from an old execution cannot remove a successor's activity.

Mutation tracking policy

Keep modifiesState as the source of truth for Ask/Plan filtering. Replace requiresMutationLease with an explicit policy:

ts
mutationTracking: "automatic" | "internal" | "none";
  • automatic is the default for ordinary state-changing tools.
  • internal applies to wrappers such as sandbox execution that track individual writable host capabilities themselves.
  • none applies to orchestration metadata tools such as spawn, cancel, send, and follow-up; their child execution is reserved in the manager.
  • Direct/runtime MCP execution remains explicitly tracked because MCP schemas do not reliably describe mutability.

Production writable contexts must have a mutation owner. Transitional fixtures may make it optional while migrating tests, but mutating wrappers must fail closed when it is absent.

Child execution and follow-ups

  • Reserve a subagent-run activity before queueing or returning a successful spawn/follow-up result.
  • Generate a fresh actorRunId for every runThread execution, including a resumed durable thread.
  • Store the controller and actor run ID together.
  • Ensure an active run with queued messages either keeps its reservation through the follow-up chain or atomically hands it to the successor before settlement.
  • A follow-up initiated by a later root turn is owned by that later turn, regardless of the thread's original source message.
  • Retain the existing thread-specific FIFO follow-up lock; it is not app-wide writer admission.

Targeted cancellation

  1. Add the existing thread cancellation tombstone.
  2. Close the current actor run synchronously.
  3. Abort its controller so pending consent is declined and cleared.
  4. Remove a queued scheduler entry if applicable.
  5. Await only that actor's activity tokens for the bounded safe-drain interval.
  6. If a tool ignores abort, leave the actor closed and tracked until the exact token settles.
  7. Mark terminal cancellation only once physical settlement is truthful; otherwise expose “Stop requested · tool still finishing.”

Other actors remain admitted. A late token settlement removes only itself. An owner-turn finalization that times out on its own stubborn token fails locally, while unrelated turns continue and the token remains tracked until settlement.

Turn-scoped finalization

  1. Wait for only Implementer executions reserved by the root turn.
  2. Under finalization admission, re-read owned thread status and pending messages.
  3. Atomically check that the turn has no activity tokens and seal the turnId; if either the join predicate or activity changed, retry from step 1.
  4. Deploy and commit only after the atomic zero-activity seal succeeds.
  5. Dispose the turn record in the root handler's finally block.

Finalization must never inspect app-wide writer activity. A late same-turn spawn/follow-up receives a targeted “This turn is already finalizing” protocol error; another turn remains open.

Git and provider operations

  • In commitAllChanges, wrap the complete Local Agent getGitUncommittedFilesgitAddAllgitCommit checkpoint in one appOperationCoordinator operation with read access to app-path and write access to repository.
  • Acquire coordination before checking status so a queued finalizer re-evaluates the repository instead of acting on a stale pre-wait result.
  • Ordinary file writes remain concurrent while that Git transaction runs.
  • Treat a second finalizer's “nothing to commit” result as successful completion and persist the current HEAD as that turn's immutable commitHash.
  • In deployAllFunctionsIfNeeded, wrap the complete same-app Supabase reconciliation → delete → deploy batch in one provider resource claim. Reconciliation must happen after the claim is acquired so it uses the latest filesystem.
  • A second same-app deployment waits for the first batch, then reconciles and deploys from the current shared tree. Ordinary file edits remain concurrent.
  • Accept last-completer-wins deployment semantics for the shared tree. Cross-app coordination when two apps target the same Supabase project is deferred.
  • Preserve existing coordinators for app paths, deletion, rename, runtime lifecycle, provider/environment transitions, and chat/app teardown.
  • Serialize package-manager and Nitro setup operations per app with a read app-path plus write repository-worktree claim. Refuse these operations while a recording owns the working tree.
  • Git checkpoints and provider deployment batches use refuseWhenRecording so bounded root finalization cannot queue behind a user-controlled recording session.
  • run_pre_commit intentionally verifies and may format the current shared tree, including edits from concurrent turns. Its existing consent copy says that it stages all changes; failures describe shared-tree state and must not be attributed exclusively to the calling turn.

Reviewer

  • Remove app-wide writer polling and stop producing waiting_for_writer.
  • Prefer immutable assistant-message sourceCommitHash/commitHash review ranges.
  • For a working-tree fallback, review the captured diff and compare its existing target hash afterward.
  • Retain the current review_outdated transition when the rebuilt target hash differs. Keep findings advisory and allow manual rerun; do not auto-rerun or block writers.
  • Keep deprecated waiting_for_writer schema/hydration compatibility for rows created by older builds.

Components Affected

  • src/pro/main/ipc/handlers/local_agent/subagents/mutation_lease.ts — replace app-wide maps and admission with the owner/run/token activity tracker, then rename the module.
  • src/pro/main/ipc/handlers/local_agent/subagents/subagent_manager.ts — remove lease reservation/release, add execution reservations, generation-safe cancellation, own-turn sealing, and concurrent Reviewer behavior.
  • src/pro/main/ipc/handlers/local_agent/tools/types.ts — add mutation owner identity and explicit tracking policy.
  • src/pro/main/ipc/handlers/local_agent/tool_definitions.ts — use automatic activity tracking for ordinary mutating tools.
  • src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts — construct root identity, close it on cancellation, seal/release by turn, and update MCP wrapping.
  • src/pro/main/ipc/handlers/local_agent/tools/mcp_type_defs.ts — replace explicit lease admission with activity tracking.
  • src/pro/main/ipc/handlers/local_agent/tools/execute_sandbox_script.ts — track writable host calls internally without double tracking the wrapper.
  • src/pro/main/ipc/handlers/local_agent/processors/file_operations.ts — coordinate the complete Git checkpoint and Supabase reconciliation batches, re-evaluate state after admission, and accept a concurrent no-op commit.
  • src/pro/main/ipc/handlers/local_agent/subagents/state.ts and transition.ts — stop producing writer-wait transitions while retaining legacy read compatibility.
  • Associated unit, integration, and E2E tests — replace exclusion assertions with concurrency and lifecycle-race coverage.

Data Model Changes

No database migration. Turn, actor-run, activity, quarantine, and seal state remain process-local. A process restart ends live executions; resumed durable threads create fresh actor-run identities.

Keep the legacy waiting_for_writer enum value for backward-compatible hydration, but do not emit it for new Reviewer runs.

API Changes

  • waitForSubagentsAndBeginFinalizationwaitForOwnedSubagentsAndSealTurn.
  • endRootFinalization(appId) → release by turnId.
  • Delete acquireMutationLease, releaseMutationLease, assertMutationLease, hasMutationLease, beginAppFinalization, endAppFinalization, app-wide admission helpers, active-tool app quarantine helpers, and their writer-conflict diagnostics after cutover.
  • Delete requiresMutationLease after every tool has an explicit/default activity-tracking policy.
  • Do not retain compatibility aliases using lease terminology.

Implementation Plan

Phase 1: Introduce Owner-Scoped Tracking

  • Add root turnId and actorRunId construction from the assistant placeholder message ID.
  • Add MutationActivityOwner to writable AgentContext construction and fixtures.
  • Implement mutation_activity_tracker.ts with opaque token settlement, actor closure/drain, child reservations, turn sealing, and diagnostics.
  • Add unit tests for multiple actors, multiple parallel tokens, sealing, scoped quarantine, late cleanup, and disposal.

Phase 2: Migrate Child Lifecycle and Cancellation

  • Generate a fresh actor-run identity for every child execution/follow-up.
  • Reserve child runs before queueing and settle them after the execution/follow-up chain unwinds.
  • Store { controller, actorRunId } together and target cancellation by generation.
  • Keep delayed consent bound to the child signal and recheck actor admission immediately before mutation registration.
  • Update chat deletion to drain only that chat and app deletion/reset to enumerate and drain every affected owner.
  • Close chat/app admission synchronously before teardown enumeration, then drain the owners captured behind that fence before deleting app paths or rows.

Phase 3: Migrate Mutation Entry Points

  • Replace generic tool admission with automatic activity tracking.
  • Migrate direct/runtime MCP and sandbox-hosted MCP paths.
  • Mark sandbox execution as internally tracked and cover each writable host capability exactly once.
  • Audit direct root-handler writes such as ensureDyadGitignored and either track them or place them explicitly before sealing.
  • Preserve mutation counting, file tracking, consent, Ask/Plan filtering, and blueprint gates.

Phase 4: Narrow Finalization and Resource Coordination

  • Replace app-wide finalization with owned-child join plus atomic turn sealing.
  • Prevent late same-turn mutations/follow-ups after sealing without affecting another turn.
  • Coordinate the complete Git status/add/commit transaction inside commitAllChanges with read app-path and write repository claims.
  • Check repository status only after the Git checkpoint claim is acquired.
  • Make an already-committed shared tree a successful no-op finalization.
  • Coordinate the complete deployAllFunctionsIfNeeded reconciliation/delete/deploy batch with a same-app provider claim.
  • Reconcile Supabase functions only after provider admission, while leaving ordinary file edits concurrent.
  • Update finalization and cancellation copy to name the owned task or narrow resource.

Phase 5: Enable Concurrency and Remove the Lease

  • Remove app-wide lease acquisition/release from spawn, run, follow-up, scheduling, teardown, and root tools.
  • Remove Reviewer app-wide writer waits, retain target-hash review_outdated detection, and stop emitting waiting_for_writer.
  • Delete app-wide lease/admission/finalization/quarantine APIs and old conflict messages.
  • Rename the module and remove requiresMutationLease terminology.
  • Retain only legacy durable-state hydration compatibility.

Phase 6: Verification and Cleanup

  • Run focused tracker, sub-agent manager, tool wrapper, MCP, sandbox, finalization, file-operation, and transition tests.
  • Add a two-chat same-app integration test covering overlapping roots and Implementers.
  • Add one E2E case verifying both chats complete without the former conflict; do not assert isolated output.
  • Run npm run fmt, npm run lint, npm run ts, and the relevant unit/integration suites.
  • Run npm run build before the E2E suite, then execute the targeted E2E test.
  • Search for stale lease/error terminology and update comments, snapshots, and diagnostics.

Testing Strategy

  • Two root turns on one app execute mutating tools concurrently.
  • Two Implementers in different chats and two Implementers in one turn execute concurrently, including overlapping scopes.
  • A root, ordinary child tool, direct MCP tool, and sandbox-hosted write each track exactly once.
  • Multiple parallel mutation tools under one actor settle independently.
  • Turn A seals/finalizes without waiting for or rejecting Turn B.
  • A spawn/follow-up racing Turn A's seal is either reserved before sealing or rejected locally afterward.
  • A queued follow-up cannot escape through the run-to-follow-up handoff gap.
  • Cancelling A does not stop or quarantine B.
  • A's abort-ignoring tool stays tracked; B mutates and finalizes; A settles late without deleting B's state.
  • A late cleanup from Thread X run 1 cannot affect Thread X run 2.
  • Delayed consent after actor cancellation cannot execute a mutation.
  • Root cancellation/failure closes its turn and cancels its owned children before terminal UI reporting.
  • Chat deletion drains its chat; app deletion/reset closes and drains all app owners before path removal.
  • Two concurrent finalizers produce valid serialized Git outcomes: combined commit plus no-op, or two valid commits, without index.lock races.
  • Reviewer begins while writers are active; commit-backed targets remain immutable and working-tree target-hash drift produces review_outdated without an automatic rerun.
  • Two same-app Supabase finalizers execute reconciliation/deletion/deployment batches sequentially, and the second reconciles only after acquiring provider coordination.
  • Existing mutation counters, pre-commit behavior, consent, destructive-action guards, and app-operation coordinator tests remain green.

Risks & Mitigations

RiskLikelihoodImpactMitigation
Same-file edits overwrite or invalidate each otherHighHighAccepted shared-tree behavior; keep diffs/tool output visible and Git history recoverable.
Late follow-up escapes finalizationMediumHighReserve before queueing and atomically seal the owning turn.
Old cancellation/cleanup closes a successor runMediumHighFresh actor-run IDs and token-checked opaque handles.
Abort-ignoring mutation continues writingMediumHighClose only its actor, retain tokens until settlement, fail only its owning finalization on timeout.
Concurrent Git operations corrupt index/refsHighHighSerialize the entire status/add/commit checkpoint under the repository coordinator.
Concurrent deploys overwrite provider stateMediumMediumCoordinate only unsafe provider batches and accept transparent last-completer-wins semantics.
Commit includes another turn's changesHighMediumAccepted contract; neutral copy and truthful diff/commit metadata.
Reviewer report becomes staleHighMediumReview captured targets, retain existing hash-based review_outdated detection, and provide manual rerun without blocking writers.
Tracker record leaksLowMediumIdempotent handles, handler finally disposal, teardown tests, and diagnostics.
“Remove locks” accidentally weakens destructive safetyMediumHighExplicitly preserve app-operation and resource-specific coordinators; cover deletion/rename/reset in tests.

Success Criteria

  • The second same-app chat or Implementer reaches mutation execution without any app-writer conflict or app-global wait.
  • Cancellation, quarantine, timeout, and cleanup affect only the exact owning actor run.
  • Finalization waits only for its root turn's dependency graph.
  • The former Local Agent writer/finalization conflict messages are unreachable.
  • Concurrent finalizers do not produce Git index/ref corruption; “nothing to commit” is successful.
  • Destructive app lifecycle and resource-specific safety tests continue to pass.
  • No database migration or new runtime dependency is introduced.

Resolved Option Questions

  • Supabase deployments: Serialize the complete same-app deployAllFunctionsIfNeeded reconciliation/delete/deploy batch under the existing provider resource. A later batch reconciles after admission. Do not block ordinary edits, and defer cross-app locking for apps that share one Supabase project.
  • Reviewer staleness: Keep the existing target-hash rebuild and review_outdated behavior in MVP. Remove writer waiting, do not add watchers or snapshots, and require manual reruns.
  • Git checkpoints: Coordinate status/add/commit locally inside commitAllChanges, not through a broader Git-service refactor. Acquire read app-path plus write repository, evaluate status after admission, and treat an already-clean tree as success.

Decision Log

  • Concurrency applies across chats and within a chat, including overlapping Implementer scopes.
  • Changes remain in one shared working tree; no isolation or selective commit attribution will be added.
  • Finalization waits only for work owned by that root turn.
  • Cancellation and quarantine target one actor-run generation and never block another chat.
  • Root turn identity uses the assistant placeholder message ID; durable thread identity is not execution ownership.
  • Ordinary mutations are concurrent; only unsafe Git/provider/runtime/destructive primitives retain narrow coordination.
  • Same-app Supabase function batches run sequentially and reconcile after provider admission; cross-app same-project coordination is deferred.
  • Local Agent checkpoint status/add/commit runs under one narrow coordinator operation inside commitAllChanges.
  • Combined commits, successful no-op finalization, and last-completer-wins deployment are accepted.
  • No concurrency toggle or banner is required for MVP; status copy must remain truthful and owner-specific.
  • Existing Reviewer target-hash staleness detection remains; app-wide Reviewer waiting is removed and reruns stay manual.
  • Legacy waiting_for_writer remains readable but is no longer emitted.

Generated by dyad:swarm-to-plan