Simplify session lifecycle and MCP cleanup
This commit is contained in:
@@ -36,6 +36,10 @@ type trustPrompter interface {
|
||||
promptTrust(processID, presetName, reason string)
|
||||
}
|
||||
|
||||
type scratchpadSink interface {
|
||||
scratchpadsChanged()
|
||||
}
|
||||
|
||||
// toolHost adapts the running session + scratchpad store + trust store
|
||||
// to the MCP ToolHost interface. SPEC §7 tools route through here.
|
||||
type toolHost struct {
|
||||
@@ -55,6 +59,7 @@ type toolHost struct {
|
||||
attention attentionSink
|
||||
focus focusSink
|
||||
prompter trustPrompter
|
||||
scratch scratchpadSink
|
||||
|
||||
timersMu sync.Mutex
|
||||
nextTimer int
|
||||
@@ -129,7 +134,7 @@ func (h *toolHost) SpawnAgent(callerID string, args mcp.SpawnAgentArgs) (mcp.Pro
|
||||
}
|
||||
}
|
||||
if p == nil {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("unknown_agent", "unknown agent preset %q", args.Agent)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindUnknownAgent, "unknown agent preset %q", args.Agent)
|
||||
}
|
||||
display := args.Name
|
||||
if display == "" {
|
||||
@@ -148,7 +153,7 @@ func (h *toolHost) SpawnProcess(callerID string, args mcp.SpawnProcessArgs) (mcp
|
||||
args.Kind = "command"
|
||||
}
|
||||
if args.Kind != "command" && args.Kind != "terminal" {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("invalid_kind", "spawn_process: kind must be 'command' or 'terminal'")
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindInvalidKind, "spawn_process: kind must be 'command' or 'terminal'")
|
||||
}
|
||||
env := h.mergeEnv(args.Env)
|
||||
if args.Kind == "terminal" {
|
||||
@@ -163,11 +168,11 @@ func (h *toolHost) SpawnProcess(callerID string, args mcp.SpawnProcessArgs) (mcp
|
||||
if args.Preset != "" {
|
||||
if !h.trust.IsTrusted(args.Preset) {
|
||||
h.askForTrust(callerID, args.Preset, "spawn_process")
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("needs_trust", "command preset %q is not trusted in this project — patterm has surfaced a confirmation; retry after the user accepts", args.Preset)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindNeedsTrust, "command preset %q is not trusted in this project — patterm has surfaced a confirmation; retry after the user accepts", args.Preset)
|
||||
}
|
||||
ps := h.commandPresetByName(args.Preset)
|
||||
if ps == nil {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("not_found", "command preset %q not found", args.Preset)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindNotFound, "command preset %q not found", args.Preset)
|
||||
}
|
||||
display := args.Name
|
||||
if display == "" {
|
||||
@@ -181,7 +186,7 @@ func (h *toolHost) SpawnProcess(callerID string, args mcp.SpawnProcessArgs) (mcp
|
||||
return h.processInfoOf(c), nil
|
||||
}
|
||||
if len(args.Argv) == 0 {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("invalid_args", "spawn_process: either preset or argv required")
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindInvalidArgs, "spawn_process: either preset or argv required")
|
||||
}
|
||||
display := args.Name
|
||||
if display == "" {
|
||||
@@ -198,17 +203,17 @@ func (h *toolHost) SpawnProcess(callerID string, args mcp.SpawnProcessArgs) (mcp
|
||||
func (h *toolHost) StartProcess(callerID, processID string) (mcp.ProcessInfo, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
if c.Kind != KindCommand {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("wrong_kind", "start_process: only command entries can be started post-creation (this is %s)", c.Kind)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindWrongKind, "start_process: only command entries can be started post-creation (this is %s)", c.Kind)
|
||||
}
|
||||
if c.IsLive() {
|
||||
return h.processInfoOf(c), nil
|
||||
}
|
||||
if c.PresetRef != "" && !h.trust.IsTrusted(c.PresetRef) {
|
||||
h.askForTrust(callerID, c.PresetRef, "start_process")
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("needs_trust", "command preset %q is not trusted in this project", c.PresetRef)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindNeedsTrust, "command preset %q is not trusted in this project", c.PresetRef)
|
||||
}
|
||||
cols, rows := h.size()
|
||||
if err := h.sess.Start(processID, cols, rows); err != nil {
|
||||
@@ -221,14 +226,14 @@ func (h *toolHost) StartProcess(callerID, processID string) (mcp.ProcessInfo, er
|
||||
func (h *toolHost) RestartProcess(callerID, processID string, sig syscall.Signal) (mcp.ProcessInfo, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
if c.Kind != KindCommand && !c.IsLive() {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("wrong_kind", "restart_process: %s entries can only be restarted while live", c.Kind)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindWrongKind, "restart_process: %s entries can only be restarted while live", c.Kind)
|
||||
}
|
||||
if c.Kind == KindCommand && c.PresetRef != "" && !h.trust.IsTrusted(c.PresetRef) {
|
||||
h.askForTrust(callerID, c.PresetRef, "restart_process")
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("needs_trust", "command preset %q is not trusted in this project", c.PresetRef)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindNeedsTrust, "command preset %q is not trusted in this project", c.PresetRef)
|
||||
}
|
||||
cols, rows := h.size()
|
||||
if err := h.sess.Restart(processID, sig, cols, rows); err != nil {
|
||||
@@ -241,7 +246,7 @@ func (h *toolHost) RestartProcess(callerID, processID string, sig syscall.Signal
|
||||
func (h *toolHost) StopProcess(callerID, processID string, sig syscall.Signal) (mcp.ProcessInfo, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.ProcessInfo{}, mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.ProcessInfo{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
if err := h.sess.Kill(processID, sig); err != nil {
|
||||
return mcp.ProcessInfo{}, err
|
||||
@@ -252,7 +257,7 @@ func (h *toolHost) StopProcess(callerID, processID string, sig syscall.Signal) (
|
||||
func (h *toolHost) CloseProcess(callerID, processID string) error {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
_ = c // close removes by id; the lookup just validates existence.
|
||||
return h.sess.Close(processID, syscall.SIGTERM)
|
||||
@@ -261,10 +266,10 @@ func (h *toolHost) CloseProcess(callerID, processID string) error {
|
||||
func (h *toolHost) RenameProcess(callerID, processID, name string) error {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
if name == "" {
|
||||
return mcp.Errorf("invalid_args", "rename_process: name required")
|
||||
return mcp.Errorf(mcp.ErrorKindInvalidArgs, "rename_process: name required")
|
||||
}
|
||||
c.SetName(name)
|
||||
return nil
|
||||
@@ -272,7 +277,7 @@ func (h *toolHost) RenameProcess(callerID, processID, name string) error {
|
||||
|
||||
func (h *toolHost) SelectProcess(callerID, processID string) error {
|
||||
if h.sess.FindChild(processID) == nil {
|
||||
return mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
if h.focus != nil {
|
||||
h.focus.focusProcess(processID)
|
||||
@@ -299,7 +304,7 @@ func (h *toolHost) ListProcesses(callerID, kindFilter string) []mcp.ProcessInfo
|
||||
func (h *toolHost) GetProcessStatus(callerID, processID string) (mcp.ProcessStatus, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.ProcessStatus{}, mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.ProcessStatus{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
info := h.processInfoOf(c)
|
||||
st := mcp.ProcessStatus{ProcessInfo: info}
|
||||
@@ -337,7 +342,7 @@ func (h *toolHost) GetProjectStatus(callerID string) (mcp.ProjectStatus, error)
|
||||
func (h *toolHost) GetProcessOutput(callerID, processID, mode string, sinceOffset int64) (mcp.ProcessOutput, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.ProcessOutput{}, mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.ProcessOutput{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
out := mcp.ProcessOutput{
|
||||
Mode: mode,
|
||||
@@ -376,14 +381,14 @@ func (h *toolHost) GetProcessOutput(callerID, processID, mode string, sinceOffse
|
||||
out.NewOffset = end
|
||||
return out, nil
|
||||
default:
|
||||
return mcp.ProcessOutput{}, mcp.Errorf("invalid_args", "unknown mode %q (want grid|stream)", mode)
|
||||
return mcp.ProcessOutput{}, mcp.Errorf(mcp.ErrorKindInvalidArgs, "unknown mode %q (want grid|stream)", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *toolHost) GetProcessRawOutput(callerID, processID string, sinceOffset int64) (mcp.RawOutput, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.RawOutput{}, mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.RawOutput{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
b, end := c.StreamRead(sinceOffset)
|
||||
return mcp.RawOutput{
|
||||
@@ -396,11 +401,11 @@ func (h *toolHost) GetProcessRawOutput(callerID, processID string, sinceOffset i
|
||||
func (h *toolHost) SearchOutput(callerID, processID, pattern, kind string, limit int) (mcp.SearchResult, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.SearchResult{}, mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return mcp.SearchResult{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return mcp.SearchResult{}, mcp.Errorf("invalid_args", "regex: %v", err)
|
||||
return mcp.SearchResult{}, mcp.Errorf(mcp.ErrorKindInvalidArgs, "regex: %v", err)
|
||||
}
|
||||
b, _ := c.StreamRead(0)
|
||||
text := string(b)
|
||||
@@ -425,11 +430,11 @@ func (h *toolHost) SearchOutput(callerID, processID, pattern, kind string, limit
|
||||
func (h *toolHost) WaitForPattern(callerID, processID, pattern string, timeoutSeconds float64, scope string) (bool, string, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return false, "", mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return false, "", mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return false, "", mcp.Errorf("invalid_args", "regex: %v", err)
|
||||
return false, "", mcp.Errorf(mcp.ErrorKindInvalidArgs, "regex: %v", err)
|
||||
}
|
||||
if scope == "" {
|
||||
scope = "grid"
|
||||
@@ -450,7 +455,7 @@ func (h *toolHost) WaitForPattern(callerID, processID, pattern string, timeoutSe
|
||||
b, _ := c.StreamRead(0)
|
||||
text = stripANSI(string(b))
|
||||
default:
|
||||
return false, "", mcp.Errorf("invalid_args", "unknown scope %q (want grid|scrollback)", scope)
|
||||
return false, "", mcp.Errorf(mcp.ErrorKindInvalidArgs, "unknown scope %q (want grid|scrollback)", scope)
|
||||
}
|
||||
if m := re.FindString(text); m != "" {
|
||||
return true, m, nil
|
||||
@@ -468,7 +473,7 @@ func (h *toolHost) WaitForPattern(callerID, processID, pattern string, timeoutSe
|
||||
func (h *toolHost) GetProcessPorts(callerID, processID string) ([]mcp.PortSighting, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return nil, mcp.Errorf("not_found", "no such process %q", processID)
|
||||
return nil, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
}
|
||||
src := c.Ports()
|
||||
out := make([]mcp.PortSighting, 0, len(src))
|
||||
@@ -485,7 +490,7 @@ func (h *toolHost) GetProcessPorts(callerID, processID string) ([]mcp.PortSighti
|
||||
func (h *toolHost) SendInput(callerID string, args mcp.SendInputArgs) (mcp.SendInputResult, error) {
|
||||
c := h.sess.FindChild(args.ProcessID)
|
||||
if c == nil {
|
||||
return mcp.SendInputResult{}, mcp.Errorf("not_found", "no such process %q", args.ProcessID)
|
||||
return mcp.SendInputResult{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", args.ProcessID)
|
||||
}
|
||||
if !c.IsLive() {
|
||||
return mcp.SendInputResult{}, fmt.Errorf("process %q is %s", args.ProcessID, c.Status())
|
||||
@@ -539,7 +544,7 @@ func encodeInput(args mcp.SendInputArgs) ([]byte, error) {
|
||||
case "key":
|
||||
return encodeKey(args.Key)
|
||||
}
|
||||
return nil, mcp.Errorf("invalid_args", "send_input: unknown kind %q", args.Kind)
|
||||
return nil, mcp.Errorf(mcp.ErrorKindInvalidArgs, "send_input: unknown kind %q", args.Kind)
|
||||
}
|
||||
|
||||
// encodeKey maps a SPEC §7 named key to bytes. We use legacy xterm
|
||||
@@ -601,7 +606,7 @@ func encodeKey(key string) ([]byte, error) {
|
||||
case "f12":
|
||||
return []byte("\x1b[24~"), nil
|
||||
}
|
||||
return nil, mcp.Errorf("invalid_args", "unknown key %q", key)
|
||||
return nil, mcp.Errorf(mcp.ErrorKindInvalidArgs, "unknown key %q", key)
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
@@ -616,7 +621,7 @@ func encodeKey(key string) ([]byte, error) {
|
||||
func (h *toolHost) SendMessage(callerID, targetID, message string) error {
|
||||
target := h.sess.FindChild(targetID)
|
||||
if target == nil {
|
||||
return mcp.Errorf("not_found", "no such process %q", targetID)
|
||||
return mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", targetID)
|
||||
}
|
||||
caller := h.sess.FindChild(callerID)
|
||||
line, err := classifySendMessage(caller, target, callerID, message)
|
||||
@@ -637,7 +642,7 @@ func (h *toolHost) SendMessage(callerID, targetID, message string) error {
|
||||
// top-level process.
|
||||
func classifySendMessage(caller, target *Child, callerID, message string) (string, error) {
|
||||
if target.ID == callerID {
|
||||
return "", mcp.Errorf("not_related", "send_message: cannot send to self")
|
||||
return "", mcp.Errorf(mcp.ErrorKindNotRelated, "send_message: cannot send to self")
|
||||
}
|
||||
if caller != nil && target.ParentID == caller.ID {
|
||||
return "[orchestrator] " + message + "\r", nil
|
||||
@@ -648,7 +653,7 @@ func classifySendMessage(caller, target *Child, callerID, message string) (strin
|
||||
if caller == nil && target.ParentID == "" {
|
||||
return "[orchestrator] " + message + "\r", nil
|
||||
}
|
||||
return "", mcp.Errorf("not_related", "send_message: %q is neither parent nor child of caller (siblings must route through the parent in v1)", target.ID)
|
||||
return "", mcp.Errorf(mcp.ErrorKindNotRelated, "send_message: %q is neither parent nor child of caller (siblings must route through the parent in v1)", target.ID)
|
||||
}
|
||||
|
||||
func (h *toolHost) RequestHumanAttention(callerID, processID, reason string) error {
|
||||
@@ -661,7 +666,7 @@ func (h *toolHost) RequestHumanAttention(callerID, processID, reason string) err
|
||||
func (h *toolHost) TimerWait(callerID string, seconds float64, label string) (string, error) {
|
||||
caller := h.sess.FindChild(callerID)
|
||||
if caller == nil {
|
||||
return "", mcp.Errorf("not_found", "caller %q not known to patterm", callerID)
|
||||
return "", mcp.Errorf(mcp.ErrorKindNotFound, "caller %q not known to patterm", callerID)
|
||||
}
|
||||
h.timersMu.Lock()
|
||||
h.nextTimer++
|
||||
@@ -685,7 +690,27 @@ func (h *toolHost) TimerWait(callerID string, seconds float64, label string) (st
|
||||
// Scratchpads / Meta
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *toolHost) Scratchpads() *scratchpad.Store { return h.pads }
|
||||
func (h *toolHost) ScratchpadList() ([]scratchpad.Entry, error) { return h.pads.List() }
|
||||
|
||||
func (h *toolHost) ScratchpadRead(name string) (string, string, error) {
|
||||
return h.pads.Read(name)
|
||||
}
|
||||
|
||||
func (h *toolHost) ScratchpadWrite(name, content, expectedRevision string) (string, error) {
|
||||
rev, err := h.pads.Write(name, content, expectedRevision)
|
||||
if err == nil && h.scratch != nil {
|
||||
h.scratch.scratchpadsChanged()
|
||||
}
|
||||
return rev, err
|
||||
}
|
||||
|
||||
func (h *toolHost) ScratchpadAppend(name, content string) error {
|
||||
err := h.pads.Append(name, content)
|
||||
if err == nil && h.scratch != nil {
|
||||
h.scratch.scratchpadsChanged()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *toolHost) WhoAmI(callerID string) mcp.WhoAmI {
|
||||
w := mcp.WhoAmI{
|
||||
|
||||
Reference in New Issue
Block a user