The embedding_cache table existed in the schema but was never queried.
IndexDocument() now batch-checks the cache before calling the embedding
API, writes fresh results back, and merges cached + fresh vectors.
Graceful fallback: if cache lookup fails, falls through to full API call.
Closes#326
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix ErrorBoundary retry to remount children via Fragment key counter
- Guard PieChart against zero-total slices and single-slice paddingAngle
- Disable Recharts animations to prevent state corruption on rapid filter
- Add per-section ErrorBoundary in usage page and per-donut isolation
- Fix Brush key to be content-aware for proper reset on filter change
- Fix chat page flex containment chain (overflow-hidden, min-h-0, shrink-0)
- Fix list marker color: use currentColor instead of hardcoded foreground
- Add hover download button with filename/size on chat image thumbnails
- Add download button and file info bar to image lightbox overlay
- Add hover download button on inline markdown images
- Increase agent selector dropdown height on desktop (sm:max-h-80)
- Select default agent (is_default) instead of first in list
- Increase session title generation timeout to 120s
* fix(skills): parse YAML multiline blocks and lists in skill frontmatter
parseSimpleYAML and ParseSkillFrontmatter only handled single-line
key: value pairs. YAML multiline block scalars (|, >) and list values
(- item) were parsed incorrectly:
description: |
Multi-line text here...
was stored as description="|" (literal pipe character), and indented
continuation lines were misinterpreted as new key-value pairs.
Fix: parseSimpleYAML now tracks indented continuation lines after
block scalar indicators (|, >, |-, >-) and empty-value keys (for
list blocks). Continuation lines are joined with spaces. List items
(- value) have the prefix stripped.
ParseSkillFrontmatter now delegates to parseSimpleYAML instead of
duplicating the parsing logic.
* fix(skills): preserve empty-value keys in parseSimpleYAML
When a key has no value and no indented continuation (e.g. "slug:"),
flushBlock now stores "" instead of silently dropping the key.
Maintains backward compatibility with the previous parser behavior.
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
The .runtime directory on the data volume may be owned by goclaw:goclaw
(from older images or Docker volume initialization). pkg-helper runs as
root but without CAP_DAC_OVERRIDE, so it cannot create files in
goclaw-owned directories. This caused persistAdd() to fail silently —
runtime-installed system packages (bash, pandoc, etc.) were lost on
container recreate.
Fix: set .runtime directory ownership to root:goclaw (mode 0750) so
pkg-helper can write apk-packages while goclaw can still traverse.
Three layers for robustness:
- Dockerfile: pre-create .runtime with correct split ownership in image
- docker-entrypoint.sh: fix ownership on existing volumes (upgrade path)
- pkg-helper: self-healing ensurePersistDir() at startup as defense-in-depth
Subdirs (pip/, npm-global/, pip-cache/) remain goclaw-owned since those
are written by the app process, not pkg-helper.
Fixes#323
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
* fix(skills): use DB file_path for skill directory resolution + list archived skills
Two fixes:
1. Skills store methods (ListSkills, ListAllSkills, GetSkill, GetSkillByID,
LoadSkill) constructed BaseDir from s.baseDir + slug + version, ignoring
the file_path column stored in the database. When GOCLAW_DATA_DIR differs
from the directory used during initial skill seeding, this caused
dependency rescans to look in the wrong directory, find no scripts, and
silently skip — leaving missing_deps stale in the database even after
successful pip/npm installs.
Fix: buildSkillInfo now accepts an optional filePath parameter from the
DB and uses it as the authoritative BaseDir when set.
2. ListSkills only returned skills with status='active', hiding archived
skills from the admin UI. Skills with missing dependencies are archived
on seed, making them invisible — admins couldn't see them to install
deps and reactivate. Now includes archived skills in the listing.
* fix(skills): use 'deleted' status for soft-delete, distinct from 'archived'
DeleteSkill previously set status='archived', which is also used for skills
with missing dependencies. Now that ListSkills includes archived skills
(so admins can see and fix missing deps), soft-deleted skills were
reappearing after page refresh.
Use status='deleted' for user-deleted skills to keep them hidden while
allowing archived (missing deps) skills to remain visible in the admin UI.
* fix(skills): complete file_path resolution + exclude deleted from rescan
- ListAllSkills: add `status != 'deleted'` to prevent rescan from
resurrecting user-deleted skills
- ListAccessible: add file_path to SELECT so runtime skill resolution
uses DB path instead of stale baseDir
- SearchByEmbedding: use DB file_path when available for path resolution
- CreateSkillManaged: ON CONFLICT visibility reset now matches both
'archived' and 'deleted' statuses
- Update delete result message and status comment
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
MCP tools that accept no parameters return {type: "object"} without a
properties field, which is valid per JSON Schema spec. However, OpenAI
API strictly requires "properties" on object schemas and rejects
requests with HTTP 400: "object schema missing properties".
Add default empty properties map when type is "object" and the MCP
server provides none.
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
* fix(tools): resolve Windows build errors and harden filesystem security
* fix(tools): remove stale Windows skip in hardlink test
Now that checkHardlink uses GetFileInformationByHandle on Windows,
the test should run on all platforms.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
Post-turn team task dispatch was only wired for WS chat.send path.
Now also wired for /v1/chat/completions, /v1/responses, and
/v1/agents/{id}/wake HTTP endpoints so team tasks dispatch correctly
from all API entry points.
WS handleSend called loop.Run() directly without injecting
PendingTeamDispatch into context, bypassing the gate that requires
search/list before team task creation (ptd was nil → gate skipped).
- Add InjectTeamDispatch helper: creates ptd, returns drain func
that dispatches pending tasks + releases team lock (panic-safe)
- Wire PostTurnProcessor to ChatMethods via setter
- defer drainTeamDispatch() in handleSend goroutine
- ExtractorChain now supports per-entry retry (max_retries) and chain-level
timeout (context.WithTimeout), matching MediaProviderChain pattern
- Low quality content breaks out of retry (not transient), errors retry
- Fix ResolveProviderType: skip generic "openai_compat" DB type so
OpenRouter routes via name-based inference to correct /chat/completions
endpoint instead of falling through to /images/generations (caused 404)
- Remove dead "openai_compat" entry from dbTypeToMediaType map
- Seed data: defuddle default max_retries=2
- Add team.leader.processing backend event (emitted from onDrain before
announce run starts) so all WS clients show processing status
- Capture announce run.started events to stream leader's summarization
in real-time (thinking, tool calls, text)
- Add leader_processing phase to ActivityIndicator and ChatTopBar
- isBusy stays true during leader processing → stop button visible
* fix(codex,memory): sanitize tool call IDs, fix memory delete, add GPT-5.4
- codex_build: sanitize colons in tool call IDs before sending to
Responses API (OpenAI rejects non-alphanumeric chars except _ and -)
- memory_docs: check RowsAffected on delete — return error instead of
silent 200 when no document matched
- memory UI: pass document's agent_id to delete call so deletion works
from the "all agents" view (previously sent undefined as agent ID)
- codex models: add GPT-5.4 and GPT-5.4 Mini, update default model
* fix(codex): update default model assertion in test to gpt-5.4
* fix(codex): regex sanitize all invalid chars in tool call IDs, restore GPT-5.1 models
- toFcID: replace all non-alphanumeric/underscore/dash chars (not just
colons) to prevent future Responses API rejections
- Restore GPT-5.1 Codex Max and Codex Mini in model dropdown to avoid
breaking existing user configurations
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
Mitigate XXE Billion Laughs (entity expansion DoS) in DOCX parsing.
5 ET.parse() calls used vulnerable stdlib parser instead of defusedxml
which was already a project dependency.
LLMs hallucinate file paths in responses (e.g. ./system/generated/file.png)
when the actual file is at /app/workspace/teams/.../generated/file.png.
Backend: /v1/files/ now falls back to searching the workspace directory
tree by basename when exact path isn't found. Generated filenames
(goclaw_gen_*) include nanosecond timestamps and are globally unique.
Frontend: relative file paths now use just the basename in the /v1/files/
URL, letting the backend fallback find the actual file. Auth token
appended as ?token= query param for authenticated file access.
File preview dialog was showing {"error":"unauthorized"} because
fetch() calls to /v1/files/ lacked the Bearer token. Now appends
?token= query param (accepted by the files endpoint) to all resolved
file URLs including relative path detection.
* feat(discord): add writer management commands (!addwriter, !removewriter, !writers)
Port Telegram's file writer commands to Discord with guild-wide scope.
Uses wildcard scope (guild:{guildID}:*) to match per-user contexts.
Also fixes system prompt injection to find guild-wide writers.
* fix(discord): use per-user scope for writer self-check
CheckPermission with guild-wide wildcard scope (guild:{guildID}:*)
fails to match auto-bootstrapped per-user perms. Use per-user scope
(guild:{guildID}:user:{senderID}) which matches both per-user exact
and guild-wide wildcard stored patterns via matchWildcard.
* fix(discord): deduplicate writers in prompt injection, add context timeout
- Deduplicate writers by UserID after merging guild-wide and per-user
scope results in buildGroupWriterPrompt to prevent duplicates when a
user has both grant types.
- Add 10s context timeout to Discord writer command handlers instead of
using context.Background() to prevent DB calls from hanging.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- Add isBusy derived state (isRunning || teamTasks active) for stop button
and top bar status — reflects team task activity after main agent completes
- Align ActiveRunZone with MessageBubble layout: bubble wrapper for streaming
text, compact tool cards with divide-y borders
- Detect relative file paths (./path) in markdown and render as file chips
routed through /v1/files/ endpoint
- Fix auto-scroll jitter during streaming by using instant scroll and
ignoring programmatic scroll events in the near-bottom check
fix: Windows deployment — broken symlinks, missing packages, dep scanner false positives
- Dockerfile: add pdfplumber, pdf2image, anthropic, poppler-utils, bash to ENABLE_FULL_SKILLS
- Dockerfile: resolve broken git symlinks from Windows clone (core.symlinks=false)
- Dockerfile: strip CRLF from docker-entrypoint.sh at build time
- .gitattributes: enforce LF line endings for shell scripts
- dep_scanner.go: track nested subdirectories + scripts dir basename as local modules
Reorder team task reminders to inject before the user message instead of
after, ensuring the user's message is always the last in the messages
array. Proxy providers (LiteLLM, OpenRouter) reject requests ending with
an assistant-role message (unsupported prefill).
Also add a safety net in the OpenAI provider to strip any trailing
assistant message that might slip through in the future.
Remove leading tabs that wasted tokens in LLM system prompt.
Restore timezone example (Asia/Ho_Chi_Minh) and channel auto-fill info
that were dropped in #307.
Replace loose string matching ("502", "503", "504") with telegoapi.Error
type assertion — prevents false positives from error messages that
happen to contain these substrings. Also classifies 5xx as
post-connect errors since the server received the request.
- Replace sequential polling dispatch with semaphore-bounded goroutines
(max 20 concurrent handlers) to prevent polling hangs from slow media
downloads without risking unbounded goroutine growth
- Add sync.WaitGroup for graceful shutdown — Stop() waits for in-flight
handlers with a 15s timeout
- Add progressReader that aborts downloads stalled for 60s (no data)
- Return MediaError from resolveMedia so callers know what failed and why
- Annotate model content with error context when media is skipped
(e.g. "[sent media (image) — skipped: exceeds 20 MB limit]")
- Send Telegram reply to user when their file is too large or download fails
- Use errMediaTooLarge sentinel in all size-check paths for consistent
error classification
Add early detection and graceful degradation when the model exhausts its
iteration budget without producing a text response:
- Inject 75% budget nudge telling model to start summarizing
- Strip all tool schemas on final iteration to force text-only response
- Adaptive web_fetch maxChars (60K→20K→10K) via iteration progress context
- Bump DefaultMaxIterations 20→30 for web-research workloads
- Add userFriendlyError() helper to hide 5xx errors from users
- Add Loader2 animate-spin to all save/update/create/delete buttons
- Add useMinLoading(600ms) to StickySaveBar for visible feedback
- Move toast notifications from components into hooks (single source)
- Remove inline saved/saveError state in favor of toast system
- Add toast to all mutation hooks: agents, channels, skills, builtin-tools,
sessions, teams, storage, tts, cron, providers, config
- Add i18n toast keys for en/vi/zh across all namespaces
- Deduplicate toast calls in team board components
Model confused context-history media tags (<media:image>) with current
message media, triggering read_image on non-existent images. Change
lightweightMediaTags() to use descriptive brackets (e.g. [sent an image])
so LLMs can distinguish history descriptions from actionable media tags.
- Reduce quickClassify threshold from 60 bytes to 15 runes to prevent
false positives (e.g. "làm đơn giản thôi" no longer matches "thôi")
- Use utf8.RuneCountInString for accurate Unicode length check
- Replace substring match with whole-word boundary check to avoid
false positives like "nonstop" matching "stop"
- Remove statusKeywords; only exact "?" is fast-pathed as status query,
all other status queries go through LLM classification
- Increase LLM classify timeout from 5s to 10s
- Differentiate steer (inject into running loop) from newTask (queue
behind active run) instead of treating both as mid-run injection
- Fix TrimLeft → TrimPrefix to strip only one underscore separator
- Add registryName to indexedResult for parallel path cache
- Use registryName for bootstrapToolAllowlist and loopDetector checks
- Add server-side sanitization for ToolCallPrefix input
- Remove dead chatMessages alias and unrelated ParseStripAssistantPrefill
- Remove orphaned stripAssistantPrefill i18n keys
- Add 13 unit tests for StripToolPrefix
* feat(agent): support tool call prefix stripping for proxy providers
Proxy providers like LiteLLM and OpenRouter may prepend a prefix to
tool call names returned by the model (e.g. "proxy_exec" instead of
"exec"). This broke tool policy validation, registry lookup, and
hardcoded name checks for "team_tasks" and "spawn" in both serial
and parallel execution paths.
Add per-agent toolCallPrefix configuration that strips the configured
prefix from incoming tool call names before registry resolution. The
stripping is applied at resolveToolCallName() which is called before
permission checks, registry execution, and spawn/team_tasks detection.
- Add StripToolPrefix() supporting literal ("proxy_") and template
("{tool_name}") patterns
- Add toolCallPrefix to ToolPolicySpec with backward compat from old
"toolPrefix" JSON key
- Fix config save using spread operator to prevent dropping new fields
- Add UI input in Tool Policy section with i18n (en/vi/zh)
* chore: ignore AI tool config directories
Add .gemini/, .claude/, .opencode/ to .gitignore to prevent
committing user-specific AI tool configurations.
The scanner only tracked subdirectories as local modules, so cross-file
imports like `from extract_form_field_info import ...` were incorrectly
reported as missing pip packages, causing install failures.
On first start with a fresh Docker volume, mkdir for .runtime
subdirectories can fail due to a volume initialisation race condition,
causing the container to restart loop. The directories are only needed
for agent-installed packages (pip/npm), so the failure is non-fatal.
Address issues identified in PR #137 with a cleaner approach:
- Sandbox isolation: add SandboxCwd/ResolveSandboxPath helpers to map
filesystem tool paths to agent-scoped container subdirectories,
preventing cross-agent file access via read/write/edit/list tools
- DooD volume mounting: add resolveHostWorkspacePath with multi-strategy
container ID detection (/proc/self/mountinfo, HOSTNAME, os.Hostname)
and 5s timeout on docker inspect
- Sandbox hints: expand from 1 pattern (binary not found) to 6 patterns
(permission denied, network disabled, read-only FS, missing file,
resource limits) with MaybeFsBridgeHint for filesystem tools
- Nginx DNS: add Docker resolver (127.0.0.11) with dynamic upstream
variable to handle backend container IP changes
- MCP args: switch from comma-separated to space-separated parsing
with quote support for --flag="value with spaces" patterns
- Refactor: rename ExecTool.workingDir to workspace for consistency,
extract sandbox.DefaultContainerWorkdir constant