fix scratchpad routing by caller project
This commit is contained in:
@@ -150,6 +150,12 @@ func (r *ProjectRegistry) Count() int {
|
|||||||
return len(r.projects)
|
return len(r.projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProjectRegistry) DefaultProject() *Project {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return r.projects[r.defaultProjectKey]
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProjectRegistry) Shutdown() {
|
func (r *ProjectRegistry) Shutdown() {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
projects := make([]*Project, 0, len(r.projects))
|
projects := make([]*Project, 0, len(r.projects))
|
||||||
@@ -418,24 +424,24 @@ func (r *ProjectRegistry) TimerList(callerID string) ([]mcp.TimerInfo, error) {
|
|||||||
return r.hostForCaller(callerID).TimerList(callerID)
|
return r.hostForCaller(callerID).TimerList(callerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectRegistry) ScratchpadList() ([]scratchpad.Entry, error) {
|
func (r *ProjectRegistry) ScratchpadList(callerID string) ([]scratchpad.Entry, error) {
|
||||||
return r.hostForCaller("").ScratchpadList()
|
return r.hostForCaller(callerID).ScratchpadList(callerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectRegistry) ScratchpadRead(name string) (string, string, error) {
|
func (r *ProjectRegistry) ScratchpadRead(callerID, name string) (string, string, error) {
|
||||||
return r.hostForCaller("").ScratchpadRead(name)
|
return r.hostForCaller(callerID).ScratchpadRead(callerID, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectRegistry) ScratchpadWrite(name, content, expectedRevision string) (string, error) {
|
func (r *ProjectRegistry) ScratchpadWrite(callerID, name, content, expectedRevision string) (string, error) {
|
||||||
return r.hostForCaller("").ScratchpadWrite(name, content, expectedRevision)
|
return r.hostForCaller(callerID).ScratchpadWrite(callerID, name, content, expectedRevision)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectRegistry) ScratchpadAppend(name, content string) error {
|
func (r *ProjectRegistry) ScratchpadAppend(callerID, name, content string) error {
|
||||||
return r.hostForCaller("").ScratchpadAppend(name, content)
|
return r.hostForCaller(callerID).ScratchpadAppend(callerID, name, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectRegistry) ScratchpadDelete(name string) error {
|
func (r *ProjectRegistry) ScratchpadDelete(callerID, name string) error {
|
||||||
return r.hostForCaller("").ScratchpadDelete(name)
|
return r.hostForCaller(callerID).ScratchpadDelete(callerID, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectRegistry) WhoAmI(callerID string) mcp.WhoAmI {
|
func (r *ProjectRegistry) WhoAmI(callerID string) mcp.WhoAmI {
|
||||||
|
|||||||
@@ -811,13 +811,13 @@ func (h *toolHost) TimerList(callerID string) ([]mcp.TimerInfo, error) {
|
|||||||
// Scratchpads / Meta
|
// Scratchpads / Meta
|
||||||
// ───────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func (h *toolHost) ScratchpadList() ([]scratchpad.Entry, error) { return h.pads.List() }
|
func (h *toolHost) ScratchpadList(string) ([]scratchpad.Entry, error) { return h.pads.List() }
|
||||||
|
|
||||||
func (h *toolHost) ScratchpadRead(name string) (string, string, error) {
|
func (h *toolHost) ScratchpadRead(_ string, name string) (string, string, error) {
|
||||||
return h.pads.Read(name)
|
return h.pads.Read(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *toolHost) ScratchpadWrite(name, content, expectedRevision string) (string, error) {
|
func (h *toolHost) ScratchpadWrite(_, name, content, expectedRevision string) (string, error) {
|
||||||
rev, err := h.pads.Write(name, content, expectedRevision)
|
rev, err := h.pads.Write(name, content, expectedRevision)
|
||||||
if err == nil && h.scratch != nil {
|
if err == nil && h.scratch != nil {
|
||||||
h.scratch.scratchpadsChanged()
|
h.scratch.scratchpadsChanged()
|
||||||
@@ -825,7 +825,7 @@ func (h *toolHost) ScratchpadWrite(name, content, expectedRevision string) (stri
|
|||||||
return rev, err
|
return rev, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *toolHost) ScratchpadAppend(name, content string) error {
|
func (h *toolHost) ScratchpadAppend(_, name, content string) error {
|
||||||
err := h.pads.Append(name, content)
|
err := h.pads.Append(name, content)
|
||||||
if err == nil && h.scratch != nil {
|
if err == nil && h.scratch != nil {
|
||||||
h.scratch.scratchpadsChanged()
|
h.scratch.scratchpadsChanged()
|
||||||
@@ -833,7 +833,7 @@ func (h *toolHost) ScratchpadAppend(name, content string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *toolHost) ScratchpadDelete(name string) error {
|
func (h *toolHost) ScratchpadDelete(_, name string) error {
|
||||||
err := h.pads.Delete(name)
|
err := h.pads.Delete(name)
|
||||||
if err == nil && h.scratch != nil {
|
if err == nil && h.scratch != nil {
|
||||||
h.scratch.scratchpadsChanged()
|
h.scratch.scratchpadsChanged()
|
||||||
|
|||||||
@@ -98,3 +98,65 @@ func TestSwitchProjectPreservesProjectProcessTrees(t *testing.T) {
|
|||||||
t.Fatalf("switching back should preserve both project process trees")
|
t.Fatalf("switching back should preserve both project process trees")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProjectRegistryScratchpadsRouteByCallerProject(t *testing.T) {
|
||||||
|
t.Setenv("XDG_DATA_HOME", t.TempDir())
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
reg := newProjectRegistry(preset.Set{}, defaultSettings(), nil, 80, 24)
|
||||||
|
defer reg.Shutdown()
|
||||||
|
|
||||||
|
projectA, err := reg.Open(ctx, t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open project A: %v", err)
|
||||||
|
}
|
||||||
|
projectB, err := reg.Open(ctx, t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open project B: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := projectA.Session.Spawn(SpawnSpec{
|
||||||
|
Kind: KindCommand,
|
||||||
|
Argv: []string{"sh", "-c", "trap 'exit 0' TERM; while :; do sleep 1; done"},
|
||||||
|
Name: "a-caller",
|
||||||
|
}, 80, 24)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("spawn project A caller: %v", err)
|
||||||
|
}
|
||||||
|
b, err := projectB.Session.Spawn(SpawnSpec{
|
||||||
|
Kind: KindCommand,
|
||||||
|
Argv: []string{"sh", "-c", "trap 'exit 0' TERM; while :; do sleep 1; done"},
|
||||||
|
Name: "b-caller",
|
||||||
|
}, 80, 24)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("spawn project B caller: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = projectA.Session.Kill(a.ID, syscall.SIGTERM)
|
||||||
|
_ = projectB.Session.Kill(b.ID, syscall.SIGTERM)
|
||||||
|
})
|
||||||
|
waitUntilLive(t, a)
|
||||||
|
waitUntilLive(t, b)
|
||||||
|
|
||||||
|
if _, err := reg.ScratchpadWrite(a.ID, "note.md", "project A", ""); err != nil {
|
||||||
|
t.Fatalf("write project A scratchpad: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := reg.ScratchpadWrite(b.ID, "note.md", "project B", ""); err != nil {
|
||||||
|
t.Fatalf("write project B scratchpad: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotA, _, err := reg.ScratchpadRead(a.ID, "note.md")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read project A scratchpad: %v", err)
|
||||||
|
}
|
||||||
|
gotB, _, err := reg.ScratchpadRead(b.ID, "note.md")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read project B scratchpad: %v", err)
|
||||||
|
}
|
||||||
|
if gotA != "project A" || gotB != "project B" {
|
||||||
|
t.Fatalf("scratchpad routing leaked between projects: A=%q B=%q", gotA, gotB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ func TestToolHostScratchpadDeleteRemovesPadAndRefreshes(t *testing.T) {
|
|||||||
host := newToolHost(nil, pads, nil, preset.Set{}, nil, 120, 40)
|
host := newToolHost(nil, pads, nil, preset.Set{}, nil, 120, 40)
|
||||||
host.scratch = recorder
|
host.scratch = recorder
|
||||||
|
|
||||||
if err := host.ScratchpadDelete("doomed.md"); err != nil {
|
if err := host.ScratchpadDelete("", "doomed.md"); err != nil {
|
||||||
t.Fatalf("ScratchpadDelete: %v", err)
|
t.Fatalf("ScratchpadDelete: %v", err)
|
||||||
}
|
}
|
||||||
if recorder.count != 1 {
|
if recorder.count != 1 {
|
||||||
@@ -128,7 +128,7 @@ func TestToolHostScratchpadDeleteRemovesPadAndRefreshes(t *testing.T) {
|
|||||||
if _, _, err := pads.Read("doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
if _, _, err := pads.Read("doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
||||||
t.Fatalf("read deleted pad error = %v, want os.ErrNotExist", err)
|
t.Fatalf("read deleted pad error = %v, want os.ErrNotExist", err)
|
||||||
}
|
}
|
||||||
if err := host.ScratchpadDelete("doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
if err := host.ScratchpadDelete("", "doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
||||||
t.Fatalf("delete missing error = %v, want os.ErrNotExist", err)
|
t.Fatalf("delete missing error = %v, want os.ErrNotExist", err)
|
||||||
}
|
}
|
||||||
if recorder.count != 1 {
|
if recorder.count != 1 {
|
||||||
|
|||||||
@@ -177,14 +177,14 @@ func (h *blockingToolHost) TimerResume(string, string) error { return nil }
|
|||||||
func (h *blockingToolHost) TimerList(string) ([]TimerInfo, error) {
|
func (h *blockingToolHost) TimerList(string) ([]TimerInfo, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (h *blockingToolHost) ScratchpadList() ([]scratchpad.Entry, error) { return nil, nil }
|
func (h *blockingToolHost) ScratchpadList(string) ([]scratchpad.Entry, error) { return nil, nil }
|
||||||
func (h *blockingToolHost) ScratchpadRead(string) (string, string, error) {
|
func (h *blockingToolHost) ScratchpadRead(string, string) (string, string, error) {
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
}
|
}
|
||||||
func (h *blockingToolHost) ScratchpadWrite(string, string, string) (string, error) {
|
func (h *blockingToolHost) ScratchpadWrite(string, string, string, string) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
func (h *blockingToolHost) ScratchpadAppend(string, string) error { return nil }
|
func (h *blockingToolHost) ScratchpadAppend(string, string, string) error { return nil }
|
||||||
func (h *blockingToolHost) ScratchpadDelete(string) error { return nil }
|
func (h *blockingToolHost) ScratchpadDelete(string, string) error { return nil }
|
||||||
func (h *blockingToolHost) WhoAmI(string) WhoAmI { return WhoAmI{} }
|
func (h *blockingToolHost) WhoAmI(string) WhoAmI { return WhoAmI{} }
|
||||||
func (h *blockingToolHost) Help(string, string) HelpResponse { return HelpResponse{} }
|
func (h *blockingToolHost) Help(string, string) HelpResponse { return HelpResponse{} }
|
||||||
|
|||||||
@@ -97,11 +97,11 @@ type ToolHost interface {
|
|||||||
TimerList(callerID string) ([]TimerInfo, error)
|
TimerList(callerID string) ([]TimerInfo, error)
|
||||||
|
|
||||||
// Scratchpads.
|
// Scratchpads.
|
||||||
ScratchpadList() ([]scratchpad.Entry, error)
|
ScratchpadList(callerID string) ([]scratchpad.Entry, error)
|
||||||
ScratchpadRead(name string) (content string, revision string, err error)
|
ScratchpadRead(callerID, name string) (content string, revision string, err error)
|
||||||
ScratchpadWrite(name, content, expectedRevision string) (revision string, err error)
|
ScratchpadWrite(callerID, name, content, expectedRevision string) (revision string, err error)
|
||||||
ScratchpadAppend(name, content string) error
|
ScratchpadAppend(callerID, name, content string) error
|
||||||
ScratchpadDelete(name string) error
|
ScratchpadDelete(callerID, name string) error
|
||||||
|
|
||||||
// Meta.
|
// Meta.
|
||||||
WhoAmI(callerID string) WhoAmI
|
WhoAmI(callerID string) WhoAmI
|
||||||
@@ -724,7 +724,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
|||||||
return ts, 0, "", nil
|
return ts, 0, "", nil
|
||||||
|
|
||||||
case "scratchpad_list":
|
case "scratchpad_list":
|
||||||
entries, err := h.ScratchpadList()
|
entries, err := h.ScratchpadList(callerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, codeInternal, err.Error(), nil
|
return nil, codeInternal, err.Error(), nil
|
||||||
}
|
}
|
||||||
@@ -737,7 +737,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
|||||||
if err := unmarshalParams(params, &p); err != nil {
|
if err := unmarshalParams(params, &p); err != nil {
|
||||||
return nil, codeInvalidParams, err.Error(), nil
|
return nil, codeInvalidParams, err.Error(), nil
|
||||||
}
|
}
|
||||||
content, rev, err := h.ScratchpadRead(p.Name)
|
content, rev, err := h.ScratchpadRead(callerID, p.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, codeInternal, err.Error(), nil
|
return nil, codeInternal, err.Error(), nil
|
||||||
}
|
}
|
||||||
@@ -752,7 +752,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
|||||||
if err := unmarshalParams(params, &p); err != nil {
|
if err := unmarshalParams(params, &p); err != nil {
|
||||||
return nil, codeInvalidParams, err.Error(), nil
|
return nil, codeInvalidParams, err.Error(), nil
|
||||||
}
|
}
|
||||||
rev, err := h.ScratchpadWrite(p.Name, p.Content, p.ExpectedRevision)
|
rev, err := h.ScratchpadWrite(callerID, p.Name, p.Content, p.ExpectedRevision)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Optimistic-concurrency miss returns ok:false + current_revision
|
// Optimistic-concurrency miss returns ok:false + current_revision
|
||||||
// rather than a JSON-RPC error so callers can re-read + merge.
|
// rather than a JSON-RPC error so callers can re-read + merge.
|
||||||
@@ -772,7 +772,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
|||||||
if err := unmarshalParams(params, &p); err != nil {
|
if err := unmarshalParams(params, &p); err != nil {
|
||||||
return nil, codeInvalidParams, err.Error(), nil
|
return nil, codeInvalidParams, err.Error(), nil
|
||||||
}
|
}
|
||||||
if err := h.ScratchpadAppend(p.Name, p.Content); err != nil {
|
if err := h.ScratchpadAppend(callerID, p.Name, p.Content); err != nil {
|
||||||
return nil, codeInternal, err.Error(), nil
|
return nil, codeInternal, err.Error(), nil
|
||||||
}
|
}
|
||||||
return map[string]any{"ok": true}, 0, "", nil
|
return map[string]any{"ok": true}, 0, "", nil
|
||||||
@@ -784,7 +784,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
|||||||
if err := unmarshalParams(params, &p); err != nil {
|
if err := unmarshalParams(params, &p); err != nil {
|
||||||
return nil, codeInvalidParams, err.Error(), nil
|
return nil, codeInvalidParams, err.Error(), nil
|
||||||
}
|
}
|
||||||
if err := h.ScratchpadDelete(p.Name); err != nil {
|
if err := h.ScratchpadDelete(callerID, p.Name); err != nil {
|
||||||
return nil, codeInternal, err.Error(), nil
|
return nil, codeInternal, err.Error(), nil
|
||||||
}
|
}
|
||||||
return map[string]any{"ok": true}, 0, "", nil
|
return map[string]any{"ok": true}, 0, "", nil
|
||||||
|
|||||||
Reference in New Issue
Block a user