Files
SivanCola 9717c4ff2c fix(session): preserve composition admission and idle handoffs
Problem: an admitted turn could fail before adding its user message and unlock
its composition. Controller-owned continuations could strand ACP config changes,
and MCP disconnect could remove code needed by a subsequent reconnect.

Root cause: the started latch followed message insertion, ACP drained only its
own prompt returns, and connection cleanup owned session package resources.

Fix: durably latch composition admission before executing or acknowledging a
turn and refuse work while the latch is pending. Notify owners at real idle
transitions and drain ACP changes through its existing serialization domain.
Separate transport state from package lifetime, retain script sibling bundles,
exclude VCS metadata, reject linked transcript exports and freeze late tool names.
Add ACP exact-binding/history tests, actual old-reader evidence, bilingual notes
and the complete light/dark wide/390px before-and-after screenshot set.

Verification: deterministic async/sync admission and failed-save regressions,
ten staged background-continuation/config-switch runs, real MCP reconnect cycles,
shared lifecycle race tests, package coverage, vet, lint and repository checks.
Frontend full suite and independent Desktop/SDK/Electron suites remain green.

Refs: #13613
2026-10-17 19:36:35 +08:00

36 lines
1.0 KiB
Go

package plugin
import (
"fmt"
"os"
"path/filepath"
)
// CompositionProcessRoot prepares a private directory beneath the MCP state owner.
func CompositionProcessRoot(s Spec) (string, error) {
rel, err := filepath.Rel(filepath.Join(s.StateDir, "sessions"), s.ProcessStateDir)
if s.StateDir == "" || err != nil || !filepath.IsLocal(rel) || rel == "." || filepath.Base(rel) != rel {
return "", fmt.Errorf("invalid MCP process state directory")
}
for _, dir := range []string{s.StateDir, filepath.Join(s.StateDir, "sessions"), s.ProcessStateDir} {
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", err
}
info, err := os.Lstat(dir)
if err != nil {
return "", err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("MCP process state crosses a symbolic link")
}
}
return s.ProcessStateDir, nil
}
func compositionTransportState(s Spec) string {
if s.ProcessStateDir != "" && s.Package != "" {
return filepath.Join(s.ProcessStateDir, "runtime")
}
return s.ProcessStateDir
}