From 61c917d96f6ec08f364e160c6d372309ef7eac93 Mon Sep 17 00:00:00 2001 From: repfigit <--global> Date: Sun, 15 Feb 2026 23:11:20 -0500 Subject: [PATCH] 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 --- cmd/picoclaw/main.go | 6 ++++ pkg/migrate/upgrade.go | 51 +++++++++++++++++++++++++++ pkg/migrate/upgrade_test.go | 70 +++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 pkg/migrate/upgrade.go create mode 100644 pkg/migrate/upgrade_test.go diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 10b53948b..3987e06c0 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -405,6 +405,9 @@ func agentCmd() { os.Exit(1) } + // Apply workspace upgrades before starting the agent + migrate.UpgradeWorkspace(cfg.WorkspacePath()) + msgBus := bus.NewMessageBus() agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) @@ -540,6 +543,9 @@ func gatewayCmd() { os.Exit(1) } + // Apply workspace upgrades before starting the agent + migrate.UpgradeWorkspace(cfg.WorkspacePath()) + msgBus := bus.NewMessageBus() agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) diff --git a/pkg/migrate/upgrade.go b/pkg/migrate/upgrade.go new file mode 100644 index 000000000..1dc9e5392 --- /dev/null +++ b/pkg/migrate/upgrade.go @@ -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") +} diff --git a/pkg/migrate/upgrade_test.go b/pkg/migrate/upgrade_test.go new file mode 100644 index 000000000..358e6588c --- /dev/null +++ b/pkg/migrate/upgrade_test.go @@ -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") + } + }) +}