feat: add macOS Catalina, ARMv7 support, and fix history compression
This commit is contained in:
parent
f1223eec42
commit
6e1deb8945
9 changed files with 244 additions and 11 deletions
40
.github/workflows/release.yml
vendored
40
.github/workflows/release.yml
vendored
|
|
@ -100,3 +100,43 @@ jobs:
|
|||
gh release edit "${{ inputs.tag }}" \
|
||||
--draft=${{ inputs.draft }} \
|
||||
--prerelease=${{ inputs.prerelease }}
|
||||
|
||||
release-darwin-legacy:
|
||||
name: Build darwin/amd64 (macOS 10.15+ / Catalina)
|
||||
needs: [create-tag, release]
|
||||
runs-on: macos-13
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout tag
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Setup Go from go.mod
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Build darwin/amd64 legacy binary
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
CGO_CFLAGS: "-mmacosx-version-min=10.15"
|
||||
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
|
||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
||||
run: make build-darwin-legacy
|
||||
|
||||
- name: Create archive
|
||||
run: |
|
||||
cd build
|
||||
tar -czf picoclaw_Darwin_x86_64_catalina.tar.gz picoclaw-darwin-amd64-catalina
|
||||
|
||||
- name: Upload to release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload "${{ inputs.tag }}" \
|
||||
build/picoclaw_Darwin_x86_64_catalina.tar.gz \
|
||||
--clobber
|
||||
|
|
|
|||
|
|
@ -31,10 +31,16 @@ builds:
|
|||
- s390x
|
||||
- mips64
|
||||
- arm
|
||||
goarm:
|
||||
- "7"
|
||||
main: ./cmd/picoclaw
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
|
||||
dockers_v2:
|
||||
- id: picoclaw
|
||||
|
|
|
|||
20
Makefile
20
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: all build install uninstall clean help test
|
||||
.PHONY: all build build-all build-darwin-legacy install uninstall clean help test
|
||||
|
||||
# Build variables
|
||||
BINARY_NAME=picoclaw
|
||||
|
|
@ -86,12 +86,30 @@ build-all: generate
|
|||
@mkdir -p $(BUILD_DIR)
|
||||
GOOS=linux GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(CMD_DIR)
|
||||
GOOS=linux GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(CMD_DIR)
|
||||
GOOS=linux GOARCH=arm GOARM=7 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-armv7 ./$(CMD_DIR)
|
||||
GOOS=linux GOARCH=loong64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-loong64 ./$(CMD_DIR)
|
||||
GOOS=linux GOARCH=riscv64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR)
|
||||
GOOS=darwin GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 ./$(CMD_DIR)
|
||||
GOOS=darwin GOARCH=arm64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./$(CMD_DIR)
|
||||
GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
|
||||
@echo "All builds complete"
|
||||
|
||||
## build-darwin-legacy: Build darwin/amd64 binary compatible with macOS 10.15 (Catalina)+
|
||||
## Requires building natively on a macOS host (Intel or Rosetta). Uses CGO with
|
||||
## -mmacosx-version-min=10.15 so that macOS 12+ symbols are weak-linked and the
|
||||
## binary runs on macOS 10.15 and later.
|
||||
build-darwin-legacy: generate
|
||||
@echo "Building $(BINARY_NAME) for darwin/amd64 (macOS 10.15+ compatible)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
CGO_ENABLED=1 \
|
||||
CGO_CFLAGS="-mmacosx-version-min=10.15" \
|
||||
CGO_LDFLAGS="-mmacosx-version-min=10.15" \
|
||||
MACOSX_DEPLOYMENT_TARGET=10.15 \
|
||||
GOOS=darwin GOARCH=amd64 \
|
||||
$(GO) build $(GOFLAGS) $(LDFLAGS) \
|
||||
-o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64-catalina ./$(CMD_DIR)
|
||||
@echo "Legacy darwin build complete: $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64-catalina"
|
||||
|
||||
## install: Install picoclaw to system and copy builtin skills
|
||||
install: build
|
||||
@echo "Installing $(BINARY_NAME)..."
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -149,6 +149,39 @@ make build-all
|
|||
make install
|
||||
```
|
||||
|
||||
### macOS 10.15 (Catalina) Compatibility
|
||||
|
||||
> [!NOTE]
|
||||
> Official precompiled Darwin x86_64 binaries target macOS 12.0+. A separate
|
||||
> **`picoclaw_Darwin_x86_64_catalina.tar.gz`** asset is published with every
|
||||
> release specifically for macOS 10.15 (Catalina) and later.
|
||||
|
||||
**Requirements for a local Catalina-compatible build:**
|
||||
|
||||
| Requirement | Version |
|
||||
|---|---|
|
||||
| macOS host | 10.15 or later (Intel or Rosetta) |
|
||||
| Xcode Command Line Tools | any recent version |
|
||||
| Go toolchain | bundled via `go.mod` (`go install`) |
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sipeed/picoclaw.git
|
||||
cd picoclaw
|
||||
make deps
|
||||
|
||||
# Produces build/picoclaw-darwin-amd64-catalina
|
||||
make build-darwin-legacy
|
||||
```
|
||||
|
||||
This target sets `CGO_ENABLED=1`, `CGO_CFLAGS=-mmacosx-version-min=10.15`, and
|
||||
`MACOSX_DEPLOYMENT_TARGET=10.15` so that any macOS 12+ symbols are weak-linked
|
||||
and the binary loads correctly on macOS 10.15.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `make build-darwin-legacy` must be run on a **macOS host** (Intel or Apple
|
||||
> Silicon with Rosetta). Cross-compiling this target from Linux is not supported
|
||||
> because it requires Clang/Apple's linker.
|
||||
|
||||
## 🐳 Docker Compose
|
||||
|
||||
You can also run PicoClaw using Docker Compose without installing anything locally.
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -27,7 +27,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
|
|
|
|||
|
|
@ -214,31 +214,69 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message
|
|||
}
|
||||
|
||||
sanitized := make([]providers.Message, 0, len(history))
|
||||
// Index (into sanitized) of the last assistant message that carried ToolCalls.
|
||||
// -1 means no such message has been seen yet.
|
||||
lastCallAssistantIdx := -1
|
||||
|
||||
for _, msg := range history {
|
||||
switch msg.Role {
|
||||
case "tool":
|
||||
if len(sanitized) == 0 {
|
||||
logger.DebugCF("agent", "Dropping orphaned leading tool message", map[string]interface{}{})
|
||||
if lastCallAssistantIdx < 0 {
|
||||
logger.DebugCF("agent", "Dropping orphaned tool message: no preceding tool-call assistant",
|
||||
map[string]interface{}{"tool_call_id": msg.ToolCallID})
|
||||
continue
|
||||
}
|
||||
last := sanitized[len(sanitized)-1]
|
||||
if last.Role != "assistant" || len(last.ToolCalls) == 0 {
|
||||
logger.DebugCF("agent", "Dropping orphaned tool message", map[string]interface{}{})
|
||||
// Any user message or any assistant message between the tool-call
|
||||
// assistant and the current position means this result belongs to a
|
||||
// now-lost turn and should be dropped.
|
||||
interveningTurn := false
|
||||
for i := lastCallAssistantIdx + 1; i < len(sanitized); i++ {
|
||||
if r := sanitized[i].Role; r == "user" || r == "assistant" {
|
||||
interveningTurn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if interveningTurn {
|
||||
logger.DebugCF("agent", "Dropping orphaned tool message: intervening turn",
|
||||
map[string]interface{}{"tool_call_id": msg.ToolCallID})
|
||||
continue
|
||||
}
|
||||
// ID-based check: the ToolCallID must match one of the tool calls in
|
||||
// the associated assistant message. This catches the case where a
|
||||
// mid-compression cut left a result whose parent call was dropped but
|
||||
// another assistant-with-calls happened to precede it in the kept half.
|
||||
if msg.ToolCallID != "" {
|
||||
found := false
|
||||
for _, tc := range sanitized[lastCallAssistantIdx].ToolCalls {
|
||||
if tc.ID == msg.ToolCallID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
logger.DebugCF("agent", "Dropping orphaned tool message: ID mismatch",
|
||||
map[string]interface{}{"tool_call_id": msg.ToolCallID})
|
||||
continue
|
||||
}
|
||||
}
|
||||
sanitized = append(sanitized, msg)
|
||||
|
||||
case "assistant":
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
if len(sanitized) == 0 {
|
||||
logger.DebugCF("agent", "Dropping assistant tool-call turn at history start", map[string]interface{}{})
|
||||
logger.DebugCF("agent", "Dropping assistant tool-call turn at history start",
|
||||
map[string]interface{}{})
|
||||
continue
|
||||
}
|
||||
prev := sanitized[len(sanitized)-1]
|
||||
if prev.Role != "user" && prev.Role != "tool" {
|
||||
logger.DebugCF("agent", "Dropping assistant tool-call turn with invalid predecessor", map[string]interface{}{"prev_role": prev.Role})
|
||||
logger.DebugCF("agent", "Dropping assistant tool-call turn with invalid predecessor",
|
||||
map[string]interface{}{"prev_role": prev.Role})
|
||||
continue
|
||||
}
|
||||
// Record where this assistant lands so tool-result validation can
|
||||
// reference it. len(sanitized) is the index after append.
|
||||
lastCallAssistantIdx = len(sanitized)
|
||||
}
|
||||
sanitized = append(sanitized, msg)
|
||||
|
||||
|
|
|
|||
|
|
@ -773,6 +773,9 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
|
|||
newHistory = append(newHistory, keptConversation...)
|
||||
newHistory = append(newHistory, history[len(history)-1]) // Last message
|
||||
|
||||
// Sanitize tool pairs in case the mid-point cut landed inside a paired turn.
|
||||
newHistory = sanitizeToolPairs(newHistory)
|
||||
|
||||
// Update session
|
||||
agent.Sessions.SetHistory(sessionKey, newHistory)
|
||||
agent.Sessions.Save(sessionKey)
|
||||
|
|
@ -930,10 +933,105 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
|
|||
if finalSummary != "" {
|
||||
agent.Sessions.SetSummary(sessionKey, finalSummary)
|
||||
agent.Sessions.TruncateHistory(sessionKey, 4)
|
||||
|
||||
// The truncation window boundary may have landed inside a tool-call/result
|
||||
// pair. Sanitize now so the persisted history is clean for every subsequent
|
||||
// request and does not trigger provider API errors.
|
||||
remaining := agent.Sessions.GetHistory(sessionKey)
|
||||
if sanitized := sanitizeToolPairs(remaining); len(sanitized) != len(remaining) {
|
||||
agent.Sessions.SetHistory(sessionKey, sanitized)
|
||||
}
|
||||
|
||||
agent.Sessions.Save(sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeToolPairs ensures every assistant message with ToolCalls has matching
|
||||
// tool results, and every tool result has a corresponding tool_call in an
|
||||
// assistant message. Orphaned messages are removed to prevent provider API
|
||||
// errors (e.g. "tool_use ids were provided that do not have a tool_use block")
|
||||
// that arise when history compression cuts in the middle of a paired turn.
|
||||
//
|
||||
// For assistant messages where only SOME tool calls have results, the unmatched
|
||||
// calls are stripped from the ToolCalls slice while the rest of the message
|
||||
// (including any text content) is preserved.
|
||||
func sanitizeToolPairs(messages []providers.Message) []providers.Message {
|
||||
if len(messages) == 0 {
|
||||
return messages
|
||||
}
|
||||
|
||||
// Pass 1: collect all tool_call IDs and tool_result IDs present.
|
||||
toolCallIDs := make(map[string]bool)
|
||||
toolResultIDs := make(map[string]bool)
|
||||
for _, m := range messages {
|
||||
for _, tc := range m.ToolCalls {
|
||||
if tc.ID != "" {
|
||||
toolCallIDs[tc.ID] = true
|
||||
}
|
||||
}
|
||||
if m.Role == "tool" && m.ToolCallID != "" {
|
||||
toolResultIDs[m.ToolCallID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: filter and repair.
|
||||
result := make([]providers.Message, 0, len(messages))
|
||||
dropped := 0
|
||||
for _, m := range messages {
|
||||
switch m.Role {
|
||||
case "tool":
|
||||
// Drop tool results whose tool_call is not present in any assistant message.
|
||||
if m.ToolCallID != "" && !toolCallIDs[m.ToolCallID] {
|
||||
dropped++
|
||||
continue
|
||||
}
|
||||
result = append(result, m)
|
||||
|
||||
case "assistant":
|
||||
if len(m.ToolCalls) == 0 {
|
||||
result = append(result, m)
|
||||
continue
|
||||
}
|
||||
// Partition ToolCalls by whether their result exists.
|
||||
matched := make([]providers.ToolCall, 0, len(m.ToolCalls))
|
||||
for _, tc := range m.ToolCalls {
|
||||
if toolResultIDs[tc.ID] {
|
||||
matched = append(matched, tc)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case len(matched) == len(m.ToolCalls):
|
||||
result = append(result, m) // all matched: keep as-is
|
||||
case len(matched) > 0:
|
||||
// Partial match: keep message with only the matched calls.
|
||||
stripped := m
|
||||
stripped.ToolCalls = matched
|
||||
result = append(result, stripped)
|
||||
dropped += len(m.ToolCalls) - len(matched)
|
||||
case m.Content != "":
|
||||
// No matched calls but has text: keep text only.
|
||||
result = append(result, providers.Message{Role: "assistant", Content: m.Content})
|
||||
dropped += len(m.ToolCalls)
|
||||
default:
|
||||
// No content, no matched calls: drop entirely.
|
||||
dropped++
|
||||
}
|
||||
|
||||
default:
|
||||
result = append(result, m)
|
||||
}
|
||||
}
|
||||
|
||||
if dropped > 0 {
|
||||
logger.WarnCF("agent", "Sanitized orphaned tool pairs from history", map[string]interface{}{
|
||||
"dropped_items": dropped,
|
||||
"before": len(messages),
|
||||
"after": len(result),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// summarizeBatch summarizes a batch of messages.
|
||||
func (al *AgentLoop) summarizeBatch(ctx context.Context, agent *AgentInstance, batch []providers.Message, existingSummary string) (string, error) {
|
||||
prompt := "Provide a concise summary of this conversation segment, preserving core context and key points.\n"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//go:build !amd64 && !arm64 && !riscv64 && !mips64 && !ppc64
|
||||
//go:build !amd64 && !arm64 && !riscv64 && !mips64 && !ppc64 && !arm
|
||||
|
||||
package channels
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//go:build amd64 || arm64 || riscv64 || mips64 || ppc64
|
||||
//go:build amd64 || arm64 || riscv64 || mips64 || ppc64 || arm
|
||||
|
||||
package channels
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue