Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions server/internal/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -3520,6 +3520,80 @@ func gateResumeToReusedWorkdir(task *Task, taskCtx *execenv.TaskContextForEnv, e
return reused
}

// shouldReusePriorWorkdir keeps the local_directory lock invariant without
// forcing every squad-leader follow-up onto a fresh provider session. Worker
// tasks already expose their current local-directory assignment, so their
// existing reuse behavior remains unchanged. Leader tasks intentionally skip
// that assignment and its lock; they may therefore reuse only directories
// that resolve to the {workspace}/{task}/workdir shape, carry Prepare-time
// managed-env provenance for the same workspace/issue/agent, and carry a
// matching daemon task-context marker.
//
// Reuse eligibility is deliberately keyed off .managed_env.json (written by
// execenv.Prepare) and NOT .gc_meta.json (written only after the task reaches
// terminal state). The server's task-complete handler reconciles a follow-up
// and wakes the runtime before the prior task's daemon handler writes the GC
// file, so a successor can be claimed inside that window; keying off the
// terminal file raced and dropped the session (MUL-4886). Both proofs this
// function reads — the env-root provenance and the workdir task-context marker
// — are written at Prepare time, so neither depends on completion ordering.
func shouldReusePriorWorkdir(task Task, localAssignment *localDirectoryAssignment, workspacesRoot string) bool {
if task.PriorWorkDir == "" || localAssignment != nil {
return false
}
if !task.IsLeaderTask {
return true
}

root, err := filepath.EvalSymlinks(workspacesRoot)
if err != nil {
return false
}
workdir, err := filepath.EvalSymlinks(task.PriorWorkDir)
if err != nil {
return false
}
info, err := os.Stat(workdir)
if err != nil || !info.IsDir() {
return false
}
rel, err := filepath.Rel(root, workdir)
if err != nil || !filepath.IsLocal(rel) {
return false
}
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) != 3 || parts[0] != task.WorkspaceID || parts[1] == "" || parts[2] != "workdir" {
return false
}
if task.AgentID == "" || task.IssueID == "" {
return false
}
// Managed-env provenance is written only for non-local managed issue envs,
// so its presence (plus the workspace/issue/agent match) proves this is a
// safe daemon-managed reuse target and not a residual local_directory path.
prov, err := execenv.ReadManagedEnvProvenance(filepath.Dir(workdir))
if err != nil || prov.ManagedBy != execenv.ManagedEnvProvenanceManagedBy ||
prov.WorkspaceID != task.WorkspaceID || prov.IssueID != task.IssueID ||
prov.AgentID != task.AgentID {
return false
}

data, err := os.ReadFile(filepath.Join(workdir, execenv.TaskContextMarkerRelPath))
if err != nil {
return false
}
var marker struct {
ManagedBy string `json:"managed_by"`
AgentID string `json:"agent_id"`
IssueID string `json:"issue_id"`
}
if json.Unmarshal(data, &marker) != nil {
return false
}
return marker.ManagedBy == execenv.TaskContextMarkerManagedBy &&
marker.AgentID == task.AgentID && marker.IssueID == task.IssueID
}

