feat(attachments): inject reference files into the run + clean up files on delete

TaskRunner appends attached files (absolute paths) to the run prompt as the
read-only Reference files section. Task and list deletes now remove the
on-disk attachment dir eagerly, and a startup AttachmentOrphanRecovery sweep
drops any attachments/<taskId>/ whose task no longer exists (covers list
cascade and planning-discard paths).
This commit is contained in:
Mika Kuns
2026-06-22 17:22:36 +02:00
parent 5be4b5c5fb
commit 6a0c0f59a5
15 changed files with 265 additions and 12 deletions

View File

@@ -21,6 +21,7 @@ public sealed class TaskRunner
private readonly ILogger<TaskRunner> _logger;
private readonly ITaskStateService _state;
private readonly TaskRunTokenRegistry _tokens;
private readonly AttachmentStore _attachments;
public TaskRunner(
IClaudeProcess claude,
@@ -31,7 +32,8 @@ public sealed class TaskRunner
WorkerConfig cfg,
ILogger<TaskRunner> logger,
ITaskStateService state,
TaskRunTokenRegistry tokens)
TaskRunTokenRegistry tokens,
AttachmentStore attachments)
{
_claude = claude;
_dbFactory = dbFactory;
@@ -42,6 +44,7 @@ public sealed class TaskRunner
_logger = logger;
_state = state;
_tokens = tokens;
_attachments = attachments;
}
public async Task RunAsync(TaskEntity task, string slot, CancellationToken ct, bool alreadyClaimed = false)
@@ -54,6 +57,7 @@ public sealed class TaskRunner
ListConfigEntity? listConfig;
List<SubtaskEntity> subtasks;
List<string>? attachmentPaths = null;
using (var context = _dbFactory.CreateDbContext())
{
var listRepo = new ListRepository(context);
@@ -67,6 +71,11 @@ public sealed class TaskRunner
var subtaskRepo = new SubtaskRepository(context);
subtasks = await subtaskRepo.GetByTaskIdAsync(task.Id, ct);
var attachmentRepo = new TaskAttachmentRepository(context);
var attachments = await attachmentRepo.ListByTaskIdAsync(task.Id, ct);
if (attachments.Count > 0)
attachmentPaths = attachments.Select(a => Path.Combine(_attachments.TaskDir(task.Id), a.FileName)).ToList();
}
// Determine working directory: worktree or sandbox.
@@ -114,7 +123,8 @@ public sealed class TaskRunner
// Build prompt: title + description + only the OPEN sub-tasks (resolved ones are dropped).
var prompt = TaskPromptComposer.Compose(
task.Title, task.Description,
subtasks.Select(s => (s.Title, s.Completed)));
subtasks.Select(s => (s.Title, s.Completed)),
attachmentPaths);
// Run 1.
var result = await RunOnceAsync(task.Id, task.Title, slot, runDir, resolvedConfig, 1, false, prompt, ct);