Compaction summaries were too generic ("provide a concise summary"),
causing loss of task progress, decisions, and identifier corruption
after summarization.
Port from OpenClaw TS (compaction.ts):
- Structured MUST PRESERVE sections: active tasks, progress, last
request, decisions, TODOs, commitments
- Identifier preservation: preserve UUIDs, hashes, URLs, file names
exactly as written (no shortening/reconstruction)
- Prioritize recent context over older history
- Shared prompt constant used by both mid-loop and background
compaction paths
Problem: Agent sessions accumulated 71K+ input tokens (83% history)
because read_file and exec had no output limits. SOUL personality
drowned by massive context.
Changes:
- read_file: add offset/limit params + 50K char output cap with
pagination hints (model can re-read with offset)
- exec/shell: cap output at 30K chars with smart head+tail truncation
(preserves errors/summaries at tail)
- pruning: add per-result 30% context guard, tune softTrimRatio
0.3→0.25 and softTrimMaxChars 4K→3K, add tail-aware soft trim
- mid-loop: allow pruning to re-trigger each iteration (was one-shot)
Design: cap at source, preserve full data in session, prune at
consumption time. read_file offset/limit enables recovery of
truncated content.
Closes#532
- Replace prefix truncation with SHA-256 hash-based shortening for oversized tool call IDs (40-char OpenAI/Azure limit)
- Normalize provider-prefixed model IDs (e.g. openai/o3-mini) before capability checks for temperature and max_completion_tokens
- Add regression tests for ID collision, correlation, and prefixed model routing
* feat(reasoning): add capability-aware effort resolution
- resolve requested reasoning levels against exact model capabilities
- persist requested effort on agents and expose effective effort in traces
- add backend tests for provider models, agent store, and resolution logic
Refs #591
* feat(ui): gate reasoning controls by model capabilities
- only show supported reasoning levels when provider model metadata is available
- preserve expert reasoning selections during async model loading
- surface effective reasoning details in trace dialogs and localized copy
Refs #591
* docs(api): document capability-aware reasoning controls
- describe exact-match capability lookup and downgrade behavior
- update provider model metadata and trace response documentation
- refresh the generated OpenAPI spec for the new reasoning fields
Refs #591
* feat: add provider-first reasoning controls
* docs: refresh PR 593 UI evidence callouts
* refactor: deduplicate reasoning normalize functions and remove PR evidence
- Export NormalizeReasoningEffort/NormalizeReasoningFallback from providers
package; store package now delegates instead of duplicating
- Store reasoning fallback constants alias providers canonical definitions
- Export deriveLegacyThinkingLevel from types/provider.ts; remove local
copies from agent-advanced-dialog and provider-overview
- Remove unused _providerType param from useProviderModels hook
- Fix reasoning debug log to fire for all cases with a reason (not just
non-off efforts)
- Remove docs/pr-593-evidence/ binary screenshots from repo
---------
Co-authored-by: viettranx <viettranx@gmail.com>
Port missing group chat style guidance from OpenClaw TS:
- "Write like a human" — prevents robotic/formal GPT responses
- "Avoid Markdown tables" — GPT tends to spam tables in groups
- "Use real line breaks sparingly"
These instructions exist in OpenClaw TS (groups.ts buildGroupIntro)
but were missing in GoClaw's group prompt.
GPT-4o+ models prioritize "developer" messages over "system" for
instruction adherence. GoClaw was sending "system" for all providers,
causing GPT models to poorly follow SOUL/system prompts.
Map "system" → "developer" only for native OpenAI endpoints
(api.openai.com). Non-OpenAI backends keep "system" role unchanged.
Ported from OpenClaw TS: model-compat.ts → isOpenAINativeEndpoint()
Session tools (sessions_list, session_status, sessions_history, sessions_send)
were using resolveAgentIDString(ctx) which returns the agent UUID, but session
keys are built using agent_key. This caused all session tool operations to fail
silently or return "access denied" for every channel.
Replace resolveAgentIDString() with ToolAgentKeyFromCtx() in all four session
tools. Add fail-closed guard for empty agent key.
* fix(ui): restore provider-owned codex pool inherit state
* docs(pr): add before-after UI evidence
* refactor: remove dead hasProviderDefaults param and harden pool rendering
- Remove unused _hasProviderDefaults parameter from buildDraftRouting
and routingDraftSignature, clean up all call sites and useMemo deps
- Filter deleted providers from selectedPoolProviderNames to prevent
ghost entries when a saved pool member no longer exists
- Add symmetric backend test for inherit + non-nil provider defaults
---------
Co-authored-by: viettranx <viettranx@gmail.com>
Upload handler previously archived skills immediately when deps were
missing. Now calls InstallDeps() first (owner/master tenant only) and
falls back to archive on failure.
Changes:
- Auto-install missing deps during skill upload (same flow as seeder)
- Atomic DB persist: deps state written with CreateSkillManaged in one call
- Per-slug upload mutex prevents concurrent race conditions
- Frontend: warning state (amber triangle) instead of throwing error
- Non-cancellable context for DB write after dep install
- SQLite StoreMissingDeps now works for custom skills (not just system)
- Comprehensive unit + integration tests
Closes#468
- Reject incompatible explicit provider embedding dimensions on create/update
- Ignore previously saved incompatible dimensions at runtime, fall back to 1536
- Share RequiredMemoryEmbeddingDimensions constant from store package
- Remove dimensions input from provider UI (always 1536 per pgvector schema)
- Add HTTP, runtime, and validation unit tests
Closes#548
Supersedes #410
Telegram: senderLabel now includes full name alongside username, e.g.
[From: @nguyennlt (Nguyễn Trần)] instead of just [From: @nguyennlt].
Also uses FirstName + LastName when no username is set.
WhatsApp: add missing [From:] annotation — was the only channel not
annotating sender identity for the LLM.
## Summary
- Agent rename now updates Name field in IDENTITY.md (agent-level + per-user copies for open agents) via both HTTP and WS paths
- Fixes lossy identity rebuild: previous logic reconstructed IDENTITY.md from only Name/Emoji/Avatar, silently dropping LLM-written fields (Creature, Purpose, Vibe, etc.)
- Uses targeted field replacement (bootstrap.UpdateIdentityField) preserving original formatting
- Adds ListUserContextFilesByName to AgentContextStore (PG + SQLite)
- Fixes LastIndex bug in UpdateIdentityField where values containing colons (e.g. Avatar URLs) would break field replacement
- Removes ~335 lines of dead config.json fallback code (agentStore is always set)
- Adds 9 unit tests for UpdateIdentityField covering plain/markdown formats, URLs, edge cases
* fix(cron): Run Now executes correctly and reset stale running status (#568)
- PG store RunJob now sets next_run_at = NULL before executeOneJob so
loadClaimedJob (which requires next_run_at IS NULL) succeeds instead
of silently skipping the forced run
- recomputeStaleJobs on startup resets last_status 'running' → 'interrupted'
for jobs that were mid-execution when the server crashed
- UI: add typed CronJobPatch interface matching backend store.CronJobPatch
(deliverChannel/deliverTo/stateless) — replaces Record<string, unknown>
on updateJob and onUpdate props across cron detail components
- UI: fix enabled/disabled label in overview tab to reflect local state
instead of stale job prop
* fix(cron): add error handling for ExecContext calls per code review
- RunJob: return error if claiming job (next_run_at = NULL update) fails
instead of silently proceeding to executeOneJob which would then skip
- recomputeStaleJobs: log warning on failure, log count of reset jobs
- Add log/slog import to cron_exec.go
* fix(providers): claude-cli ChatSync image format mismatch (#577)
When images are present, Chat() now uses stream-json output format to
match the stream-json input format required by Claude CLI >= v2.1.87.
Also adds concurrent execution guard to RunJob() preventing
double-execution when Run Now is clicked rapidly.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- Fix hardcoded dark edge color on deselect — use neutral #64748b
- Reduce graph fetch limit 200→50 to match component render cap
- Fix O(n*m) node lookup on theme change — use Map for O(1)
- Remove double fitView (prop + manual call)
- Add KGEdgeData interface replacing `as any` casts for type safety
- Use entityMap (useMemo Map) for O(1) entity lookup instead of O(n) find
- Pre-compute EDGE_STYLE_DEFAULT/FADED constants to reduce GC pressure
- Skip edge object recreation when style unchanged during selection
- Wrap EntityNode in memo() with memoized container style
- Cap force simulation ticks at 200 for graphs with 100+ nodes
Two fixes:
1. Remove assistant prefill from team task reminders. The injected
[user]+[assistant]+[user] pattern caused LLMs to treat the canned
ack as "turn complete", returning NO_REPLY for every user message
in group sessions with active tasks. Reminders are now merged into
the user message as prefix tags.
2. Add PeerKind propagation to team notification routing. TaskTicker
and progress notifications were missing PeerKind on InboundMessage,
causing them to route to phantom DM sessions instead of the correct
group session. PeerKind is now carried through event payloads,
notify queue metadata, and all inbound message publications.
Extract wake_heartbeat and stateless from JSON payload into first-class
columns on cron_jobs. Adds migration 000033 with backfill from existing
payload data. Updates PG + SQLite stores, RPC handlers, and UI i18n.
Root cause: runLoop() had no recover() — panics killed the goroutine while
leaving the session queue's activeRuns entry orphaned. Subsequent messages
to the same session would hang indefinitely (zombie state).
Fix: Two-layer panic recovery:
1. loop.go: runLoop() now returns error on panic instead of crashing goroutine,
allowing normal cleanup flow (session queue, traces, events)
2. queue.go: executeRun() defense-in-depth recovery ensures activeRuns cleanup
and scheduleNext() even if agent-level recovery fails
CreateProvider now uses ON CONFLICT (tenant_id, name) DO UPDATE instead of
plain INSERT. When a provider with the same name already exists (e.g. orphaned
after agent deletion), it updates the existing record instead of failing with
a unique constraint violation.
Applied to both PG and SQLite implementations.
- manager.go: add resolveEnvVars() to expand env:VARNAME in MCP headers (copy-safe)
- manager_env_test.go: 3 test cases for env resolution
- validate.go: add "name" to channelInstanceAllowedFields
- validate_test.go: test channel instance name field retention
- bus/types.go: add TopicAgentDeleted event + AgentDeletedPayload
- agents_delete.go: emit agent:deleted event with provider name for async cleanup
- gateway.go: log-only subscriber for orphaned provider warning (no auto-delete — FK safety)
- resolver.go: fallback to deps.Workspace for master tenant agents with empty ag.Workspace
(fixes filesystem escape where filepath.Join("","system") resolved to /system)
- sessions_list.go (PG): check rowsAffected after UPDATE, INSERT with ON CONFLICT DO UPDATE
when session not yet in DB (cron/heartbeat sessions)
- sessions_ops.go (SQLite): same UPSERT pattern with ? placeholders
- memory_handlers.go: fallback to X-GoClaw-User-Id header when body.user_id is empty
- loop.go: replace blocking range with select+ctx.Done() for parallel tool collection;
document finalization trade-off on early cancel
- loop_cancel_test.go: tests for context cancellation and normal completion paths
- gateway_consumer_post_turn.go: always auto-complete team task in default case,
fallback message when outcome.Result is nil
- gateway.go: call sched.Stop() with 5s drain before context cancel on shutdown
- anthropic_stream.go: check ctx.Err() in scanner loop, bounds check toolCallJSON index,
accumulate signature_delta events into ThinkingSignature
- anthropic_request.go: include signature field in buildRawBlock for thinking blocks
- types.go: add ThinkingSignature field to ChatResponse
- loop_run.go: nil guard on result before FinishTrace access
- gateway.go: defensive ApplyDBSecrets before setupTTS in config reload subscriber
Add env var support for gateway.allowed_origins so container deployments
can set CORS origins reliably even if config file is overwritten by UI
save cycles. Follows same pattern as GOCLAW_OWNER_IDS.
- Backend: resolve real default agent UUID from DB when cron job has no
agent_id, instead of using literal "default" string that fails lookup
- UI: replace free-text agent input with dropdown in create form
- UI: wrap update params in `patch` object matching backend expectation
- UI: send empty string for agentId to explicitly clear agent selection
- UI: add name attribute to Select elements to fix form field warning
- Convert @mentions to <a href="https://t.me/username"> in HTML formatter
(Telegram Bot API does NOT auto-link @username in parse_mode: HTML)
- Protect @mentions from italic regex mangling underscored usernames
- Change read_image/read_audio/read_video system prompt from "REQUIRED"
to conditional — model only reads media when relevant to user's question
- Annotate history media tags so model distinguishes group context images
from current-turn images
* fix: skip JS ES module imports in Python string literals during dep scan
dep_scanner falsely matched JS `import mermaid from '...'` inside Python
f-strings as a pip dependency, causing functional skills to be archived.
Add jsFromStringRe check: if a line matching pyImportRe also contains
`from '...'`/`from "..."` (JS ES module pattern), skip it. Valid Python
`import X` never has `from '...'` on the same line.
Closes#544
* fix: add missing stub method + additional edge case tests
- Add MigrateUserDataOnMerge to stubAgentStore in context_file_interceptor_test.go
(fixes CI build failure — stub was missing new AgentStore interface method)
- Add TestScanFile_MultipleJSImportsInsidePythonString for broader false positive coverage
---------
Co-authored-by: viettranx <viettranx@gmail.com>
dispatchTaskToAgent() constructed InboundMessage without TenantID,
causing DB-level MCP servers to be invisible when dispatching team
tasks from the dashboard. Same root cause as #560.
All channel handlers (Telegram, Discord, Slack, Feishu) bypassed
BaseChannel.HandleMessage() and manually constructed InboundMessage
without setting TenantID. This caused the consumer to fall back to
MasterTenantID, making DB-level MCP server grants invisible on
non-master tenants while config-file MCP servers worked fine.
Closes#560
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The upload() method in http-client.ts was manually building headers
(only Authorization + User-Id), missing X-GoClaw-Tenant-Id. This caused
custom skills to always be created on the Master tenant. Replace with
authHeaders() which includes all required headers.
Closes#557
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add prepare-compose.sh for composible modules, options and overlays
* feat(prepare-compose): add --quiet and --skip-validation flags
Add loud() function for conditional output. Use --quiet to suppress
normal output, --skip-validation to bypass docker compose config check.
* chore: add mise config to gitignore
* chore: add GOCLAW_DIR env var and update context paths
- Add GOCLAW_DIR to prepare-compose.sh for .env generation
- Fix context paths in docker-compose files to use ${GOCLAW_DIR:-.}
- Restore symlinks in compose.d/ and compose.options/
* fix: broken symlink, safe .env update, export COMPOSE_FILE before validate
- Fix compose.d/00-goclaw.yml symlink: docker-compose.yml → ../docker-compose.yml
- Add compose.d/.gitkeep so directory is tracked
- Export COMPOSE_FILE before running docker compose config validation
- Replace rm+sed .env update pattern with safe temp file approach
- Quote GOCLAW_DIR value in .env to handle paths with spaces
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- fix(ui): combobox dropdown flips above input near viewport bottom using
bottom-anchored positioning so filtered items shrink upward correctly
- fix(ui): add onSelect callback to Combobox for commit-only events,
preventing keystroke-adds-badge bug in multi-select mode
- feat(ui): MultiUserPicker shared component for allow/deny user fields
- fix(ui): migrate channel allow_from textarea, team allowed/denied users,
and config allow_from input to MultiUserPicker with unified search
- fix(ui): remove dead getKnownUsers from use-teams hook
- feat: migrate per-user data on contact merge (context files, overrides,
profiles, memory) with DO NOTHING conflict strategy — canonical tenant
user data always wins over newly merged contact data
Admin users were seeing team task notifications from other channels
(Discord, Telegram, Lark) in their web UI chat due to bypassing the
UserID event filter. Added channel check to handleTeamTaskEvent matching
the existing pattern in handleAgentEvent.
- Enable merge UI for linking channel contacts to tenant_users
- Contact → tenant_user resolution with cached lookup (60s TTL)
- MCP per-user credentials via user-keyed connection pool
- Secure CLI per-user credentials with AES-256-GCM encryption
- Unified UserPickerCombobox searching contacts + tenant_users
- Group contact collection with chat title in all channels
- Group permission inheritance via wildcard user_id="*"
- Fix heartbeat using wrong userID in group chats
- Filter internal senders from contact collection
- Add contact_type column (user/group) to channel_contacts
- SQLite schema v2 migration for desktop edition
- GetGatewayURL/IsGatewayReady: use 127.0.0.1 to avoid WebKit resolving
localhost to ::1 (IPv6) when gateway only binds IPv4, causing "Load failed"
- Skip CORS warning for lite edition (localhost-only, no production exposure)
- Skip server IP detection for lite edition (no multi-tenant risk, avoids
leaking user's public IP in logs and unnecessary network calls)