Compare commits

..
5 Commits
16 changed files with 236 additions and 40 deletions
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/lukaszraczylo/claude-mnemonic/pkg/hooks"
"github.com/lukaszraczylo/claude-mnemonic/pkg/sanitize"
)
// Input is the hook input from Claude Code.
@@ -148,14 +149,14 @@ func handleUserPrompt(ctx *hooks.HookContext, input *Input) (string, error) {
obsText += "Key facts:\n"
for _, fact := range facts {
if factStr, ok := fact.(string); ok {
obsText += fmt.Sprintf("- %s\n", factStr)
obsText += fmt.Sprintf("- %s\n", sanitize.StripSystemXML(factStr))
}
}
obsText += "\n"
}
if narrative, ok := obsMap["narrative"].(string); ok && narrative != "" {
obsText += narrative + "\n\n"
obsText += sanitize.StripSystemXML(narrative) + "\n\n"
}
obsTokens := estimateTokens(obsText)
+1 -1
View File
@@ -15,7 +15,7 @@ require (
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82
github.com/stretchr/testify v1.11.1
github.com/sugarme/tokenizer v0.3.0
github.com/yalue/onnxruntime_go v1.25.0
github.com/yalue/onnxruntime_go v1.27.0
golang.org/x/sync v0.19.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
+2
View File
@@ -54,6 +54,8 @@ github.com/sugarme/regexpset v0.0.0-20200920021344-4d4ec8eaf93c h1:pwb4kNSHb4K89
github.com/sugarme/regexpset v0.0.0-20200920021344-4d4ec8eaf93c/go.mod h1:2gwkXLWbDGUQWeL3RtpCmcY4mzCtU13kb9UsAg9xMaw=
github.com/yalue/onnxruntime_go v1.25.0 h1:nlhVau1BpLZ/BYr+WpPZCJRD/WES0qo6dK7aKyyAs3g=
github.com/yalue/onnxruntime_go v1.25.0/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
github.com/yalue/onnxruntime_go v1.27.0 h1:c1YSgDNtpf0WGtxj3YeRIb8VC5LmM1J+Ve3uHdteC1U=
github.com/yalue/onnxruntime_go v1.27.0/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1 +1 @@
1.23.2
1.24.3
@@ -1 +1 @@
1.23.2
1.24.3
@@ -1 +1 @@
1.23.2
1.24.3
@@ -1 +1 @@
1.23.2
1.24.3
+9 -3
View File
@@ -49,6 +49,10 @@ func BuildObservationPrompt(exec ToolExecution) string {
inputJSON, _ := json.MarshalIndent(toolInput, " ", " ")
outputJSON, _ := json.MarshalIndent(toolOutput, " ", " ")
// Strip system XML artifacts from tool data before building prompt
cleanInput := StripSystemXML(string(inputJSON))
cleanOutput := StripSystemXML(string(outputJSON))
timestamp := time.UnixMilli(exec.CreatedAtEpoch).Format(time.RFC3339)
var sb strings.Builder
@@ -58,8 +62,8 @@ func BuildObservationPrompt(exec ToolExecution) string {
if exec.CWD != "" {
sb.WriteString(fmt.Sprintf(" <working_directory>%s</working_directory>\n", exec.CWD))
}
sb.WriteString(fmt.Sprintf(" <parameters>%s</parameters>\n", truncate(string(inputJSON), 3000)))
sb.WriteString(fmt.Sprintf(" <outcome>%s</outcome>\n", truncate(string(outputJSON), 5000)))
sb.WriteString(fmt.Sprintf(" <parameters>%s</parameters>\n", truncate(cleanInput, 3000)))
sb.WriteString(fmt.Sprintf(" <outcome>%s</outcome>\n", truncate(cleanOutput, 5000)))
sb.WriteString("</observed_from_primary_session>")
return sb.String()
@@ -84,8 +88,10 @@ func BuildSummaryPrompt(req SummaryRequest) string {
sb.WriteString("Write progress notes of what was done, what was learned, and what's next. This is a checkpoint to capture progress so far. The session is ongoing - you may receive more requests and tool executions after this summary. Write \"next_steps\" as the current trajectory of work (what's actively being worked on or coming up next), not as post-session future work. Always write at least a minimal summary explaining current progress, even if work is still in early stages, so that users see a summary output tied to each request.\n\n")
if req.LastAssistantMessage != "" {
// Strip system XML artifacts from captured transcript content
cleanMsg := StripSystemXML(req.LastAssistantMessage)
sb.WriteString("Claude's Full Response to User:\n")
sb.WriteString(truncate(req.LastAssistantMessage, 4000))
sb.WriteString(truncate(cleanMsg, 4000))
sb.WriteString("\n\n")
}
+8
View File
@@ -0,0 +1,8 @@
// Package sdk provides SDK agent integration for claude-mnemonic.
package sdk
import "github.com/lukaszraczylo/claude-mnemonic/pkg/sanitize"
// StripSystemXML removes known Claude Code internal XML blocks from text.
// Delegates to the shared sanitize package.
var StripSystemXML = sanitize.StripSystemXML
+10 -10
View File
@@ -13,7 +13,7 @@
{
"name": "claude-mnemonic",
"description": "Persistent memory system for Claude Code - Go implementation with SQLite and ChromaDB vector search",
"version": "0.11.37",
"version": "0.11.47",
"author": {
"name": "lukaszraczylo",
"url": "https://github.com/lukaszraczylo"
@@ -23,25 +23,25 @@
"license": "MIT",
"homepage": "https://github.com/lukaszraczylo/claude-mnemonic",
"releases": {
"latest": "0.11.37",
"latest": "0.11.47",
"versions": {
"0.11.37": {
"releaseDate": "2026-02-11",
"0.11.47": {
"releaseDate": "2026-03-06",
"downloads": {
"darwin-arm64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.11.37/claude-mnemonic_0.11.37_darwin_arm64.tar.gz",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.11.47/claude-mnemonic_0.11.47_darwin_arm64.tar.gz",
"format": "tar.gz",
"sha256": "4f0fa6e2034a56baea3e506fc0697b43aba9393cf03fa560ab40cb9520ff804f"
"sha256": "486ce84e6c5d0dc21f3516cd0f69dae510b74c100aba921eef232dd2b0040bd8"
},
"linux-amd64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.11.37/claude-mnemonic_0.11.37_linux_amd64.tar.gz",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.11.47/claude-mnemonic_0.11.47_linux_amd64.tar.gz",
"format": "tar.gz",
"sha256": "f1138fba85ccfb49541f1c9b0401abd9028f216729cee6784ec0ad1c8f9dde83"
"sha256": "fe1692b789977f5466995ebdd8f2ee1ef3eed01c1121f6f17d8350baaf247dac"
},
"windows-amd64": {
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.11.37/claude-mnemonic_0.11.37_windows_amd64.zip",
"url": "https://github.com/lukaszraczylo/claude-mnemonic/releases/download/v0.11.47/claude-mnemonic_0.11.47_windows_amd64.zip",
"format": "zip",
"sha256": "e05227844ed0123b874285cdec5a365cd17f1a094537fb0309c00694e9c30289"
"sha256": "507de7f2dd07394e06676ff0e569ce2c359d9faa7c5a222335be0b66c7e68ec7"
}
}
}
+63
View File
@@ -0,0 +1,63 @@
// Package sanitize provides content cleaning utilities for stripping
// Claude Code internal XML artifacts from captured text.
package sanitize
import (
"regexp"
"strings"
)
// systemXMLTags lists Claude Code internal XML tags that should be stripped
// from captured content before processing. These are system-level artifacts
// that pollute observations and summaries when stored.
var systemXMLTags = []string{
// Claude Code task/agent system
"task-notification",
// System reminders injected by Claude Code
"system-reminder",
// Claude-mnemonic's own context injection
"relevant-memory",
// Hook output wrappers
"user-prompt-submit-hook",
// Large output persistence
"persisted-output",
// Tool loading system
"available-deferred-tools",
// Fast mode info
"fast_mode_info",
// Anthropic internal
"antml_thinking",
"antml_function_calls",
}
// systemXMLRegexps are compiled regexps for each tag, built once at init.
var systemXMLRegexps []*regexp.Regexp
func init() {
systemXMLRegexps = make([]*regexp.Regexp, len(systemXMLTags))
for i, tag := range systemXMLTags {
// Match opening tag (with optional attributes), content (including newlines), and closing tag
systemXMLRegexps[i] = regexp.MustCompile(`(?s)<` + regexp.QuoteMeta(tag) + `[^>]*>.*?</` + regexp.QuoteMeta(tag) + `>`)
}
}
// StripSystemXML removes known Claude Code internal XML blocks from text.
// This prevents system artifacts like <task-notification>, <system-reminder>,
// and <relevant-memory> from being stored in observations and summaries.
func StripSystemXML(s string) string {
// Quick check: if no angle brackets, nothing to strip
if !strings.Contains(s, "<") {
return s
}
for _, re := range systemXMLRegexps {
s = re.ReplaceAllString(s, "")
}
// Clean up resulting double-blank-lines from removed blocks
for strings.Contains(s, "\n\n\n") {
s = strings.ReplaceAll(s, "\n\n\n", "\n\n")
}
return strings.TrimSpace(s)
}
+113
View File
@@ -0,0 +1,113 @@
package sanitize
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStripSystemXML(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "no XML tags",
input: "Hello, this is plain text with no XML.",
expected: "Hello, this is plain text with no XML.",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "task-notification block",
input: "Before\n<task-notification>\n<task-id>abc123</task-id>\n<status>completed</status>\n<summary>Agent completed</summary>\n<result>Some result text</result>\n</task-notification>\nAfter",
expected: "Before\n\nAfter",
},
{
name: "system-reminder block",
input: "Content before\n<system-reminder>\nSome system reminder text\n</system-reminder>\nContent after",
expected: "Content before\n\nContent after",
},
{
name: "relevant-memory block",
input: "<relevant-memory>\n# Relevant Knowledge\n## 1. Something\n</relevant-memory>\nActual content",
expected: "Actual content",
},
{
name: "system-reminder with attributes",
input: "Before <system-reminder type=\"hook\">reminder text</system-reminder> After",
expected: "Before After",
},
{
name: "multiple different tags",
input: "Start\n<task-notification><status>done</status></task-notification>\nMiddle\n<system-reminder>reminder</system-reminder>\nEnd",
expected: "Start\n\nMiddle\n\nEnd",
},
{
name: "persisted-output block",
input: "Result: <persisted-output>\nLarge output stored at: /tmp/foo\n</persisted-output> done",
expected: "Result: done",
},
{
name: "available-deferred-tools block",
input: "<available-deferred-tools>\nTool1\nTool2\n</available-deferred-tools>\nUser message",
expected: "User message",
},
{
name: "fast_mode_info block",
input: "Text <fast_mode_info>\nFast mode uses same model\n</fast_mode_info> more text",
expected: "Text more text",
},
{
name: "preserves non-system XML",
input: "<observation><type>bugfix</type><title>Fixed thing</title></observation>",
expected: "<observation><type>bugfix</type><title>Fixed thing</title></observation>",
},
{
name: "no angle brackets fast path",
input: "Just plain text without any special characters",
expected: "Just plain text without any special characters",
},
{
name: "collapses triple newlines",
input: "Before\n<system-reminder>x</system-reminder>\n\n\nAfter",
expected: "Before\n\nAfter",
},
{
name: "real-world task notification",
input: "Here is the analysis:\n<task-notification>\n<task-id>a077fd04aed547ce1</task-id>\n<tool-use-id>toolu_01Kw2GwsArQQR1F9aV3VfzhP</tool-use-id>\n<status>completed</status>\n<summary>Agent \"Analyze agency-agents repo structure\" completed</summary>\n<result>Now I have all the information I need. Let me compile a comprehensive report.\n\n## Agency-Agents Directory Exploration Report\n\n### 1. Agent File Count and Organization\n\n**Total Agent Files (excluding strategy docs):** 61 agents across 9 categories</result>\n</task-notification>\nThe repo has 61 agents.",
expected: "Here is the analysis:\n\nThe repo has 61 agents.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := StripSystemXML(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func BenchmarkStripSystemXML(b *testing.B) {
// Text with no XML (fast path)
b.Run("no_xml", func(b *testing.B) {
text := "This is plain text without any XML tags at all."
b.ResetTimer()
for i := 0; i < b.N; i++ {
StripSystemXML(text)
}
})
// Text with system XML to strip
b.Run("with_xml", func(b *testing.B) {
text := "Before\n<task-notification>\n<task-id>abc</task-id>\n<status>done</status>\n</task-notification>\n<system-reminder>reminder text here</system-reminder>\nAfter"
b.ResetTimer()
for i := 0; i < b.N; i++ {
StripSystemXML(text)
}
})
}
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ONNX_VERSION="1.23.2"
ONNX_VERSION="1.24.3"
ASSETS_DIR="internal/embedding/assets/lib"
PLATFORM="${1:-all}"
FORCE_DOWNLOAD=false
+19 -16
View File
@@ -194,29 +194,32 @@ download_release() {
fi
# Verify download integrity via checksum
local checksum_url="${download_url}.sha256"
local checksums_url="https://github.com/${GITHUB_REPO}/releases/download/${version}/checksums.txt"
info "Verifying download integrity..."
if curl -sSL -o "$tmp_dir/checksum.sha256" "$checksum_url" 2>/dev/null; then
if curl -fsSL -o "$tmp_dir/checksums.txt" "$checksums_url" 2>/dev/null; then
local expected_hash actual_hash
expected_hash=$(awk '{print $1}' "$tmp_dir/checksum.sha256")
if command -v shasum &> /dev/null; then
actual_hash=$(shasum -a 256 "$tmp_dir/release.${archive_ext}" | awk '{print $1}')
elif command -v sha256sum &> /dev/null; then
actual_hash=$(sha256sum "$tmp_dir/release.${archive_ext}" | awk '{print $1}')
expected_hash=$(grep "${archive_name}" "$tmp_dir/checksums.txt" | awk '{print $1}')
if [[ -z "$expected_hash" ]]; then
warn "Archive ${archive_name} not found in checksums.txt, skipping verification"
else
warn "No SHA256 tool found (shasum or sha256sum), skipping checksum verification"
actual_hash=""
fi
if [[ -n "$actual_hash" ]]; then
if [[ "$expected_hash" != "$actual_hash" ]]; then
error "Checksum verification failed! Expected: $expected_hash Got: $actual_hash"
if command -v shasum &> /dev/null; then
actual_hash=$(shasum -a 256 "$tmp_dir/release.${archive_ext}" | awk '{print $1}')
elif command -v sha256sum &> /dev/null; then
actual_hash=$(sha256sum "$tmp_dir/release.${archive_ext}" | awk '{print $1}')
else
warn "No SHA256 tool found (shasum or sha256sum), skipping checksum verification"
actual_hash=""
fi
if [[ -n "$actual_hash" ]]; then
if [[ "$expected_hash" != "$actual_hash" ]]; then
error "Checksum verification failed! Expected: $expected_hash Got: $actual_hash"
fi
success "Checksum verified"
fi
success "Checksum verified"
fi
else
warn "No checksum file available at $checksum_url, skipping verification"
warn "No checksum file available, skipping verification"
fi
info "Extracting archive..."
if [[ "$archive_ext" == "zip" ]]; then
if ! command -v unzip &> /dev/null; then
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-mnemonic-dashboard",
"version": "v0.10.5-15-g385d05a-dirty",
"version": "v0.11.43-2-g11fd196-dirty",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-mnemonic-dashboard",
"version": "v0.10.5-15-g385d05a-dirty",
"version": "v0.11.43-2-g11fd196-dirty",
"dependencies": {
"vis-data": "^7.1.9",
"vis-network": "^9.1.9",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mnemonic-dashboard",
"version": "v0.10.5-15-g385d05a-dirty",
"version": "v0.11.43-2-g11fd196-dirty",
"private": true,
"type": "module",
"scripts": {