The ScopeTracker tracks which files are currently being worked on by active tasks to detect and prevent concurrent modifications.
ScopeTracker Class
Constructor
Creates an empty scope tracker with no registered scopes.
Methods
register()
Registers a task’s file scope as active.
register(taskId: string, scope: string[]): void
Array of file paths this task will modify
release()
Releases a task’s scope when the task completes.
release(taskId: string): void
ID of the task to release
getOverlaps()
Detects overlapping file scopes between a candidate task and active tasks.
getOverlaps(taskId: string, scope: string[]): ScopeOverlap[]
ID of the candidate task (excluded from overlap check)
File paths to check for overlaps
Array of overlapping scopes with other active tasksinterface ScopeOverlap {
taskId: string; // ID of the conflicting task
overlappingFiles: string[]; // Files that overlap
}
getLockedFiles()
Returns all files currently locked by active tasks.
getLockedFiles(): string[]
Sorted array of all file paths currently being modified
Usage Example
import { ScopeTracker } from "@longshot/orchestrator";
const tracker = new ScopeTracker();
// Register task scopes
tracker.register("task-1", ["src/auth.ts", "src/middleware/auth.ts"]);
tracker.register("task-2", ["src/api/users.ts", "src/types/user.ts"]);
// Check for overlaps before dispatching a new task
const candidateScope = ["src/auth.ts", "src/utils.ts"];
const overlaps = tracker.getOverlaps("task-3", candidateScope);
if (overlaps.length > 0) {
console.warn("Scope overlap detected!");
for (const overlap of overlaps) {
console.log(`Conflicts with ${overlap.taskId}:`, overlap.overlappingFiles);
}
// Decision: defer task, route to subplanner, or accept risk
} else {
tracker.register("task-3", candidateScope);
// Safe to dispatch
}
// When task completes
tracker.release("task-1");
// View all locked files
console.log("Currently locked:", tracker.getLockedFiles());
// Output: ["src/api/users.ts", "src/types/user.ts", "src/auth.ts", "src/utils.ts"]
Conflict Detection Strategy
The scope tracker enables different conflict handling strategies:
1. Strict Blocking
Reject any task with overlapping scope:
const overlaps = tracker.getOverlaps(taskId, scope);
if (overlaps.length > 0) {
throw new Error(`Cannot dispatch: conflicts with ${overlaps[0].taskId}`);
}
2. Deferred Dispatch
Queue task until conflicting scopes are released:
const overlaps = tracker.getOverlaps(taskId, scope);
if (overlaps.length > 0) {
deferredQueue.push({ taskId, scope, blockedBy: overlaps });
return;
}
tracker.register(taskId, scope);
3. Subplanner Routing
Route large overlapping scopes to the subplanner for decomposition:
const overlaps = tracker.getOverlaps(taskId, scope);
if (overlaps.length > 0 && scope.length > SUBPLANNER_THRESHOLD) {
// Subplanner can break it into non-overlapping subtasks
return routeToSubplanner(task);
}
4. Risk-Aware Dispatch
Accept low overlap risks, escalate high overlap:
const overlaps = tracker.getOverlaps(taskId, scope);
const overlapCount = overlaps.reduce((sum, o) => sum + o.overlappingFiles.length, 0);
if (overlapCount > 3) {
return routeToSubplanner(task);
} else if (overlapCount > 0) {
logger.warn(`Dispatching task with ${overlapCount} file overlaps`);
}
tracker.register(taskId, scope);
Scope Granularity
File paths should be normalized before registration:
function normalizeScope(scope: string[]): string[] {
return scope
.map(p => p.replace(/\\/g, "/")) // Normalize separators
.map(p => p.replace(/^\.?\//, "")) // Remove leading ./
.filter(p => p.length > 0); // Remove empty
}
const normalizedScope = normalizeScope(task.scope);
tracker.register(task.id, normalizedScope);
Cleanup Pattern
Always release scopes in a finally block:
tracker.register(taskId, scope);
try {
await executeTask(task);
} finally {
tracker.release(taskId);
}