// gateCodexResumeToRolloutPresence drops the prior Codex session when its
// rollout is not actually present in the task's CODEX_HOME sessions. A reused
// workdir keeps PriorSessionID (gateResumeToReusedWorkdir), but Codex session
Expand Down Expand Up @@ -3873,8 +3947,10 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string, slot i
// WorkDir is the user's own path (always present) but the reuse path
// loses the envRoot association the GC loop needs, and re-running
// Prepare against a stable user path is cheap (no clone, no copy).
// Squad-leader tasks also skip reuse so a pre-fix leader session recorded
// against the user's local_directory cannot be re-entered without a lock.
// Leader tasks have no localAssignment; shouldReusePriorWorkdir separately
// requires Prepare-time managed-env provenance and a daemon-owned marker
// before allowing reuse, so a pre-fix leader session recorded against
// local_directory still fails closed.
var agentMcpConfig json.RawMessage
var effectiveMcpConfig json.RawMessage
var cursorMcpAuthSource string
Expand Down Expand Up @@ -3946,7 +4022,7 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string, slot i
defer d.unmarkActiveCodexStore(store)
}
}
if task.PriorWorkDir != "" && localAssignment == nil && !task.IsLeaderTask {
if shouldReusePriorWorkdir(task, localAssignment, d.cfg.WorkspacesRoot) {
env = execenv.Reuse(execenv.ReuseParams{
WorkspacesRoot: d.cfg.WorkspacesRoot,
Profile: d.cfg.Profile,
Expand Down
78 changes: 78 additions & 0 deletions server/internal/daemon/execenv/execenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,25 @@ func Prepare(params PrepareParams, logger *slog.Logger) (*Environment, error) {
return nil, fmt.Errorf("execenv: write context files: %w", err)
}

// Persist managed-env provenance for non-local issue envs at Prepare time
// (not on completion, where .gc_meta.json is written). A same-issue
// follow-up can be claimed the instant the prior task completes — before
// the prior handler writes .gc_meta.json — so reuse eligibility must be
// provable from an artifact that exists the moment the env is created. Only
// managed (non-local_directory) issue envs get this marker; that is exactly
// the set squad-leader reuse targets (MUL-4886). Non-fatal: a write failure
// only costs the next follow-up its session reuse (it falls back to a fresh
// session), which must never block dispatching this task.
if params.LocalWorkDir == "" && params.Task.IssueID != "" {
if err := WriteManagedEnvProvenance(envRoot, ManagedEnvProvenance{
WorkspaceID: params.WorkspaceID,
IssueID: params.Task.IssueID,
AgentID: params.Task.AgentID,
}); err != nil && logger != nil {
logger.Warn("execenv: write managed env provenance failed (non-fatal); a follow-up may start a fresh session", "error", err)
}
}

// For Codex, set up a per-task CODEX_HOME seeded from ~/.codex/ with skills.
if params.Provider == "codex" {
codexHome := filepath.Join(envRoot, "codex-home")
Expand Down Expand Up @@ -723,6 +742,65 @@ func ReadGCMeta(envRoot string) (*GCMeta, error) {
return &meta, nil
}

const managedEnvProvenanceFile = ".managed_env.json"

// ManagedEnvProvenanceManagedBy discriminates a managed-env provenance file
// the daemon wrote from any lookalike JSON that happens to share the path.
const ManagedEnvProvenanceManagedBy = "multica-daemon-managed-env"

// ManagedEnvProvenance is persisted to .managed_env.json inside the env root at
// Prepare time (NOT on completion, unlike .gc_meta.json). It records that this
// env root is a daemon-managed, non-local_directory issue env owned by a
// specific workspace/issue/agent.
//
// Its whole reason to exist is timing. A squad-leader follow-up on the same
// issue can be claimed the instant the prior task completes — the server's
// task-complete handler reconciles the follow-up and wakes the runtime before
// the prior task's daemon handler writes .gc_meta.json. Keying reuse
// eligibility off .gc_meta.json therefore raced: the successor read a
// not-yet-written file and started a fresh session (MUL-4886). This marker is
// on disk from the moment the env is created, so the successor can prove reuse
// safety inside that window. It is written ONLY for non-local managed issue
// envs, so its presence is itself the "safe to reuse, not a user
// local_directory" assertion; see shouldReusePriorWorkdir.
type ManagedEnvProvenance struct {
ManagedBy string `json:"managed_by"`
WorkspaceID string `json:"workspace_id"`
IssueID string `json:"issue_id"`
AgentID string `json:"agent_id"`
}

// WriteManagedEnvProvenance persists the reuse-eligibility marker at the env
// root. Callers must only invoke it for non-local_directory issue envs, since
// the file's presence is the non-local assertion. ManagedBy is stamped here so
// callers cannot forget the discriminator.
func WriteManagedEnvProvenance(envRoot string, p ManagedEnvProvenance) error {
if envRoot == "" {
return nil
}
p.ManagedBy = ManagedEnvProvenanceManagedBy
data, err := json.Marshal(p)
if err != nil {
return fmt.Errorf("marshal managed env provenance: %w", err)
}
return os.WriteFile(filepath.Join(envRoot, managedEnvProvenanceFile), data, 0o644)
}

// ReadManagedEnvProvenance reads the Prepare-time reuse-eligibility marker from
// an env root. A missing or malformed file returns an error; callers fail
// closed (no reuse) on any error.
func ReadManagedEnvProvenance(envRoot string) (*ManagedEnvProvenance, error) {
data, err := os.ReadFile(filepath.Join(envRoot, managedEnvProvenanceFile))
if err != nil {
return nil, err
}
var p ManagedEnvProvenance
if err := json.Unmarshal(data, &p); err != nil {
return nil, err
}
return &p, nil
}

// 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.
Expand Down
98 changes: 98 additions & 0 deletions server/internal/daemon/execenv/managed_env_provenance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package execenv

import (
"os"
"path/filepath"
"testing"
)

// TestPrepareManagedIssueEnvWritesProvenance verifies Prepare drops the
// reuse-eligibility marker for a normal (non-local) issue env, carrying the
// owning workspace/issue/agent. This is the artifact shouldReusePriorWorkdir
// keys off so a follow-up claimed before .gc_meta.json is written can still
// prove the workdir is a safe reuse target (MUL-4886).
func TestPrepareManagedIssueEnvWritesProvenance(t *testing.T) {
root := t.TempDir()
env, err := Prepare(PrepareParams{
WorkspacesRoot: root,
WorkspaceID: "ws-prov-001",
TaskID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
AgentName: "Prov Agent",
Task: TaskContextForEnv{
IssueID: "issue-prov-1",
AgentID: "agent-prov-1",
},
}, testLogger())
if err != nil {
t.Fatalf("Prepare failed: %v", err)
}
defer env.Cleanup(true)

prov, err := ReadManagedEnvProvenance(env.RootDir)
if err != nil {
t.Fatalf("read managed env provenance: %v", err)
}
if prov.ManagedBy != ManagedEnvProvenanceManagedBy {
t.Fatalf("managed_by = %q, want %q", prov.ManagedBy, ManagedEnvProvenanceManagedBy)
}
if prov.WorkspaceID != "ws-prov-001" || prov.IssueID != "issue-prov-1" || prov.AgentID != "agent-prov-1" {
t.Fatalf("provenance owner mismatch: %+v", prov)
}
}

// TestPrepareLocalDirectoryWritesNoProvenance pins the local_directory branch:
// a task bound to a user-supplied path must NOT get a managed-env provenance
// marker, so a squad leader can never treat the user's directory as a reusable
// managed workdir. Absence of the marker is the fail-closed signal.
func TestPrepareLocalDirectoryWritesNoProvenance(t *testing.T) {
root := t.TempDir()
localDir := t.TempDir()
env, err := Prepare(PrepareParams{
WorkspacesRoot: root,
WorkspaceID: "ws-prov-local",
TaskID: "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
AgentName: "Local Agent",
LocalWorkDir: localDir,
Task: TaskContextForEnv{
IssueID: "issue-prov-local",
AgentID: "agent-prov-local",
},
}, testLogger())
if err != nil {
t.Fatalf("Prepare failed: %v", err)
}
defer env.Cleanup(true)

if _, err := ReadManagedEnvProvenance(env.RootDir); !os.IsNotExist(err) {
t.Fatalf("local_directory Prepare must not write managed env provenance; got err = %v", err)
}
// The user's own directory must never receive the marker either.
if _, err := os.Stat(filepath.Join(localDir, managedEnvProvenanceFile)); !os.IsNotExist(err) {
t.Fatal("managed env provenance leaked into the user's local directory")
}
}

// TestPrepareNonIssueEnvWritesNoProvenance confirms non-issue envs (chat,
// autopilot, quick-create) get no provenance: reuse targets only issue tasks,
// so writing it elsewhere would be dead weight.
func TestPrepareNonIssueEnvWritesNoProvenance(t *testing.T) {
root := t.TempDir()
env, err := Prepare(PrepareParams{
WorkspacesRoot: root,
WorkspaceID: "ws-prov-chat",
TaskID: "cccccccc-dddd-eeee-ffff-000000000000",
AgentName: "Chat Agent",
Task: TaskContextForEnv{
ChatSessionID: "chat-1",
AgentID: "agent-chat",
},
}, testLogger())
if err != nil {
t.Fatalf("Prepare failed: %v", err)
}
defer env.Cleanup(true)

if _, err := ReadManagedEnvProvenance(env.RootDir); !os.IsNotExist(err) {
t.Fatalf("non-issue Prepare must not write managed env provenance; got err = %v", err)
}
}
Loading
Loading