Merge pull request #171 from multica-ai/forrestchang/issue-resume-qa
feat(daemon): reuse workdir across tasks on same agent+issue
This commit is contained in:
commit
3b58dff375
5 changed files with 57 additions and 15 deletions
|
|
@ -600,26 +600,33 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string) (TaskR
|
|||
AgentInstructions: instructions,
|
||||
AgentSkills: convertSkillsForEnv(skills),
|
||||
}
|
||||
env, err := execenv.Prepare(execenv.PrepareParams{
|
||||
WorkspacesRoot: d.cfg.WorkspacesRoot,
|
||||
TaskID: task.ID,
|
||||
AgentName: agentName,
|
||||
Provider: provider,
|
||||
Task: taskCtx,
|
||||
}, d.logger)
|
||||
if err != nil {
|
||||
return TaskResult{}, fmt.Errorf("prepare execution environment: %w", err)
|
||||
|
||||
// Try to reuse the workdir from a previous task on the same (agent, issue) pair.
|
||||
var env *execenv.Environment
|
||||
if task.PriorWorkDir != "" {
|
||||
env = execenv.Reuse(task.PriorWorkDir, provider, taskCtx, d.logger)
|
||||
}
|
||||
if env == nil {
|
||||
var err error
|
||||
env, err = execenv.Prepare(execenv.PrepareParams{
|
||||
WorkspacesRoot: d.cfg.WorkspacesRoot,
|
||||
TaskID: task.ID,
|
||||
AgentName: agentName,
|
||||
Provider: provider,
|
||||
Task: taskCtx,
|
||||
}, d.logger)
|
||||
if err != nil {
|
||||
return TaskResult{}, fmt.Errorf("prepare execution environment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject runtime-specific config (meta skill) so the agent discovers .agent_context/.
|
||||
if err := execenv.InjectRuntimeConfig(env.WorkDir, provider, taskCtx); err != nil {
|
||||
d.logger.Warn("execenv: inject runtime config failed (non-fatal)", "error", err)
|
||||
}
|
||||
defer func() {
|
||||
if cleanupErr := env.Cleanup(!d.cfg.KeepEnvAfterTask); cleanupErr != nil {
|
||||
d.logger.Warn("cleanup env failed", "task_id", task.ID, "error", cleanupErr)
|
||||
}
|
||||
}()
|
||||
// NOTE: No cleanup — workdir is preserved for reuse by future tasks on
|
||||
// the same (agent, issue) pair. The work_dir path is stored in DB on
|
||||
// task completion and passed back via PriorWorkDir on the next claim.
|
||||
|
||||
prompt := BuildPrompt(task)
|
||||
|
||||
|
|
@ -643,7 +650,7 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string) (TaskR
|
|||
return TaskResult{}, fmt.Errorf("create agent backend: %w", err)
|
||||
}
|
||||
|
||||
d.logger.Info("starting agent", "provider", provider, "task_id", task.ID, "workdir", env.WorkDir, "branch", env.BranchName, "env_type", env.Type, "model", entry.Model, "timeout", d.cfg.AgentTimeout.String(), "resume_session", task.PriorSessionID)
|
||||
d.logger.Info("starting agent", "provider", provider, "task_id", task.ID, "workdir", env.WorkDir, "reused", task.PriorWorkDir != "" && env.WorkDir == task.PriorWorkDir, "branch", env.BranchName, "env_type", env.Type, "model", entry.Model, "timeout", d.cfg.AgentTimeout.String(), "resume_session", task.PriorSessionID)
|
||||
|
||||
session, err := backend.Execute(ctx, prompt, agent.ExecOptions{
|
||||
Cwd: env.WorkDir,
|
||||
|
|
|
|||
|
|
@ -147,6 +147,36 @@ func Prepare(params PrepareParams, logger *slog.Logger) (*Environment, error) {
|
|||
return env, nil
|
||||
}
|
||||
|
||||
// Reuse wraps an existing workdir into an Environment and refreshes context files.
|
||||
// Returns nil if the workdir does not exist (caller should fall back to Prepare).
|
||||
func Reuse(workDir, provider string, task TaskContextForEnv, logger *slog.Logger) *Environment {
|
||||
if _, err := os.Stat(workDir); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
env := &Environment{
|
||||
RootDir: filepath.Dir(workDir),
|
||||
WorkDir: workDir,
|
||||
Type: WorkspaceTypeDirectory,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Detect if this is a git worktree.
|
||||
if gitRoot, ok := detectGitRepo(workDir); ok {
|
||||
env.Type = WorkspaceTypeGitWorktree
|
||||
env.BranchName = getDefaultBranch(workDir)
|
||||
env.gitRoot = gitRoot
|
||||
}
|
||||
|
||||
// Refresh context files (issue_context.md, skills).
|
||||
if err := writeContextFiles(workDir, provider, task); err != nil {
|
||||
logger.Warn("execenv: refresh context files failed", "error", err)
|
||||
}
|
||||
|
||||
logger.Info("execenv: reusing env", "workdir", workDir, "type", env.Type, "branch", env.BranchName)
|
||||
return env
|
||||
}
|
||||
|
||||
// Cleanup tears down the execution environment.
|
||||
// If removeAll is true, the entire env root is deleted. Otherwise, workdir is
|
||||
// removed but output/ and logs/ are preserved for debugging.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type Task struct {
|
|||
IssueID string `json:"issue_id"`
|
||||
Agent *AgentData `json:"agent,omitempty"`
|
||||
PriorSessionID string `json:"prior_session_id,omitempty"` // Claude session ID from a previous task on this issue
|
||||
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on this issue
|
||||
}
|
||||
|
||||
// AgentData holds agent details returned by the claim endpoint.
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ type AgentTaskResponse struct {
|
|||
Agent *TaskAgentData `json:"agent,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PriorSessionID string `json:"prior_session_id,omitempty"` // session ID from a previous task on same issue
|
||||
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on same issue
|
||||
}
|
||||
|
||||
// TaskAgentData holds agent info included in claim responses so the daemon
|
||||
|
|
|
|||
|
|
@ -224,6 +224,9 @@ func (h *Handler) ClaimTaskByRuntime(w http.ResponseWriter, r *http.Request) {
|
|||
IssueID: task.IssueID,
|
||||
}); err == nil && prior.SessionID.Valid {
|
||||
resp.PriorSessionID = prior.SessionID.String
|
||||
if prior.WorkDir.Valid {
|
||||
resp.PriorWorkDir = prior.WorkDir.String
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("task claimed by runtime", "task_id", uuidToString(task.ID), "runtime_id", runtimeID, "agent_id", uuidToString(task.AgentID), "prior_session", resp.PriorSessionID)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue