feat: add workspace upgrade migration for AGENT.md media section

Existing deployments have an AGENT.md without media sending instructions,
causing the LLM to tell users it cannot send files. This adds an
idempotent upgrade that appends the media section on startup if missing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
repfigit 2026-02-15 23:11:20 -05:00
parent f30893fbb7
commit 61c917d96f
3 changed files with 127 additions and 0 deletions

View file

@ -405,6 +405,9 @@ func agentCmd() {
os.Exit(1) os.Exit(1)
} }
// Apply workspace upgrades before starting the agent
migrate.UpgradeWorkspace(cfg.WorkspacePath())
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
@ -540,6 +543,9 @@ func gatewayCmd() {
os.Exit(1) os.Exit(1)
} }
// Apply workspace upgrades before starting the agent
migrate.UpgradeWorkspace(cfg.WorkspacePath())
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)

51
pkg/migrate/upgrade.go Normal file
View file

@ -0,0 +1,51 @@
package migrate
import (
"os"
"path/filepath"
"strings"
"github.com/sipeed/picoclaw/pkg/logger"
)
// UpgradeWorkspace applies incremental upgrades to an existing workspace.
// Each upgrade is idempotent — it checks whether it has already been applied
// before making changes.
func UpgradeWorkspace(workspace string) {
upgradeAgentMediaSection(workspace)
}
// upgradeAgentMediaSection ensures AGENT.md contains the media sending instructions.
// Added in v0.x to teach the LLM it can send files via the message tool.
func upgradeAgentMediaSection(workspace string) {
agentPath := filepath.Join(workspace, "AGENT.md")
data, err := os.ReadFile(agentPath)
if err != nil {
return // File doesn't exist or unreadable — skip
}
content := string(data)
// Already applied
if strings.Contains(content, "## Media & File Sending") {
return
}
section := `
## Media & File Sending
You CAN send files directly to users. When you need to share a file (image, document, audio, video), use the ` + "`message`" + ` tool with the ` + "`media`" + ` parameter containing the local file path(s). The file will be delivered natively through the user's channel (Telegram, Discord, Slack, etc.). Do NOT tell users you cannot send files just send them.`
content += section
if err := os.WriteFile(agentPath, []byte(content), 0644); err != nil {
logger.ErrorCF("migrate", "Failed to upgrade AGENT.md", map[string]interface{}{
"error": err.Error(),
})
return
}
logger.InfoC("migrate", "Upgraded AGENT.md with media sending instructions")
}

View file

@ -0,0 +1,70 @@
package migrate
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestUpgradeAgentMediaSection(t *testing.T) {
t.Run("appends media section to existing AGENT.md", func(t *testing.T) {
workspace := t.TempDir()
agentPath := filepath.Join(workspace, "AGENT.md")
os.WriteFile(agentPath, []byte("# Agent Instructions\n\nBe helpful."), 0644)
UpgradeWorkspace(workspace)
data, err := os.ReadFile(agentPath)
if err != nil {
t.Fatalf("reading AGENT.md: %v", err)
}
content := string(data)
if !strings.Contains(content, "## Media & File Sending") {
t.Error("expected media section to be appended")
}
if !strings.Contains(content, "You CAN send files directly") {
t.Error("expected media instructions in content")
}
// Original content preserved
if !strings.Contains(content, "# Agent Instructions") {
t.Error("original content should be preserved")
}
})
t.Run("idempotent — does not duplicate section", func(t *testing.T) {
workspace := t.TempDir()
agentPath := filepath.Join(workspace, "AGENT.md")
os.WriteFile(agentPath, []byte("# Agent Instructions\n\nBe helpful."), 0644)
UpgradeWorkspace(workspace)
UpgradeWorkspace(workspace)
data, _ := os.ReadFile(agentPath)
count := strings.Count(string(data), "## Media & File Sending")
if count != 1 {
t.Errorf("expected exactly 1 media section, got %d", count)
}
})
t.Run("skips when AGENT.md does not exist", func(t *testing.T) {
workspace := t.TempDir()
// No AGENT.md created — should not panic or error
UpgradeWorkspace(workspace)
})
t.Run("skips when section already present", func(t *testing.T) {
workspace := t.TempDir()
agentPath := filepath.Join(workspace, "AGENT.md")
original := "# Agent\n\n## Media & File Sending\n\nAlready here."
os.WriteFile(agentPath, []byte(original), 0644)
UpgradeWorkspace(workspace)
data, _ := os.ReadFile(agentPath)
if string(data) != original {
t.Error("file should not be modified when section already exists")
}
})
}