Files
Agent-System-Backend/internal/runmgr/memory.go
weichenandCursor 613c068c79 Isolate scratch sessions from project-only capabilities
Why: A scratch session is a private general-agent chat; it must work for its
owner without the RDM provider and must not share documents or context with any
other conversation.
Evidence: agent-exec run fb04c4a9 (pi, success); go test ./internal/store ./internal/server ./internal/runmgr ./internal/app pass.
Impact: Owner work/read on scratch bypasses the domain provider; non-owners and
shared adapters get NotFound. Scratch runs skip project context, knowledge
retrieval, owner/project context entries, user/project memory, and cross-session
reads (both directions). Knowledge add/search 400, rdm.* and formal project
commands 403, settings writes 400, uploads skip indexing, and deleting the sole
conversation removes the scratch project.
Refs: Agent-System-Dev docs/superpowers/specs/2026-10-22-scratch-session-general-agent-design.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 02:53:31 +00:00

121 lines
3.6 KiB
Go

package runmgr
import (
"context"
"errors"
"time"
"agent-native/runtime/memory"
"agent-system-backend/internal/store"
core "agent-system-core"
)
// storeMemory 把 agent-native 的记忆端口接到 memory_entries 表(N3 WP5)。
// owner/项目态由适配器持有(端口本身不传租户态)。
type storeMemory struct {
st *store.Store
owner string
projectID string
checked bool
allowImplicitDefault bool
// scratch 为真时端口对 user/project 记忆完全关闭:临时会话不读也不写
// 跨会话记忆(U2 隔离)。会话内状态不受影响。
scratch bool
}
// ErrScratchMemory 临时会话不写记忆:记忆是跨会话的共享层。
var ErrScratchMemory = errors.New("memory is not available in scratch sessions")
// NewStoreMemory 构建端口实现(owner 为空时用默认租户)。
func NewStoreMemory(st *store.Store, owner, projectID string) memory.Store {
return newStoreMemory(st, owner, projectID, false)
}
func newStoreMemory(st *store.Store, owner, projectID string, checked bool) *storeMemory {
if owner == "" {
owner = store.OwnerDefault
}
return &storeMemory{st: st, owner: owner, projectID: projectID, checked: checked}
}
func newAccountCheckedStoreMemory(st *store.Store, owner, projectID string, allowImplicitDefault bool) *storeMemory {
port := newStoreMemory(st, owner, projectID, true)
port.allowImplicitDefault = allowImplicitDefault
return port
}
// Recall 按 scope 过滤(空 = 不限,保持迁移前 Memories() 的全量语义),
// 过期与空内容条目的剔除与文案渲染在 memory.Block(同一形态)。
func (m *storeMemory) Recall(ctx context.Context, q memory.RecallQuery) ([]memory.Entry, error) {
if m.scratch {
return nil, nil
}
projectID := ""
if q.Scope == "project" {
projectID = m.projectID
}
list, err := m.st.ListMemory(m.owner, q.Scope, projectID)
if err != nil {
return nil, err
}
limit := q.Limit
if limit <= 0 {
limit = memory.MaxRecallEntries
}
out := make([]memory.Entry, 0, len(list))
for _, e := range list {
out = append(out, memory.Entry{
ID: e.ID, Scope: e.Scope, ProjectID: string(e.ProjectID), Type: e.Type,
Content: e.Content, Source: e.Source, Confidence: e.Confidence,
CreatedAt: e.CreatedAt, ExpiresAt: e.ExpiresAt,
})
if len(out) >= limit {
break
}
}
return out, nil
}
// StoreMemory 是 runCtx 的端口出口(llm_direct 经接口断言取用)。
func (c *runCtx) StoreMemory() memory.Store {
if c.mgr == nil || c.mgr.st == nil {
return nil
}
var port *storeMemory
if c.mgr.accountEffectiveAuthorization != nil {
port = newAccountCheckedStoreMemory(c.mgr.st, c.run.Owner, c.run.ProjectID, c.mgr.accountAllowImplicitLocal)
} else {
port = newStoreMemory(c.mgr.st, c.run.Owner, c.run.ProjectID, false)
}
port.scratch = c.scratchProject()
return port
}
// Append 写回一条(project scope 用适配器的项目上下文)。
func (m *storeMemory) Append(ctx context.Context, e memory.Entry) error {
if m.scratch {
return ErrScratchMemory
}
projectID := e.ProjectID
if e.Scope == "project" && projectID == "" {
projectID = m.projectID
}
now := time.Now().UTC()
created := e.CreatedAt
if created.IsZero() {
created = now
}
entry := core.MemoryEntry{
ID: e.ID, Scope: e.Scope, ProjectID: core.ID(projectID), Type: e.Type,
Content: e.Content, Source: e.Source, Confidence: e.Confidence,
CreatedAt: created, ExpiresAt: e.ExpiresAt,
}
var err error
if m.checked {
_, err = m.st.PutMemoryAccountChecked(m.owner, entry, m.allowImplicitDefault)
} else {
_, err = m.st.PutMemory(m.owner, entry)
}
return err
}