plans/concurrent-local-agent-writers.md
Generated by swarm planning session on 2026-08-22
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.
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.
waiting_for_writer rows while no longer producing that status.Replace mutation_lease.ts with an in-memory activity tracker whose identity hierarchy is:
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:
open or finalizing phase per turn.Suggested API:
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.
Keep modifiesState as the source of truth for Ask/Plan filtering. Replace requiresMutationLease with an explicit policy:
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.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.
subagent-run activity before queueing or returning a successful spawn/follow-up result.actorRunId for every runThread execution, including a resumed durable thread.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.
turnId; if either the join predicate or activity changed, retry from step 1.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.
commitAllChanges, wrap the complete Local Agent getGitUncommittedFiles → gitAddAll → gitCommit checkpoint in one appOperationCoordinator operation with read access to app-path and write access to repository.HEAD as that turn's immutable commitHash.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.app-path plus write repository-worktree claim. Refuse these operations while a recording owns the working tree.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.waiting_for_writer.sourceCommitHash/commitHash review ranges.review_outdated transition when the rebuilt target hash differs. Keep findings advisory and allow manual rerun; do not auto-rerun or block writers.waiting_for_writer schema/hydration compatibility for rows created by older builds.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.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.
waitForSubagentsAndBeginFinalization → waitForOwnedSubagentsAndSealTurn.endRootFinalization(appId) → release by turnId.acquireMutationLease, releaseMutationLease, assertMutationLease, hasMutationLease, beginAppFinalization, endAppFinalization, app-wide admission helpers, active-tool app quarantine helpers, and their writer-conflict diagnostics after cutover.requiresMutationLease after every tool has an explicit/default activity-tracking policy.turnId and actorRunId construction from the assistant placeholder message ID.MutationActivityOwner to writable AgentContext construction and fixtures.mutation_activity_tracker.ts with opaque token settlement, actor closure/drain, child reservations, turn sealing, and diagnostics.{ controller, actorRunId } together and target cancellation by generation.ensureDyadGitignored and either track them or place them explicitly before sealing.commitAllChanges with read app-path and write repository claims.deployAllFunctionsIfNeeded reconciliation/delete/deploy batch with a same-app provider claim.review_outdated detection, and stop emitting waiting_for_writer.requiresMutationLease terminology.npm run fmt, npm run lint, npm run ts, and the relevant unit/integration suites.npm run build before the E2E suite, then execute the targeted E2E test.index.lock races.review_outdated without an automatic rerun.| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Same-file edits overwrite or invalidate each other | High | High | Accepted shared-tree behavior; keep diffs/tool output visible and Git history recoverable. |
| Late follow-up escapes finalization | Medium | High | Reserve before queueing and atomically seal the owning turn. |
| Old cancellation/cleanup closes a successor run | Medium | High | Fresh actor-run IDs and token-checked opaque handles. |
| Abort-ignoring mutation continues writing | Medium | High | Close only its actor, retain tokens until settlement, fail only its owning finalization on timeout. |
| Concurrent Git operations corrupt index/refs | High | High | Serialize the entire status/add/commit checkpoint under the repository coordinator. |
| Concurrent deploys overwrite provider state | Medium | Medium | Coordinate only unsafe provider batches and accept transparent last-completer-wins semantics. |
| Commit includes another turn's changes | High | Medium | Accepted contract; neutral copy and truthful diff/commit metadata. |
| Reviewer report becomes stale | High | Medium | Review captured targets, retain existing hash-based review_outdated detection, and provide manual rerun without blocking writers. |
| Tracker record leaks | Low | Medium | Idempotent handles, handler finally disposal, teardown tests, and diagnostics. |
| “Remove locks” accidentally weakens destructive safety | Medium | High | Explicitly preserve app-operation and resource-specific coordinators; cover deletion/rename/reset in tests. |
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.review_outdated behavior in MVP. Remove writer waiting, do not add watchers or snapshots, and require manual reruns.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.commitAllChanges.waiting_for_writer remains readable but is no longer emitted.Generated by dyad:swarm-to-plan