Update architecture overview with streaming TTS provider layer. Expand tools
system docs with voice/model resolution. Update HTTP and WebSocket RPC
documentation for voice endpoints. Record Phase 02 completion in changelog.
Update CLAUDE.md with voice picker and streaming TTS patterns.
Add VoicePicker with live search and preview button. Integrate into prompt
settings section. Add voice API hooks (useVoices, useRefreshVoices). Localize
UI strings for en/vi/zh. Fix snake_case JSON field mapping (voice_id, preview_url).
Add StreamingTTSProvider interface for audio streaming support. Implement voice
caching with TTL+LRU. Add ElevenLabs model validation and voice list retrieval.
Update module map to list internal/audio/ (unified manager, TTS active,
STT/Music/SFX stubbed/partial) and clarify internal/tts/ as a
24-symbol backward-compat alias layer. Add changelog entry for Phase 1.
Add optional Audio *AudioConfig pointer field on Config with STT and
Music sub-structs. Nil-safe — absent in JSON5 decodes as nil, no
breaking change. cfg.Tts retained unchanged for backward compat.
setupAudioExtras stub wired for Phase 3/4 STT/Music provider
registration.
internal/tts becomes a thin backward-compat alias over internal/audio:
15 type aliases, 6 constants, 5 constructors, 5 compile-time signature
guards. All pre-refactor callers compile unchanged. alias_test.go
enforces symbol coverage and type identity. Old per-provider files
(manager, types, elevenlabs, openai, edge, minimax) are removed in
the same commit to keep history bisectable.
MCP servers with require_user_credentials (e.g. Notion) were defined
but never loaded into the agent's tool registry. Three gaps:
1. getUserMCPTools was defined but never called — add call in
makeBuildFilteredTools before FilterTools runs each iteration.
2. hasMCPTools stayed false when only user-credential servers existed,
so agentToolPolicyWithMCP never injected "group:mcp" into alsoAllow.
Now set true when mcpUserCredSrvs is non-empty.
3. Per-user BridgeTools were registered in the registry but never added
to the "mcp" tool group, so expandSpec("group:mcp") returned empty.
Add MergeToolGroup helper for additive group updates.
Also add debug log when getUserMCPTools skips due to empty userID.
Stale recovery sweeps traces by `start_time < NOW() - threshold`, which
measures trace age rather than inactivity. Any threshold low enough to
be useful (2-10 min) kills legitimate long-running agent runs: research
chains, large code generation, extended shell commands routinely exceed
10 minutes.
Disabled in Start() — function kept in place for easy re-enable once a
`last_span_at` column is added so recovery can gate on "no activity for
N minutes" instead of "started > N min ago".
Trade-off: zombie traces from gateway crashes may remain `running` in
DB. Accepted: primary abort path (router 2-phase + trace.status WS
event) handles the common case; safety-net gap preferred over false
kills of healthy runs.
Integration test RecoverStaleNow() still works (manual trigger, not
loop-dependent) so coverage of the recovery function itself is
preserved for when it's re-enabled.
matchesBinaryDeny used unanchored regex on joined args, causing `-v` pattern
to false-positive on `--version`. Split deny_verbose into matchesBinaryVerbose
with start-anchored per-arg matching: `-v` blocks `-v`, `-vv`, `-v=1` but not
`--version`. deny_args keeps joined matching for multi-token patterns.
v3 pipeline's parallel path (makeExecuteToolRaw) skipped the tool.call
WebSocket event, so web UI and desktop UI silently dropped tool cards
during real-time streaming. Only page refresh (which reloads history)
revealed the tool calls. Both UIs rely on tool.call to create entries
that later tool.result events can update.
Fix: mirror the sequential path's emission in makeExecuteToolRaw.
Bus.Broadcast is RWMutex-guarded, safe to call from parallel goroutines.
Add tests at two layers to prevent regression:
- Pipeline layer (stages_test.go): guards the dispatch contract —
multiple tool calls route through ExecuteToolRaw + ProcessToolResult
rather than ExecuteToolCall. Previously the parallel path had zero
test coverage, which is why this bug escaped.
- Agent layer (loop_pipeline_tool_callbacks_test.go): guards the
emission contract — both sequential and parallel wrappers emit
tool.call with correct payload and routing context. Mutation-verified.
- AddError() now skips broadcast after Finish() to prevent cancelled
goroutines from emitting error events to UI after user stops enrichment
- batchSummarize skips AddError when context is cancelled (expected on stop)
- Rescan always re-enqueues unenriched docs alongside new/updated files,
worker-level dedup prevents double-processing
* fix: handle ignored errors, unsafe type assertions, missing panic recovery
- Cron scheduler (PG + SQLite): check all ExecContext errors in
recomputeStaleJobs, run log insert, job delete, and post-run update.
Previously these errors were silently discarded, which could leave
job state inconsistent without any log trace.
- Discord: use comma-ok type assertions on sync.Map placeholder loads
to prevent potential panics from bare type assertions.
- Slack: use comma-ok type assertions in sweepMaps for dedup and
thread participation eviction to prevent potential panics.
- Feishu: add safego.Recover to WebSocket goroutine so a panic in
the WS client doesn't silently kill the goroutine.
- Agent export: add tenant owner/admin permission check to canExport.
Previously only agent owner and system owner could export — tenant
admins were incorrectly denied.
- Channel health: use errors.Is/errors.As for context.DeadlineExceeded,
net.DNSError, and net.OpError before falling back to string matching.
DNS NXDOMAIN is now correctly classified as non-retryable.
* fix(review): revert export to system-only + add missing rows.Err check
- Revert canExport tenant role check — export/import is restricted to
agent owner and system owner by design
- Add rows.Err() check after recomputeStaleJobs loop in PG cron
(parity with SQLite implementation)
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
* fix(facebook): preserve fb_mode metadata in outbound routing + admin reply detection
Two issues fixed:
1. Messenger auto-reply never delivered because fb_mode metadata was
stripped during outbound message construction. The routing whitelist
in gateway_consumer_normal.go and channels/events.go only copied
thread_id/local_key/group_id — facebook-specific keys (fb_mode,
sender_id, page_id, reply_to_comment_id) were dropped, causing
facebook.Send() to fall into the comment path and fail with
"reply_to_comment_id missing".
2. Added admin reply detection: before sending a bot reply, check via
Graph API if the last page message in the conversation was sent by
an admin (human) rather than the bot itself. Skips bot reply when
admin already responded, preventing duplicate messages.
Uses timestamp comparison with bot's own send history to distinguish
bot-sent vs admin-sent page messages (both have from.id = page_id).
* chore: exclude compiled binary from git
* fix(facebook): ignore bot echoes in messenger cooldown
* fix(facebook): add memory cleanup for admin reply maps and reduce echo window
- Add adminReplied and botSentAt eviction to runDedupCleaner to prevent
unbounded memory growth on high-traffic pages
- Reduce botEchoWindow from 60s to 15s to avoid misclassifying real admin
replies as bot echoes
- Restore doc comments on routingMetaKeys and copyRoutingMeta
- Add cross-reference comment between consumer and events routing key lists
- Simplify admin-reply skip log (remove redundant map re-read)
---------
Co-authored-by: khanhtran <>
Co-authored-by: Plateau Nguyen <nguyennlt.ncc@gmail.com>
Co-authored-by: viettranx <viettranx@gmail.com>
release.yaml is tag-triggered (v[0-9]+.[0-9]+.[0-9]+), not
branch-triggered. There is no go-semantic-release — releases
are manual tag pushes. Merging to main only triggers CI.
SSE progress hook crashed React when backend returned nested error
object from writeError ({"error": {"code": ..., "message": ...}})
instead of flat string. Now parses both formats correctly.
PolicyEngine.IsOwner lacked the "system" fallback that isHTTPOwnerID
already had — when owner_ids is empty, "system" user was rejected from
all backup/restore endpoints. Added consistent fallback logic.
Also added slog.Warn to all silent owner-check rejections across
backup, restore, tenant backup/restore, and S3 handlers.
- Add bgalert package: classify provider errors (auth, billing,
model_not_found), store alert in system_configs, broadcast WS event
- Wire AlertDeps into consolidation (episodic, semantic, dreaming)
and vault enrich workers to report failures after retry exhaustion
- Auto-clear alert when admin changes provider-related system configs
- Add BackgroundErrorBanner component with dismiss + "Fix in Settings"
- Lift settings modal state to AppLayout so banner can open it
- Add EventBackgroundError to admin-only WS event filter
- Add i18n translations (en/vi/zh) for alert messages and reasons
- Replace char-based heuristic (chars/4) with tiktoken BPE for accurate
token counting, especially for non-ASCII content (Vietnamese/Chinese)
- Add pruningEstimator wrapper with tiktoken/fallback dual-path
- Raise default soft trim budget from 3K to 6K chars (3K head + 3K tail)
- Media tools (read_image, read_document, read_audio, read_video) get
higher soft trim budget (8K: 4K head + 4K tail) and skip hard clear
entirely — their vision/audio descriptions are irreplaceable
- Add per-result context guard (Pass 0): force-trim any single tool
result exceeding 30% of context window
Both v3 mid-loop compaction and v2 background summarization were
dropping MediaRefs when summarizing old messages, making previously
shared images/documents permanently inaccessible to the agent.
Now collect up to 30 most recent MediaRefs from compacted messages
and attach them to the summary/first-kept message so they survive
the compaction cycle.
Extract shouldSkipEnrichment() replacing 4 scattered goclaw_gen_ checks.
Filter also skips UUID, hex hash, digit-only, short, and known junk filenames.
Fix cancelFuncs never being populated — Stop() was a no-op leaving UI stuck.
When share_memory=true, memory_search tool used MemoryUserID(ctx)=""
(cross-user) but auto-inject still passed raw userID, limiting episodic
L0 injection to the current user only. Now uses store.MemoryUserID(ctx)
so shared-memory agents get cross-user episodic summaries in auto-inject.
Group-scoped agents could read sessions from other groups via session
tools (sessions_list, sessions_history, session_status, sessions_send)
because they only checked agent_key, not group context. This caused
cron jobs to leak data from unrelated groups into reports.
Add isSessionInScope() guard to all 4 session tools with colon-bounded
chatID matching. New share_sessions setting (default false) controls
cross-group visibility, following the same pattern as share_memory and
share_knowledge_graph. Web UI toggle and i18n strings included.
63 test cases covering guild/DM/group users, realistic Zalo IDs,
boundary exactness, multi-colon chatIDs, and the exact bug scenario.
- Sigma.js graph: restore doc_type coloring (revert Louvain community detection)
- Fix animation flash after FA2 layout finishes by removing post-processing
camera reset and redundant noverlap/compactOrphans in stopLayout
- Fix vault tree "Load more" state bug when filtering by doc_type:
add treeVersion counter to force re-mount and reset auto-expand state
- Fix meta map loss: loadRoot now merges instead of replacing, preserving
subtree entries from previous loadSubtree calls
- Add compact graph DTO endpoints and hooks for KG and vault graphs
- Add semantic zoom tiers, adaptive FA2 settings, and node sizing
- Add tenant-level filesystem path restrictions via system_configs table
- Merge tenant paths with global skills directories in allowedWithTeamWorkspace()
- Propagate tenant paths to subagents via RunContext
- Seed allowed_paths from config.json to system_configs on startup
- Fix TestStoreTask_RaceToClaimSameTask: use composite PK for team members
- Add case-insensitive path comparison on Windows in isPathInside()
- Add allowed_paths config for cross-drive access on Windows
- Wire allowed_paths to read/write/edit/list file tools
- Add POST /v1/agents/sync-workspace endpoint to propagate workspace changes
- Add comprehensive tests for cross-drive, tenant isolation, symlink escape
Ensures model sees the hint to ask for timezone even after bootstrap
completes, preventing timezone from being permanently missed if user
skips the question during initial onboarding.
Instead of assuming Asia/Saigon or UTC for all users, the model now
asks for timezone when the user mentions times/schedules/reminders.
After first ask, timezone is stored in USER.md for future sessions.
Closes#833 discussion.
* fix(ci): skip CI condition in semantic-release for main branch
go-semantic-release auto-detects the default branch from GitHub API
(which is dev), but releases are cut from main. The CI condition
rejects runs on non-default branches. Use --no-ci to bypass this
check since the workflow already gates on push to main.
* docs: document CI/CD pipelines, release flow, and v2.66.0 changelog
- CLAUDE.md: add CI/CD & Releases section with workflow table, tag
patterns, Docker variants, beta/desktop release commands
- CONTRIBUTING.md: expand Releases section with standard (auto),
beta (manual tag), and desktop release workflows
- docs/17-changelog.md: add v2.66.0 entry covering IDOR fix, BytePlus
provider, per-agent grants, beta pipeline, and CI fixes
* fix(telegram): handle group-to-supergroup migration seamlessly
When a Telegram group upgrades to a supergroup, the chat ID changes and
all existing references become stale. This caused send failures (400),
orphaned sessions, and required manual re-pairing.
Add dual-path migration handling:
- Proactive: intercept inbound MigrateToChatID before isServiceMessage
- Reactive: detect 400 + MigrateToChatID on send, migrate DB, retry
DB migration updates in a single transaction (scoped by tenant + channel):
- paired_devices: sender_id, chat_id
- sessions: session_key, user_id
- channel_contacts: sender_id
- channel_pending_messages: history_key
Also invalidates in-memory caches (approvedGroups, pairingReplySent,
groupHistory) and handles media sends via migration retry in Send().
* fix(tools): quote-aware shell operator detection in credentialed exec (#700) (#702)
* fix(tools): quote-aware shell operator detection in credentialed exec (#700)
- Replace detectShellOperators with detectUnquotedShellOperators in
credentialed exec path — respects single/double quoting so that
characters like | inside argument values (e.g. --jq '.[0] | .name')
are not falsely flagged as shell operators
- Pass raw command string (preserving quotes) to executeCredentialed
instead of reconstructing from parsed args
- Downgrade "no credential found" log from Warn to Debug (fires for
every non-credentialed command, too noisy at Warn)
- Add extractUnquotedSegments() helper with comprehensive tests
* fix(tools): handle backslash escape outside quotes in shell operator detection
extractUnquotedSegments did not handle \ as an escape character outside
of quotes, causing \" to incorrectly enter double-quote mode. This hid
subsequent shell operators from detection (e.g. gh \"arg\" | env would
not detect the unquoted pipe).
Add backslash escape handling in the unquoted state to match
go-shellwords parsing behavior. Both \ and the escaped character are
emitted as unquoted content so operator detection still catches them.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
* feat(infra): tracing recovery, browser cleanup, CLI fixes, UI workspace split (#709)
- Tracing: recover stale running traces/spans on startup (PG + SQLite)
- Browser: Chrome orphan cleanup via launcher PID, timeouts, Leakless
- Claude CLI: WaitDelay 5s + context-cancel early exit
- Agent loop: safety-net defer to finalize orphan root traces
- UI: split workspace sharing into separate Memory and KG toggles
- Minor: for-range idiom, min() builtin
* fix(prompt): skip credentialed CLI context when exec tool is denied
Agents with exec in their deny list cannot run CLI commands, so
injecting wrangler/gh credential context is misleading — the LLM
sees instructions for tools it cannot use. Gate the section on
exec being present in the filtered tool list.
* fix(ui): improve traces table layout and readability
Compact columns: status as icon-only, merge time+duration into one column.
Truncate long user IDs, clean <media:*> tags from preview, move badges to second line.
* fix(security): harden exec path exemption matching
- Add absolute path exemption for dataDir/skills-store/ (fixes skill
scripts using absolute paths like /app/data/skills-store/ being denied)
- Strip surrounding quotes before prefix matching (LLMs often quote paths)
- Reject path traversal ("..") in exempt fields to prevent escape
- Switch from "any field exempt → skip" to per-field matching: only exempt
if ALL fields that match the deny pattern are individually exempt
- Closes pipe/comment bypass vectors where an exempt path in one argument
would exempt the entire command including non-exempt paths
Includes 27 test cases covering: legitimate access, quoted paths,
path traversal, unicode bypass, pipe/comment bypass, mixed args.
* feat(whatsapp): add native WhatsApp channel with whatsmeow (#720)
Replace Node.js Baileys bridge with native go.mau.fi/whatsmeow — zero
external dependencies. QR auth, media support, markdown formatting,
typing indicators, dual JID/LID identity, group policies, pairing.
Resolves#703
* fix(chat): load message history on first conversation click (#730)
* fix(chat): load message history when selecting existing conversation from clean state
The skipNextHistoryRef was unconditionally set when sessionKey transitioned
from empty to non-empty. This prevented loadHistory() from running when
clicking an existing conversation from the initial /chat page. The skip
was only intended for the new-chat send flow where the optimistic message
is already displayed.
Guard the skip with expectingRunRef so it only activates when a message
send is in flight.
Closes#729
* docs: add UI diff evidence for PR #730
Before/after screenshots and HTML comparison report showing
first conversation click behavior fix.
* fix(permissions): use cron-specific permission check for cron tool (#725)
* fix(security): harden exec path exemption matching (#721)
- Add absolute path exemption for dataDir/skills-store/ (fixes skill
scripts using absolute paths like /app/data/skills-store/ being denied)
- Strip surrounding quotes before prefix matching (LLMs often quote paths)
- Reject path traversal ("..") in exempt fields to prevent escape
- Switch from "any field exempt → skip" to per-field matching: only exempt
if ALL fields that match the deny pattern are individually exempt
- Closes pipe/comment bypass vectors where an exempt path in one argument
would exempt the entire command including non-exempt paths
Includes 27 test cases covering: legitimate access, quoted paths,
path traversal, unicode bypass, pipe/comment bypass, mixed args.
* fix(permissions): use cron-specific permission check for cron tool
Cron tool was hardcoded to check `file_writer` configType via
CheckFileWriterPermission(), ignoring the `cron` configType that
the UI actually saves when granting cron permissions. This caused
agents in group chats to be denied cron access even with correct
permission configured.
Add ConfigTypeCron constant and CheckCronPermission() that checks
`cron` configType first, falling back to `file_writer`.
---------
Co-authored-by: Viet Tran <viettranx@gmail.com>
* fix(exec): allow uploaded files in active workspaces (#748)
Shell-aware command parsing, dynamic workspace exemptions, and symlink canonicalization for exec path denial. Fixes#739.
* refactor(exec): extract path exemption logic to separate file
Move shell-aware parsing, dynamic workspace exemptions, path alias
variants, and canonicalization functions from shell.go (688 LOC) to
shell_path_exemption.go (284 LOC) for maintainability.
* feat(agent): centralized tenant user identity resolution for credentials
Add CredentialUserID context key that resolves channel contacts to merged
tenant users for credential lookups (SecureCLI, MCP). Keeps UserID
unchanged for session/workspace scoping. Resolves group senders, group
contacts, and unresolved DMs via ContactStore with 60s TTL cache.
* fix(ui): improve traces table column layout
Add width constraints and whitespace-nowrap to prevent column wrapping
on narrow viewports. Cherry-picked from dev-v3.
* fix(ui): enable search filtering on knowledge graph view (#758)
Search box on /knowledge-graph only filtered table view. Add useMemo client-side filtering of graph entities by name/type/description, only show relations where both endpoints match.
Closes#759
* feat: add zoom controls to knowledge graph (#757)
Add +/- buttons and percentage display to the knowledge graph stats bar
so users can zoom without relying solely on mouse wheel. Uses existing
react-force-graph-2D zoom() API with 1.5x step and 300ms animation.
Closes#755
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(providers): use DB name for ClaudeCLI, ACP, and Anthropic registration
Provider Name() methods returned hardcoded strings, so DB-registered
providers with custom names got wrong registry key — causing "provider
not found" fallback. Add WithClaudeCLIName/WithACPName/WithAnthropicName
options, pass p.Name from DB registration paths. Config-based paths
keep hardcoded defaults.
Closes#742
* fix(agent): correct soft-trim head/tail budget allocation when tail is important (#723)
Co-authored-by: quxy5 <quxy5@outlook.com>
* feat(v3): core architecture redesign — pipeline, memory, vault, evolution, providers, orchestration (#790)
* feat(v3): add core interface contracts and migration for v3 redesign
Foundation interfaces: TokenCounter, WorkspaceContext, DomainEventBus,
ProviderAdapter/Capabilities. Pipeline: Stage, RunState, MessageBuffer,
substates, Pipeline orchestrator. Memory: EpisodicStore, AutoInjector,
KG temporal extensions, consolidation workers. System integration:
PromptConfig, ToolCapability, Retriever. Orchestration: OrchestrationMode,
EvolutionMetrics/SuggestionStore. Migration 000037: episodic_summaries,
evolution tables, KG temporal columns. Schema version 36→37.
* refactor(plans): mark all v3 design phases complete with file references
* fix(v3): address code review findings on design contracts
- C1: add missing l0_abstract column to episodic_summaries migration
- C2: align EpisodicSummary ID/TenantID/AgentID to uuid.UUID
- H1: document tenant_id scoping requirement on EpisodicStore
- H2: add UNIQUE constraint on (agent_id, user_id, source_id) for dedup
- H4: clarify ProviderAdapter vs Provider relationship in doc
- M3: set state.ExitCode on BreakLoop/AbortRun in pipeline
- M6: store full PipelineConfig in Pipeline struct
- Edge: add WHERE embedding IS NOT NULL on HNSW index
* fix(v3): second-pass review fixes
- H1: use context.WithoutCancel for finalize + set ExitCode on ctx cancel
- H2: use utf8.RuneCountInString consistently in FallbackCounter
- H3: longest-prefix-match in ModelContextWindow (prevents wrong tokenizer)
- H4: return unsubscribe cleanup func from consolidation.Register
* feat(v3): implement DomainEventBus with worker pool, dedup, and retry
Worker pool processes events from buffered channel. SourceID-based dedup
prevents duplicate processing. Exponential backoff retry on handler error.
Panic recovery per handler. Graceful shutdown via Drain(). 8/8 tests pass
with race detector.
* feat(v3): implement ProviderAdapter for Anthropic, OpenAI, DashScope, Codex
Add CapabilitiesAware to all 6 providers. Create ProviderAdapter
implementations that delegate to existing buildRequestBody/parseResponse
for DRY. ClaudeCLI and ACP get capabilities only (subprocess transport).
DashScope wraps OpenAI adapter with StreamWithTools=false override.
* feat(v3): implement WorkspaceContext Resolver for 6 scenarios
Stateless resolver produces immutable WorkspaceContext at run start.
Handles personal/group/predefined/team-shared/team-isolated/delegation.
Wired into loop_context.go behind v3PipelineEnabled flag (additive,
v2 path unchanged). Includes delegation path boundary check,
master tenant bypass, and tenant slug path composition.
* feat(v3): implement tiktoken TokenCounter with BPE encoding + cache
Adds tiktoken-go for accurate cl100k_base/o200k_base token counting.
Per-message FNV-1a hash cache avoids re-encoding unchanged history.
Falls back to rune/3 heuristic for unknown models. NewTokenCounter
factory selects implementation at build time.
* feat(v3): promote 12 other_config JSONB fields to dedicated agent columns
Extract emoji, agent_description, thinking_level, max_tokens,
self_evolve, skill_evolve, skill_nudge_interval, reasoning_config,
workspace_sharing, chatgpt_oauth_routing, shell_deny_groups, and
kg_dedup_config from the catch-all other_config JSONB into proper
columns with DB-level types and defaults.
- Migration: PG (000037) + SQLite (schema v6→7) with backfill
- Go: AgentData struct + simplified Parse* methods
- Store: SELECT/INSERT/scan updated for both PG and SQLite
- Gateway: create/update handlers accept promoted fields
- HTTP: export/import with legacy backward compat
- Web UI: all 15 frontend files read/write from top level
* feat(v3): implement Knowledge Vault with unified search, wikilinks, and FS sync
Migration 000038 adds vault_documents (FTS+pgvector), vault_links, vault_versions
tables. VaultStore interface with PG implementation for document CRUD, hybrid
FTS+vector search, and bidirectional link management. All queries enforce
tenant_id isolation including JOIN-based scoping on link operations.
FS sync layer: SHA-256 content hashing, VaultInterceptor hooks into write_file/
read_file for auto-registration and lazy sync, fsnotify watcher with 500ms
debounce. Wikilink engine parses [[target]] syntax, resolves targets via
3-step strategy, and maintains vault_links on write.
VaultSearchService fans out queries across vault, episodic, and KG stores in
parallel with per-source score normalization and weighted merge. AutoInjector
and Retriever implementations for pipeline integration.
Three agent tools: vault_search (unified discovery), vault_link (explicit
linking), vault_backlinks (dependency tracing). Feature-flagged via
v3_vault_enabled agent setting.
* feat(v3): wire vault into gateway startup + add unit tests
Wire VaultStore embedding provider, VaultSearchService, VaultInterceptor
on read/write tools, and register vault_search/vault_link/vault_backlinks
tools in gateway_vault_wiring.go. All wiring gated by stores.Vault != nil.
Add 28 unit tests for ContentHash, ContentHashFile, and ExtractWikilinks
covering edge cases, unicode, display text, context windows, and offsets.
* feat(v3): implement stage-based pipeline loop with 8 pluggable stages
Decompose monolithic agent loop into internal/pipeline/ package:
- 6 stages: Context, Think, Prune+MemoryFlush, Tool, Observe+Checkpoint, Finalize
- Foundation types: Stage interface, RunState with 7 typed substates, MessageBuffer
- Pipeline orchestrator with setup/iteration/finalize 3-phase execution
- Callback-based PipelineDeps avoids circular import with agent package
- Feature-flagged via v3PipelineEnabled in Loop.Run()
- All 7 exit conditions preserved (no tools, max iter, truncation, loop kill,
read-only streak, tool budget, ctx cancel)
* feat(v3): wire pipeline callbacks to Loop methods + add 71 unit tests
Wire 15 of 17 PipelineDeps callbacks from Loop methods via closures:
- Context: LoadContextFiles, BuildMessages, EnrichMedia, InjectReminders
- Think: BuildFilteredTools, CallLLM (stream/sync)
- Prune: PruneMessages, CompactMessages
- Memory: RunMemoryFlush
- Finalize: SanitizeContent, FlushMessages, UpdateMetadata, BootstrapCleanup, MaybeSummarize
- Remaining: ExecuteToolCall, CheckReadOnly (deep loop.go integration)
Add comprehensive test suite (71 tests, all passing with -race):
- MessageBuffer: 10 tests (append, flush, replace, counts)
- Pipeline.Run: 14 tests (3-phase flow, exit conditions, ctx cancel)
- Stage tests: 47 tests (ThinkStage nudges/truncation, PruneStage budget,
ToolStage parallel/exit, ObserveStage content, CheckpointStage interval,
FinalizeStage cleanup)
* feat(v3): wire remaining 2 callbacks (ExecuteToolCall, CheckReadOnly)
Complete callback wiring — 17/17 PipelineDeps callbacks now active:
- ExecuteToolCall: resolves tool name, executes via registry, processes
result via existing processToolResult with loop detection bridge
- CheckReadOnly: delegates to checkReadOnlyStreak via bridge runState
- Bridge runState shares loop detection state between pipeline and agent
* fix(v3): eliminate data race in tool execution + capture injected messages
- Remove parallel tool execution path — serialize all tool calls to avoid
data races on shared bridgeRS (loop detector, media results, deliverables)
- Loop kill checked after each tool (mid-batch early exit)
- BuildFilteredTools: capture and append injected tool-awareness messages
- Rename test to reflect sequential execution
* feat(v3): wire ResolveWorkspace, safe parallel tools, ContextStage tests
- Wire ResolveWorkspace callback via workspace.NewResolver() with
ResolveParams from Loop fields (no longer a nil stub)
- Re-add safe parallel tool execution: split into ExecuteToolRaw
(parallel I/O) + ProcessToolResult (sequential state mutation)
with opaque rawData pass-through (no double execution)
- Add 12 unit tests for ContextStage (8) + MemoryFlushStage (3)
- Split tool callbacks to loop_pipeline_tool_callbacks.go (under 200 lines)
- Capture buildFilteredTools injected messages
* feat(v3): add episodic memory store + temporal KG columns
Phase 1 — Episodic Store:
- Migration 000039: episodic_summaries table with pgvector, FTS, L0 abstracts
- EpisodicStore PG impl: CRUD, hybrid FTS+vector search, ExistsBySourceID,
PruneExpired. Idempotent via source_id UNIQUE constraint.
Phase 2 — Temporal KG:
- Migration 000040: valid_from/valid_until on kg_entities + kg_relations,
partial indexes for current-facts queries, epoch→timestamptz backfill
- ListEntitiesTemporal: current-only, point-in-time, or include-expired modes
- SupersedeEntity: atomic expire-old + insert-new in single transaction
Schema version bumped to 40.
* fix(v3): review fixes for episodic store + temporal KG
- C1: Fix column name mismatch turn_count vs message_count in Go SQL
- C2: Remove redundant migration 000040 (000037 already adds temporal KG columns)
- H1: Use time.Time not int64 for TIMESTAMPTZ columns in SupersedeEntity
- H2: Add tenant_id scoping to Get/Delete for tenant isolation
- M2: Fix scanEntityTemporal to convert TIMESTAMPTZ→UnixMilli correctly
- L1: Remove unused uuid import from episodic_search.go
- Schema version corrected to 39 (only 000039 is new)
* feat(v3): implement consolidation pipeline with 3 event-driven workers
Event chain: session.completed → EpisodicWorker → episodic.created →
SemanticWorker → entity.upserted → DedupWorker
- EpisodicWorker: reuses compaction summary or calls LLM, generates L0
abstract (extractive), idempotent via source_id check
- SemanticWorker: extracts KG facts from episodic summary via existing
Extractor, sets temporal valid_from, publishes entity.upserted
- DedupWorker: runs DedupAfterExtraction on new entity IDs (terminal)
- L0 abstract: sentence-based extraction (~50 tokens), no LLM needed
- All workers registered via DomainEventBus.Subscribe()
* feat(v3): implement progressive loading with L0 auto-inject + unified search
- AutoInjector: searches episodic store, builds L0 prompt section (~200 tokens),
skips trivial messages via stopword filter
- L1Cache: in-memory LRU (500 entries, 1h TTL) for structured overviews
- UnifiedSearch: cross-tier search merging episodic + document results by score
- ContextStage integration: AutoInject callback appends memory section to system prompt
- MemorySection field added to ContextState for observability
* feat(v3): add memory_expand tool for L2 episodic retrieval
New tool: memory_expand(id) returns full episodic summary with metadata.
Complements memory_search L0/L1 results with deep L2 access.
Nil-safe: returns error message when episodic store not available.
Gateway wiring + memory_search depth param + kg_search temporal param
deferred to runtime integration phase.
* feat(v3): complete Phase 5 — tool extensions + gateway wiring
- memory_search: add depth param + episodic tier search merged with docs
- kg_search: add as_of temporal param, use ListEntitiesTemporal
- memory_expand: registered in gateway startup
- Gateway: Episodic field in Stores, PGEpisodicStore in factory,
embedding provider wired, tools connected to episodic store
* fix(v3): Phase 3 review fixes — tenant isolation + AutoInject args
- C1: Add tenant_id filter to ftsSearch, vectorSearch, List queries
(prevents cross-tenant episodic memory leaks)
- C2: Fix AutoInject callback signature — agent/tenant captured by
closure, only userMessage + userID passed explicitly
- H1: Add tenant_id to List query
* feat(v3): wire per-agent v3 flags from DB into dual-mode gate
Parse v3_pipeline_enabled, v3_memory_enabled, v3_retrieval_enabled from
agent other_config JSONB via ParseV3Flags(). Resolver now sets all flags
on LoopConfig so the existing gate in loop_run.go reads from DB.
- V3Flags struct + ParseV3Flags() + ValidateV3Flags() in store layer
- v3MemoryEnabled/v3RetrievalEnabled added to Loop, LoopConfig, PipelineConfig
- Auto-inject gated on V3RetrievalEnabled (was unconditional)
- Structured perf logging for v3 pipeline runs
- v3 flag validation on both WS agent.update and HTTP PUT endpoints
* feat(v3): wire AutoInjector into pipeline for L0 memory auto-inject
Create AutoInjector at gateway startup from episodic store, pass through
ResolverDeps → LoopConfig → Loop. Pipeline adapter builds AutoInject
callback capturing agent/tenant context via closure.
ContextStage already gates on V3RetrievalEnabled + AutoInject != nil.
* feat(v3): add tool metadata map + capability-based deny rules
Registry gains per-tool ToolMetadata map with RegisterWithMetadata()
and GetMetadata() (infers defaults from tool name when not explicit).
PolicyEngine gains DenyCapability() for RBAC integration — tools with
denied capabilities filtered at step 8 after existing 7-step pipeline.
* fix(v3): add RWMutex to PolicyEngine capability deny fields
DenyCapability() and SetRegistry() now guarded by sync.RWMutex.
FilterTools reads snapshot under RLock. Prevents data race when
capability rules are modified concurrently with tool filtering.
* feat(v3): implement delegate tool for inter-agent task delegation
New `delegate` tool wraps existing agent_links infrastructure
(CanDelegate, DelegateTargets). Supports async (fire-and-forget)
and sync (block with timeout) modes. Permission checked via
AgentLinkStore. Events emitted: delegate.sent/completed/failed.
DelegateRunFunc injected by gateway to avoid circular dependency.
* feat(v3): complete 3 deferred implementations
1. OrchestrationMode resolution: ResolveOrchestrationMode() checks
team membership → delegate links → spawn (priority order).
2. PG EvolutionMetricsStore: RecordMetric, QueryMetrics, aggregate
tool/retrieval metrics, TTL cleanup. All queries tenant-scoped.
3. BridgePromptBuilder: implements PromptBuilder interface by
delegating to existing BuildSystemPrompt(). Appends v3 memory
L0 section when enabled. Ready for template engine swap later.
* fix(v3): address code review findings on commits 5-6
- C1: CanDelegate now tenant-scoped (fail-closed on missing tenant)
- H1: Sync delegate timeout capped at 600s
- H2: Async goroutine gets 10min deadline (prevents leaks)
- H3: JSONB casts use COALESCE/NULLIF guards (handles missing fields)
- M1/M2: Remove dead code (formatVaultSection, memoryL0ToStrings)
* fix(teams): stop auto-creating agent_links for team members
Teams use agent_team_members table directly — agent_links caused
context confusion between team dispatch and delegation systems.
- Remove autoCreateTeamLinks() calls from team create + member add
- Remove link cleanup from member remove
- Remove dead autoCreateTeamLinks() function
- Append DELETE to migration 000039: clear team-created agent_links
* fix(v3): tenant isolation for all agent_links queries + PromptBuilder Instructions
- DelegateTargets, GetLinkBetween, SearchDelegateTargets,
SearchDelegateTargetsByEmbedding, DeleteTeamLinksForAgent all now
scoped by tenant_id (fail-closed on missing tenant)
- BridgePromptBuilder now maps Instructions/InstructionContent to
AGENTS.md context file (was silently dropped)
* feat(v3): wire orchestration mode + evolution metrics into agent loop
- Orchestration mode: resolver resolves mode from team/links, tool filter
hides delegate/team_tasks based on mode, prompt builder injects delegation
targets section
- Evolution metrics: non-blocking goroutine records tool execution metrics
(name, success, duration) via EvolutionMetricsStore in both v2 loop and
v3 pipeline paths (sequential + parallel)
- Fix review findings: tenant ID propagated via store.WithTenantID in
background goroutine, 5s timeout prevents goroutine leak
* feat(v3): implement suggestion engine with pluggable analysis rules
- PG EvolutionSuggestionStore: CRUD for agent_evolution_suggestions table
- SuggestionEngine: aggregates 7-day metrics, runs rules, deduplicates
pending suggestions per type before creating new ones
- 3 initial rules: LowRetrievalUsage (usage_rate<0.2), ToolFailure
(success_rate<0.1), RepeatedTool (>100 calls/week → suggest skill)
- EventSuggestionCreated event type added to eventbus
- Cron wiring deferred to gateway startup integration pass
* feat(v3): implement auto-adapt guardrails with apply/rollback
- AdaptationGuardrails: max delta per cycle, min data points, locked
params, rollback-on-drop percentage
- ApplySuggestion: applies threshold suggestions to agent other_config
JSONB, stores baseline for rollback
- RollbackSuggestion: restores baseline values from suggestion params
- EvaluateApplied: compares post-apply metrics to baseline, auto-rolls
back when quality drops beyond threshold
- Scope limited to retrieval params only (never security settings)
* feat(v3): wire evolution stores + daily/weekly cron for suggestions
- Add EvolutionMetrics + EvolutionSuggestions to Stores struct + PG factory
- Wire EvolutionMetricsStore into ResolverDeps (cmd/gateway_managed.go)
- Add gateway_evolution_cron.go: daily suggestion analysis + weekly
evaluation/rollback for applied suggestions
- Cron runs as background goroutine with 5-min timeout per cycle
* fix(v3): address code review findings on evolution engine
- C1: persist baseline parameters before marking suggestion as applied
(was building map but never saving — rollback would always fail)
- H1: add tenant_id isolation to UpdateSuggestionStatus, GetSuggestion,
and new UpdateSuggestionParameters method
* test(v3): add unit tests for orchestration, suggestions, guardrails, prompt
- orchestration_mode_test: orchModeDenyTools (4 modes) + ResolveOrchestrationMode
(4 scenarios with mock stores)
- suggestion_rules_test: LowRetrievalUsage, ToolFailure, RepeatedTool with
threshold boundary tests (at/below/above min data points)
- evolution_guardrails_test: DefaultGuardrails values + CheckGuardrails
(insufficient data, locked params, zero-min fallback)
- prompt_builder_orchestration_test: BridgePromptBuilder orchestration section
presence/absence across 4 scenarios + target content verification
* test(v3): add integration tests for evolution metrics + suggestions
- Test helper: shared PG connection with sync.Once migration, per-test
tenant+agent seed with cleanup
- Evolution metrics: RecordMetric, AggregateToolMetrics (success rate),
Cleanup (TTL deletion)
- Evolution suggestions: full CRUD, UpdateSuggestionParameters (baseline
persist), tenant isolation (cross-tenant read blocked)
- Pipeline E2E: seed 25 failed tools + 55 low-usage retrievals, verify
SuggestionEngine creates suggestions, verify dedup on second run
- Fix: migration 039 de-duped (episodic_summaries already in 037)
- Fix: NULL reviewed_by scan via sql.NullString
* feat(v3): add HTTP API handlers for evolution, vault, episodic, orchestration, v3-flags
5 new handler files exposing v3 backend stores as REST endpoints:
- evolution_handlers.go: metrics query/aggregate + suggestions CRUD
- vault_handlers.go: cross-agent document listing + search + links
- episodic_handlers.go: episodic summaries list + hybrid search
- orchestration_handlers.go: computed mode + delegate targets (read-only)
- v3_flags_handlers.go: per-agent v3 feature flag get/toggle
Store fixes from code review:
- episodic FTS: use inline to_tsvector (no stored tsv column)
- episodic: conditional user_id filter in List + Search (admin view)
- episodic: add tenant_id to ExistsBySourceID + PruneExpired
- evolution: require tenant_id in context (no struct fallback)
- evolution: check RowsAffected on suggestion updates
- vault: optional agent_id filter in ListDocuments (cross-agent)
* feat(v3): add web UI for evolution tab, v3 settings, vault page, episodic memory
Agent Detail enhancements:
- V3 Settings section: pipeline/memory/retrieval flag toggles
- Orchestration section: mode badge + delegate targets display
- Evolution section: added metrics + suggestions v3 flag toggles
- Evolution tab: Recharts metrics charts + suggestion review table
with approve/reject/rollback actions + guardrails card
New pages:
- /vault: Knowledge Vault document registry with cross-agent listing,
hybrid search dialog, document detail with wikilinks
- Memory page: added Episodic Memory tab with summary cards,
expandable details, key topic badges, and hybrid search
Infrastructure:
- HttpClient: added patch() method
- Query keys: v3Flags, orchestration, evolution namespaces
- 4 new hooks: use-v3-flags, use-orchestration, use-evolution-metrics,
use-evolution-suggestions, use-vault, use-episodic
- i18n: vault namespace (en/vi/zh), agents + memory keys updated
- Reused formatRelativeTime from lib/format.ts (eliminated 3 duplicates)
* refactor(http): add bindJSON helper and migrate all decode call sites
Replace 36 json.NewDecoder(r.Body).Decode + error blocks with bindJSON
across 20 HTTP handler files. Standardizes decode error responses to
structured writeError format. Fixes unchecked decode in handleIndexAll.
* refactor(store): adopt sqlx for PG scan operations (Phase 1+2)
Add jmoiron/sqlx v1.4.0 with camelToSnake json tag mapper.
Migrate scan-heavy PG store methods to sqlx Get/Select:
- tracing.go: GetTrace, ListTraces, ListChildTraces, GetTraceSpans, GetCostSummary
- heartbeat.go: Get, ListDue, ListLogs
- providers.go: GetProvider, GetProviderByName, ListProviders, ListAllProviders
- mcp_servers.go: GetServer, GetServerByName, ListServers
- pairing.go: ListPending, ListPaired
- agents_export_queries.go: 5 export functions
- agents_export_team_queries.go: exportTeamMembers, ExportAgentLinks
All writes (INSERT/UPDATE/DELETE), execMapUpdate, and dynamic WHERE
builders remain raw SQL. Zero behavior change.
* refactor(store): adopt sqlx for SQLite scan operations (Phase 3)
Migrate SQLite store scan methods to sqlx Get/Select:
- providers.go: GetProvider, GetProviderByName, ListProviders, ListAllProviders
- tenants.go: GetTenant, GetTenantBySlug, ListTenants, GetTenantUser, ListUsers, ListUserTenants
- mcp_servers.go: GetServer, GetServerByName, ListServers
Create sqlx_scan_structs.go with sqliteTime-aware scan structs
(providerRow, tenantRow, tenantUserRow, mcpServerRow) to handle
SQLite TEXT timestamp parsing via StructScan.
* refactor(store): migrate PG bulk scan operations to sqlx (Phase 4)
Migrate scan-heavy methods across 6 PG store files:
- tenant_store.go: GetTenant, GetTenantBySlug, ListTenants, GetTenantUser,
ListUsers, ListUserTenants — removed 3 scan helpers
- teams.go: ListTeams, GetTeam, ListMembers, ListMembersByTenant
- teams_tasks_activity.go: ListComments, ListEvents, ListFollowUps
- pending_message_store.go: ListPending, ListByHistoryKey
- skills_grants.go: ListAgentGrants
- config_permissions.go: CheckPermission
~20 scan ops converted. Files with encryption post-processing,
pq.Array, pgvector, or dynamic SQL kept raw.
* refactor(store): extract shared CamelToSnake mapper, add UUIDArray usage note
- Move camelToSnake to internal/store/column_mapper.go (DRY)
- Both pg and sqlitestore packages now import shared CamelToSnake
- Add planned-use comment on UUIDArray type
* refactor(cli): migrate commands from config.json to HTTP API, add providers/setup/TUI
- Add unified HTTP client (gateway_http_client.go) with auth, error parsing, typed generics
- Rewrite agent list/add/delete to use gateway HTTP API instead of config.json
- Rewrite channels list to HTTP API, add channels add/delete subcommands
- Replace models command with full providers CRUD (list/add/update/delete/verify)
- Add setup wizard command (provider → agent → channel post-onboard flow)
- Add Bubble Tea TUI behind build tag (tui/!tui with noop fallback)
- Update onboard next-steps to mention goclaw setup
- Add build-tui Makefile target
- Fix URL path injection (url.PathEscape on all user-supplied path segments)
- Fix UTF-8 truncation in skills description display
* refactor(store): add explicit db struct tags, fix sqlx mapper for heartbeat scan error
Switch sqlx mapper from NewMapperFunc (which only applies CamelToSnake to
field names, not tag values) to NewMapperFunc("db", CamelToSnake) with
explicit db:"column_name" tags on all store structs.
Root cause: NewMapperFunc("json", fn) sets mapFunc but not tagMapFunc,
so camelCase json tags like "agentId" were used as-is instead of being
converted to "agent_id", causing "missing destination name" scan errors.
Fix: use db struct tags as the source of truth for column mapping.
Every DB entity field gets db:"column_name", nested JSON configs and
runtime-only structs get db:"-".
* test(store): add integration tests for 13 store interfaces (70 tests)
Cover Tier 1 (critical) + Tier 2 (security) stores with integration tests
running against pgvector pg18. Coverage from 2.4% to ~54%.
Stores tested: Session, Agent, Team/Task, Memory, KnowledgeGraph, Vault,
MCP Server, API Key, ConfigPermission, Contact.
Infrastructure: fixture builders (seedTeam, seedMCPServer, etc.),
mock EmbeddingProvider, multi-tenant helpers, expanded cleanup.
* fix(store): resolve NULL scan bugs in MCP server and task metadata
- mcp_servers: COALESCE nullable TEXT columns (display_name, command,
url, api_key, tool_prefix) to prevent sqlx scan failures
- mcp_servers_access: COALESCE nullable JSONB columns in ListAgentGrants
(tool_allow, tool_deny, config_overrides) to prevent silent row drops
- teams_tasks: default task metadata to '{}' instead of nil to satisfy
NOT NULL constraint on CreateTask
- sqlx_helpers: export InitSqlx for integration test setup
* feat(pipeline): fix v3 pipeline context injection, tracing, KG temporal filters
- Pipeline context: add InjectContext + LoadSessionHistory callbacks to
ContextStage, propagate enriched ctx via state.Ctx for iteration stages
- Pipeline tracing: wrap makeCallLLM with emitLLMSpanStart/End, wrap
makeExecuteToolCall/Raw with emitToolSpanStart/End
- Token counter: switch pipeline from FallbackCounter to TiktokenCounter
- KG temporal: add valid_until IS NULL filter to all entity/relation
queries (list, search, vector, FTS, traversal CTE, stats)
- Skills: add SkillEmbedder interface for future hybrid BM25+vector search
- Cache: remove unused tenantResolve dead code from PermissionCache
- Store: fix NULL scan bugs in tracing metadata and agent skill_nudge
- Test: add TestStoreKG_TemporalFilter integration test
- UI: add v3 version badge, evolution section, memory/traces improvements
* refactor(store): migrate KG store from raw sql.Rows to sqlx StructScan
Migrate 6 knowledge graph store files from manual rows.Scan() to
pkgSqlxDB.GetContext/SelectContext with intermediate scan row structs.
- Add entityRow, relationRow, traversalRow, dedupCandidateRow structs
with json.RawMessage for jsonb and time.Time for timestamptz columns
- Add toEntity()/toRelation() converters (UnixMilli + json.Unmarshal)
- Add sqlxTx() helper for wrapping *sql.Tx with sqlx mapper
- Fix ScanDuplicates passing time.Now().Unix() to TIMESTAMPTZ column
- Fix ListEntitiesTemporal missing tenant scope (scopeClause)
- Fix SupersedeEntity missing tenant scope and tenant_id on INSERT
- Fix DedupCandidate.CreatedAt using Unix() instead of UnixMilli()
- Update agents_export_queries.go to reuse new scan row structs
- Net -160 lines of manual scan boilerplate removed
* refactor(store): migrate memory, skills, agents, sessions, mcp, cron, vault stores to sqlx
Batch migration of 19 store files from raw rows.Scan() to
pkgSqlxDB.GetContext/SelectContext with intermediate scan row structs.
Groups migrated:
- Memory: memory_docs, memory_admin, memory_search, memory_embedding_cache
- Episodic: episodic_search, episodic_summaries
- Skills: skills, skills_admin, skills_embedding, skills_export_queries
- Agents: agents (backfill+shares), agents_context, agents_export_team_standalone
- Sessions: sessions_list (List, ListPaged, ListPagedRich)
- MCP: mcp_servers_access, mcp_export_queries
- Cron: cron_exec (GetRunLog)
- Vault: vault_documents (ListDocuments, ftsSearch, vectorSearch)
- Tenant: tenant_configs (ListDisabled, ListAll)
7 new scan row files created. Net -510 lines of manual scan boilerplate.
INSERT/UPDATE/DELETE and scalar COUNT queries kept as raw SQL.
* fix(store): fix 3 sqlx scan struct db tag issues found by audit
- Fix vault FTS alias mismatch: `AS rank` → `AS score` (critical: runtime scan error)
- Fix episodic key_topics type: json.RawMessage → pq.StringArray (TEXT[] column)
- Fix agentShareRow.CreatedAt: string → time.Time, wire to output struct
* feat(providers): implement Wave 2 provider resilience and intelligence
9-phase implementation covering:
- Request middleware chain with composable body transformers
- OpenAI prompt caching, service tier, and fast mode middlewares
- Error classification (9 categories) with two-tier failover
- Model registry with forward-compat resolvers (Anthropic + OpenAI)
- Embedding providers (OpenAI + Voyage) with 1536-dim validation
- Cooldown/probe system with per-provider:model state tracking
- Markdown-aware chunking shared across 5 channels
- Session recall via FTS + pgvector on episodic summaries
- Dreaming/promotion pipeline for long-term memory consolidation
Migrations: 000040 (episodic search index), 000041 (promoted_at column)
Schema version: 39 → 41
* feat(providers): wire model registry into gateway provider construction
Create InMemoryRegistry with Anthropic + OpenAI forward-compat resolvers
at gateway startup. Pass to all Anthropic and OpenAI providers created
from both config and DB sources.
* feat(consolidation): wire DomainEventBus and consolidation pipeline
Create DomainEventBus at gateway startup, thread through resolver →
LoopConfig → Loop → PipelineDeps. Emit session.completed event after
each run finalization. Register consolidation pipeline (episodic →
semantic → KG dedup → dreaming) with event bus subscriptions.
* fix(store): fix episodic key_topics pq.Array, ON CONFLICT, and migration 040 immutability
- episodic_summaries.go Create: json.Marshal(KeyTopics) → pq.Array (text[] column)
- episodic_search.go scanEpisodic/scanEpisodicRow: json.RawMessage → pq.StringArray
- episodic_summaries.go Create: ON CONFLICT add WHERE source_id IS NOT NULL for partial index
- migration 040: add immutable_array_to_string wrapper (array_to_string is STABLE in PG)
* test(store): add 17 integration tests for skills, cron, episodic, tenant configs
- Skills store: 6 tests (CRUD, grants, tenant isolation)
- Cron store: 4 tests (job CRUD, run log sqlx scan, pagination, tenant isolation)
- Episodic store: 4 tests (summary CRUD, list, FTS search, tenant isolation)
- Tenant configs: 3 tests (tool/skill disable, list, tenant isolation)
- Test helper: add cleanup for skills, cron, episodic tables
* fix(permissions): use cron-specific permission check for cron tool (#725)
* fix(security): harden exec path exemption matching (#721)
- Add absolute path exemption for dataDir/skills-store/ (fixes skill
scripts using absolute paths like /app/data/skills-store/ being denied)
- Strip surrounding quotes before prefix matching (LLMs often quote paths)
- Reject path traversal ("..") in exempt fields to prevent escape
- Switch from "any field exempt → skip" to per-field matching: only exempt
if ALL fields that match the deny pattern are individually exempt
- Closes pipe/comment bypass vectors where an exempt path in one argument
would exempt the entire command including non-exempt paths
Includes 27 test cases covering: legitimate access, quoted paths,
path traversal, unicode bypass, pipe/comment bypass, mixed args.
* fix(permissions): use cron-specific permission check for cron tool
Cron tool was hardcoded to check `file_writer` configType via
CheckFileWriterPermission(), ignoring the `cron` configType that
the UI actually saves when granting cron permissions. This caused
agents in group chats to be denied cron access even with correct
permission configured.
Add ConfigTypeCron constant and CheckCronPermission() that checks
`cron` configType first, falling back to `file_writer`.
---------
Co-authored-by: Viet Tran <viettranx@gmail.com>
* fix(chat): load message history on first conversation click (#730)
* fix(chat): load message history when selecting existing conversation from clean state
The skipNextHistoryRef was unconditionally set when sessionKey transitioned
from empty to non-empty. This prevented loadHistory() from running when
clicking an existing conversation from the initial /chat page. The skip
was only intended for the new-chat send flow where the optimistic message
is already displayed.
Guard the skip with expectingRunRef so it only activates when a message
send is in flight.
Closes#729
* docs: add UI diff evidence for PR #730
Before/after screenshots and HTML comparison report showing
first conversation click behavior fix.
* feat(whatsapp): port native WhatsApp channel with whatsmeow from dev
Cherry-pick 0db1e93a with manual conflict resolution:
- cmd/channels_cmd.go: kept dev-v3 HTTP API approach (not config-based)
- go.mod/go.sum: merged deps, ran go mod tidy
* feat(ui): v3 web UI enhancement — branded loading, rich markdown, vault graph, sidebar polish
- Branded loading: HTML pre-loader with logo pulse/shimmer, fade-out on app ready, PageLoader logo swap
- Rich markdown: wikilinks, mermaid (lazy-loaded), math/KaTeX, callouts/admonitions plugins
- Vault graph: force-directed document graph view with table/graph toggle
- Agent filters: type filter (open/predefined) on agents page
- Sidebar: tenant name + role badge in footer
- Query keys: add vault + episodic entries
* fix(ui): address code review — mermaid XSS, Safari compat, cache key
- Change mermaid securityLevel from "loose" to "strict" (XSS prevention)
- Add requestIdleCallback fallback for Safari < 17
- Fix vault all-links query key to use sorted doc IDs (stale cache fix)
- Remove dead abortRef from MermaidBlock
- Add prefers-reduced-motion to loader animation
* docs: update CLAUDE.md with v3 architecture + complete changelog
- Add 10 new v3 internal modules to project structure (pipeline, eventbus, consolidation, tokencount, vault, workspace, etc.)
- Add native WhatsApp channel, edition system, providerresolve, updater to structure
- Update Key Patterns with 8 v3-specific patterns (pipeline, eventbus, 3-tier memory, vault, evolution, orchestration, middleware)
- Add comprehensive V3 Redesign section to changelog covering:
- 8-stage pipeline with dual-mode gate
- DomainEventBus + consolidation workers
- 3-tier memory (working/episodic/semantic)
- Knowledge Vault with wikilinks
- Self-evolution engine
- Orchestration + delegate tool
- WorkspaceContext resolver
- ModelRegistry + provider adapter
- Feature flags
- Request middleware
- sqlx migration + 70+ integration tests
* fix(security): harden file path validation, tenant isolation, and tool access
- handleSign: validate path within workspace/dataDir before signing HMAC token
- handleSign: enforce tenant-scoped restriction for RBAC-enabled editions
- handleServe: add workspace/dataDir boundary check for ft= signed requests
- handleServe: remove cross-tenant findInWorkspace fallback for ft= requests
- TenantDataDir/TenantWorkspace: guard against empty slug resolving to parent
- exec tool: add tenants/ to AllowPathExemptions for tenant skill execution
- list_files: add AllowPaths support and wire skills directory access
* docs(v3): update 16 docs + add 2 new docs for v3 architecture
Update all docs/ to reflect v3 implementation (64 commits, 31K insertions):
- 00: architecture overview with 7 new packages
- 01: agent loop with pipeline, orchestration, evolution
- 02: providers with Wave 2 resilience (middleware, failover, registry)
- 03: tools with delegate, vault_search, vault_link, memory_expand
- 04,08,10,11,14,23: targeted v3 additions
- 06: store with 6 new tables, promoted columns, sqlx
- 07: memory with 3-tier architecture, consolidation pipeline
- 18,19: HTTP/WS API with v3 endpoints and methods
- 21: evolution system (metrics, suggestions, auto-adapt)
- model-steering: model registry relationship
New docs:
- 22-v3-http-endpoints.md: 12 v3 HTTP endpoints
- 24-knowledge-vault.md: vault architecture, wikilinks, search
* feat(v3): wire delegate tool, fix pipeline callbacks, clean dead code
Wire delegate tool end-to-end:
- Register DelegateTool in gateway with DelegateRunFunc
- Implement runFn: resolve target agent, build session key, propagate tracing
- Add async announce via msgBus (reuses subagent announce handler)
- Populate context fields (TenantID, Channel, ChatID) for routing
- Set DelegationID + ParentAgentID on RunRequest for event correlation
Fix pipeline callbacks:
- BreakLoop now completes remaining stages in current iteration
- EnrichMedia signature updated to use RunState for message buffer access
- Add non-streaming event emission for channel compatibility
- Fix user message flush tracking in v3 pipeline
Clean dead code (510 LOC removed):
- Delete memory/l1_cache.go, unified_search.go (superseded by vault search)
- Delete vault/auto_injector_impl.go, retriever_impl.go (never wired)
- Delete vault/sync_worker.go (never started)
- Remove orphaned EventMemoryLint, EventSuggestionCreated constants
* feat(v3): finalize stage — emit session.completed, NO_REPLY, strip directives
- Emit session.completed event for consolidation pipeline (episodic → semantic → dreaming)
- Detect NO_REPLY before flush so silent content is persisted for context
- Strip [[...]] message directives from user-facing content (v2 parity)
- Wire StripMessageDirectives, IsSilentReply, EmitSessionCompleted callbacks
* feat(vault): full CRUD — backend endpoints, UI dialogs, content preview
Backend (5 new HTTP endpoints):
- POST/PUT/DELETE /v1/agents/{id}/vault/documents — create, update, delete
- POST/DELETE /v1/agents/{id}/vault/links — create, delete
- Server-side validation for doc_type and scope enums
- Agent ownership verification on link creation
- FK cascade handles link cleanup on document delete
Frontend (React):
- Create document dialog (title, path, type, scope)
- Edit mode in detail dialog (inline title/type/scope editing)
- Delete document with confirmation
- Create link dialog (from/to doc, link type, context)
- Delete link with inline confirmation on badges
- Content preview (collapsible, lazy-loads via /v1/storage/files/)
- Mutation hooks with query invalidation
- i18n keys for en/vi/zh
* fix(v3): critical pipeline parity fixes — ChatRequest, reasoning, passback, media
- Enrich ChatRequest with all provider options (temperature, sessionKey,
agentID, userID, channel, workspace, tenantID) matching v2
- Add ResolveReasoningDecision for thinking models (o3, DeepSeek-R1, Kimi)
- Wire uniquifyToolCallIDs to prevent OpenAI 400 on duplicate IDs
- Add assistant message passback (Phase, RawAssistantContent) for Anthropic
- Emit block.reply for intermediate content (non-streaming channels)
- Add ContentSuffix append + ForwardMedia merge in FinalizeStage
- Build final assistant message with MediaRefs for session persistence
- Use effectiveMaxTokens() + OptMaxTokens constant
- Guard truncation retry on len(ToolCalls) > 0 + parseError check
- Accumulate ThinkingTokens in usage
- Deduplicate emitRun closure (shared from callbackSet)
- Fix EnrichMedia to receive RunState with actual messages
- Persist user message in makeFlushMessages (matching v2)
* fix(store): coerce NOT NULL JSONB columns to empty object on agent update
When switching provider away from ChatGPT OAuth, the UI sends
chatgpt_oauth_routing: null which violates the NOT NULL constraint.
Coerce null → '{}' for all NOT NULL JSONB promoted columns:
chatgpt_oauth_routing, reasoning_config, workspace_sharing,
shell_deny_groups, kg_dedup_config.
* feat(v3): v3 info modal redesign, agent links CRUD tab, sidebar rename
- Rewrite v3 info modal with 8 feature cards (pipeline, memory,
retrieval, vault, evolution, orchestration, resilience, registry)
with v2→v3 comparisons, stat badges, full i18n (en/vi/zh)
- Add tabbed layout to teams page: Agent Teams | Agent Links tabs
- Agent Links tab with full CRUD via existing WS RPC methods
(list/create/edit/delete) with Radix Select, Combobox, mutual
agent exclusion in create dialog
- Sidebar menu renamed to "Agent Link & Team"
- Backend: add source_display_name, source_emoji, target_emoji
to agent link joined queries for consistent display
* fix(store): include personal chats in cron delivery targets
Add 'user' contact_type to ListDeliveryTargets SQL filter in both
PG and SQLite stores. Previously only group/topic contacts appeared
in cron channel/chat dropdowns.
* fix(v3): duplicate messages, missing thinking, span numbering
- ThinkStage: skip AppendPending for final answer (no tool calls),
let FinalizeStage build the definitive message with sanitization
and MediaRefs — fixes duplicate assistant messages in session history
- RunResult: add Thinking field, propagate through v2 finalizeRun,
v3 convertRunResult, run.completed event, and chat.send response
- UI: capture thinkingRef before clearing on run.completed, include
in final message object so thinking renders without page refresh
- Span numbering: pass Iteration+1 in v3 callback to match v2's
1-based iteration display in trace span names
* fix(v3): wire delegation targets into BuildSystemPrompt
buildOrchestrationSection() was implemented and tested but never
wired into the actual BuildSystemPrompt() flow. Only the unused
BridgePromptBuilder had it. Add DelegateTargets + OrchMode fields
to SystemPromptConfig and inject "## Delegation Targets" section
so agents with agent_links see their delegation targets in prompt.
* fix(v3): sync mediaResults from bridgeRS to pipeline state
syncBridgeToState copied loopKilled, asyncToolCalls, deliverables
from the v2 bridgeRS but missed mediaResults. Tool results with
MEDIA: prefix were extracted by processToolResult into bridgeRS
but never propagated to state.Tool.MediaResults — causing
FinalizeStage to produce empty MediaRefs and RunResult.Media.
* fix(v3): populate SessionCompletedPayload in session.completed events
Both v2 loop and v3 pipeline emitted session.completed events with
nil Payload, causing episodicWorker type assertion to fail silently.
Episodic summaries were never created.
- Expand EmitSessionCompleted callback to pass msgCount, tokensUsed,
compactionCount from pipeline state
- V3 path: build payload from state.Messages.TotalLen(),
state.Think.TotalUsage, state.Compact.CompactionCount
- V2 path: build payload from history + rs.totalUsage +
sessions.GetCompactionCount()
* fix(v3): remaining pipeline parity gaps — skill postscript, team task count
- Add SkillPostscript callback to PipelineDeps + FinalizeStage,
matching v2's skill evolution nudge after complex tool runs
- Wire makeSkillPostscript() in adapter with same logic as v2
(skillEvolve + skillNudgeInterval + totalToolCalls threshold)
- Sync teamTaskCreates from bridgeRS to pipeline EvolutionState
(was already syncing teamTaskSpawns but missed creates)
* fix(evolution): add JSON struct tags to metric aggregates
ToolAggregate and RetrievalAggregate had no JSON tags, causing
Go to marshal field names as PascalCase (ToolName, CallCount)
while the UI expects snake_case (tool_name, call_count). Charts
rendered empty containers with no data bars.
Also change AvgDuration (time.Duration) to AvgDurationMs (float64)
for JSON-friendly serialization — time.Duration marshals as
nanoseconds which is unusable in frontend.
* fix(v3): add debug logging to episodic worker for consolidation pipeline
Add INFO/WARN logs at entry, summary decision, and creation points
to diagnose why episodic_summaries table stays empty in production.
* chore: silence noisy tenant_cache debug logs
* refactor(ui): replace v3 settings section with engine version picker + tabbed info modal
Replace flat toggle list with radio-style version cards (v2/v3) and
feature mini-cards. Redesign v3 info modal from single scroll to 3-tab
layout (Core Engine, Memory & Knowledge, Orchestration) with Lucide
icons and v2→v3 comparison cards.
- Add batchUpdate to use-v3-flags hook for atomic v2 switch
- Create engine-version-section with VersionCard + FeatureMiniCard
- Create v3-info-modal/ with 5 modular components (<45 LOC each)
- Add i18n keys (detail.engine + v3Info.tabs) for en/vi/zh
- Wire into agent-overview-tab, agent-header, agent-card
- Delete v3-settings-section.tsx + agent-v3-info-modal.tsx
* feat(v3): pass media files through delegate tool results
Delegate tool now carries media files (images, audio, etc.) produced
by the delegatee back to the parent agent. DelegateRunFunc returns
DelegateResult{Content, Media} instead of plain string.
- Add DelegateResult struct with Content + Media fields
- Convert agent.MediaResult to bus.MediaFile in gateway wire
- Attach media to sync result and async announce message
- Add MediaCount to DelegateCompletedPayload for observability
- Set MetaParentAgent in async announce metadata
* merge: bring main bug fixes into dev-v3
Cherry-pick 8 commits from origin/main:
- fix(telegram): handle group-to-supergroup migration (#698)
- feat(providers): add OpenRouter identification headers (#705)
- fix: deterministic prompt ordering for LLM cache hit (#719)
- fix(security): harden exec path exemption matching (#721)
- fix: invalidate storage size cache on delete and move (#726)
- fix: use errors.Is() for sentinel comparisons (#727)
- fix(desktop): add defaultValues to form dialogs (#737)
- Release: credential resolver, WhatsApp native, exec hardening (#754)
Conflict resolution:
- store/pg exports: accept main's errors.Is() additions
- shell.go: accept main's extracted matchesAnyPathExemption helper
- gateway_providers: merge both WithAnthropicName + WithAnthropicRegistry
- loop_types + resolver: merge v3 fields (OrchMode, DelegateTargets,
EvolutionMetricsStore) with main's new UserResolver/ContactStore
- gateway_setup: keep dev-v3's tenant-scoped path exemptions
- channels_cmd: keep dev-v3's HTTP API approach
* feat(v3): add foundation packages for architecture refactor (Phase 1)
Purely additive — zero changes to existing files. Creates shared types
and helpers that Phase 2-4 will migrate callers to:
- internal/store/base/: Dialect interface, BuildMapUpdate, nullable/JSON
helpers, scope clause builder, table metadata (44 tests)
- internal/orchestration/: ChildResult capture from v2/v3, media type
conversion with round-trip tests (10 tests)
- internal/providers/sse_reader.go: Shared SSE scanner replacing inline
bufio.Scanner boilerplate in 3 providers (8 tests)
* refactor(store): unify pg/ and sqlitestore/ helpers via base/ package (Phase 2)
- Create PG and SQLite dialect implementations (base.Dialect interface)
- Replace 15 duplicate helpers in pg/helpers.go with aliases to base.*
- Replace 13 duplicate helpers in sqlitestore/helpers.go with aliases
- Rewrite execMapUpdate and execMapUpdateWhereTenant to use
base.BuildMapUpdate with dialect-specific placeholders
- Rewrite scopeClause/scopeClauseAlias as thin wrappers around
base.BuildScopeClause
- Remove duplicate execMapUpdateWhereTenant from pg/agents.go
pg/helpers.go: 267→175 LOC, sqlitestore/helpers.go: 226→130 LOC
* refactor(orch): extract BatchQueue[T] generic for announce queues (Phase 3)
- Create internal/orchestration/batch_queue.go: generic producer-consumer
queue replacing duplicated sync.Map+mutex pattern (10 tests, race-safe)
- Simplify cmd/gateway_announce_queue.go: team queue uses BatchQueue,
removes announceQueueState/getOrCreate/drain/tryFinish (~40 LOC saved)
- Simplify cmd/gateway_subagent_announce_queue.go: subagent queue uses
BatchQueue, same pattern reduction (~40 LOC saved)
- Update cmd/gateway_consumer_handlers.go: callers use new signatures
* fix: remove defer accumulation in announce loop, clean comment tombstone
- Remove `defer ptd.ReleaseTeamLock()` inside for loop that accumulated
deferred calls per iteration (explicit ReleaseTeamLock already called)
- Remove dead comment tombstone in pg/agents.go
* fix: scope defer in announce loop via closure for panic safety
Wrap announce processing in closure so defer ptd.ReleaseTeamLock()
runs once per iteration instead of accumulating. Explicit release
still called for normal path; defer catches panics.
* refactor(providers): wire shared SSEScanner into 3 providers (Phase 4b)
Replace inline bufio.Scanner+SSE parsing boilerplate with shared
SSEScanner from sse_reader.go in:
- openai.go: data-only SSE (OpenAI, DashScope, Kimi)
- codex.go: data-only SSE (OpenAI Codex)
- anthropic_stream.go: event+data SSE (uses EventType() for switch)
Removes ~24 LOC of duplicated scanner setup + manual line parsing.
* refactor(agent): force v3 pipeline, remove v2 runLoop (Phase 4A)
- Delete runLoop() from loop.go (~745 LOC removed), keep shared helpers
(resolveToolCallName, hasParseErrors, truncateToolArgs)
- Remove v2/v3 gate in loop_run.go: always call runViaPipeline()
- Remove v3PipelineEnabled field from Loop struct + LoopConfig + resolver
- Always resolve workspace in loop_context.go (was behind v3 gate)
- Deprecate PipelineEnabled in V3Flags (kept for backward compat parsing)
All agents now always use v3 pipeline. No behavioral change for agents
that already had v3_pipeline_enabled=true (which was all production agents).
* refactor(gateway): decompose gateway.go from 1295 to 476 LOC (Phase 4B)
Extract sections of runGateway() into focused files:
- gateway_deps.go: gatewayDeps struct for shared state
- gateway_http_wiring.go: wireHTTPHandlersOnServer (~207 LOC)
- gateway_events.go: event subscribers + teamTaskEventType (~367 LOC)
- gateway_lifecycle.go: signal handling, shutdown, server start (~222 LOC)
- gateway_tools_wiring.go: cron/heartbeat/session tool wiring (~116 LOC)
Also extracted: startCronAndHeartbeat, makeDelegateAnnounceCallback.
Pure structural refactoring — no behavior change.
* test(agent): add v3 force migration guard tests
Verify v2 runLoop is deleted, V3PipelineEnabled field removed from
LoopConfig, and V3Flags backward compat parsing still works.
Compile-time guards prevent accidental re-introduction of v2 code.
* feat(delegate): wire ChildResult + fix media passthrough (Phase 3 gap)
- Use orchestration.CaptureFromRunResult in delegate run callback
to standardize result capture via ChildResult
- Use MediaResultToBusFiles in CaptureFromRunResult (DRY)
- Fix delegate_tool.go metadata key: delegate_id → delegation_id
* refactor(ui): remove v2/v3 pipeline toggle, always show V3 (Phase 4E)
- engine-version-section.tsx: remove pipeline toggle, show V3 read-only
- use-agent-version.ts: always return "v3" (no flag check)
- use-v3-flags.ts: remove v3_pipeline_enabled from toggleable flags
- Update i18n strings (en/vi/zh): remove v2-specific tooltip text
* test: add Phase 5 test infrastructure for v3 architecture refactor
- pg/pg_dialect_test.go: 4 tests for PG Dialect (placeholder, transform,
returning, interface compliance)
- sse_reader_test.go: 4 edge case tests (empty data, scanner error,
event type persistence, no data after [DONE])
- gateway_announce_format_test.go: 10 tests for team + subagent
announce formatting (single/batch, success/failure, snapshot)
Coverage: base/ 96%, orchestration/ 100%, providers/ 57%
* chore: remove stale runLoop references + apply go fix (Phase 6)
- Update comments referencing deleted runLoop in pipeline callbacks,
loop_types, stage.go
- go fix: reflect.TypeFor, range over int, strings.Builder
* docs: update architecture docs for v3 refactor completion (Phase 6)
- CLAUDE.md: add store/base/, orchestration/ to project structure;
remove v2 runLoop and dual-mode gate references; rename Pipeline (v3)
to Pipeline; add SSEScanner and BatchQueue to key patterns
- docs/00-architecture-overview.md: update module map with new packages,
remove [V3] markers (now standard)
- docs/17-changelog.md: add V3 Architecture Refactor entry (6 phases)
* feat(evolution): skill draft auto-generation + go fix cleanup
- Add skill draft template generation from evolution suggestions
- Wire skill apply endpoint in evolution HTTP handlers
- Apply go fix across codebase (range-over-int, reflect.TypeFor, etc.)
- Minor refactors: simplify switch/case, reduce string builder allocs
- Gateway deps: add skills loader field
* perf(prompt): deterministic tool order + Anthropic cache boundary split
Sort tool names in buildToolingSection for cache-stable output.
Insert GOCLAW_CACHE_BOUNDARY marker before Time section; Anthropic
provider splits system prompt into 2 blocks (stable cached, dynamic not).
Backward compat: no marker → single cached block.
* perf(prompt): optimize cache boundary position + add Execution Bias
Move Memory Recall and stable context files (AGENTS.md, TOOLS.md,
USER_PREDEFINED.md) above cache boundary. Dynamic per-user files
(USER.md, BOOTSTRAP.md) stay below. Add Execution Bias section
(full mode only) forcing action-oriented tool use. Fix duplicate
header when Project Context split across boundary.
* feat(prompt): add PromptMode task/none + 3-layer resolution
Expand PromptMode from full|minimal to full|task|minimal|none.
Task mode = enterprise automation: keeps Tooling, Execution Bias,
Safety-slim, Persona-slim, Skills-search, MCP-search, Memory-slim,
Workspace, Runtime, Delegation. Drops verbose sections (Tool Call
Style, Self-Evolution, Spawning, Recency, etc.).
None mode returns identity line only. 3-layer mode resolution:
runtime override > auto-detect (subagent/cron) > agent config
(other_config.prompt_mode) > default (full).
* feat(prompt): provider prompt contributions (stable/dynamic/overrides)
Add PromptContribution struct + PromptContributor interface for
provider-specific prompt customizations. Providers can inject
StablePrefix (before cache boundary), DynamicSuffix (after boundary),
or override sections by ID (e.g. execution_bias). Nil-safe: providers
that don't implement the interface get default behavior.
* feat(prompt): pinned skills with hybrid inline+search mode
Add per-agent pinned_skills config (max 10, from other_config JSONB).
Pinned skills always inline in prompt via BuildPinnedSummary.
Non-pinned discovered via skill_search. Hybrid section shows both
pinned XML and search instructions. Works in full and task modes.
* fix(prompt): validate prompt_mode from DB before cast
Reject unknown prompt_mode values from OtherConfig JSONB
(e.g. typos like "taks") by checking against validPromptModes set.
Invalid values default to "" (full mode). Prevents broken prompts
where no mode flags match.
* fix(prompt): wire SectionIDToolCallStyle for provider override
Tool Call Style section now uses sectionContent() like Execution Bias,
allowing providers to override it via PromptContribution.SectionOverrides.
* feat(store): implement 9 SQLite store backends for v3 parity
Close feature gap between PostgreSQL and SQLite (desktop/lite) editions.
96 methods across 9 stores: AgentLinks, SubagentTasks, SecureCLI,
SecureCLIGrants, EvolutionMetrics, EvolutionSuggestions, Episodic,
KnowledgeGraph, Vault. Schema v8→v9 adds 4 tables.
Key design decisions:
- LIKE-based search replaces tsvector/FTS5 (unavailable in modernc.org/sqlite)
- Go-side StringSimilarity for KG dedup (replaces pgvector cosine)
- Recursive CTE traversal with comma-delimited path cycle detection
- AES-256-GCM encryption on all SecureCLI read/write paths
- F15: SecureCLI disabled when EncryptionKey empty
- ON CONFLICT DO UPDATE (never INSERT OR REPLACE) to preserve FK cascades
* docs(store): add SQLite parity section to store data model docs
Document 9 new SQLite store implementations, schema v9, and
feature parity gaps (LIKE vs FTS, Jaro-Winkler vs vector dedup).
* fix(docker): skip web-builder stage when ENABLE_EMBEDUI=false
Use BuildKit conditional stage pattern so web-builder is not executed
when embedding is disabled. Also update pnpm-lock.yaml for 6 new deps
that were missing from the lockfile (markdown/math/mermaid packages).
* feat(vault): embed metadata.summary for richer vector search
Include summary from metadata JSONB in embedding text (title + path +
summary) for better semantic search. Update tsvector generated column
to include summary in FTS index. Zero new columns — summary stored in
existing metadata JSONB field. Backward compat: docs without summary
still embed title+path only.
* fix(vault-ui): wider detail dialog, markdown rendering, create tooltip
- Detail dialog: sm:max-w-lg → sm:max-w-2xl, content area 200→300px
- Content preview: render markdown via MarkdownRenderer instead of <pre>
- Create button: add tooltip explaining "select agent first" when disabled
* feat(ui): prompt_mode dropdown + pinned_skills multi-select
Add PromptSettingsSection to agent overview tab:
- prompt_mode: Select dropdown (full/task/minimal/none)
- pinned_skills: Tag input with max 10, click to remove
Both save to agent other_config JSONB via existing onUpdate flow.
Self-contained save button appears only when values change.
* fix(vault-ui): enlarge dialog to max-w-4xl, constrain markdown heading sizes
Dialog sm:max-w-2xl → sm:max-w-4xl. Markdown headings capped at
text-base/text-sm via Tailwind child selectors to prevent oversized
h1/h2 in content preview. Content area raised to 400px.
* refactor(ui): move pinned skills to dedicated section with skill select
Split pinned_skills out of PromptSettingsSection into PinnedSkillsSection.
Uses useAgentSkills hook for proper dropdown with granted skills list.
Placed right below SkillsSection in agent overview tab. Badge chips
with X to remove, Select dropdown to add. Max 10 enforced.
* fix(ui): add border wrap to capabilities section for visual consistency
* feat(ui): redesign prompt mode as compact cards, replace v3 badge
- Rewrite prompt settings from select dropdown to 2×2 compact cards
with lucide icons (Zap/Wrench/Package/CircleOff) and ring selection
- Move prompt settings to top of agent overview tab
- Replace v3 badge with prompt mode badge in header, card, and list row
- Add i18n keys for prompt mode labels/descriptions (en/vi/zh)
- Extract readPromptMode() to shared agent-display-utils
- Remove dead useAgentVersion hook and v3Tooltip/v2Tooltip i18n keys
- Remove v3 badge from EngineVersionSection (keep feature toggles)
- Use cn() for badge class composition with twMerge safety
* feat(ui): add section tags and token estimates to prompt mode cards
Each card now shows which system prompt sections are included
(Persona, Tools, Safety, Skills, MCP, Memory, etc.) as compact
tags, plus estimated base token range (~2-4K for full, ~10 for none).
Helps users understand the impact of each mode at a glance.
* fix(ui): correct token estimates with actual tiktoken measurements
Measured via tiktoken (cl100k) on realistic config:
full=~1.7K+, task=~1.1K+, minimal=~660, none=~6 base tokens.
The "+" suffix indicates context files/skills/MCP add more.
* fix(ui): use tiktoken-measured token ranges for prompt mode cards
Measured across 3 scenarios (bare/typical/heavy) via tiktoken cl100k:
full=~500-2.9K, task=~350-1.2K, minimal=~350-820, none=~6 tokens.
Ranges reflect real configs from minimal (3 tools) to heavy
(10 tools, long persona, MCP, sandbox, pinned skills).
* fix(ui): use production-measured token counts for prompt mode cards
Measured via tiktoken on real production agent (tieu-ho) with
4 context files (AGENTS.md, SOUL.md, IDENTITY.md, USER_PREDEFINED.md),
14 tools, memory, KG, skills, Telegram channel:
full=~3.1K, task=~2.2K, minimal=~1.9K, none=~6 tokens.
* refactor(ui): accurate section tags per prompt mode from systemprompt.go
Section tags now match exact gating logic in BuildSystemPrompt():
- full: persona, tools, exec bias, call style, safety, skills,
MCP, memory, sandbox, evolution, channel hints (11 sections)
- task: style echo, tools, exec bias, safety (slim), skills (search),
MCP (search), memory (slim) (7 sections)
- minimal: tools, safety (2 sections + shared context files)
- none: no tags shown, no token count (trivially ~6 tokens)
Removed workspace/identityOnly tags (shared across all modes).
* feat(ui): replace engine version toggles with V3 capabilities modal
- Remove 3 toggle switches (Pipeline, Memory, Retrieval) from agent
detail — all features are now always-on for v3 agents
- Replace with compact static badges layout
- New V3 Capabilities modal with 4 tabs:
Pipeline (8-stage flow), Memory (L0/L1/L2 tiers),
Knowledge (KG, Vault, Dreaming), Orchestration (Delegate, Evolution)
- Each tab has Lucide icons and technical descriptions of how
features actually work
- Separate i18n namespace v3-capabilities in en/vi/zh locales
* refactor(ui): move V3 badge to agent header, remove engine version section
- Add clickable V3 badge next to agent name in header
- Clicking opens V3 Capabilities modal
- Remove Engine Version section from overview tab entirely
- Remove unused EngineVersionSection import
* feat: v3 prompt engine overhaul — 7-phase restructuring
Phase 1: Fix mode resolution — subagent/cron cap at task (not minimal),
heartbeat stays minimal, pinned skills injected in all modes, USER_PREDEFINED
added to minimal allowlist.
Phase 2: Context file restructuring — new CAPABILITIES.md (domain expertise,
separated from SOUL.md), AGENTS_MINIMAL.md for heartbeat sessions, both added
to stable context files and minimal allowlist.
Phase 3: Summoner update — all 4 prompt builders generate CAPABILITIES.md,
fallback 2-call stores capabilities alongside SOUL.md.
Phase 4: Open agent deprecation — creation silently upgrades open→predefined
in both HTTP and WS endpoints.
Phase 5: Bootstrap auto-contact — sender name from channel metadata injected
into bootstrap context for 1-turn onboarding (DM only).
Phase 6: System prompt preview — GET /v1/agents/{id}/system-prompt-preview
endpoint with mode param, token counting, section parsing.
Phase 7: Agent creation UX — removed open agent type toggle, schema simplified,
description always required, prompt mode cards updated with v3 token estimates.
* fix(vault-ui): redesign detail dialog and link dialog UX
- Vault detail: show content preview by default (not collapsed),
move type/scope to header badges, hash to subtle footer
- Link dialog: replace native <select> with searchable Combobox
for target document and link type (5 presets + custom)
- Fix Vietnamese i18n: add proper diacritics to v3-capabilities
* fix(i18n): keep prompt section badges in English across all locales
Technical terms like Persona, Tools, Safety, Memory, Skills, MCP,
Sandbox, Evolution should not be translated — they are UI labels
matching system prompt section names.
* feat(ui): system prompt preview in Files tab + README restructure
Files tab: add "System Prompt" item in sidebar below context files.
When selected, shows readonly preview with mode selector (full/task/
minimal/none), token count badge, and cache boundary highlighting.
Fetches from GET /v1/agents/{id}/system-prompt-preview.
README: remove Claw Ecosystem comparison tables, remove OpenClaw port
reference, rename "What Makes It Different" to "Core Features" with v3
additions (8-stage pipeline, 4-mode prompt, 3-tier memory, knowledge
vault, self-evolution), slim built-in tools to category summary table.
* feat: replace architecture images with v3 sketchnotes
Add 9 new architecture sketchnote images generated for v3:
- 8-Stage Agent Pipeline
- 4-Mode Prompt System
- 3-Tier Memory Architecture
- Multi-Tenant Architecture
- Agent Orchestration
- Knowledge Vault
- Provider Adapter System
- Self-Evolution System
- DomainEventBus
Remove old architecture images (architecture.jpg, goclaw_multi_tenant.png,
agent-delegation.jpg, agent-teams.jpg).
Update README to reference new images in Architecture and Orchestration
sections. Consolidate orchestration section (remove separate delegation
and teams subsections).
* feat(readme): add remaining 4 architecture sketchnotes with sections
Add Knowledge Vault, Self-Evolution, Provider Adapters, and Event-Driven
Architecture sections with corresponding sketchnote images and concise
descriptions. All 9 v3 architecture diagrams now in README.
* feat: reorder README architecture images, evolution guardrails fix, memory tools enhancement
README: reorder architecture sketchnotes (Multi-Tenant first), remove
pinnedSkills from task mode sections badge.
Backend: evolution guardrails fix, memory auto-injector and tools
enhancement, gateway HTTP wiring update.
* feat(desktop): add 12 v3 feature sections to agent detail panel
Add evolution expansion (skill learning, v3 flags), prompt mode selector,
evolution dashboard tab (CSS bar charts, suggestions, guardrails),
thinking/reasoning config, orchestration display, context pruning,
compaction, subagents (lite-limited), tool policy, sandbox config,
and pinned skills management.
Extract agent detail state into use-agent-detail-state hook.
Add getWithParams to ApiClient. Add i18n keys to en/vi/zh locales.
* fix(vault): team-scope security — prevent cross-team data corruption and leaks
- Add team_id UUID + custom_scope to vault_documents (PG migration 043, SQLite migration 10)
- COALESCE-based UNIQUE prevents silent cross-team data overwrite on ON CONFLICT
- PG trigger auto-corrects scope…
* ci(release): switch from auto-deploy to tag-based release
Replace go-semantic-release (auto on push main) with tag-based trigger:
- Push tag v1.2.3 → auto release (stable only, not beta/rc)
- Manual dispatch with optional tag input as fallback
- gh release create with --generate-notes replaces semantic-release changelog
- Beta/RC tags still handled by release-beta.yaml (unchanged)
* fix(ci): resolve 4 workflow failures — tests, race, Windows build
- slack: update splitAtLimit tests to match ChunkMarkdown behavior
- providerresolve: use ChatGPTOAuthRouting field instead of OtherConfig
- scheduler: buffer started channel to prevent race in DropOldPolicy
- backup: split checkDiskSpace into platform files (Unix/Windows)
* feat(vault): modal document view, compact sidebar, non-owner team access
UI:
- Replace bottom detail panel with modal dialog for all doc views
- Redesign sidebar with type icons, compact layout, inline metadata
- Always show team select, fix missing load() call for teams hook
- Align header heights between sidebar and main panel
Backend:
- Add TeamIDs filter to VaultListOptions and VaultSearchOptions
- Non-owner users now see personal + their teams' docs (was personal-only)
- Refactor PG search to use searchTeamFilter for FTS and vector queries
- Update both PG and SQLite stores with shared team filter helpers
* fix(backup): guard nil context in checkPgDump preflight
* feat(channels): add Facebook Messenger and Pancake channel integrations (#731)
Add two new channel implementations for Facebook Fanpage (comment + Messenger
auto-reply, first inbox DM) and Pancake/pages.fm (multi-platform inbox via
Facebook, Zalo, Instagram, TikTok, WhatsApp, LINE).
Key features:
- Facebook: comment auto-reply, Messenger auto-reply, first inbox DM,
HMAC-SHA256 webhook verification, multi-page webhook routing
- Pancake: multi-platform inbox, outbound echo dedup with HTML normalization,
race-condition-safe echo fingerprinting, platform-aware formatting
- Bootstrap skip: pre-fill USER.md from channel metadata (Pancake)
- SanitizeDisplayName across all channels (defense in depth)
Code audit fixes:
- Fix truncateForTikTok byte→rune slicing (UTF-8 corruption)
- Fix empty message.ID shared dedup slot (silent message loss)
- Fix DisplayName markdown injection in buildPrefilledUser
- Consolidate duplicate ChannelMeta type (agent→bootstrap)
- Compile-time interface assertions, alphabetical type constants
- Per-message logs demoted to slog.Debug, errors.As for wrapped errors
- UI: alphabetical channel ordering, complete config schemas
- Remove deprecated WhatsApp bridge_url, fix nested error parsing
* Delete plans/260406-1304-goclaw-v3-core-design directory
* fix(vault): adaptive label truncation and zoom-aware link labels in graph view
Node labels now truncate based on zoom level (10→18→30→full chars),
showing full text only when zoomed in close enough to read.
Link labels hidden below 1.5x zoom to reduce visual clutter.
Selected nodes always show full label regardless of zoom.
* refactor(vault): remove vault_link/vault_backlinks tools, auto-sync wikilinks in enrichment pipeline
- Remove vault_link and vault_backlinks builtin tools (replaced by auto-linking)
- Add DeleteDocLinksByType to VaultStore interface (PG + SQLite) to selectively
delete links by type without destroying semantic links
- Integrate SyncDocLinks into enrichment worker: [[wikilinks]] are now
auto-extracted from document content and synced as vault links on every write
- Fix SyncDocLinks to use DeleteDocLinksByType("wikilink") instead of
DeleteDocLinks which was deleting all link types including semantic
- Add missing coreToolSummaries for delegate, memory_expand, vault_search
(previously showed as "(custom tool)" in system prompt)
- Increase enrichSimilarityLimit from 5 to 10 for richer auto-linking
* fix(ui): persist chat messages across sessions and fix first-message drop
Migrate chat message state from component-local useState to a Zustand
store so messages survive session switches and back-navigation.
- Infinite re-render loop (React #185): inline `?? []` fallback in
Zustand selector created a new array reference every render; fixed
with a stable EMPTY_MESSAGES module constant.
- First message disappearing in new chat: addLocalMessage used the
component-level sessionKey which is still "" when send fires
(navigate is async); fixed by threading explicit sessionKey through
onMessageAdded in useChatSend.
* fix(ui): add LRU eviction to chat messages store
Keep at most 25 cached sessions in memory. When the limit is exceeded,
evict the oldest idle sessions (never evict sessions with an active run).
* feat(vault): replace auto-linking with LLM-classified relationship types
Replace vector-similarity-only auto-linking (creating generic "semantic"
links) with an LLM classification step that determines actual relationship
types (reference, depends_on, extends, related, supersedes, contradicts)
and generates meaningful context descriptions.
- Add enrich_classify.go: orchestration, retry with escalating timeouts,
candidate gathering with bidirectional dedup, 20-doc cap per batch
- Add enrich_classify_prompt.go: system prompt, JSON parsing with partial
success model, UTF-8 safe truncation
- Restructure processBatch: summarize → embed → classify → dedup+wikilinks
- Move dedup recording after classify (failed classify allows re-enrichment)
- Remove autoLink method (fully replaced by classifyLinks)
- Add DeleteDocLinksByTypes to VaultStore interface + PG/SQLite (IN clause)
- Guard old link deletion behind len(newLinks) > 0 (no data loss on all-SKIP)
* fix(evolution): fix 7 bugs in evolution flow — guardrails, cron, dedup, dead types
- Remove broken delta comparison in CheckGuardrails (compared usage rate vs max delta)
- Replace hardcoded +0.05 threshold bump with configurable MaxDeltaPerCycle, cap at 0.95
- Remove dead MetricFeedback and SuggestMemoryPrune constants + UI references
- Make SuggestToolOrder approval actionable — disables tool via BuiltinToolTenantConfigStore
- Replace time.Ticker(24h) with wall-clock scheduling (3 AM daily, Sundays weekly eval)
- Add PG advisory lock on pinned connection for multi-instance safety
- Add EvolutionSuggest flag check in cron (metrics-only agents skip suggestion analysis)
- Change suggestion dedup from type-only to (type, metric_key) composite key
* fix(tracing): include thinking tokens in cost calculation
CalculateCost previously ignored usage.ThinkingTokens entirely, causing
cost underestimation for every reasoning-capable model (OpenAI o3/o4-mini,
Codex/GPT-5, Anthropic extended thinking).
Thinking tokens are reported by providers as a sub-count of completion
tokens — OpenAI completion_tokens already includes reasoning_tokens,
Codex output_tokens likewise, and Anthropic output_tokens counts thinking
at the output rate. Default behaviour (ReasoningPerMillion=0) now matches
provider billing semantics: full CompletionTokens priced at OutputPerMillion,
no double-counting.
When ReasoningPerMillion is explicitly configured, the completion bucket is
split into visible output + thinking and each priced independently. Defensive
clamp to zero when Anthropic's thinkingChars/4 estimate exceeds reported
OutputTokens.
Adds ReasoningPerMillion field to ModelPricing and 7 regression tests
covering the double-count gate, distinct-rate split, Anthropic estimate
overrun, and backward compatibility for non-reasoning models.
* feat(pipeline): per-provider context window, cache/tenant/pipeline hardening
Ship Group A (bug fixes) + Group B (pipeline enhancements) from
plans/260410-1009-openclaw-ts-feature-port — five changes that share
cmd/ wiring and pipeline plumbing so they commit as a unit.
cache: InMemoryCache now supports periodic sweep + max-size cap via
variadic options. PermissionCache wires 60s sweep + 10k entry cap +
Close() hook in gateway shutdown so long-running gateways don't leak
per-user permission entries. Backward compatible for zero-arg callers.
store: ContactCollector.seen cache key now includes tenant_id + channel_
instance so the same sender in different tenants (or different bot
instances in the same tenant) no longer silently skip upserts against
each other. Zero-tenant (Desktop) behaviour preserved.
pipeline: EffectiveContextWindow is resolved once per run in ContextStage
via a ResolveContextWindow callback (backed by providers.ModelRegistry)
so PruneStage bills history against the actual model window instead of
a stale static config. Nil resolver / unknown model fall back to
Config.ContextWindow for backward compatibility. Locked to the model
observed at context build time to prevent mid-run budget drift.
pipeline: PipelineConfig.ReserveTokens carves out an optional safety
buffer subtracted from the history budget so compaction fires slightly
before the hard limit — protects against provider over-delivery and
token counter drift on streaming responses. Zero (default) preserves
legacy budget math.
agent: ModelRegistry flows gateway → ResolverDeps → LoopConfig → Loop →
pipeline adapter so resolver can look up per-model capabilities at run
time without re-touching gateway internals.
20 regression tests across cache, contact collector, and pipeline cover
the critical paths: cross-tenant isolation, cache sweep + eviction +
Close idempotency, per-model window override + fallback, and reserve
token buffer behaviour. All passing with -race on both go build ./...
and go build -tags sqliteonly ./....
* feat(memory): context-aware recall query for auto-inject
Auto-inject previously searched episodic memory using only the latest
user message. Follow-up questions like "what's my favorite?" returned
poor matches because the embedding lost the conversational frame.
InjectParams now carries an optional RecentContext field that pgAuto
Injector prepends to the search query as "Context: ... \nQuery: ..."
before running the FTS+vector hybrid search. The "Context:"/"Query:"
framing works with both instruction-tuned embedding models (which
respect the labels) and plain models (neutral separators).
ContextStage walks the message history backward, collects up to 2
trailing user turns capped at 300 runes total, and threads the snippet
through the AutoInject callback to the injector. Empty RecentContext
preserves legacy single-message search semantics — zero-risk fallback
for callers that haven't adopted the new field.
Rune-based truncation (not byte) keeps vi/zh locales safe: a byte-wise
tail-clip would slice multi-byte runes and emit invalid UTF-8 to the
embedding model, degrading exactly the cases Phase 9 is meant to fix.
tailClipRunes helper covers Vietnamese, Chinese, Japanese, emoji.
13 regression tests: recall query builder (unicode-safe clip,
whitespace handling, position ordering), buildRecentContext (order
preservation, turn cap, truncation, non-user skip), and
tailClipRunes (CJK, short input, zero cap). All passing with -race.
Refs plans/260410-1009-openclaw-ts-feature-port/phase-09-active-
memory-recall.md — minimal-viable delivery; Tier 2 LLM re-ranking
and per-session recall cache deferred until operational data shows
context-aware search alone is insufficient.
* feat(ui/graph): migrate vault + KG graph to Sigma.js + Graphology
Replace react-force-graph-2d (Canvas 2D, 500-node ceiling) with Sigma.js
(WebGL, 5k+ nodes at 60fps) for vault and knowledge graph visualization.
Layout pipeline uses Louvain community detection + grid seeding + FA2
refinement + noverlap post-processing. Communities are placed in a grid
(sorted by size) so related nodes cluster visually instead of collapsing
into a ring shape.
Shared graph infrastructure in ui/web/src/components/graph/:
- sigma-graph-container: lifecycle, synchronous pre-computed layout,
2-hop BFS highlight on hover, opacity edge pulse, prefers-reduced-motion
- sigma-graph-controls: reusable zoom/stats bar with camera tracking
- sigma-graph-search: combobox with ARIA wiring and keyboard navigation
- sigma-graph-filters: type filter dropdown (outside-click + Escape)
- sigma-graph-minimap: canvas overlay with viewport rect + click-to-jump
- sigma-graph-keyboard-help: help popover triggered by ? key
- use-sigma-keyboard: +/- R F Esc / shortcuts
- graph-utils: sizing, middle-truncation helpers
Adapters rewritten to output Graphology Graph instances. Both views use
curved arrow edges, focus ring for keyboard users, role="application"
ARIA, responsive top bar, and node limits up to 5k.
Removes react-force-graph-2d, d3-force, @types/d3-force.
Adds sigma, @sigma/edge-curve, graphology, graphology-layout,
graphology-layout-forceatlas2, graphology-layout-noverlap,
graphology-communities-louvain, graphology-types.
* feat(vault): workspace rescan endpoint with symlink-safe walker
Add POST /v1/agents/{agentID}/vault/rescan to backfill vault_documents
from filesystem. Walks agent workspace, registers missing/changed files,
publishes enrichment events for async summarize + embed + link classify.
Backend:
- SafeWalkWorkspace: symlink-safe walker with exclusion patterns,
resource limits (5K files, 500MB, 50MB/file), context deadline
- RescanWorkspace: hash-based dedup, path-based scope detection
(teams/{id}/ → team scope), idempotent via UpsertDocument ON CONFLICT
- Per-agent mutex (409 Conflict for concurrent rescans)
- Tenant-scoped workspace resolution (config.TenantWorkspace)
- Enrichment worker: semaphore-based parallel summarize (max 3 concurrent)
- Media files without summary skip LLM summarize, embed title+path only
- DRY: export InferTitle/InferDocType from vault pkg, remove from interceptor
- Auto-register text uploads in vault via onTextUploaded callback
Frontend:
- Rescan button in vault page header (FolderSync icon)
- Toast with result counts, 409 warning, error handling
- i18n strings for en/vi/zh
Security: skip all symlinks, boundary checks, per-file size limit,
tenant isolation via server-side workspace resolution.
* feat(ts-port): reasoning strip, dreaming config + weighted scoring
Phase 6 — Reasoning token stripping:
- ReasoningDecision.StripThinking auto-flags Kimi + DeepSeek-Reasoner
- Guard clauses in Anthropic/OpenAI/Codex stream handlers
- Usage.ThinkingTokens + RawAssistantContent preserved (billing + tool passback safe)
Phase 8 — Per-agent dreaming config:
- MemoryConfig.Dreaming JSONB (no migration), resolver callback pattern
- Enabled/DebounceMs/Threshold/VerboseLog fields with partial-override merge
- ConsolidationDeps gains optional AgentStore
Phase 10 — Dreaming weighted scoring:
- Migration 000045 adds recall_count/recall_score/last_recalled_at on episodic_summaries
- ComputeRecallScore 4-component formula (freq/rel/recency/freshness, 14d half-life)
- memory_search fire-and-forget RecordRecall; ListUnpromotedScored in DreamingWorker
- Bootstrap-friendly filter: unrecalled entries bypass thresholds
- Debounce stamped on filter-empty skip to prevent starvation loop
Phase 5 follow-up — last_compaction_at in sessions.metadata JSONB:
- v3 PruneStage.CompactMessages and v2 maybeSummarize both stamp timestamp
- Zero migration; exported const SessionMetaKeyLastCompactionAt
RequiredSchemaVersion: 44 → 45 (PG), SchemaVersion: 12 → 13 (SQLite).
27 new tests; builds pass under PG and sqliteonly tags.
* feat(ui): dreaming config section and context usage badge
Phase 11 — web UI components:
- DreamingConfig TS interface + nested block in MemorySection (4 controls)
- Context usage badge in ChatTopBar: used/max/percent with color ramp
- Badge tooltip surfaces last_compaction_at from session.metadata
- i18n keys in en/vi/zh: configSections.dreaming.*, chat.contextUsage.*
- Hidden on mobile via hidden sm:flex (follow-up: compact variant)
* feat(vault): tenant-wide rescan with nullable agent_id + media preview
Vault rescan redesigned from per-agent to tenant-wide:
- POST /v1/vault/rescan replaces POST /v1/agents/{id}/vault/rescan
- agent_id nullable in vault_documents (PG migration 046 + SQLite v14)
- Path-based scope inference: agents/{key}/ → personal, teams/{uuid}/ → team, root → shared
- Interceptor sets agent_id=NULL for team-scoped file writes
- Enrichment worker batch key handles empty agent_id
- web-fetch/ directory excluded from vault scan at any depth
- Media preview: images render via authenticated blob URL, binary files show metadata
- Scan button no longer requires agent selection
* fix(vault): rescan path resolution, batch enrichment pipeline, cross-agent create
- Fix rescan storing stripped paths (teams/{uuid}/... → file.md) causing
enrichment worker to fail locating files on disk. Now stores full
workspace-relative path with backward-compat fallback for old records.
- Refactor enrichment into chunked pipeline: batch 5 files per LLM call
(was 1:1), 3 parallel goroutines via semaphore — reduces LLM calls ~80%.
- Extract shared chatWithRetry for summarize + classify (3 retries,
escalating timeouts 5m→7m→10m).
- Add POST /v1/vault/documents route for cross-agent doc creation
(agentID optional, defaults scope to shared when no agent).
- Add path traversal validation (reject .. and leading /) on create.
- Skip media files in syncWikilinks to prevent binary garbage parsing.
- Bound syncWikilinks file read to 4MB.
* feat(vault): cross-agent API, WS enrichment progress, nullable agent_id
Backend:
- Add cross-agent routes for all vault CRUD (/v1/vault/documents/*, /v1/vault/links/*)
alongside existing per-agent routes for backward compat.
- All handlers skip agent ownership check when agentID is empty.
- Path traversal validation (reject .. and leading /) on create.
- Scope defaults to "shared" when creating without agent.
- Enrichment progress broadcast via WS event (vault.enrich.progress)
using existing MessageBus infrastructure.
- GET /v1/vault/enrichment/status for polling fallback.
Frontend:
- Fix VaultDocument.agent_id type to optional/nullable — prevents
runtime crash on shared/team docs without agent.
- All vault hooks use cross-agent URLs (/v1/vault/...) — no more
skipping docs with null agent_id in graph/links/mutations.
- Remove dead agentId params from useUpdateDocument, useDeleteDocument,
useCreateLink, useDeleteLink, useVaultLinks, LinkBadge.
- Pass team_id from selected team filter to create dialog.
- WS-based enrichment progress bar on vault page (useWsEvent hook).
- Disable rescan button during enrichment, auto-clear after 3s.
* fix(ui/graph): use random+strongGravity FA2 for sparse vault graphs
Sparse graphs (edges/node < 0.8) previously used Louvain community
detection which created hundreds of singleton communities → grid layout.
Now uses random disc init + FA2 with strongGravityMode + higher gravity
to pull orphan nodes inward. Dense graphs keep Louvain-seeded layout.
* perf: eliminate N+1 query patterns across import, vault, and usage pipelines
- Convert importCron/importUserProfiles/importUserOverrides to multi-row
UPSERT with chunking (200→2 queries per import)
- Add GetTenantsByIDs batch method to TenantStore (PG+SQLite), refactor
handleMine() to single-query tenant fetch (N→1)
- Refactor buildAgentKeyMap() to use existing GetByKeys with tenant-scoped
context instead of per-key SELECT (N→1)
- Add GetDocumentsByIDs, GetDocumentByBasename, CreateLinks batch methods
to VaultStore (PG+SQLite)
- Batch-fetch docs in enrichment Phase 0, carry title through pipeline to
avoid per-doc refetch in classify phase
- Replace ListDocuments(limit=500) fallback in wikilink resolution with
targeted GetDocumentByBasename DB query
- Batch CreateLink calls in SyncDocLinks and classifyLinks
- Refactor usage handleGet/handleSummary to use ListPagedRich instead of
List+GetOrCreate N+1 loop (100→1 queries)
- Batch INSERT for team import members/comments/events/links (250→5)
- Add migration 47: UNIQUE constraint on cron_jobs(agent_id,tenant_id,name)
with dedup, SQLite schema v15
* fix(sqlite): fix pre-existing test failures in cron and schema migration tests
- Add missing reloadClaimed=false arg to executeOneJob call in cron toggle test
- Fix schema migration tests that applied full schema then rolled back version,
causing duplicate column errors on re-migration. Now recreates table without
post-targetVersion columns before re-running migrations.
* fix(ui/vault): adaptive graph layout, color palette, and link name resolution
- Replace density-branched FA2 layout with unified adaptive algorithm
based on orphan ratio instead of misleading edge density threshold
- Remove Louvain community grid seeding (caused ring/rectangle artifacts)
- Enable strongGravity + linLog + outboundAttractionDistribution for
organic cluster layout on both vault and KG graphs
- Update vault type colors: note→teal, media→pink (softer for dominant types)
- Increase light-mode edge visibility (slate-400 @ 80% vs gray-300 @ 60%)
- Enrich vault links API with doc_names map for outlink target titles
- Show document names instead of truncated UUIDs in link badges
- Use existing backlink title field from backend JOIN
- Horizontal scroll for link badges to preserve content viewport
- Move "Add Link" button to bottom bar in detail dialog
- Remove unused graphology-communities-louvain dependency
* fix(cron): prevent scheduler loop from blocking when a job hangs (#820)
* fix(cron): prevent scheduler loop from blocking when a job hangs
The cron scheduler's runLoop calls checkAndRunDueJobs() every second,
which previously used wg.Wait() to block until ALL claimed jobs complete.
If any single job hung (LLM provider timeout, agent loop stuck, network
issue), wg.Wait() would block indefinitely, preventing the scheduler
from ever checking for new due jobs — effectively killing all cron
scheduling until a container restart.
Changes:
- Remove wg.Wait() from both PG and SQLite cron schedulers — jobs now
run as independent goroutines that don't block the check loop
- Add panic recovery to PG runLoop (safeCheckAndRunDueJobs wrapper)
and per-job goroutines, matching the existing safego.Recover pattern
in the SQLite scheduler
- Add 10-minute context timeout to the cron job handler so a hung
agent run is cancelled instead of blocking forever
- Use select with context.Done() in the handler to respect the timeout
when waiting for the scheduler outcome
- Invalidate PG job cache per-job on completion instead of after the
(now-removed) batch wait
The SQLite scheduler already had safego.Recover on job goroutines but
still used wg.Wait() — this commit removes that blocking wait as well.
* fix(cron): make job timeout configurable + add SQLite runLoop panic recovery
- Add `cron.job_timeout` config field (Go duration string, default "10m")
so operators can tune the per-job timeout for complex agent workloads
without code changes
- Add `safeCheckJobs` panic recovery wrapper to SQLite cron runLoop,
matching the PG scheduler's `safeCheckAndRunDueJobs` for consistency
- Use dynamic timeout string in error message for better diagnostics
* fix: remove unused "time" import from gateway_cron.go
* fix(cron): apply same fixes to SQLite DB scheduler (sqlitestore)
The SQLite DB-backed scheduler (used by desktop edition with SQLite
backend) had the exact same wg.Wait() blocking issue and missing
panic recovery as the PG scheduler. Apply identical fixes:
- Remove wg.Wait() — jobs run as independent goroutines
- Add safeCheckAndRunDueJobs panic recovery wrapper for runLoop
- Add per-job panic recovery and cache invalidation
---------
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
* fix(cron): improve panic recovery and config validation
- Use safego.Recover for per-job panic recovery in PG and SQLite cron
schedulers (captures stack traces vs inline fmt.Sprint)
- Log warning when cron job_timeout config value is invalid instead of
silently falling back to default
* feat(vault): replace create dialog with multi-file upload modal
- Add POST /v1/vault/upload multipart endpoint with extension whitelist,
50MB/file + 50 file count limits, path traversal prevention
- Replace manual create dialog with upload dialog: radio destination
(Shared/Agent/Team), drag-drop zone, scrollable file list
- Files written to workspace folders, auto-enrich via EventVaultDocUpserted
- Split vault_handlers.go (991 lines) into 4 focused modules (~260 lines each)
- Add i18n keys for upload dialog (en/vi/zh)
* fix(mcp): full reconnect for SSE/HTTP after server-side restart (#812)
* fix(mcp): full reconnect for SSE/HTTP after server-side restart
When an MCP server using SSE or streamable-http transport restarts
(container redeploy, crash, OOM), the old client holds a stale session
ID. tryReconnect only called Ping() on the dead client, which keeps
POSTing to /message?sessionId=<dead> — the server returns 404, and
after maxReconnectAttempts the connection is permanently dead.
For pool connections, poolHealthLoop only set connected=false without
attempting any reconnect at all.
Fix:
- Store connection params (url, headers, command, args, env) in
serverState so tryReconnect can create fresh connections.
- tryReconnect now has two phases: fast path (ping existing client for
transient blips) and slow path (close old client, create fresh one
via createClient + Start + Initialize, swap ss.client).
- Add updateBridgeToolClients to propagate the new client pointer to
all registered BridgeTools after a full reconnect.
- Add poolTryReconnect with the same two-phase pattern for
pool-managed connections.
- Add BridgeTool.swapClient for safe client pointer replacement.
Closes#810
* fix(mcp): address review findings — atomic client pointer, close-after-swap, cooldown
Red-team review found critical issues in the initial reconnect fix:
1. Data race: BridgeTool.client written by healthLoop, read by Execute
concurrently without synchronization. Fix: BridgeTool now holds
*atomic.Pointer[mcpclient.Client] shared with serverState.clientPtr.
Execute uses atomic Load(), reconnect uses atomic Store().
2. Close-before-swap: old client was closed before new one was ready.
If createClient failed, ss.client pointed to closed client permanently.
Fix: create and validate new client first, swap atomically, then close old.
3. Pool BridgeTool orphaning: poolTryReconnect swapped ss.client but
existing BridgeTools held old pointer with no update mechanism.
Fix: atomic.Pointer propagates automatically — no explicit update needed.
4. Permanent death: after maxReconnectAttempts, server was permanently
dead with no recovery. Fix: 5-minute cooldown then reset attempts.
5. healthFailures not reset in fast path: minor inconsistency fixed.
Also fixes external caller in loop_mcp_user.go and adds ClientPtr()
accessor to poolEntry for atomic pointer sharing.
* refactor(mcp): extract fullReconnect helper, add nil guard, clarify dual-pointer design
- Extract shared fullReconnect() used by both tryReconnect and
poolTryReconnect — eliminates 30-line duplication (-17 lines net)
- Add nil guard on clientPtr.Load() in BridgeTool.Execute to prevent
nil deref in edge cases (test fixtures, initialization race)
- Add doc comment on serverState explaining dual-pointer design
(client for healthLoop, clientPtr for BridgeTools)
* refactor(mcp): DRY reconnect logic, fix client pointer thread safety
- Extract reconnectWithBackoff() shared by Manager and Pool reconnect paths
- Use clientPtr.Load() in Stop/Evict/evict paths to avoid race with fullReconnect
- Add tool staleness note to fullReconnect
- Remove dead poolEntry.Client()
* feat(cron): run evolution analysis every 6 hours instead of daily
Change suggestion analysis schedule from once daily (3 AM) to every 6
hours (3:00/9:00/15:00/21:00). Weekly evaluation still runs on Sundays
at 3 AM only.
* fix(ui): redesign vault create dialog — larger modal, Radix components, overflow fix
Replace native radio/select with Radix RadioGroup and Select components.
Widen modal from max-w-md to max-w-lg, add scroll constraint for
long content, fix element overflow on mobile.
* fix(agent): use agentUUID instead of agent key in DomainEvent for epi… (#826)
* fix(agent): use agentUUID instead of agent key in DomainEvent for episodic worker
DomainEvent.AgentID was set to l.id (agent_key string like "goctech-leader")
instead of l.agentUUID.String() (UUID). This caused episodic worker to fail
with "invalid input syntax for type uuid" on every session.completed event,
breaking the entire 3-tier memory consolidation pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(consolidation): parse tenant/agent IDs safely in episodic worker
Replace uuid.MustParse with uuid.Parse so a malformed tenant_id or
agent_id in a DomainEvent no longer panics the worker goroutine. Parse
up front and return a descriptive error before touching the store, so
bad IDs surface with context instead of leaking into a raw PG error.
Add regression tests asserting Handle rejects non-UUID agent_id and
tenant_id with clear errors and skips creation — guards against the
l.id vs l.agentUUID mix-up fixed in the prior commit.
---------
Co-authored-by: tuannt23065 <tuannt23065@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: viettranx <viettranx@gmail.com>
* fix(deps): bump vite 8.0.3 → 8.0.8 to patch 3 CVEs
- CVE-2026-39363 (High): arbitrary file read via dev server WebSocket
- CVE-2026-39364 (High): server.fs.deny bypass with query strings
- CVE-2026-39365 (Medium): path traversal in optimized deps .map handling
Dev-scope only. Updates both ui/web and ui/desktop/frontend lockfiles.
* Update copyright holder
* fix(backup): detect pg server major for version-aware pg_dump hints (#829) (#830)
* fix(backup): detect pg server major for version-aware pg_dump hints
pg_dump aborts when its major version is older than the server's, so
backup preflight now runs SHOW server_version_num against the live PG
server and uses the detected major to drive:
- the missing-pg_dump hint (names the exact postgresqlNN-client)
- a new compat check that flags an installed-but-too-old pg_dump as
not-ready, instead of letting backup fail partway through the dump
Adds ParsePgDumpMajor helper for Debian/Homebrew/EDB output shapes,
covered by table-driven unit tests. Normalizes nil ctx once at
RunPreflight boundary so downstream exec.CommandContext calls are safe.
Stubs detectPGServerMajor + checkPgDumpServerCompat for the sqliteonly
build.
UI: removes the duplicate static hint from the preflight alert box so
the dynamic, actionable warning is the single source of truth. Drops
the obsolete pgDumpHint i18n key from en/vi/zh locale files.
Refs #829
* fix(docker): bump runtime base alpine 3.22 → 3.23
Alpine 3.23 main ships postgresql16-client, postgresql17-client, and
postgresql18-client simultaneously. Alpine 3.22 only shipped up to
postgresql17-client, so the backup preflight's dynamic hint to install
postgresql18-client previously failed with "no such package" on PG 18
deployments.
No client package is pre-bundled: the on-demand install via the
Packages page now resolves for any supported PG major.
Refs #829
* fix(eventbus): add non-UUID AgentID publish-time guard
Publish-time observer warns (does not block) when DomainEvent.AgentID is
non-empty and fails uuid.Parse. Distinct log field `non_uuid_agent_id`
avoids colliding with existing telemetry that parses `agent_id` as UUID.
Safety net for PR #826-class regressions. Validator lives in bus.Publish
so all call sites are covered without per-publisher instrumentation.
Phase 1 Fix C of agent identity hardening.
* fix(agent): set AgentUUID in memoryflush SystemPromptConfig
memoryflush.go constructed SystemPromptConfig without AgentUUID, while
loop_history.go set both AgentID (agent_key) and AgentUUID. buildRuntimeSection
renders below the cache boundary so the omission did not bust the prompt
cache — but the runtime identity line dropped the UUID during compaction
flush turns.
Extract a pure helper buildMemoryFlushPromptConfig so tests can assert the
config shape without a full Loop fixture (M7 mitigation from red-team
review).
Phase 1 Fix A of agent identity hardening.
* fix(vault): validate agent_id/team_id form params at HTTP boundary
validateTeamMembership short-circuits on owner role and nil teamAccess
(lite edition), leaving downstream mustParseUUID calls as a silent-nil
trap. Validate UUIDs at the HTTP boundary — before workspace resolution,
store upsert, or event publish — so bad form input is rejected for every
caller regardless of role or edition.
Closes the owner/lite gap identified in red-team H9.
Phase 1 Fix B of agent identity hardening.
* fix(router): canonicalize cache key, exact-segment match, tenant-aware IsRunning
Three related fixes to agent router identity handling:
1. Canonicalize on resolve — Router.Get now stores entries under the
canonical tenantID:agent_key after a successful resolver call,
regardless of whether the caller passed a UUID or agent_key. Prevents
fragmentation where the same logical agent would occupy two cache
slots. Callers that pass the UUID form are now un-cached and resolve
on every call; all production callers pass agent_key today.
2. Exact-segment match — matchAgentCacheKey helper replaces HasSuffix in
Router.Remove and Router.InvalidateAgent. Prevents substring collisions
like "tenantX:sub-foo" being wiped when invalidating "foo". Rejects
empty agentKey to guard against wildcard wipes.
3. Tenant-scoped IsRunning (C6) — Router.IsRunning now accepts ctx and
uses agentCacheKey for lookup. Pre-fix, the bare r.agents[agentID]
lookup always returned false in tenant-scoped deployments, causing
agents.list to report every live agent as idle.
Updates the sole caller at gateway/methods/agents.go:134.
Phase 2 of agent identity hardening (TD-3).
* chore(agents): remove dead UUID invalidation in agents_update
Post-Phase-2 canonicalization, router cache entries are always keyed as
tenantID:agent_key. The belt-and-suspenders m.agents.InvalidateAgent(ag.ID.String())
call never matched any entry — exact-segment match on the final ":"-
delimited segment cannot match a UUID as the agentKey.
Removes the dead call and the misleading comment that justified it.
Phase 2 follow-up (H7).
* feat(gateway/methods): add cache-aware resolveAgentUUIDCached helper
Three-part addition so WS handlers can accept agent_key input without a
DB roundtrip on every request:
1. Router.GetCached — lock-free TTL cache read, no resolver fallback.
2. Loop.UUID() — canonical UUID accessor (agent_key stays on ID()).
3. resolveAgentUUIDCached — fast-path checks router cache via an
agentUUIDProvider interface, falls back to resolveAgentUUID on miss
or when input is a UUID string (un-cached post-canonicalization).
Helper delegates to pure DB lookup when router is nil, so handlers with
no router dependency still work. Tests cover nil-router, cache-miss, and
UUID-input paths — cache-hit coverage is exercised via downstream handler
integration tests.
Phase 3 foundation (TD-2).
* refactor(ws/heartbeat): accept agent_key or UUID via cache-aware resolver
All 7 heartbeat handlers (get/set/toggle/test/logs/checklistGet/checklistSet)
now accept either a UUID or an agent_key for params.agentId. Behavior
additive — existing UUID-only clients keep working.
Adds HeartbeatMethods.SetAgentRouter setter wired from cmd/gateway_methods.go
so the fast path avoids a DB roundtrip for cached agents. Preserves the
literal "invalid agentId" error string (no i18n key change).
Phase 3 of agent identity hardening.
* refactor(ws/config): accept agent_key or UUID for permission handlers
All 3 handlers (handleList, handleGrant, handleRevoke) now accept either
a UUID or an agent_key for params.agentId via resolveAgentUUIDCached.
Adds ConfigPermissionsMethods.SetAgentRouter setter wired from
cmd/gateway_methods.go (already staged in previous commit).
Phase 3 of agent identity hardening.
* refactor(ws/channels): accept agent_key or UUID (adds agentStore to struct)
NFR-2 exception (Phase 3 C2): ChannelInstancesMethods struct gains an
agentStore field, required to support agent_key input via the cache-aware
resolver. Constructor signature expanded and single caller at
cmd/gateway_channels_setup.go:146 updated.
Router is passed nil — channel_instances methods are not wired to the
router today, so falls back to pure DB lookup. Acceptable given create
is a rare operation. Preserves the existing i18n key MsgInvalidID.
Phase 3 of agent identity hardening.
* refactor(ws/teams): accept agent_key or UUID + fix handleAddMember error leak
Two teams handlers migrated to resolveAgentUUIDCached:
- handleRemoveMember (params.AgentID) accepts agent_key or UUID.
- handleTaskAssign (params.AgentID) accepts agent_key or UUID.
handleAddMember already used resolveAgentInfo, but leaked raw store
errors via "agent: " + err.Error() on resolution failure. H10 fix
replaces the leak with i18n.MsgInvalidID — no more store schema info
surfacing to WS clients.
Phase 3 of agent identity hardening.
* refactor(store/pg): add parseUUID error variant
Two new helpers alongside existing mustParseUUID:
- parseUUID(s) (uuid.UUID, error) — use for INSERT/UPDATE/UPSERT/DELETE
and SELECT-WHERE paths where silent nil hides bugs. Returns a wrapped
error that includes the raw value (for debuggability) and the
underlying uuid parse error (for errors.Is unwrap).
- parseUUIDOrNil(s) uuid.UUID — intentionally silent, for read-only
SELECT-WHERE paths where empty result is acceptable.
mustParseUUID is kept as a transitional alias delegating to parseUUIDOrNil
so existing call sites compile unchanged. Sub-step 4d will rename it.
Phase 4 Step 4a of agent identity hardening (TD-1).
* refactor(store/pg): propagate error from optAgentUUID wrapper
optAgentUUID now returns (*uuid.UUID, error). All three known callers
updated to check the error and wrap with context:
1. vault_documents.go UpsertDocument — also migrates tenant_id and
team_id to parseUUID in the same hot path.
2. vault_documents.go Search — migrates tenant_id and agent_id.
3. vault_documents_enrichment.go FindSimilarDocs — migrates tenant_id,
agent_id, and doc_id.
Pre-Phase-4, the wrapper silently converted garbage agent_id strings to
uuid.Nil, letting the downstream INSERT hit a FK violation with cryptic
PG code 23503. Now bad input fails at the caller boundary with a clear
Go error wrapping the raw value.
Phase 4 Step 4e of agent identity hardening (TD-1).
Covers sub-step 4b migrations for vault_documents.go and
vault_documents_enrichment.go as a side effect.
* refactor(store/pg): error-propagate parseUUID in memory_docs
5 CRITICAL sites migrated from mustParseUUID to parseUUID with wrapped
errors: GetDocument, PutDocument, DeleteDocument, ListDocuments,
IndexDocument. All handle bad agent_id input at the caller boundary
instead of hitting FK violation (writes) or returning empty results
(reads).
Scout classified GetDocument as both CRITICAL and WARN — migrating all
5 is safer and consistent (M8 note).
Phase 4 Step 4b.2 of agent identity hardening (TD-1).
* refactor(store/pg): error-propagate parseUUID in knowledge_graph
5 CRITICAL sites migrated: UpsertEntity (INSERT), GetEntity (SELECT),
DeleteEntity (DELETE), ListEntities (SELECT), SearchEntities (SELECT).
All now return wrapped parse errors at the caller boundary instead of
hitting FK violations (writes) or returning empty results (reads).
Phase 4 Step 4b.3 of agent identity hardening (TD-1).
* refactor(store/pg): error-propagate parseUUID in vault_documents
CRITICAL sites migrated in the following methods (9 total):
- GetDocument, GetDocumentByID, GetDocumentsByIDs, GetDocumentByBasename
- DeleteDocument, ListDocuments, CountDocuments, UpdateHash
UpsertDocument and Search were migrated earlier as part of the
optAgentUUID wrapper fix (commit 6f6b52fb).
4 sites remain in the private helpers appendTeamFilter and
buildSearchTeamFilter — these are WARN-acceptable (team IDs from opts
structs are already validated at WS boundary). They will rename to
parseUUIDOrNil in sub-step 4d.
Phase 4 Step 4b.1 of agent identity hardening (TD-1).
* refactor(store/pg): error-propagate parseUUID in knowledge_graph_relations
All 11 sites migrated (counted higher than scout's 7 CRITICAL because
plan treats the file as a single commit unit):
- UpsertRelation (agent, source, target — 3 sites in one INSERT)
- DeleteRelation (agent, id)
- ListRelations, ListAllRelations (agent + entity)
- IngestExtraction (agent)
- PruneByConfidence, Stats (agent)
Phase 4 Step 4b.4 of agent identity hardening (TD-1).
* refactor(store/pg): error-propagate parseUUID in knowledge_graph_dedup
All 8 sites migrated:
- DedupAfterExtraction, ScanDuplicates (agent)
- MergeEntities (agent, target, source — 3 sites in one transaction)
- ListDedupCandidates (agent)
- DismissCandidate (agent, candidate)
Phase 4 Step 4b.5 of agent identity hardening (TD-1).
* refactor(store/pg): error-propagate parseUUID in remaining kg + memory files
Five files, targeted CRITICAL-site migrations:
- knowledge_graph_temporal.go: SupersedeEntity (line 71 — C1 from red-team,
UPDATE + INSERT in transaction). ListEntitiesTemporal remains on
mustParseUUID (WARN-acceptable SELECT WHERE).
- memory_admin.go: ListAllDocuments, GetDocumentDetail, ListChunks (3 sites,
all return errors). Adds fmt import.
- memory_search.go: Search (1 site, agent_id resolves hybrid FTS+vector).
- knowledge_graph_traversal.go: Traverse (2 sites — agent + startEntity).
- vault_documents_enrichment.go: UpdateSummaryAndReembed (2 sites — tenant,
doc). FindSimilarDocs was migrated earlier with optAgentUUID wrapper fix.
Phase 4 Steps 4b.6, 4b.8, 4b.9, 4b.11, 4b.12 of agent identity hardening.
Still pending:
- vault_links.go (16 sites — next commit)
- knowledge_graph_embedding.go line 124 (SAFE per scout — audit only)
- agents_export_queries.go lines 312,354 (SAFE cursor per scout — audit only)
* refactor(store/pg): error-propagate parseUUID in vault_links
All 16 sites migrated. Covers BatchCreateLinks (2 sites in loop with
fail-fast per Q7), CreateLink, DeleteLink, GetOutLinks, GetOutLinksBatch,
GetBacklinks, DeleteDocLinks, DeleteDocLinksByType, DeleteDocLinksByTypes.
BatchCreateLinks previously silently dropped bad UUIDs via loop-over-
validated-docs; post-P4 the parse layer rejects the entire batch on
first bad doc_id — a documented contract change per Q7.
Phase 4 Step 4b.7 of agent identity hardening (TD-1).
* refactor(store/pg): rename mustParseUUID to parseUUIDOrNil (honest name)
Final step of Phase 4: rename the silently-nil-on-error helper so its
behavior is self-documenting at the call site. No behavior change — all
remaining call sites (~24) were already classified as WARN-acceptable
(read-only SELECT WHERE paths with validated inputs from cursor
pagination or WS boundary) during sub-step 4b migration.
The parseUUID error variant stays as-is for CRITICAL writes.
Also cleans up a now-stale comment in vault_handler_upload.go.
Phase 4 Step 4d of agent identity hardening (TD-1).
* test(integration): fix pre-existing bugs + post-Phase-4 contract update
Three fixes required to run the integration suite after Phase 4:
1. v3_vault_store_test.go makeVaultDoc: AgentID is *string (nullable),
not string — was a pre-existing type error from v3 commit 8f56ddaa
that blocked the test binary build. Pass &agentID.
2. vault_classify_test.go imports: remove unused `store/pg` import —
pre-existing from v3 commit 4b7a2e6e.
3. TestVaultClassify_DeleteDocLinksByTypes_MultipleTypes: fix off-by-one
in target-doc loop. Pre-existing runtime panic from v3 commit — loop
created 7 targets but linkTypes slice has 8 entries, accessing docs[8]
on a len-8 slice. Changed loop to `range 8`.
4. TestVaultClassify_DeleteDocLinksByTypes_NonExistentDoc: swap
"fake-doc-id-12345" placeholder for a well-formed but unused UUID.
Post-Phase-4, parseUUID rejects bare strings at the caller boundary;
the "non-existent = no-op" contract requires a syntactically valid
UUID. Per Q7 documented contract change.
Phase 4 Step 4g integration gate.
* docs(agent-identity): add agent_key vs UUID convention doc
Distills the dual-identity rules from the agent identity hardening work
into a single permanent reference. Covers all 5 trap zones with post-
hardening status, batch fail-fast contract change, router cache
canonicalization, FK safety net, telemetry type-layer defense, and
dual-tenant agent_key semantics. Adds a CLAUDE.md pointer under Key
Patterns so contributors find it via the standard onboarding path.
* docs(code): add inline comments at agent identity trap zones
Clarify the agent_key vs UUID contract inline at the four highest-ROI
trap zones: the Loop struct identity fields, DomainEvent identity
fields, the parseUUID/parseUUIDOrNil store helpers, and the WS method
resolver helpers. Each comment explains the invariant and links to
docs/agent-identity-conventions.md so future readers can find the full
rules without archaeology. Comments only — no runtime behavior change.
* refactor(comments): remove plan/phase refs from agent identity hardening code
Rewrite inline comments added during the agent identity hardening so
they explain the code as it stands today, rather than tying to internal
plan terminology (phase numbers, FR/NFR/H/M/C codes, PR references,
trap zone labels). Commit history already carries the plan archaeology.
Comments now keep the non-obvious invariants (cache boundaries, bypass
gaps, silent-nil traps, dual-tenant semantics) and drop the scaffolding.
Comment-only — no runtime behavior change.
* fix(agent): use agent_key in v3 workspace path resolver
The v3 workspace resolver block in injectContext was passing
l.agentUUID.String() as ResolveParams.AgentID, but the resolver uses
this value as a filesystem path segment. The v2 path in
loop_pipeline_callbacks.go and the session_key anchor both use
agent_key, so a UUID-based v3 path would produce a parallel filesystem
for the same logical agent the moment any consumer reads the v3
workspace context.
No active bug today because workspace.FromContext has zero consumers,
but this closes the landmine before the v3 wiring lands.
* fix(router): evict stale canonical entry on double-check TTL miss
Router.Get's canonical double-check branch trusted any existing entry
under the canonical key without re-checking TTL. If an earlier
agent_key caller wrote the entry and the TTL expired, a later UUID-form
caller would resolve fresh, hit the canonical branch, find the stale
entry, and return it — indefinitely — because the raw-UUID key was
never the map key and the initial-miss eviction branch did nothing.
Re-check TTL inside the double-check branch and evict+rewrite when
stale. Regression test primes a canonical entry with cachedAt set
2×TTL in the past, then asserts a UUID-form Get re-invokes the
resolver and the returned agent reflects the fresh resolver output.
* fix(store/pg): error-propagate parseUUID in kg EmbedEntity
The EmbedEntity helper ran UPDATE kg_entities SET embedding = $1
WHERE id = parseUUIDOrNil(entityID). A bad UUID would resolve to
uuid.Nil and the UPDATE would silently match no rows. Today's only
caller passes a freshly-minted UUID, but swapping to parseUUID with
an explicit Warn log closes the latent trap and aligns with the
hardening policy (writes must fail fast on bad input).
* fix(store/pg): error-propagate parseUUID in kg dedup insert paths
insertDedupCandidate and ScanDuplicates both swallowed uuid.Parse
errors before feeding entity IDs to the INSERT. Currently safe
because all inputs originate from DB SELECT rows or caller-validated
UUIDs, but aligning with parseUUID keeps write-path policy uniform:
bad UUID → clean Go error + skip, not silent uuid.Nil write.
* test(ws): cover resolveAgentUUIDCached cache hit fast path
The existing tests exercise only the DB fallback path via errorAgentStore.
Add a test that primes the router cache with a stub implementing both
agent.Agent and agentUUIDProvider, then asserts the cache-aware helper
returns the cached UUID without touching the store — the raison d'etre
of the helper. Uses the same sentinel-error store so a broken fast path
surfaces as a sentinel error rather than a silent pass.
* fix(http/agents): validate agent_key slug on update path
handleUpdate accepted any string in the agent_key allowlist field
without running it through isValidSlug. A client could rename an
agent to "weird:key", which would break router cache exact-segment
invalidation (the cache splits on the last colon for invalidation
matching). Add the slug check inline after the allowlist filter and
return MsgInvalidSlug on failure. The slug regex already rejects
colons, slashes, whitespace, and other characters that would confuse
path rendering or cache key parsing — add a dedicated predicate test
covering the full trap surface.
* test(router): pin stale raw-UUID entry eviction on Get
Pin that a pre-hardening fragmented entry written under the raw UUID
cache key (tenantID:<uuidStr>) is still evicted by the TTL branch in
Router.Get when a UUID-form caller arrives after TTL expiry. The test
synthesizes the fragmented entry directly via a test-only map write,
then asserts the subsequent Get evicts the raw-UUID entry, re-invokes
the resolver once, and writes the canonical tenantID:agentKey entry —
leaving no fragmented entries behind.
* feat(vault): extension whitelist + document docType + media summary
- Add extension_whitelist.go to deterministically skip binary .bin/.exe/.dll
- Add media_summary.go: synthesize docType + generate short summary from vault fields
- Update rescan.go: detect document type for PDF/office files + propagate docType
- Update safe_walk.go test: .bin now filtered by whitelist, use .txt instead
- Update rescan_test.go: add document docType coverage
- Add link_types.go: define AUTO_LINK_TASK, AUTO_LINK_DELEGATION link types
* feat(store): vault media linking schema migration + document support
- Add migration 000048: vault_documents.path_basename + vault_links.metadata (PG)
- Update SQLite schema.sql: add path_basename, metadata columns + v15→v16 markers
- Add schema.go backfill: loop-patch v15→v16 + basename extraction for existing docs
- Update vault_documents store: SELECT/INSERT with path_basename (PG + SQLite)
- Update vault_links store: SELECT/INSERT with metadata (PG + SQLite)
- Add vault_source_cleanup: helper to purge orphaned sources + links by source ID
- Add basename_helper.go + test: extract filename from path for DB sync
- Update version.go: RequiredSchemaVersion → 48, SQLite SchemaVersion → 16
- Update vault_store.go: DeleteLinksBySource + BatchFindByDelegationIDs interface
* feat(vault): task + delegation auto-linking in enrich worker
- Add enrich_auto_linking.go: deterministic auto-link logic for task/delegation contexts
- Add team_task_siblings.go (PG + SQLite): find all tasks in same delegation for batch link
- Update enrich_worker.go: call enrich_auto_linking Phase 2 hook + skip re-embed for binary
- Update team_store.go: add TaskSiblings interface method + TeamStore binding
- Add context_keys.go + test: define ContextDelegationIDKey + ContextTaskIDKey for propagation
- Update vault_interceptor.go: extract + inject delegation ID from request
- Update run_context.go: propagate DelegationID from request
- Update loop_context.go: inject DelegationID from run context
- Update teams_tasks.go: DeleteTask(s) → cleanup auto-links via vault_source_cleanup
- Update teams_tasks_activity.go: DetachFileFromTask → cleanup related auto-links
- Update vault_documents_enrichment.go: populate summary from media_summary
- Update cmd/gateway.go: wire TeamStore to handler bootstrap
* feat(ui): vault document docType + metadata support
- Update vault.ts: add VaultDocument union type, metadata field in VaultLink
- Update vault-graph-adapter.ts: map document nodes to green color node class
- Update vault-detail-dialog.tsx: handle isBinary branch for download vs preview
- Update vault-document-sidebar.tsx: filter by type with document type icon + label
- Update vault-documents-table.tsx: render document type badge + path_basename column
- Add i18n translations: type.document + type.document_desc across en/vi/zh
* fix(tests): align sqlite smoke test vault AgentID pointer type
* build(ci): add coverage ratchet gate with per-package floors
Add automated coverage floor validation:
- scripts/check_coverage.go: parses coverage.out, compares to per-package
thresholds, supports --update to lock current floors
- scripts/coverage_thresholds.json: 61 packages, initial floors from Phase 1
- .github/workflows/ci.yaml: new 'Coverage ratchet gate' step in CI
- internal/testutil/: test utilities (context builders, TestDB, doc.go)
* test(store/pg): deepen v3 integration tests — Wave 1
Add comprehensive integration tests for core v3 store APIs:
- v3_session_store_ext_test.go: pagination, isolation, rich-list
- v3_agent_store_ext_test.go: context files, profiles, overrides, types
- v3_agent_links_store_test.go: delegation links CRUD
- v3_cron_store_ext_test.go: cron CRUD, enable toggle, run log
- v3_vault_store_ext_test.go: vault docs CRUD/search/links
- v3_memory_store_ext_test.go: memory BM25 search/isolation
* test(gateway,http): add unit tests for server/methods/handlers
Add server and RPC handler tests:
- gateway/ratelimit_test.go: rate limiter pure unit
- gateway/event_filter_test.go: event routing logic
- gateway/server_test.go: handleHealth, tokenAuth, checkOrigin, desktopCORS
- gateway/methods/sessions_test.go: sessions RPC handlers
- gateway/methods/skills_test.go: skills RPC handlers
- gateway/methods/cron_test.go: cron RPC handlers
- http/files_path_security_test.go: path traversal, workspace boundary, auth
- http/auth_helpers_test.go: extractBearerToken, tokenMatch, extractUserID/AgentID
* test(tasks,agent): cover TaskTicker and agent helpers
Add lifecycle and utility function tests:
- tasks/task_ticker_test.go: TaskTicker lifecycle, recoverAll, followup
- agent/pruning_test.go: resolvePruningSettings, findAssistantCutoff, takeHead/Tail
- agent/extractive_memory_test.go: ExtractiveMemoryFallback, dedup
- agent/intent_classify_test.go: quickClassify, containsWholeWord, ClassifyIntent
- agent/loop_utils_test.go: uniquifyToolCallIDs, shouldShareKG, InvalidateUserWorkspace
- agent/inject_and_misc_test.go: truncateForLog, processInjectedMessage, drainInjectChannel
* test(config,skills,backup,mcp): wave 2 integration tests
Add Wave 2 package coverage:
- config/config_extras_test.go: NormalizeAgentID, ExpandHome, ResolveAgent, env overlays
- skills/search_bm25_test.go: BM25 index/search/ranking
- skills/loader_test.go: SKILL.md loader, frontmatter, dedup, versioning
- backup/fs_archive_test.go: ArchiveDirectory, skip fn, tar prefix
- backup/manifest_dsn_test.go: SanitizeDSN, ParseDSN, WritePgpass, CleanEnv
- mcp/util_bm25_test.go: pool, manager, bridge BM25, env resolution
* test(channels): extend slack/discord/telegram/whatsapp coverage
Add channel integration handler tests:
- slack/mention_test.go: isBotMentioned, stripBotMention, user cache, sweepMaps
- slack/media_test.go: classifyMime, buildMediaTags
- discord/handler_test.go: resolveDisplayName, tryHandleCommand, classifyMediaType
- telegram/format_extended_test.go: markdown→HTML, table rendering, chunkHTML
- telegram/handlers_utils_test.go: detectMention, hasOtherMention, isServiceMessage
- telegram/channel_parse_test.go: parseChatID, isEnabled, effectiveMentionMode
- whatsapp/inbound_test.go: extractTextContent, extractQuotedText
- whatsapp/chunk_text_test.go: chunkText, markdownToWhatsApp
- whatsapp/media_utils_test.go: mimeToExt, classifyDownloadError
* test(cache,sessions,knowledgegraph): slim wave 3 coverage push
Add Wave 3 package coverage:
- cache/permission_cache_test.go: PermissionCache 9 methods + invalidation
- sessions/key_extra_test.go: session key builders
- sessions/manager_extra_test.go: SetHistory, Save, loadAll
- knowledgegraph/extractor_helpers_test.go: Extract with mock provider, splitChunks, mergeResults
* docs: changelog entry for coverage improvement waves
Document test coverage improvement initiative:
- 43 new test files across 3 waves
- Per-package coverage floors in coverage_thresholds.json
- CI ratchet gate prevents regression
- ~9000 lines of new test code, 61 packages covered
* fix(telegram): preserve italic content in htmlTagToMarkdown
Go regexp's $1_ is parsed as a reference to a named group "1_" (underscores are
valid identifier chars), not $1 followed by literal underscore. This dropped the
captured italic content, turning <i>foo</i> into "__" instead of "_foo_".
Use ${1}_ with explicit braces to delimit the group reference, and update the
format_extended_test suite to assert the full round-trip for <i> and <em>.
* fix(feishu): route thread replies via Lark reply endpoint
Bot responses to messages inside Lark topic threads were dropped
outside the thread because outbound Send always used the new-message
endpoint. This change:
- Adds LarkClient.ReplyMessage() that POSTs to
/open-apis/im/v1/messages/{id}/reply with reply_in_thread=true
- Parses thread_id from im.message.receive_v1 events (distinct from
root_id which fires on any quote reply) and stamps
feishu_reply_target_id into the message metadata
- Propagates the key through cmd/gateway_consumer_normal.go and the
new package-level routingMetaKeys var in internal/channels/events.go
so block replies and retry notifications also land in thread
- Routes sendText, sendMarkdownCard, sendImage, sendFile, and
sendMediaAttachment via a new deliverMessage helper that falls back
to SendMessage with a warning log on reply endpoint errors (e.g.
thread root deleted)
- Adds 10 unit tests covering routing, fallback, content
double-encoding, and the thread_id gate that prevents plain quote
replies from being silently promoted to threads
Closes#818
* feat(feishu): auto-fetch Lark docx URLs into agent context
When a user pastes a Lark or Feishu docx URL in chat, the channel
now detects the URL and fetches the document's raw text via the
Lark Docs API, injecting the content into the agent input inline
so the model can reason over the linked doc without a tool call.
- New Channel.resolveLarkDocs pipeline step runs before reply
context fetch in handleMessageEvent (step 7a)
- LarkClient.GetDocRawContent calls /open-apis/docx/v1/documents
/{id}/raw_content with the existing tenant access token;
permission and not-found errors map to ErrDocAccessDenied
- Per-channel LRU cache (128 entries, 5 min TTL) dedupes repeat
URL references within the window; soft failures are NOT cached
so permission grants become visible immediately
- Rune-safe content truncation at 8000 runes handles CJK docs
without splitting mid-rune
- Bounded concurrency (max 3 parallel fetches) and a per-message
cap of 10 doc URLs act as spam guards
- Tight URL regex anchors the hostname class so a lazy match
cannot bypass via query-string embedding
- 18 new unit tests cover URL extraction edge cases, cache LRU
and TTL semantics, Lark API error code mapping, resolver
end-to-end with nil cache, access denied soft failure,
per-message cap, and UTF-8 truncation
Required Lark app permission: docx:document:readonly, plus
per-document access grant from the doc owner.
Partial fix for #818 (Phase 2 of 3 — thread reply + writer
commands are tracked separately).
* fix(backup): surface Detail alongside Hint in preflight warnings
Missing-status checks previously only surfaced Hint to users, leaving
them with a fix (Install postgresqlNN-client) but no explanation of the
cause (pg_dump NN cannot dump PostgreSQL MM server). Now both warning
and missing statuses append Detail before Hint.
Follow-up to #830.
* test(providers,zalo): wave A coverage push
Push two deferred packages above their coverage floors:
- internal/providers 57.0% -> 62.2% (+5.2%, target ≥60%)
Targeted pure-logic adapter transformation code: registry,
openai/dashscope/codex ToRequest/FromResponse/FromStreamChunk,
Azure header branch, token source propagation, function_call +
incomplete + reasoning parse branches.
- internal/channels/zalo 7.2% -> 65.3% (+58.1%, target ≥20%)
Factory table-driven creds/config cases, New() defaults, callAPI
success/error/malformed, getMe/getUpdates, sendMessage/sendPhoto,
Send routing + photo extraction, Stop signal, processUpdate
dispatch, handleTextMessage empty-sender drop, downloadMedia four
branches (png/jpg fallback/404/empty).
Minimal refactor: converted package-level apiBase const to var in
internal/channels/zalo/zalo.go so tests can point at httptest server.
Zero runtime behavior change; tests restore the original value via
t.Cleanup.
Ratchet bumps (scripts/coverage_thresholds.json):
- internal/providers: 57.00 -> 62.15
- internal/channels/zalo: 7.20 -> 65.25
Race clean, dual build (default + -tags sqliteonly) clean,
personal/ sub-packages untouched.
* fix(media): route claude-cli PDFs as document blocks, scope disable_tools
read_image and read_document failed when only claude-cli was configured
because the fallback chains only listed 'anthropic'. Adding claude-cli
as fallback (PR #802) surfaced a deeper bug: buildStreamJSONInput
hardcoded 'type: image' for every content block, so PDFs routed
through claude-cli were rejected by the Anthropic API with a MIME
mismatch.
- providers/claude_cli_session.go: buildStreamJSONInput now picks the
Anthropic block type from MIME — application/pdf -> document,
image/* -> image.
- tools/read_image.go, read_document.go: add claude-cli to the
fallback priority and model defaults (empty string lets the provider
pick its own default model).
- tools/read_image.go, read_document_resolve.go: scope disable_tools to
claude-cli only instead of leaking a CLI-specific flag into the
shared Options map every provider in the chain receives.
- providers/claude_cli_session_test.go: table test covering png/pdf/
mixed/unknown MIME routing plus the empty-text edge case.
Closes#801.
* feat(feishu): add /addwriter /removewriter /writers commands
Adds parity with Telegram and Discord for file-writer management
commands, closing the UX gap where users saw an error mentioning
/addwriter but the Feishu channel had no handler.
- New maybeHandleWriterCommand routes /addwriter, /removewriter,
/writers from the Feishu inbound flow. Runs at step 5a — after
checkGroupPolicy — so commands never bypass allowlist or pairing
enforcement. Step 2a rejects slash commands in DM chats early so
users get a clear hint without waking the agent pipeline.
- Target user is identified via reply-to (fetches parent message
sender) or first non-bot @mention. A bare /addwriter with no
target shows the usage hint instead of silently self-granting,
preventing accidental privilege capture in empty-writer groups.
- Refuses to run while botOpenID is unresolved so a @mention of
the bot itself cannot be mistaken for a human target.
- 10s context timeout on each handler bounds worst-case Lark API
latency (parent message lookup, permission store access).
- feishu.New() gains variadic Option parameter with WithAgentStore
and WithConfigPermStore mirroring Telegram's pattern. The gateway
now wires pgStores.Agents and pgStores.ConfigPermissions into
Feishu channel on startup.
- 12 new unit tests with fakeConfigPermStore and httptest Lark
server cover DM rejection, nil-store graceful degradation, bootstrap
via self-mention, bare-command usage hint, bot-probe race refusal,
non-writer rejection, grant via mention, remove last-writer guard,
empty and populated list output, reply-to target resolution via
Lark im/v1/messages lookup, and non-command passthrough. 40 total
tests in the feishu package, all green with -race.
Closes#818.
* test(facebook,store/pg): wave B coverage push
Phase 03 - facebook 23.1% -> 81.9% (+58.8%, target ≥60%)
graph_api_test.go: NewGraphClient, VerifyToken, SubscribeApp, GetPost,
GetComment, GetCommentThread, ReplyToComment, SendMessage, SendTypingOn,
doRequest retry/backoff matrix (5xx retry, 401 non-retry, 429+Retry-After,
context cancel during backoff, transport error + retry), 551/subcode 24h
window non-retry, logRateLimit parse branches.
handlers_test.go: New required-field validation (4 branches), Factory
valid+malformed, WebhookHandler single-shot route, handleAPIError health
state mapping, handleCommentEvent (feature gate, edit/remove drop, page
routing, self-reply skip, dedup, enriched content path), handleMessagingEvent
(feature gate, page routing, self skip, receipt drop, text+postback dispatch,
dedup), Send Messenger+comment paths + missing-metadata error, sendFirstInbox
one-shot dedup, runDedupCleaner stale eviction, Stop closes deps, webhookRouter
register/unregister/route-once/ServeHTTP-no-instances, PostFetcher (defaults,
empty-id, cache hit/miss/expired, error propagation, GetCommentThread
empty+delegate).
Minimal refactor: converted package-level graphAPIBase const to var so tests
can point at httptest server. KISS, zero runtime behavior change.
Phase 04 - store/pg 1.3% -> 3.5% (+2.2%, unit-test-only)
scan_rows_test.go: pure-logic transformation tests for agentShareRow,
userInstanceRow, documentInfoRow, documentDetailRow, chunkInfoRow,
scoredChunkRow, episodicSummaryRow, episodicScoredRow, sessionListRow,
sessionPagedRow, sessionRichRow, entityRow, entityTemporalRow, relationRow,
relationExportRow, traversalRow, dedupCandidateRow, mcpAccessRequestRow,
cronRunLogRow, skillInfoRow+skillInfoRowWithFrontmatter, customSkillExportRow,
parseDepsColumn, parseFrontmatterAuthor, marshalFrontmatter, buildSkillInfo,
mergeEpisodicScores, hybridMerge.
pure_logic_test.go: matchWildcard pattern matching, evalPermRows priority
resolution (individual deny > individual allow > group deny > group allow),
contactResolveCache get/set/expired/negative cache.
Finding: 30% target not reachable with unit tests alone. Store/pg is
fundamentally DB-bound (~6000 LOC of SQL code vs ~200 LOC pure logic).
Reaching 30% requires running integration tests with TEST_DATABASE_URL and
-coverpkg=./internal/store/pg/ — which the current CI ratchet does NOT
do. Confirmed: integration tests yield 32% coverage on store/pg, but that
requires a wired-up test database in CI. Deferred as infrastructure change
outside Phase 04 scope.
Ratchet bumps (scripts/coverage_thresholds.json):
- internal/channels/facebook: 23.08 -> 81.80
- internal/store/pg: 1.28 -> 3.45
Race clean, dual build (default + -tags sqliteonly) clean.
* test(acp,feishu): wave C greenfield coverage push
- internal/providers/acp 0.0% → 80.0% (7 test files, ~2560 LOC)
- helpers, types round-trip, jsonrpc framing + adversarial fuzz,
session, terminal/sandbox, tool_bridge (3 permission modes),
ProcessPool lifecycle
- internal/channels/feishu 20.6% → 63.9% (15 test files)
- bot parse/policy, factory, larkws proto + WS lifecycle,
larkclient HTTP error paths, larkevents AES-CBC decrypt +
tamper detection, media send/receive, lifecycle
Zero source modifications. Race clean (-race -count=3).
Dual build verified (standard + sqliteonly).
* build(ci): ratchet bump wave C coverage floors + changelog
- scripts/coverage_thresholds.json: feishu 0 → 63.89, acp 0 → 80.05.
Minor drift auto-normalized: backup 18.80 → 19.88,
facebook 81.80 → 81.85, providers 62.15 → 62.53,
store/pg 3.45 → 3.51, tools 26.61 → 26.59 (precision)
- docs/17-changelog.md: "Deferred Coverage Waves A-C — Resolved" entry
* fix(ci): check_coverage preserves thresholds in --update mode
The preserve-loop that backfills threshold entries for packages not
present in the current coverage profile was gated on `!*update`, which
meant `go run scripts/check_coverage.go --update` with a narrow
coverprofile silently wiped floors for every package not measured.
Discovered while ratcheting wave C with a package-scoped profile.
Move the preserve-loop out of the gate so it runs in both check and
update modes. Narrow profiles now touch only observed packages and
leave the rest untouched.
* test(feishu): bound webhook dispatch wait to 2s, not test timeout
larkevents_test used `<-t.Context().Done()` as the timeout branch in
the dispatch wait select. t.Context() is only canceled when the test
function returns, so a missing dispatch call would deadlock the test
for the full default go test timeout (10 min) before failing — a real
CI hazard, discovered when an unrelated HMAC-verify experiment
prevented dispatch and the CI run sat idle for 10 minutes.
Replace with `time.After(2*time.Second)` via a named constant, plus a
comment warning future readers not to revert the pattern.
* ci: cap go test -timeout to 90s to bound CI hang impact
Deadlocked tests previously blocked CI for the full 10-minute Go
default before failing. Wave C had a live example: a test timeout
branch using `<-t.Context().Done()` (which never fires until test
return) combined with a dispatch-suppressing patch caused a 10-min
GH Actions stall.
Cap every test binary at 90s. Wave C race-heavy suites run in <20s
locally, so 90s leaves ample breathing room while failing fast on
any future deadlock. Applied to both CI workflow and Makefile so
local `make test` enforces the same bound.
Unit-level defenses (proper `time.After` timeouts in tests) are still
the right fix — this is a safety net, not a substitute.
* fix(cache): tenant-aware invalidation for builtin tools and skills
Tenant config changes for builtin tools and skills silently failed to
invalidate cached agent Loops, leaving tenants stuck on stale tool/skill
sets until the 10-minute TTL expired or an unrelated event wiped the
cache. Master-level skill CRUD had the same gap in the opposite
direction.
- Add CacheInvalidatePayload.TenantID so events can scope to one tenant
- Add Router.InvalidateTenant(tenantID) with prefix match on
"tenantID:agentKey" cache keys; uuid.Nil is a no-op
- Rework emitCacheInvalidate helpers in builtin_tools + skills handlers
to carry tenant scope; add defense-in-depth uuid.Nil guards in four
tenant-config handlers
- Update TopicCacheBuiltinTools + TopicCacheSkills subscribers to branch
on payload.TenantID (tenant event wipes that tenant, global event
keeps the existing InvalidateAll path)
- Wire emitCacheInvalidate into master skill CRUD paths (update, delete,
toggle, upload, install-deps, rescan-deps, import) that previously
only called BumpVersion
- Document the system-owner bypass in requireTenantAdmin so handlers
keep guarding uuid.Nil themselves
* test: speed up retry/cron/facebook tests, drop coverage ratchet gate
Slow tests were dominating CI feedback time and AI dev loop because they
waited through real exponential backoffs and 1s ticker intervals.
Test-only override pattern keeps production behavior 100% identical.
Speed wins (no-race wall-clock per package):
- internal/vault 16.3s -> 0.6s (-15.7s)
- internal/cron 11.7s -> 1.5s (-10.2s)
- internal/channels/facebook 6.3s -> 3.0s (-3.3s)
- Full -race ./... suite 90s+ -> 51s
Changes:
- vault: new fastBackoffsForTest(t) helper overrides enrichRetryBackoffs
+ enrichRetryTimeouts to 1ms in 3 retry tests; drop 2 duplicate tests
(FirstAttemptSuccess, MaxRetriesConstant)
- cron: extract runLoopTickInterval as package var (default 1s); test-only
setFastTick(t) helper shortens to 20ms so 6 scheduler tests no longer
sleep 1.5s each waiting for a tick
- facebook: extract graphBackoffBase as package var (default 1s); newFakeGraph
helper shortens to 1ms so HTTP retry tests don't burn 6s of real waits
Coverage ratchet removed:
- Delete scripts/check_coverage.go + scripts/coverage_thresholds.json
- Remove "Coverage ratchet gate" CI step
- Keep coverage profile + go tool cover summary as informational only
- Philosophy: signal over coverage %. Forced tests to bump % were the
root cause of the slowness this commit unwinds.
Production behavior unchanged. Coverage profile shows isolated package
coverage matches prior thresholds (vault 27.4%, cron 73.7%, facebook 81.9%).
* test(acp): extract processCloseTimeout var for fast-test override
TestProcessPool_Close_TimesOutSlowProcess was burning 5s of wall
time per run to exercise the "fake process never exits" path,
because Close() used a hard-coded `time.After(5 * time.Second)`.
Extract the timeout as a package var `processCloseTimeout` (default
5s — production unchanged) so the test can override to 20ms via
t.Cleanup-scoped swap. Same pattern as the recent cron/vault/
facebook speedups in commit 0c44149f.
Result: acp race suite 17s → 1.6s. No production behavior change.
* fix(store/pg): session tenant isolation + vault scan arity
Two unrelated integration-test failures, both real source bugs:
1. Session ListPaged/ListPagedRich leaked cross-tenant data.
buildSessionFilter only honored opts.TenantID and never read
tenant from ctx — so callers relying on ctx scoping (the common
case) got no tenant filter at all. List() already had the
correct pattern using store.TenantIDFromContext(ctx) gated by
!store.IsCrossTenant(ctx). Replicate inside buildSessionFilter;
thread ctx into the helper; opts.TenantID still wins as admin
override. Fixes TestStoreSession_ListPaged_Pagination/tenant_isolation
and TestStoreSession_ListPagedRich_TenantIsolation.
2. Vault GetDocument / GetDocumentByBasename panicked with
"sql: expected 15 destination arguments in Scan, not 14".
Migration 000047 added `path_basename` as a generated column;
the SELECT lists were updated but the Scan arg lists were not.
GetDocumentByID had the correct 15-target pattern — copy it.
vaultDocRow.PathBasename field already exists. Audit sweep of
vault_links.go + vault_documents_enrichment.go confirms no
other drift. Fixes TestStoreVault_UpsertAndGetDocument.
* ci: run integration suite against pgvector service container
Integration tests were running locally only — CI never exercised
the 29 v3_*_test.go files under tests/integration/. This left the
session tenant-isolation + vault scan-arity bugs undetected until
a manual run this session.
Changes:
- tests/integration/v3_test_helper.go: testDB() now calls
pg.InitSqlx(db) inside sharedDBOnce. Previously the test suite
relied on whichever test ran first happening to call InitSqlx,
so running with `-run <filter>` could segfault with a nil
pkgSqlxDB. Removes the ordering-dependency land mine.
- .github/workflows/ci.yaml: add services.pg block running
pgvector/pgvector:pg18 with pg_isready healthcheck, set
TEST_DATABASE_URL at job level, and add a new "Integration
tests" step after unit tests. Uses -timeout=180s (vs 90s unit)
because the first test runs migrations from scratch.
Local integration suite: 2.9s. CI cold start expected ~30-60s
with image pull + migrations.
* fix(gateway): guard config.* methods against non-master tenant scope
Non-master tenant admin calling config.patch/apply/get/schema would
corrupt master *config.Config + config.json on disk via m.cfg.ReplaceFrom
and m.Save, leaking master config across tenants. Add requireMasterScope
middleware that rejects non-master tenant ctx (system-owner role still
bypasses for consistency with requireTenantAdmin). Chain before
requireOwner on all 4 config.* methods. Adds MsgConfigMasterScopeOnly
i18n key (en/vi/zh) and 10 unit tests covering helper + middleware +
chained fail-closed behavior.
* feat(store): tenant tool settings with column-preservation upsert
Wake up the dead builtin_tool_tenant_configs.settings column (exists in
migrations 000027/SQLite 1180 since v3 tenant foundation, never read/written).
Add GetSettings/SetSettings/ListAllSettings interface methods with a
json.RawMessage Settings field on BuiltinToolTenantConfig. Both DBs use
explicit column-list DO UPDATE SET so Set(enabled) and SetSettings(raw)
never clobber each other. Add ErrInvalidTenant sentinel so nil-tenant
callers fail fast (no silent master fallback). ListAll now filters
enabled IS NOT NULL — rows created via SetSettings stay in their own lane.
9 SQLite unit tests + 4 new PG integration tests (round-trip,
column coexist, cross-tenant isolation, nil-tenant guard).
* feat(agent): tenant tool settings overlay via loop ctx injection
Plumb per-tenant tool settings into the agent Loop without touching any
tool's Execute signature. Adds WithTenantToolSettings ctx helper and
rewrites BuiltinToolSettingsFromCtx with fast-path merge semantics —
tenant layer wins over global defaults at tool-name level (no field-level
deep merge). Resolver preloads ListAllSettings for the agent's tenant at
Loop construction; store errors log + fall back to global. Zero allocs
on single-tier reads. Tier 1 (future per-agent override) is reserved and
documented in context_keys.go. 8 unit tests cover empty / single-tier /
both-merged / RunContext fallback / fast-path semantics.
* feat(http): tenant-config settings DTO + GET endpoint
Extend PUT /v1/tools/builtin/{name}/tenant-config to accept optional
enabled + settings fields (at least one required). Add GET endpoint for
the combined tenant override view. Enrich list handler with tenant_settings
alongside existing tenant_enabled. Pointer *bool + json.RawMessage DTO
distinguishes "not set" from "explicit false/null". 16KB body cap via
MaxBytesReader prevents trivial DoS. isValidSettingsJSON rejects non-object
non-null payloads so tool-specific schemas stay predictable. Backward
compat: old clients sending {"enabled": bool} still decode cleanly.
17 tests: validator subcases + DTO decode + stub-backed httptest handler.
* test(tools): verify tenant tool settings flow through media provider chain
Phase 6 of the tenant tool config refactor adopts zero production code
changes for media tools — media_provider_chain.go:77 already reads via
BuiltinToolSettingsFromCtx, and the Phase 3 merge automatically feeds
tenant overrides into that call. This adds 5 verification tests proving
the end-to-end flow: tenant layer beats global, per-agent arg still wins
over tenant, fallback to global when no tenant row, tenant-only layer
works, and cross-tool isolation (create_image override does not leak
into create_audio). Covers the 4-tier overlay on a real tool without
touching any media tool's Execute signature.
* feat(tools): tenant-aware web_search provider chain resolution
Refactor web_search Execute to resolve its provider chain per-request
via BuiltinToolSettingsFromCtx instead of iterating the singleton's
hardcoded provider list. Tenant admins can now reorder or disable
providers via builtin_tool_tenant_configs.settings using the shape
{"provider_order":[...], "brave":{"enabled":false}, ...}. Unknown
provider names in provider_order are logged + skipped — no injection
path via this settings blob. Malformed JSON falls back to defaults
(fail-open so a bad admin paste never crashes the tool). Secrets stay
in config_secrets — tenant cannot inject API keys through this path.
11 unit tests cover reorder, filter, disable, unknown-skip, malformed,
global-layer, and tenant-vs-global precedence.
Note: This is the MVP tenant adoption half of the plan's Phase 7. The
Exa + Tavily feature port from PR 825 and coordination with its author
is deferred to a separate follow-up (the port is net-new feature work
orthogonal to the overlay adoption).
* fix(http): master-scope guards on builtin_tools, packages, api-keys
Phase 0b of tenant tool config refactor. Closes 3 privilege-escalation
vulnerabilities in the same bug class as commit b419f352 (Phase 1
config.* hotfix):
- CRITICAL: PUT /v1/tools/builtin/{name} — non-master tenant admin
could overwrite global builtin_tools.settings, corrupting tool
defaults for every tenant.
- CRITICAL: POST /v1/packages/install|uninstall — non-master tenant
admin could run pip/npm/apk server-wide. Supply-chain vector.
- HIGH: POST /v1/api-keys/{id}/revoke (HTTP + WS) — tenant admin
could revoke NULL-tenant system keys because store SQL matches
(tenant_id = \$N OR tenant_id IS NULL).
Implementation:
- Export store.IsMasterScope as the single predicate; rewire Phase 1
config.* middleware to delegate (no behaviour change).
- Add http.requireMasterScope helper symmetric to requireTenantAdmin.
- Guard handleUpdate (builtin_tools) and handleInstall/handleUninstall
(packages) with master-scope check before any mutation or shell exec.
- Fix api_keys.Revoke at the handler layer: fetch key via new
APIKeyStore.Get, verify key.TenantID matches caller tenant for
non-owner callers. Applies to both HTTP and WS paths.
- Harden WS router to inject role into ctx so store.IsOwnerRole works
from WS handlers (closes a latent drift between the HTTP and WS
layers that broke the initial WS api_keys.revoke fix).
- Drop unused APIKeyStore.Delete (YAGNI + removes dormant vuln with
the same tenant_id IS NULL arm).
- Emit security.tenant_scope_violation and security.api_key_revoke_
forbidden slog events on every rejection for future SIEM alerting.
- New MsgMasterScopeRequired i18n key + en/vi/zh catalogs.
Tests cover the guard predicate, all 3 HTTP endpoints, and the full
WS api_keys.revoke matrix (cross-tenant deny, system-key deny for
tenant admins, system owner bypass, own-tenant happy path). 14 other
admin-gated write endpoints verified safe by static audit.
* feat(ui): tenant-scope aware builtin tool settings editor
Phase 5 of tenant tool config refactor. Wires the existing builtin tools
page + settings dialog to the tenant-config HTTP endpoint shipped in
Phase 4, so tenant admins can edit per-tenant tool overrides from the
same "Settings" button master admins already use.
- BuiltinToolData DTO gains tenant_settings (already returned by the
GET /v1/tools/builtin enrich path when request is tenant-scoped).
- useBuiltinTools hook gains setTenantSettings + clearTenantSettings —
both hit PUT /v1/tools/builtin/{name}/tenant-config, with null clearing
the settings column while preserving tenant_enabled (column-list
upsert on the store side, shipped in Phase 2).
- BuiltinToolSettingsDialog accepts a tenantScope flag. In tenant mode:
- initial values come from tenant_settings ?? settings (pre-fill
with global defaults when creating a fresh override)
- a small badge + hint line communicates the mode
- optional "Reset to global default" appears when an override exists
- BuiltinToolsPage branches the save callback: master scope → updateTool
(global settings), non-master → setTenantSettings. The backend enforces
the same rule defensively per Phase 0b, so this branch is a UX guard
(avoid 403) not a security boundary.
- New en/vi/zh keys: tenantOverrideBadge, tenantOverrideHint,
tenantOverrideNewHint, resetToGlobalDefault.
Verified: pnpm build clean (tsc -b + vite).
* docs: tenant tool config 4-tier overlay + Phase 0b security hotfix
- docs/03-tools-system.md § 14: 4-tier overlay architecture, opt-in
pattern for tool authors, secret vs non-secret split, cache
invalidation, master-scope guard pattern.
- docs/17-changelog.md: concise Security + Added entries for the
2026-04-12 hotfix and refactor.
- docs/23-multi-tenant-architecture.md: extend Per-Tenant Overrides
table with settings column + cross-reference to § 14; add
master-scope guard row to the Security table.
Phase 9 of tenant tool config refactor. Documents what shipped in
commits b419f352, 933c2e10, 56eb6869, 96e38c59, ed32f6e6, fbbba5e8,
6d7473b5, 1e5e84d5.
* docs(contrib): tenant-scope guard rules for admin writes
Role checks are not tenant checks — a non-master tenant admin holds
RoleAdmin in their own tenant and passes role-only middleware by
design. CLAUDE.md gains a one-line directive pointing at CONTRIBUTING
for the full decision table + anti-patterns. CONTRIBUTING gains:
- Target-table decision table (global vs tenant-scoped) with the
matching guard for each (requireMasterScope vs requireTenantAdmin).
- Shared predicate reference: store.IsMasterScope(ctx).
- Anti-pattern list for reviewers: writes to no-tenant_id tables
without master-scope check, SQL tenant_id IS NULL arms on write
paths, role-only admin gates, revoke/delete handlers that skip
pre-fetch ownership verification.
* feat(tools): add Exa + Tavily web search providers with ranked ordering
Port Exa and Tavily provider clients from PR #825 into the tenant-aware
overlay architecture (builtin_tool_tenant_configs.settings).
- Add ExaConfig + TavilyConfig to WebToolsConfig with provider_order
- Add Exa HTTP client (api.exa.ai/search, x-api-key auth)
- Add Tavily HTTP client (api.tavily.com/search, Bearer auth)
- Extend Brave + DDG constructors with per-provider maxResults
- Add config_secrets plumbing for exa/tavily API keys (apply/collect/mask/strip)
- Refactor gateway_setup.go to use WebSearchConfigFromConfig
- Add NormalizeWebSearchProviderOrder (DDG always last, dedup, unknown skip)
- Extract web_search_config.go (builder, normalizer, shared helpers)
- 11 new unit tests for provider order, builder, clamp, normalize
Credit: @kaitranntt (PR #825) for the original Exa + Tavily implementation.
* feat(tools): tenant-aware web_fetch domain policy resolution
web_fetch now reads per-tenant policy overrides from
BuiltinToolSettingsFromCtx before falling back to the tool's
default policy. Same per-call resolution pattern as web_search.
- Add resolvePolicy(ctx) with webFetchPolicyOverride struct
- Refactor Execute + doFetch + fetchRawContent to use webFetchPolicy
- InProcessExtractor also resolves policy from ctx
- Remove isDomainAllowed/isDomainBlocked (replaced by matchDomainList)
- 6 new unit tests for tenant policy override scenarios
* feat(tools): tenant-aware TTS primary provider resolution
TTS tool now reads per-tenant primary provider override from
BuiltinToolSettingsFromCtx before falling back to the manager's
default. Same per-call resolution pattern as web_search/web_fetch.
Simpler path A approach (no pool) — tenant can select which
registered provider their agents use. Per-tenant API keys deferred
to a future pool implementation if needed.
- Add resolvePrimary(ctx, mgr) with ttsOverride struct
- Refactor Execute to use resolved primary with fallback chain
- 5 new unit tests for tenant provider override scenarios
* docs(tools): tenant tool config refactor - phases 7-8 completion
- 03-tools-system.md: Add § 14 Per-Tenant Tool Configuration (4-tier overlay)
- Comprehensive overlay explanation (per-agent > tenant > global > hardcoded)
- Opt-in pattern for tool authors with code example
- Schema contracts for web_search (provider_order), web_fetch (policy), tts (primary)
- Secret vs non-secret split guidance
- Tenant admin workflow (Settings → Builtin Tools UI)
- Feature flag documentation (TenantScopedSingletons)
- 17-changelog.md: Expand Per-Tenant Tool Configuration entry with phase details
- Phase 5: Builtin tools settings editor on web UI
- Phase 7 rest (30a40bbe): Exa + Tavily web search providers
- Credit @kaitranntt for PR 825 original work
- 11 new unit tests, provider_order config
- Phase 8 (def1712f, 43ee918b): Tenant-aware singleton pools
- web_fetch domain policy resolver (6 new tests)
- tts primary provider resolver (5 new tests)
- Feature flag gating (LRU pool, 64 tenant limit, 30 min idle timeout)
All phases 1-8 now shipped on dev branch. Docs integrated with existing
23-multi-tenant-architecture.md § 14 reference. Ready for Phase 9 completion.
* feat(ui): web_search chain editor + TTS settings form
Add dedicated UI forms for web_search and TTS builtin tool settings,
replacing the raw JSON editor for these tools.
- web-search-chain-form: drag-drop provider order (Exa/Tavily/Brave),
DuckDuckGo locked as always-on fallback, color-coded rails,
per-provider max_results config
- tts-provider-form: primary provider selector + auto mode dropdown
with inline descriptions (off/inbound/tagged/always)
- Wire both into builtin-tool-settings-dialog dispatch
- i18n strings for en/vi/zh
* fix(ui): always show Settings button for tools with dedicated forms
hasEditableSettings() only checked if tool.settings was non-empty,
hiding the Settings button for web_search, tts, etc. when no settings
existed yet. Now tools with dedicated form components always show the
button so admins can create initial settings.
* fix(ui): add API key hint to web search chain form
Users had no indication where to configure Exa/Tavily/Brave API keys.
Added hint text pointing to Config → Secrets management.
* feat(tools): API key management in web search chain form
API keys for Exa, Tavily, and Brave can now be set directly in the
web search provider chain settings dialog. Keys are saved to
config_secrets (AES-256-GCM encrypted, tenant-scoped) and stripped
from the persisted settings blob. Raw key values are never returned
in API responses — only boolean set/unset status.
Backend:
- Add ConfigSecretsStore to BuiltinToolsHandler
- extractAndSaveSecrets: parse api_key from settings, save to
config_secrets, strip before persisting to builtin_tool_tenant_configs
- getSecretsStatus: returns boolean map per tool (never raw values)
- Enriched handleList response with secrets_set per tool
Frontend:
- Per-provider API key input in web search chain cards
- "Key set" status badge when key exists, "Change" button to replace
- Staged keys sent with settings on save, backend extracts
* feat(vault): tree sidebar with lazy-load folder hierarchy
Replace flat paginated document list with collapsible tree view.
Backend: two-query-per-level approach (files + DISTINCT deeper paths
for virtual folder derivation), path parsing in Go for PG+SQLite
compatibility, text_pattern_ops index for prefix range scans.
Frontend: VaultTree component with doc_type colored icons, compact
single-line nodes, truncate-middle filenames, scope dots, hover
tooltip for date. Sidebar expanded to 384px on large screens.
Also fixes pre-existing SQLite scanVaultDocRow missing path_basename.
* fix(tools): add missing tools to goclaw group and relax agent update ownership
- Add 10 missing tools to the goclaw group in policy.go: skill_manage,
publish_skill, use_skill, delegate, memory_expand, knowledge_graph_search,
vault_search, create_audio, datetime, heartbeat. Fixes skill creation
permission denied when agents use group:goclaw allow list.
- Relax agent update ownership check: tenant admins can now update any
agent in their tenant (adminMiddleware + tenant-scoped GetByID already
ensures proper authorization). Previously only agent owner or system
owner could update.
- Improve agent update error logging: include user_id and tenant_id in
structured log, return actual error message instead of generic
"internal error" for better debugging.
* fix(vault): agent filter support + rescan agent_key path matching
- Add AgentID to VaultTreeOptions, pass agent_id filter through
backend tree endpoint and frontend hook
- Fix rescan inferOwnerFromPath: match root-level {agent_key}/...
folders against agentMap (workspace uses agent_key directly,
not agents/ prefix). Keep legacy agents/ prefix for compat
- Preserve full relPath for DB storage in all patterns
- Show filename with extension in tree (use path basename, not title)
- Start tree folders collapsed (prevent stuck loading state)
- Remove orphaned useVaultGraphData call from vault-page
- Tenant isolation verified: agentMap built per-tenant via scopeClause
* fix(vault): reset enrichment progress counter on rescan start
Start() now resets done=0 before broadcasting, preventing stale
counter from previous run causing progress bar to jump/reset.
* fix(vault): only reset enrichment progress on new batch, not mid-drain
Start() is called each drain loop in processBatch to update total.
Only reset done=0 when transitioning from idle→running (new batch),
not when already running (same batch, total growing).
* fix(vault): aggregate enrichment progress across per-agent batches
After rescan agent_key fix, docs split into per-agent batch queues.
Each batch was calling Start()/Finish() independently, causing the
progress bar to flash 0/1 per agent instead of showing global progress.
Now: handler Start(total) sets global count, worker batches call
TrackBatch()/MarkBatchDone() for lifecycle, AddDone(n) auto-completes
when done >= total. Progress bar shows smooth 0→N across all agents.
* fix(vault): increase classify max_tokens to prevent truncated JSON
With 10 candidates, classify response can exceed 1024 tokens when
ctx descriptions are verbose, causing json unmarshal failures.
Bump to 2048 to accommodate worst-case 10-candidate responses.
* fix(vault): enrichment pipeline reliability + cross-agent classify
9 fixes for vault enrichment pipeline:
1. Queue key = tenant-only (was per-agent, caused multiple batches
blocking EventBus workers and progress bar flashing)
2. Classify chunks 5 candidates per LLM call (prevents response
truncation that caused parse_still_failed errors)
3. Classify prompt improved: explicit "EXACTLY one entry per
candidate", 5-entry example, ctx capped at 30 words
4. max_tokens kept at 1024 (sufficient for 5 candidates)
5. Progress AddDone removes !running guard (safe before Start)
6. Rescan defers event publishing via PendingEvents — Start()
called before workers receive events, eliminating race
7. Upload handler same deferred publish pattern
8. Frontend enrichment timer cancels stale "complete" timeout
when new enrichment starts (prevents bar disappearing)
9. Sidebar tree reloads after rescan completes
Classify now searches across entire tenant (empty agentID) to
build cross-agent links for future vault sharing. Access control
enforced at query time — agents only see their own docs.
* fix(agents): prevent NOT NULL violation on promoted columns during update
Frontend sent null for empty emoji (emoji.trim() || null), which violated
the NOT NULL constraint on the promoted emoji column (migration 000037).
This caused all agent saves to fail with 500 when emoji was unset.
- Frontend: send empty string instead of null for emoji field
- Backend (PG + SQLite): add null-coercion for all promoted NOT NULL
columns — TEXT (emoji, agent_description, thinking_level) coerce to "",
INT (skill_nudge_interval, max_tokens) coerce to 0
* fix(vault): increase max_tokens + hot-reload provider for enrichment
- Increase classify max_tokens 1024→2048, summarize 1536→4096 to
prevent truncated JSON from models like gemini-2.5-pro
- Add debug logging: raw LLM output on parse failures, finish_reason
truncation warnings with model name
- Hot-swap vault enrichment provider/model on config change without
restart (wired into TopicSystemConfigChanged handler)
- Use RWMutex-guarded llm() accessor for thread-safe provider reads
* fix(vault): wire provider hot-reload to config.patch event
The previous commit only listened to TopicSystemConfigChanged (periodic
DB refresh), but config.patch from UI fires TopicConfigChanged. Add
subscriber in gateway_lifecycle so vault enrichment picks up provider/
model changes immediately when user saves config.
* refactor(workers): per-tenant provider resolution for background workers
Replace static provider/model fields with per-tenant resolution at
processing time. Fixes architectural mismatch where singleton workers
used tenant-specific system_configs.
Changes:
- Add shared ResolveBackgroundProvider() in providerresolve package
- Refactor vault enrichWorker, episodic, dreaming workers to resolve
provider per-event using registry + systemConfigs
- Remove hot-reload machinery (no longer needed)
- Update ConsolidationDeps to use Registry/SystemConfigs
Fallback chain: background.provider → agent.default_provider → first
registered provider.
* feat(vault): add enrichment stop button + error toast
Backend:
- Add error tracking to EnrichProgress (error_count, last_error)
- Broadcast error events when LLM calls fail
- Add POST /v1/vault/enrichment/stop endpoint
- Wire enrichWorker to VaultHandler for stop functionality
Web UI:
- Add stop button (appears when enriching)
- Show error toast when enrichment errors occur
- Display error count in progress bar
- Add useStopEnrichment hook
i18n: en/vi/zh translations for new strings
* fix(providerresolve): add debug logging for background provider resolution
- Log loaded system_configs (background.provider, agent.default_provider)
- Log provider lookup attempts with source and error details
- Warn when falling back to first registered provider
- Surface config load errors instead of silently swallowing them
Helps diagnose provider resolution failures in episodic/vault workers.
* fix(telegram): preserve Telegram topic routing for delayed notifications (#850)
* Fix delayed Telegram topic routing
* refactor: export TaskLocalKeyMetadata and fix formatting
- Export TaskLocalKeyMetadata from tools package for reuse
- Remove duplicate taskLocalKeyMetadata from tasks package
- Fix gofmt indentation issue in notifyLeaders
- Add trailing newline to team_tool_helpers_test.go
---------
Co-authored-by: viettranx <viettranx@gmail.com>
* feat(pancake): comment auto-reply + first-inbox DM (#841)
* feat(pancake): add comment auto-reply + first-inbox inbox implementation
- Route COMMENT webhook events with feature gate and self-reply prevention
- Add PostFetcher with sync.Map cache + singleflight for comment context
- Implement ReplyComment() and PrivateReply() API client methods
- Split Send() into sendCommentReply() / sendInboxReply() flows
- Add sendFirstInbox() for one-time inbox DM with configurable greeting
- New config: Features.FirstInbox, CommentReplyOptions, FirstInboxMessage, PostContextCacheTTL
- 72 tests passing with race detection; both PG and SQLite builds clean
* fix(pancake): address PR 841 review issues
- Add slog.Debug when post context fetch fails in buildCommentContent
- Add comment to GetPosts explaining why it bypasses doRequest
- Add 72h TTL eviction for firstInboxSent in runDedupCleaner
- Add 30s timeout to sendCommentReply to bound API hang risk
- Replace custom containsStr helper with strings.Contains in tests
* fix(pancake): correct test assertion and remove unused config field
- Fix TestPrivateReply_ReturnsError: use HTTP 400 instead of 200 since
doRequest only returns error for status >= 400
- Remove unused MaxThreadDepth config field from CommentReplyOptions
* fix(pipeline): block_reply dedup falsely suppresses final message (#838)
Only count BlockReplies when tool calls are present (matching when
EmitBlockReply actually fires in think_stage). Final answers without
tool calls must not increment the counter, otherwise gateway dedup
falsely suppresses delivery on non-streaming channels.
- Update ObserveStage to check len(resp.ToolCalls) > 0
- Rename test to reflect new behavior (tool calls required)
- Add regression test for #838 scenario
Closes#838
* feat(skills): multi-skill ZIP upload with hash-based idempotency (#846)
feat(skills): multi-skill ZIP upload with hash-based idempotency
- Multi-skill ZIP detection: upload single ZIP containing multiple skill directories
- Hash-based idempotency: SHA-256 of SKILL.md content deduplicates re-uploads
- Grouped upload UI: ZIP group headers with per-skill badges (NEW/UNCHANGED/ERROR)
- Safety: 50-skill per-ZIP limit, TOCTOU-safe hash check under lock
- Performance: cached ZIP parsing O(N) vs O(N*M)
- Tests: 17 frontend + 9 backend tests
Closes#845
* fix(web): add test scripts to package.json
Add vitest scripts for running tests:
- test: run tests once
- test⌚ watch mode
- test:ui: vitest UI
* fix(sqlitestore): add tenant_id filter to GetSkillHashBySlug and GetNextVersion
SQLite implementations were missing tenant_id WHERE clauses that the
PostgreSQL equivalents have. While Lite edition is single-tenant in
practice, this maintains dual-DB parity and prevents potential issues
if the codebase evolves.
Follows the same pattern as other methods in the file (GetSkillFilePath,
CreateSkillManaged, etc.) that use store.TenantIDFromContext with
MasterTenantID fallback.
* feat(web): add ESLint 10 with flat config and CI integration
- Install ESLint 10 + TypeScript-ESLint + React hooks plugins
- Create eslint.config.js with flat config format
- Fix conditional hooks violations in agents-page, task-list, knowledge-chart
- Fix unused variables and useless assignments
- Add lint step to CI workflow before build
- Remove unused eslint-disable comments
Rules configured:
- rules-of-hooks: error (critical)
- exhaustive-deps: off (intentional patterns)
- no-explicit-any: off (pragmatic casts)
- no-console: off (dev convenience)
* fix(vault): re-enqueue unenriched docs on rescan when all files unchanged
When rescan finds no new/updated files but some docs still lack summaries
(e.g. previous enrichment failed due to provider timeout), automatically
re-enqueue them for enrichment retry.
- Add VaultStore.ListUnenrichedDocs() to fetch docs with empty summary
- Add EnrichWorker.EnqueueUnenriched() to emit events for retry
- Add RescanResult.Reenqueued field to track re-enqueued count
- Update UI to show "X re-queued for enrichment" toast
* refactor(vault): merge rescan and stop enrichment buttons into single toggle
* fix(vault): increase classify max_tokens to 4096 for verbose models
* fix(vault): stricter classify prompt - ctx under 50 chars, compact JSON example
* feat(vault): add confirmation dialog for stop enrichment button
* feat(vault): auto-expand first level folders in sidebar by default
* fix(vault): softer graph colors for dark mode, thinner edge lines
* fix(vault): skip goclaw_gen media in enrichment, improve graph UX
Backend:
- Skip goclaw_gen_* files in Handle, EnqueueUnenriched, gatherCandidates
- Skip in RescanWorkspace PendingEvents (progress count)
- Reduces noisy auto-links by ~59%
Frontend:
- Theme-aware node colors (Tailwind -600/-400 matching sidebar)
- Neutral gray edges (zinc-500/300), lighter inactive edges
- Disable hover label white box (defaultDrawNodeHover)
- Auto-expand + load first-level folders
- Group hover text colors for tree items
- Edge density based on camera zoom
* fix(telegram): propagate local_key for forum topic routing in team notifications (#800)
* fix(telegram): propagate local_key for forum topic routing in team notifications
Team task status messages (dispatched, completed, progress) were always
delivered to the General topic in Telegram forum groups because the
notification pipeline had no access to the originating topic's local_key.
Root cause: wireTeamProgressNotifySubscriber in gateway_events.go published
OutboundMessage with no Metadata, so the Telegram adapter had no
message_thread_id to route to the correct forum topic.
Fix has two parts:
1. Team notify path (root cause):
- Add LocalKey field to TeamTaskEventPayload (protocol)
- Extract local_key from tool context in WithContextInfo()
- Add LocalKey to NotifyRoutingMeta
- Pass LocalKey through to OutboundMessage Metadata in both
leader mode (InboundMessage) and direct mode (OutboundMessage)
2. MCP bridge context (supporting):
- Propagate local_key and session_key through bridge HTTP headers
- Add X-Local-Key and X-Session-Key to BridgeContext
- Extract and inject into tool context in gateway middleware
- Include in HMAC signature for integrity
Closes#798
* fix(telegram): pass LocalKey from task metadata in all dispatch/fail broadcast sites
The initial fix added LocalKey to the event payload and WithContextInfo(),
but 4 broadcast call sites use individual With* options instead of
WithContextInfo — so LocalKey was never populated for:
- fallback_dispatch (team_tasks_create.go)
- dispatch_unblocked (team_tool_dispatch.go)
- post_turn dispatch (team_tool_validation.go)
- blocker/fail (team_tasks_blocker.go)
Add WithLocalKey() option function and extract TaskMetaLocalKey from task
metadata at each site, matching the existing TaskMetaPeerKind pattern.
* test(mcp): add HMAC verification tests for extra params (localKey, sessionKey)
- Add tests for SignBridgeContext/VerifyBridgeContext with extra params
- Test backward compat fallback for pre-localKey sessions
- Test that param order matters in signature
- Add clarifying comment for routing context injection security model
---------
Co-authored-by: Jens Henke <jens@henke.dk>
Co-authored-by: viettranx <viettranx@gmail.com>
* chore: remove obsolete PR assets
Clean up old PR screenshot/preview files that are no longer needed.
---------
Co-authored-by: Duy /zuey/ <duy@wearetopgroup.com>
Co-authored-by: Duc Nguyen <me@vanducng.dev>
Co-authored-by: Kai (Tam Nhu) Tran <61256810+kaitranntt@users.noreply.github.com>
Co-authored-by: Plateau Nguyen <nguyennlt.ncc@gmail.com>
Co-authored-by: Reski Rukmantiyo <reski.rukmantio@lintasarta.co.id>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: QuXiangyu <771744189@qq.com>
Co-authored-by: quxy5 <quxy5@outlook.com>
Co-authored-by: Luan Vu <luanvuseo@gmail.com>
Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: Tuân <tuannt065@gmail.com>
Co-authored-by: tuannt23065 <tuannt23065@users.noreply.github.com>
Co-authored-by: badgerbees <93577481+badgerbees@users.noreply.github.com>
Co-authored-by: henkedk <jens@glomotra.com>
Co-authored-by: Jens Henke <jens@henke.dk>