From 657436de3e55a5f5c161117d1e4f0f86328034cf Mon Sep 17 00:00:00 2001 From: rabbitblood Date: Mon, 20 Apr 2026 00:35:49 -0700 Subject: [PATCH] feat: seed initial memories from org template and create payload (#1050) Add MemorySeed model and initial_memories support at three levels: - POST /workspaces payload: seed memories on workspace creation - org.yaml workspace config: per-workspace initial_memories with defaults fallback - org.yaml global_memories: org-wide GLOBAL scope memories seeded on the first root workspace during import Co-Authored-By: Claude Opus 4.6 (1M context) --- workspace-server/internal/handlers/org.go | 42 +++++++++++++++++-- .../internal/handlers/workspace.go | 4 ++ .../internal/handlers/workspace_provision.go | 30 +++++++++++++ workspace-server/internal/models/workspace.go | 12 ++++++ 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/workspace-server/internal/handlers/org.go b/workspace-server/internal/handlers/org.go index f51c3321..727c731c 100644 --- a/workspace-server/internal/handlers/org.go +++ b/workspace-server/internal/handlers/org.go @@ -74,10 +74,13 @@ func NewOrgHandler(wh *WorkspaceHandler, b *events.Broadcaster, p *provisioner.P // OrgTemplate is the YAML structure for an org hierarchy. type OrgTemplate struct { - Name string `yaml:"name" json:"name"` - Description string `yaml:"description" json:"description"` - Defaults OrgDefaults `yaml:"defaults" json:"defaults"` - Workspaces []OrgWorkspace `yaml:"workspaces" json:"workspaces"` + Name string `yaml:"name" json:"name"` + Description string `yaml:"description" json:"description"` + Defaults OrgDefaults `yaml:"defaults" json:"defaults"` + Workspaces []OrgWorkspace `yaml:"workspaces" json:"workspaces"` + // GlobalMemories is a list of org-wide memories seeded as GLOBAL scope + // on the first root workspace (PM) during org import. Issue #1050. + GlobalMemories []models.MemorySeed `yaml:"global_memories" json:"global_memories"` } type OrgDefaults struct { @@ -106,6 +109,9 @@ type OrgDefaults struct { // Rendered into each workspace's config.yaml so agent prompts can read it // generically (no hardcoded role names in prompts). See issue #51. CategoryRouting map[string][]string `yaml:"category_routing" json:"category_routing"` + // InitialMemories are default memories seeded into every workspace at + // creation time unless the workspace overrides them. Issue #1050. + InitialMemories []models.MemorySeed `yaml:"initial_memories" json:"initial_memories"` } type OrgSchedule struct { @@ -170,6 +176,9 @@ type OrgWorkspace struct { // (empty list drops the category entirely); new keys are added. See // mergeCategoryRouting. CategoryRouting map[string][]string `yaml:"category_routing" json:"category_routing"` + // InitialMemories are memories seeded into this workspace at creation + // time. If empty, defaults.initial_memories are used. Issue #1050. + InitialMemories []models.MemorySeed `yaml:"initial_memories" json:"initial_memories"` Schedules []OrgSchedule `yaml:"schedules" json:"schedules"` Channels []OrgChannel `yaml:"channels" json:"channels"` External bool `yaml:"external" json:"external"` @@ -290,6 +299,22 @@ func (h *OrgHandler) Import(c *gin.Context) { } } + // Seed org-wide global_memories on the first root workspace (issue #1050). + // These are GLOBAL scope memories visible to all workspaces in the org. + if len(tmpl.GlobalMemories) > 0 && len(results) > 0 { + rootID, _ := results[0]["id"].(string) + if rootID != "" { + rootNS := workspaceAwarenessNamespace(rootID) + // Force scope to GLOBAL regardless of what the YAML says. + globalSeeds := make([]models.MemorySeed, len(tmpl.GlobalMemories)) + for i, gm := range tmpl.GlobalMemories { + globalSeeds[i] = models.MemorySeed{Content: gm.Content, Scope: "GLOBAL"} + } + seedInitialMemories(context.Background(), rootID, globalSeeds, rootNS) + log.Printf("Org import: seeded %d global memories on root workspace %s", len(globalSeeds), rootID) + } + } + // Hot-reload channel manager once after all channels are inserted // (instead of per-workspace, avoiding N redundant DB queries + diffs). if h.channelMgr != nil { @@ -408,6 +433,15 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defa "name": ws.Name, "tier": tier, }) + // Seed initial memories from workspace config or defaults (issue #1050). + // Per-workspace initial_memories override defaults; if workspace has none, + // fall back to defaults.initial_memories. + wsMemories := ws.InitialMemories + if len(wsMemories) == 0 { + wsMemories = defaults.InitialMemories + } + seedInitialMemories(ctx, id, wsMemories, awarenessNS) + // Handle external workspaces if ws.External { if _, err := db.DB.ExecContext(ctx, `UPDATE workspaces SET status = 'online', url = $1 WHERE id = $2`, ws.URL, id); err != nil { diff --git a/workspace-server/internal/handlers/workspace.go b/workspace-server/internal/handlers/workspace.go index 464cbd91..a11f0a9a 100644 --- a/workspace-server/internal/handlers/workspace.go +++ b/workspace-server/internal/handlers/workspace.go @@ -209,6 +209,10 @@ func (h *WorkspaceHandler) Create(c *gin.Context) { log.Printf("Create: canvas layout insert failed for %s (workspace will appear at 0,0): %v", id, err) } + // Seed initial memories from the create payload (issue #1050). + // Non-fatal: failures are logged but don't block workspace creation. + seedInitialMemories(ctx, id, payload.InitialMemories, awarenessNamespace) + // Broadcast provisioning event h.broadcaster.RecordAndBroadcast(ctx, "WORKSPACE_PROVISIONING", id, map[string]interface{}{ "name": payload.Name, diff --git a/workspace-server/internal/handlers/workspace_provision.go b/workspace-server/internal/handlers/workspace_provision.go index 4c954008..92b9c55f 100644 --- a/workspace-server/internal/handlers/workspace_provision.go +++ b/workspace-server/internal/handlers/workspace_provision.go @@ -187,6 +187,36 @@ func (h *WorkspaceHandler) provisionWorkspaceOpts(workspaceID, templatePath stri // which transitions status to 'online' and broadcasts WORKSPACE_ONLINE } +// seedInitialMemories inserts a list of MemorySeed entries into agent_memories +// for the given workspace. Called during workspace creation and org import to +// pre-populate memories from config/template. Non-fatal: each insert is +// attempted independently and failures are logged. Issue #1050. +func seedInitialMemories(ctx context.Context, workspaceID string, memories []models.MemorySeed, awarenessNamespace string) { + if len(memories) == 0 { + return + } + for _, mem := range memories { + scope := strings.ToUpper(mem.Scope) + if scope == "" { + scope = "LOCAL" + } + if scope != "LOCAL" && scope != "TEAM" && scope != "GLOBAL" { + log.Printf("seedInitialMemories: skipping memory for %s — invalid scope %q", workspaceID, scope) + continue + } + if mem.Content == "" { + continue + } + if _, err := db.DB.ExecContext(ctx, ` + INSERT INTO agent_memories (workspace_id, content, scope, namespace) + VALUES ($1, $2, $3, $4) + `, workspaceID, mem.Content, scope, awarenessNamespace); err != nil { + log.Printf("seedInitialMemories: failed to insert memory for %s (scope=%s): %v", workspaceID, scope, err) + } + } + log.Printf("seedInitialMemories: seeded %d memories for workspace %s", len(memories), workspaceID) +} + func workspaceAwarenessNamespace(workspaceID string) string { return fmt.Sprintf("workspace:%s", workspaceID) } diff --git a/workspace-server/internal/models/workspace.go b/workspace-server/internal/models/workspace.go index 9e01fbbe..ff8ad0be 100644 --- a/workspace-server/internal/models/workspace.go +++ b/workspace-server/internal/models/workspace.go @@ -57,6 +57,14 @@ type UpdateCardPayload struct { AgentCard json.RawMessage `json:"agent_card" binding:"required"` } +// MemorySeed represents an initial memory to seed into a workspace at creation time. +// Used by both the POST /workspaces API and org template import to pre-populate +// agent memories from config (issue #1050). +type MemorySeed struct { + Content string `json:"content" yaml:"content"` + Scope string `json:"scope" yaml:"scope"` // LOCAL, TEAM, GLOBAL +} + type CreateWorkspacePayload struct { Name string `json:"name" binding:"required"` Role string `json:"role"` @@ -80,6 +88,10 @@ type CreateWorkspacePayload struct { X float64 `json:"x"` Y float64 `json:"y"` } `json:"canvas"` + // InitialMemories is an optional list of memories to seed into the + // workspace immediately after creation. Each entry is inserted into + // agent_memories with the workspace's awareness namespace. Issue #1050. + InitialMemories []MemorySeed `json:"initial_memories"` } type CheckAccessPayload struct {