66 Commits
Author SHA1 Message Date
thotam 3f90057c24 fix(pipeline): stop aborting runs on heuristic context budget estimates (#1587)
Runs on models without a registered tokenizer (e.g. 9router brand models)
ended with the generic "Agent couldn't generate a response" fallback even
though the real request used about 55% of the context window.

PruneStage counted history with TokenCounter, which falls back to a
chars/2 heuristic for unregistered models and overcounted about 1.8x.
Once over budget it ran memory flush (~35s, invisible in traces), then
mid-loop compaction, which cannot summarize a history made only of tool
call/result pairs. The callback reported the untouched history as
compacted, PruneStage still saw it over budget and returned AbortRun
before any LLM call, and FinalizeStage replaced the empty reply with the
fallback.

- PruneStage and ContextStage overhead count with the request guard's
  BudgetCounter. PruneStage no longer controls loop flow; the final
  request guard in ThinkStage decides.
- CompactMessages returns ErrNotCompacted when history is unchanged.
  Callers stop counting it as a compaction and do not retry it in the
  same run, while post-run summarization still sees the pressure.
- When the guard exhausts every reduction step, ThinkStage stops the run
  with a localized chat.context_budget_exceeded notice instead of an
  error, so the run's tool results are still persisted. The stop reason
  marks the trace and agent span as error; team tasks, cron and
  heartbeat treat it as a failure via RunOutcome.Failure().
- Memory flush and mid-loop compaction emit event spans.
- Web and desktop UIs treat an unset context_pruning as enabled (the
  backend default since 7639a8c0), keep it unset when untouched, and can
  re-enable pruning after it was turned off.
2026-09-29 18:04:08 +07:00
Thibault Jaigu 32eb2328f3 feat(provider): add Requesty OpenAI-compatible provider (#1579)
Register Requesty (https://router.requesty.ai/v1) next to OpenRouter on the
existing OpenAI-compatible transport: config and env vars
(GOCLAW_REQUESTY_API_KEY, GOCLAW_REQUESTY_BASE_URL), secret masking, DB and
in-memory registration, CLI setup, doctor, placeholder provider, OpenAPI enum,
Web/Desktop provider lists and docs.

The models list merges Requesty managed policies (GET /models/managed) with
the key's catalog from GET /models.
2026-09-26 03:33:33 +07:00
DennyHo0917 eeee2daa5e merge: resolve API Route provider conflicts with dev 2026-09-07 20:49:55 +08:00
DennyHo0917 d67e1b0909 feat(provider): add API Route OpenAI-compatible provider 2026-09-06 19:05:35 +08:00
Mai Tan Loi 517cf1e469 fix(onboarding): keep the chosen agent identity when the prompt is edited
Editing the description in onboarding step 3 deselected the preset, and the
display name, agent key and emoji were derived from that selection. Losing it
dropped them to a hardcoded "Fox Spirit" / "fox-spirit" / fox emoji, so picking
the Writer preset and rewriting its prompt created an agent named Fox Spirit.
The step also offered no way to name the agent at all.

Identity is now editable state that a preset only prefills:

- Add display name, emoji and agent key inputs to both onboarding steps, with
  the key auto-derived from the name until it is edited by hand.
- Editing the prompt only clears the preset highlight; the identity stays.
- Block submit on an empty name or an invalid slug instead of falling back.
- Give the web presets stable English agent keys, so the slug no longer depends
  on the UI locale, and strip the emoji prefix off the preset label before it
  becomes a display name.
- While a preset is active and its field is untouched, the name and prompt still
  follow a locale switch.

Reuses the existing agents:create.* keys, present in en/vi/zh on both surfaces.
2026-08-30 16:54:25 +07:00
Duc Nguyenandntduc bb7712a9ff fix(collaboration): harden delegated task isolation (#1486)
* feat(collaboration): isolate delegated artifacts and child runs

Isolate delegated inputs and outputs behind secure artifact exchange lifecycles. Scope Agent Link tasks by tenant and root agent, and enforce delegation spawn-tree boundaries. Add process-wide child-run admission and preserve logical media paths across native, MCP, and sandbox execution.

* fix(collaboration): harden delegated task isolation

Enforce tenant and root-agent task scope across migrations and stores. Add exactly-once async completion delivery, delegated sandbox boundaries, and confined artifact and media recovery across runtime surfaces.

* fix(collaboration): recover interrupted async tasks

* fix(collaboration): normalize persisted child-run status

---------

Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-07-30 14:17:40 +07:00
61993b4387 feat(i18n): add Russian (ru) language — full parity across backend, web & desktop (#1391)
* docs(i18n): design spec for Russian (ru) language support

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(i18n): register ru locale constant and IsSupported

* feat(i18n): scaffold ru catalog (en placeholders)

* feat(i18n): translate ru backend catalog to Russian

* fix(i18n): polish ru catalog grammar per review

* feat(i18n): wire ru into system messages, config enum, parity test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(i18n): add ru to web language constants and system-message locales

* feat(i18n): scaffold ru web locale dir (en placeholders)

* feat(i18n): wire ru resources into web i18next config

* feat(i18n): translate ru web locale to Russian (41 namespaces)

* fix(i18n): normalize ru web terminology (арендатор, привязка)

* fix(i18n): restore dropped 'named' in ru setup OAuth hint

* feat(i18n): wire ru into desktop i18next and language pickers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(i18n): add ru desktop locale (Russian, 16 namespaces)

* test(i18n): extend traces/hooks locale guards to cover ru

* chore(deploy): add repeatable fork-dev local build+deploy tooling

Adds a one-command, idempotent way to run goclaw from a locally built
image that merges fresh upstream dev with un-merged fork features
(Telegram trigger-words PR #1383 + Russian locale), independent of the
published :latest image.

- scripts/deploy-forkdev.sh: fetch origin/dev -> merge feature branches
  into local/fork-dev worktree -> push fork/dev -> build -> up -d.
- docker-compose.forkdev.yml: overlay pinning image to goclaw:forkdev
  with pull_policy:never so :latest never silently replaces the build.
- .gitignore: track the deploy helper (past deploy-*.sh), ignore the
  ../.goclaw-forkdev integration worktree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telegram): agent-callable message reaction (message action=react)

Adds a "react" action to the message tool so an agent can set an emoji
reaction on an existing message (e.g. mark its own status post 👍 once
fully paid). Mirrors the ChannelEditor wiring: ReactionSetter capability
-> Manager.ReactToMessage -> telegram Channel.ReactToMessage (validates
against Telegram's supported reaction set; ✅/❌ are rejected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(config): remove ignored session scoping controls (#1405)

Co-authored-by: Collective Developer <man@collective.dev>

* feat(telegram): agent-callable message reaction (message action=react) (#1407)

Adds a "react" action to the message tool so an agent can set an emoji
reaction on an existing message (e.g. mark its own status post 👍 once
fully paid). Mirrors the ChannelEditor wiring: ReactionSetter capability
-> Manager.ReactToMessage -> telegram Channel.ReactToMessage (validates
against Telegram's supported reaction set; ✅/❌ are rejected).

Co-authored-by: skensel <skensel@MacBook-Pro-skensel.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: build & publish fork image to GHCR (temporary, pre-upstream)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(memory): use 'russian' FTS config for memory_chunks recall

Memory keyword search (memory_search.go FTS + the generated tsv column)
used the 'simple' text-search config, which does no stemming. Russian
grammatical cases therefore never matched: a genitive query token
("Дианы") could not hit the stored nominative token ("Диана"), so
memory_search returned nothing for natural queries even though the fact
was saved. This is the sole recall path on deployments without an
embedding provider (ChatGPT-OAuth/codex cannot embed), so recall was
effectively dead there.

Switch both sides to the Snowball 'russian' config so index and query
lexemes stem to the same root ("диан"), restoring case-insensitive
recall. Migration 000094 recreates the generated tsv column (a GENERATED
expression cannot be ALTERed in place; ADD COLUMN backfills all rows).

Surface parity: desktop/SQLite N/A — SQLite memory search uses LIKE, a
separate mechanism unaffected by the PG regconfig.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: skensel <skensel@MacBook-Pro-skensel.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: ナム <namnhfreelancer@gmail.com>
Co-authored-by: Collective Developer <man@collective.dev>
2026-07-11 12:25:38 +07:00
ナムandCollective Developer e9898be505 fix(discord): honor disabled group history limit (#1406)
Co-authored-by: Collective Developer <man@collective.dev>
2026-07-09 17:25:37 +07:00
SYNITYandDangTinh311 3b8bc1bc4d feat(bitrix24): replace two MCP text inputs with a filtered dropdown (#1392)
* feat(bitrix24): replace two MCP text inputs with a filtered dropdown [B24:2794]

Bitrix24 channel creation used to demand two hand-typed strings —
mcp_server_name and mcp_base_url — plus zero indication of which MCP
servers can actually auto-onboard. Typos silently disabled provisioning
and admin had to know which servers implement /api/auto-onboard.

Ship a single dropdown backed by mcp_servers.require_user_credentials,
plus the machinery to make it work end-to-end.

Phase 1 — DB & store
* Promote require_user_credentials from settings JSONB to a top-level
  column on mcp_servers (PG migration 000089, SQLite migration v54).
* Backfill from existing settings blobs so no admin needs to re-tick.
* Add MCPServerData.RequireUserCredentials to the Go store layer, plumb
  through Create / Get / GetByName / List / Update on both stores,
  extend the export DTO, and add require_user_credentials to the HTTP
  allowlist.
* Bump RequiredSchemaVersion 87 -> 89 (jumping 88, which was on disk
  but not wired) and SchemaVersion 53 -> 54 with an
  idempotentColumnMigration guard.

Phase 2 — Bitrix24 channel factory
* Add MCPServerID (UUID string) to bitrix24 InstanceConfig, keep
  MCPServerName + MCPBaseURL as legacy fallback with a "deprecated"
  doc comment.
* Factory validation accepts either mcp_server_id alone or the legacy
  pair; half-config still fails fast.
* initMCPProvisioner prefers GetServer(id) and sources the base URL
  from mcp_servers.url when the id path is used. Legacy name path
  unchanged so pre-migration configs keep working.
* Log line now carries mcp_server_id + require_user_credentials so
  operators can eyeball the wiring.

Phase 3 — Frontend types & MCP form
* Add optional top-level require_user_credentials to MCPServerData /
  MCPServerInput in both ui/web and ui/desktop/frontend types.
* mcp-form-dialog reads the top-level flag first and falls back to
  settings.require_user_credentials so cached responses from
  pre-upgrade backends still render correctly.
* On submit send both the top-level flag AND the legacy settings
  entry so mid-rollout backends stay consistent.

Phase 4 — Bitrix24 channel form dropdown
* New mcp-select field type + MCPServerSelect component. Uses the
  shared useMCP() react-query cache and filters client-side to
  servers whose require_user_credentials is true (OR settings
  JSONB during the migration window).
* Explicit "None (disable MCP provisioning)" option so admins can
  clear the binding without editing config JSON.
* Legacy mcp_server_name / mcp_base_url text inputs kept in the
  Advanced panel, relabelled "(legacy)" with pointer help text.

Phase 5 — channel_instances.config backfill
* PG migration 000090 and SQLite migration v55 rewrite existing
  bitrix24 channel_instances.config to add mcp_server_id by
  resolving mcp_server_name against mcp_servers, tenant-scoped
  via agents.tenant_id (channel_instances doesn't carry tenant_id
  directly).
* Idempotent — only touches rows already carrying
  mcp_server_name that lack mcp_server_id. Legacy keys are left
  in place so provisioner.go can still fall back for unmigrated
  or future-created legacy configs.
* down.sql drops the mcp_server_id key. Provisioner immediately
  reverts to the legacy fallback path.

Tests
* provisioner_test.go: three new cases exercise the mcp_server_id
  path (invalid UUID string, valid UUID with missing row, valid
  UUID with a per-user row). fakeMCPStore gains a serversByID
  map and a real GetServer implementation.
* Existing legacy-config tests unchanged and still green.

Verification
* go build ./... && go build -tags sqliteonly ./...
* go vet ./internal/mcp/... ./internal/channels/bitrix24/...
    ./internal/store/... ./internal/http/...
* go test ./internal/mcp/... ./internal/channels/bitrix24/...  -> ok
* Live-tested against a local docker image on the goclaw-deploy
  postgres. Migrations 89 + 90 applied cleanly. Three existing
  bitrix24 channels (bitrix-sales / nguyen-dao-openline / tieu-vi)
  had their configs backfilled with the b24-syn-mcp UUID and the
  provisioner boots with require_user_credentials=true. UI dropdown
  correctly shows only b24-syn-mcp (the only server with the flag
  ticked) alongside a "None" clearer option.

Surface parity
* Gateway server: store + factory + provisioner + HTTP allowlist.
* API contract: adds require_user_credentials + mcp_server_id
  as optional fields on existing routes. No new endpoints.
* Web UI: MCP form + Bitrix24 channel form + shared types.
* CLI/runtime package: N/A because no CLI subcommand reads the
  mcp_server_id field.

* fix(bitrix24): derive auto-onboard base URL from mcp_servers.url origin [B24:2794]

The Phase 2 refactor swapped provisioner base-URL sourcing from the
legacy per-channel MCPBaseURL config field (which historically stored the
MCP server's ORIGIN, e.g. https://mcp.example.com) to mcp_servers.url,
which stores the JSON-RPC ENDPOINT the agent loop dials (e.g.
https://mcp.example.com/mcp). The two are semantically different but
share a single column.

mcp_client.newMCPClient then appends "/api/auto-onboard" to whatever
baseURL it receives, so the id-path started POSTing to
".../mcp/api/auto-onboard" — 404 for every per-user credential mint and
refresh. Existing users kept working only until their cached access
tokens expired.

Fix: derive the origin (scheme://host[:port]) from server.URL before
handing it to the auto-onboard client. The legacy path is untouched
because MCPBaseURL from channel config is already the origin.

  https://b24-mcp-dev.synity.so/mcp   ->  https://b24-mcp-dev.synity.so
  https://mcp.example.com/mcp/        ->  https://mcp.example.com
  https://mcp.example.com             ->  https://mcp.example.com

Table-driven test covers six shapes plus four error cases (empty,
whitespace-only, no scheme, no host). Updated the existing
TestInitMCPProvisioner_MCPServerID fixture to seed a URL with the /mcp
subpath so it regression-guards the same code path.

Verified live: user 614 sent a message that triggered the expired-cred
refresh branch; goclaw logged "self-refreshed user credentials
created=false" and the agent immediately reported
"mcp.user_tools_loaded user=614 tools=2". Before this fix the same event
logged 'auto-onboard failed: mcp auto-onboard: 404 Not Found'.

Surface parity:
  - Gateway server: provisioner + one new helper (deriveAutoOnboardBaseURL).
  - API contract: N/A because the wire shape hasn't changed.
  - Web UI: N/A because the UI still writes mcp_server_id verbatim.
  - CLI/runtime: N/A because no CLI reads the derived base URL.

---------

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-08 19:23:21 +07:00
Dmitry 2810ad96f0 feat(providers): add AI/ML API preset (#1336)
Approved by github-maintain. Clean implementation of AI/ML API as branded OpenAI-compatible provider. All CI green. Closes #1325.
2026-07-03 20:23:47 +07:00
Duy /zuey/ 53ef912441 fix(channels): add reasoning delivery modes (#135) 2026-06-03 11:01:14 +07:00
Duy /zuey/ 2da52cfaee feat(providers): add Google Cloud Vertex AI provider (#5)
* feat(providers): add Google Cloud Vertex AI provider (#576)

Add `vertex` built-in provider type that routes Gemini calls through Google
Cloud Vertex AI's OpenAI-compatible endpoint. Enterprises on GCP can now use
regional endpoints for data residency, consolidate AI spend under existing GCP
billing, enforce IAM/VPC-SC controls, and use committed-use discounts instead
of standalone Google AI Studio API keys.

Implementation reuses OpenAIProvider via the OpenAI-compat path; the only
provider-specific logic is OAuth2 auth wiring:

- New factory NewVertexProvider in internal/providers/vertex.go builds an
  *http.Client with oauth2.Transport, which auto-refreshes GCP access tokens
  (1-hour lifetime) transparently. Credentials precedence:
  inline SA JSON > credentials_file path > Application Default Credentials
  (works on GKE/Cloud Run/Compute Engine via metadata server).
- OpenAIProvider gets WithHTTPClient() + WithoutAuthHeader() options so the
  oauth2 transport injects Authorization rather than doRequest() setting a
  static Bearer header.
- Endpoint URL computed at registration time from project_id + region:
  https://{region}-aiplatform.googleapis.com/v1/projects/{p}/locations/{r}/endpoints/openapi
- Store: api_key column holds AES-256-GCM-encrypted SA JSON (same as other
  providers); settings JSONB holds {project_id, region, model}.
- Env vars: GOCLAW_VERTEX_{API_KEY,CREDENTIALS_FILE,PROJECT_ID,REGION,MODEL}.

Registration wired through all three paths: config-driven startup, DB-driven
startup, and HTTP CRUD in-memory registration. Vertex handled before the
generic "api_key empty" guard so ADC deployments register correctly.

Code-review fixes applied:

- H1 (correctness): Gemini thought_signature detection in openai.go now
  recognizes providerType="vertex" and apiBase suffix "aiplatform". Previously
  only worked because the default model string coincidentally contained
  "gemini"; custom model IDs or fine-tuned endpoint numeric IDs would drop the
  signature on passback and trigger HTTP 400 mid-tool-loop. Regression test
  added (TestVertexProviderForwardsThoughtSignatureOnToolCalls).
- M1 (hardening): region and project_id are regex-validated before URL
  concatenation to prevent hostname injection (e.g. region="evil.com/a?").
- M2 (hardening): APIBaseOverride must be https + *.googleapis.com host to
  prevent data exfiltration via crafted DB rows.
- M3 (documentation): CredentialsFile marked operator-only in the struct
  comment — never expose via admin UI or DB settings without path allow-list.

Tests: 17 Vertex-related unit tests. go build ./... + go build -tags sqliteonly
./... + go vet ./... all clean. Pre-existing TestSignMediaPath failure on
Windows (file_token.go uses path/filepath) is unrelated to this change.

* chore: trigger CI on digitopvn/goclaw fork

* ci: ping

* ci: retrigger workflows
2026-05-11 12:54:05 +07:00
viettranx f13524ec08 feat(tts): show Gemini voice character (style) as a label badge
Google does not publish gender for the 30 prebuilt Gemini voices —
inclusive design — but does publish a one-word style descriptor
(Bright, Firm, Smooth, Mature, Lively, etc.). Surface it so users can
pick by character.

- `audio.VoiceOption` gains `Labels map[string]string` (provider-
  specific descriptors). ElevenLabs already returns labels via API;
  Gemini now hardcodes `{"style": "..."}` per voice from the official
  catalog.
- Web + desktop `mapCapVoiceToPortal` passes labels through. Both
  pickers' `LABEL_KEYS` now include `"style"`, so the existing badge
  rendering picks it up — first 1–2 entries shown next to the name.
- TS `VoiceOption` interface (web + desktop) gains optional `labels`.

No gender field is invented for Gemini voices.
2026-04-20 07:28:15 +07:00
viettranx 82ca0f117b refactor(tts): collapse adapter triplication; honest concurrency comment
Post-review cleanup of Phase 4. Closes Finding #9 properly and corrects
the Finding #13 documentation lie surfaced in the code-review report.

Capability schema:
- Replace `AgentOverridable bool` with `AgentOverridableAs string` on
  ParamSchema. Empty string = not overridable; non-empty = the generic
  key alias (`"speed"`, `"emotion"`, `"style"`).
- Each provider declaration now carries the alias inline, so the
  generic↔native mapping has a single TS-readable source.

Frontend:
- Web `tts-override-block.tsx` drops the inline `GENERIC_TO_NATIVE`
  literal and derives the bidirectional adapter from the filtered
  capability params (each param self-describes its alias). Adapter
  tests rewritten around the new shape.
- Desktop `AgentDetailPanel.tsx` drops the 45-line inline IIFE in
  favour of a new `<TtsOverrideFineTune>` component that uses the
  same alias-based mapping.

Backend:
- Move `AgentTTSParamsAllowedKeys` + `ValidateAgentTTSParams` to
  `internal/audio/agent_params_adapter.go`. HTTP `validate.go` and WS
  `gateway/methods/agents_update.go` both delegate, eliminating the
  duplicated `{speed, emotion, style}` literal.

Cleanup:
- Delete orphan i18n keys `MsgTtsParamInvalidJSON` and
  `MsgTtsParamDependsOn` from `keys.go` + en/vi/zh catalogs (no
  in-code references; DependsOn is FE-only, JSON parse failures
  already surface via slog).

Documentation:
- `prompt-settings-section.tsx` Finding #13 comment rewritten to
  honestly describe the best-effort merge into a fresh local copy of
  the cached `otherConfig` prop. Concurrent-tab clobber remains
  possible — server-side JSON-merge-patch endpoint planned for v2.

Tests: 9 backend suites (race), web 217/217, desktop build clean,
both Go build tags pass.
2026-04-20 07:14:56 +07:00
viettranx 247344d689 feat(tts): per-agent params override (speed/emotion/style) with adapter
Phase 4 — final phase of the TTS params/layout/agent-override plan.

Adds a 3-key allow-list (`speed`, `emotion`, `style`) per agent stored
in `agents.other_config.tts_params`. Backend resolves and merges into
`opts.Params` PER ATTEMPT inside the fallback loop so each provider
sees its own native shape — never the primary's keys when fallback
runs (Finding #1 critical).

Backend:
- `AgentOverridable bool` on `audio.ParamSchema`. UI filter reads this
  flag from /v1/tts/capabilities; no separate TS literal mirror —
  capabilities API is the single source of truth (Finding #9).
- `audio.AdaptAgentParams(generic, provider)` maps the 3 generic keys
  to provider-native paths (e.g. `speed` → `voice_settings.speed` for
  ElevenLabs, flat `speed` for OpenAI/MiniMax, dropped for Edge/Gemini).
- `Manager.SynthesizeWithFallbackAdapted` adapts inside the loop so
  fallback providers receive correctly-shaped params.
- `manager_auto.go` and `tools/tts.go` Execute do per-attempt adaptation
  on the tenant + direct + fallback call sites.
- Drop log bumped to `slog.Info("tts.agent.params.dropped", ...)` for
  audit trail when a generic key isn't supported by the active provider.
- Cross-check test asserts every adapter switch case has at least one
  capability ParamSchema with `AgentOverridable: true`, and vice versa.

Security (red-team findings):
- Allow-list ENFORCED at write path: `validateAgentTTSParams` in HTTP
  `handleUpdate` AND WS `agents_update` rejects any `tts_params` key
  outside `{speed, emotion, style}` (Finding #5).
- 64KB body cap on agent PUT via `http.MaxBytesReader` (Finding #6).
- Explicit tenant-scope guard after `agents.GetByID` (Finding #12).
- Concurrent-tab clobber: handleSave merges `tts_params` into a fresh
  copy of `otherConfig` rather than reusing stale state (Finding #13).
- Rate-limit verified — RoleAdmin gate sufficient for v1 (Finding #15).

Frontend (web + desktop):
- `TtsOverrideBlock` rewritten: filters capability params to
  `agent_overridable === true`, renders via `DynamicParamForm`. Hides
  entirely for providers with no overridable params (Edge, Gemini).
- Bidirectional adapter (generic ↔ capability-native form state) so
  agent storage stays in generic keys while UI works in native paths.
  25 round-trip tests cover all 5 providers.
- Desktop `AgentDetailPanel` gains an inline fine-tune section gated
  on `globalProvider`, reusing the desktop `DynamicParamForm`.

i18n: `tts.override.params.title` ("Fine-tune") added to web + desktop
en/vi/zh.

Tests: all 9 backend suites green (race), web 214/214, desktop build
clean, both Go build tags pass.
2026-04-20 06:46:19 +07:00
viettranx 85ebbcfbd5 feat(tts): split DynamicParamForm into Basic and Advanced sections
Phase 3 of the TTS params/layout/agent-override plan.

Reads the `Group` field shipped in Phase 1 to partition each provider's
params into Basic (always visible) and Advanced (collapsed by default,
chevron toggle, count badge). Edge providers with no advanced params
hide the toggle entirely.

- `partitionSchema(schema)` splits params; unknown group values fall
  into Basic (forward-compat).
- `evaluateDependsOn` runs against the full shared form state, so
  cross-section dependencies (e.g. MiniMax `audio.bitrate` depends on
  basic-section `audio.format`) work without scoping.
- Count badge uses `t("tts.advanced.count", {count})` with i18next
  pluralization (`_one` for English).
- Desktop mirror with inline SVG chevron (no lucide-react dep).

i18n (web + desktop):
- New `tts.advanced.count` + `tts.advanced.count_one` keys in en/vi/zh.
- `tts.advanced.title` already present; verified.

TS contract: `ParamSchema.group?: string` added to web + desktop
`tts-capabilities.ts`. Plain TS interface — no Zod strict-validator
guard needed (verified via grep).

10 new unit tests cover partition order, unknown-group fallback, count
badge respecting DependsOn, and the cross-group MiniMax case.
189/189 web tests pass.
2026-04-20 06:28:17 +07:00
viettranx bd95022efe feat(tts): extract PortalVoicePicker and route Gemini through it
Phase 2 of the TTS params/layout/agent-override plan.

Web (`ui/web/src/components/voice-picker.tsx`):
- Extract `PortalVoicePicker` (search + VoiceRow + portal dropdown)
  from the `DynamicVoicePicker` inner body.
- Introduce `PortalVoice` shape + `mapCapVoiceToPortal` adapter so
  capability `VoiceOption` and `useVoices()` results both feed in.
- Dispatcher gains a Gemini route: static voices + provider==="gemini"
  → PortalVoicePicker with the static catalog. Other static providers
  (OpenAI) continue to use `StaticVoicePicker` (Radix Select).
- `DynamicVoicePicker` reduced to a thin fetch wrapper around
  `useVoices()` + `useRefreshVoices()`.

Desktop mirror (`ui/desktop/frontend/src/components/agents/voice-picker.tsx`):
- Same extraction + Gemini route. OpenAI keeps the Combobox-driven
  static path to preserve native UX.

Tests: 9 new cases covering Gemini→Portal dispatch, mapping shape,
absent `preview_url`/`labels`, plus the unchanged ElevenLabs/MiniMax/
OpenAI paths. 179/179 web tests pass.

No backend change. No new i18n keys (existing voice_picker.* reused).
2026-04-20 06:23:09 +07:00
viettranx 613b6e38d7 feat(tts): provider capabilities schema + Gemini TTS + dynamic param forms
Baseline groundwork for TTS expansion plan. Introduces a capabilities
system that the follow-up plan (phase-01..04) builds on.

Backend:
- `audio.ParamSchema` / `ProviderCapabilities` types + per-provider
  capabilities.go for edge, elevenlabs, minimax, openai, gemini.
- Gemini TTS provider (client, models, voices, wav encoder,
  multi-speaker via SpeakerVoice, audio-tag aware prompts).
- TTSOptions gains `Params map[string]any` (read-only) + `Speakers`.
- `VoiceListProvider` interface decouples HTTP voice handler from
  provider-specific impls.
- `nested_keys.go` resolves dot-separated param paths for nested
  provider bodies (voice_settings.stability etc.).
- Characterization + defaults-invariant tests per provider lock
  nil-params byte-equivalence before new params land.
- `/v1/tts/capabilities` HTTP endpoint + integration coverage.
- Dual-read tests (PG + SQLite) for tts_config.

Frontend (web + desktop):
- `DynamicParamForm` with depends-on evaluation, split into
  fields/logic modules. Slider primitive added.
- `AudioTagPicker` + `MultiSpeakerEditor` for Gemini.
- `voice-picker` refactored toward portal UX; combobox tightened.
- tts-capabilities API client + typed hooks.
- i18n catalogs (en/vi/zh) expanded; parity tests guard key drift.
- TTS page reorganised (voice-model-section removed; playground +
  credentials + provider-setup split cleanly).

Docs: codebase-summary, project-changelog, tts-provider-capabilities.
2026-04-20 00:05:19 +07:00
viettranx f2325f6d98 feat(desktop): add cancel-summon button in SummoningModal
Add Cancel button in SummoningModal after 60s threshold. Call POST
/v1/agents/{id}/cancel-summon via getApiClient(). Update agent status
to summon_failed. Add i18n strings for en/vi/zh in desktop locale.
2026-04-17 19:02:53 +07:00
viettranx 7c5630c27b fix(desktop): add path alias to vite config for @ imports 2026-04-16 17:41:07 +07:00
viettranx 6659ba46a8 feat(tts): consolidate TTS config UI with provider-aware pickers and synthesize endpoint
- Add 4-provider catalog (OpenAI, ElevenLabs, Edge, MiniMax) with static
  voice/model data for web and desktop
- Rewrite /tts page as 5 modular sections: provider setup, credentials,
  voice/model picker, test playground, collapsible advanced settings
- Add POST /v1/tts/synthesize endpoint with 15s timeout, 500-char cap,
  RoleOperator auth, ElevenLabs model validation, rate limit reuse
- Fix Edge TTS provider to honor per-request opts.Voice instead of
  always using construction-time default (C1)
- Gate agent detail TTS section on global provider config with
  empty-state CTA for owners, catalog-driven model select, override
  checkbox, and inline voice test button
- Remove TTS from builtin-tools dialog (STT preserved); hide tts tool
  from builtin-tools page list
- Add desktop parity: provider-aware VoicePicker, TTS config hook via
  WS config.get, empty-state component, synced i18n locales (en/vi/zh)
- Add 109 web vitest tests + 12 Go unit tests covering handler
  validation, legacy config regression, and catalog parity
2026-04-16 14:17:48 +07:00
248a39ba0c This fixes a data integrity issue affecting fresh installations of Desktop Lite and any environment where agent.other_config and serveral columns are not pre-initialized. This will stop Desktop Lite edtion from saving any agent setting. (#897)
The root cause is missing default initialization of JSON field in update flow, leading to NOT NULL violations in both SQLite and Postgres.

Fix ensures consistent behavior across Lite and Standard deployments.

Co-authored-by: Viet Tran <viettranx@gmail.com>
Co-authored-by: Chunning Ha <cn@ha.td>
2026-04-15 20:49:19 +07:00
viettranx 8d9a7c127a feat(i18n): desktop tts namespace + STT form keys
Add tts namespace to desktop i18n (en/vi/zh). Extend tools.json with STT form keys including WhatsApp privacy banner. Mirrors web ui/web locale structure.
2026-04-15 11:24:57 +07:00
viettranx 033638533d feat(desktop): port voice picker + STT form from web
Port web voice picker and STT provider form to desktop frontend. Singleton audio preview (hide when preview_url null), whatsapp_enabled toggle with privacy banner. Reuses desktop's custom Combobox primitive. Voice id persists via other_config.tts_voice_id merge in AgentDetailPanel. ToolSettingsDialog routes stt tool to new SttProviderForm.
2026-04-15 11:24:57 +07:00
viettranx 4cd79eacd7 chore(desktop): add Vitest + testing-library
Install Vitest, @testing-library/react, jest-dom, jsdom. Add test + test:watch scripts. Mirrors ui/web vitest config.
2026-04-15 11:24:57 +07:00
viettranx 6dd53b9892 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.
2026-04-11 08:46:09 +07:00
viettranx 3105a7c811 fix(ui/desktop): send promoted agent fields at top level to fix summoning stuck
Backend v3 (migration 000037) promoted emoji, agent_description, self_evolve,
thinking_level, reasoning_config from other_config JSONB to top-level columns.
Desktop app was still nesting these inside other_config, so backend saw empty
agent_description and never triggered the summoning goroutine. SummoningModal
then waited forever for agent.summoning WS events that were never emitted.

- types/agent.ts: add promoted fields to AgentData and AgentInput
- services/agent-service.ts: add emoji to AgentListItem
- hooks/use-agent-detail-state.ts: read/write promoted fields at top-level,
  keep fallback to other_config for legacy agents, drop legacy keys on save
- hooks/use-agents.ts: read emoji from top-level with fallback
- components/agents/AgentFormDialog.tsx: send agent_description and emoji at
  top-level; read with fallback
- components/agents/AgentCard.tsx: read self_evolve, emoji, agent_description
  from top-level with fallback to other_config
- components/onboarding/AgentStep.tsx: critical fix - send agent_description
  at top level so backend triggers summoning
2026-04-11 07:56:42 +07:00
+1 a6c209a84f Release: vault improvements, MCP reconnect, cron fixes, Facebook/Pancake channels (#822)
* 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()

---------

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>
2026-04-10 20:50:50 +07:00
d5628f754d feat(v3): core architecture redesign — pipeline, memory, vault, evolution, providers, orchestration (#792)
* 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)

---------

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>
2026-04-09 21:29:48 +07:00
Duc Nguyen 329b150902 fix(desktop): add defaultValues to form dialogs to prevent crash (#737)
useForm() without defaultValues causes watch() to return undefined on first render, crashing with undefined.trim(). Adds defaultValues to all 5 affected dialogs: Agent, Channel, MCP, Provider, TeamSettings.

Closes #732, closes #735
2026-04-08 15:26:33 +07:00
viettranx aa5158d4a2 feat(providers): add BytePlus ModelArk provider with Seedream/Seedance media gen
Add BytePlus ModelArk as a new OpenAI-compatible provider for Seed 2.0
models (chat, vision). Two provider types: standard API and Coding Plan
(separate base URLs, same auth).

Integrate Seedream image generation (sync API) and Seedance video
generation (async polling) into the builtin media tool chain, following
the established DashScope/Gemini patterns.

- Add WithAuthPrefix option to OpenAIProvider for future non-standard auth
- Add ProviderBytePlus/ProviderBytePlusCoding store constants and config
- Register provider from config.json and llm_providers DB table
- Add BytePlus to media chain routing, priority lists, and dispatch
- Create create_image_byteplus.go (Seedream, sync response)
- Create create_video_byteplus.go (Seedance, async poll with 5min timeout)
- Add BytePlus to web and desktop UI provider type dropdowns
- Update provider docs with BytePlus entries

Closes #686
2026-04-04 23:07:42 +07:00
viettranx 29f06cc24b fix(desktop): replace MarkdownRenderer with plain pre in trace span preview
TraceContentPreview was rendering JSON through CodeBlock with oneDark theme,
causing black background spans in light mode. Use plain <pre> with JSON
pretty-print instead — matches the original trace detail rendering.
2026-04-04 08:02:37 +07:00
viettranx 46b93a6b5f fix(desktop): guard slugify against undefined input
watch() can return undefined before form reset() runs, causing
'undefined is not an object (evaluating input.normalize)' crash.
2026-04-04 07:58:53 +07:00
viettranx 3466df726c fix(desktop): address code review findings — deduplicate slugify, restore moveFile guard, fix name collision
- H1: rename McpTestResult component import to McpTestResultDisplay (avoid type collision)
- H2: replace local slugify with shared lib/slug import in McpFormDialog
- H3: restore same-path no-op guard in storageService.moveFile
- H4: deduplicate StorageFileContent interface (use service as source of truth)
- M1: replace inline import() syntax with standard import in use-storage
2026-04-04 07:54:25 +07:00
viettranx 6be88f4178 refactor(desktop): react-arch audit — schemas, RHF forms, file splits, services, store cleanup, dir restructure
- Phase 1: add zod, react-hook-form, @hookform/resolvers; create 8 Zod schemas
- Phase 2: migrate 6 form dialogs from useState to useForm+zodResolver
- Phase 3: split 24 files >200L into ~40 focused modules (0 files >300L remaining)
- Phase 4: create services/ layer (8 service files), eliminate direct ws.call in components
- Phase 5: split chat-store into chat-message-store + chat-activity-store
- Phase 6: flatten settings/ directory — move 9 feature subdirs to top-level components/
2026-04-04 07:47:02 +07:00
viettranx 83bcb16597 refactor(channels): extract health model and modularize UI components
Post-merge cleanup for #634 channel health diagnostics:

Backend:
- Extract health types, ClassifyChannelError, mergeChannelHealth,
  buildRemediation into internal/channels/health.go
- Fix case-true anti-pattern → default in Slack config
- Remove duplicate connection-refused case in ClassifyChannelError
- Remove unused _ bool param from BaseChannel.setHealth
- Fix snapshot.Enabled == false → !snapshot.Enabled

Web UI:
- Extract channel status utilities to channels-status-utils.ts
- Extract ChannelDiagnosticsCard from channel-detail-page.tsx
- Extract ChannelAttentionPanel from system-health-card.tsx

Desktop UI:
- Extract shared getChannelStatusDisplay() to utils/channel-status.ts
- Add explicit stopped state case for visual consistency with web UI
- Add missing vi/zh locale keys for new health status states
2026-04-02 09:45:41 +07:00
Kai (Tam Nhu) Tran 4cd7b0a8d4 feat: add actionable channel health diagnostics and remediation (#634)
* feat: add channel health diagnostics

* fix(channels): harden startup failure reporting

- sanitize operator-facing failure details instead of exposing raw upstream errors

- keep registered channel instances failed during reload/startup errors and clean up pre-registration failure entries

- stop Telegram warning states from reporting running=true after polling or startup failures

* feat(channels-ui): surface actionable health diagnostics

- expand channel and overview views with richer health summaries, remediation hints, and failure history

- refine the channel dashboard layout so diagnostics are easier to scan instead of reading a wall of text

- keep shared web and desktop channel types aligned with the new health payload

* fix(web): harden channel health diagnostics states

- treat enabled channels without backend status as checking instead of stopped\n- align fallback remediation and overview attention synthesis with channel reality\n- localize diagnostics copy and reject invalid channel detail tabs\n\nRefs #628\nRefs #633

* docs(pr-assets): add channel diagnostics visual review pack
2026-04-02 09:30:11 +07:00
viettranx 24717b0f51 refactor(cron): normalize payload columns into dedicated DB fields (#33)
Extract wake_heartbeat and stateless from JSON payload into first-class
columns on cron_jobs. Adds migration 000033 with backfill from existing
payload data. Updates PG + SQLite stores, RPC handlers, and UI i18n.
2026-03-30 15:19:43 +07:00
Thieu Nguyen 7b42ab04ab fix(desktop): use 127.0.0.1 instead of localhost and suppress lite startup logs (#546)
- GetGatewayURL/IsGatewayReady: use 127.0.0.1 to avoid WebKit resolving
  localhost to ::1 (IPv6) when gateway only binds IPv4, causing "Load failed"
- Skip CORS warning for lite edition (localhost-only, no production exposure)
- Skip server IP detection for lite edition (no multi-tenant risk, avoids
  leaking user's public IP in logs and unnecessary network calls)
2026-03-29 16:03:09 +07:00
viettranx b59a623649 fix(deps): upgrade major packages across web and desktop UIs
Web UI:
- i18next 25→26, react-i18next 16→17
- lucide-react 0.468→1.7 (no brand icons used)
- tailwind-merge 2→3 (Tailwind CSS 4 compat)
- typescript 5.7→6.0 (remove deprecated baseUrl from tsconfig)

Desktop UI:
- i18next 25→26, react-i18next 16→17
- immer 10→11 (internal perf improvements)
- react-markdown 9→10, react-syntax-highlighter 15→16
- typescript 5.9→6.0
- Fix MarkdownRenderer: wrap ReactMarkdown in div for className (v10 removed prop)

Both: build verified, pnpm audit 0 vulnerabilities
2026-03-29 13:03:39 +07:00
viettranx c5cd1ad412 fix(deps): upgrade Vite 6→8, plugin-react 4→6, and patch/minor updates
Desktop + Web UI:
- @vitejs/plugin-react ^4.3.4 → ^6.0.1 (Vite 8 compat)
- vite ^6.0.0 → ^8.0.3 (CVE: Rollup path traversal, Picomatch ReDoS)
- tailwindcss/vite 4.2.0 → 4.2.2, tailwindcss 4.2.0 → 4.2.2

Web UI patch/minor:
- @tanstack/react-query 5.90→5.95, react-router 7.13.0→7.13.2
- recharts 3.8.0→3.8.1, zustand 5.0.11→5.0.12, ws 8.19→8.20
- i18next 25.8→25.10, react-i18next 16.5→16.6, framer-motion 12.34→12.38

Both: pnpm audit reports 0 vulnerabilities
2026-03-29 12:59:04 +07:00
viettranx a506b297cf fix(i18n): update maxHistoryShare tooltip from 0.75 to 0.85 default 2026-03-29 10:18:27 +07:00
viettranx 3695353de7 refactor(providers): extract Novita defaults into constants and add to desktop UI
DRY: replace hardcoded Novita API base/model across 3 files with
store.NovitaDefaultAPIBase and store.NovitaDefaultModel constants.
Add Novita to desktop frontend provider list.
2026-03-29 10:16:28 +07:00
viettranx 0c6cd02285 refactor(compaction): remove minMessages guard, use token-only threshold
Message count-based compaction trigger (default 200) was a legacy safety
net from when token estimation was unreliable. With calibrated overhead
estimation using actual LLM PromptTokens, the token threshold alone is
sufficient — matching industry standard (Claude Code, Anthropic API,
LangChain all use token-only).

Also fixes EstimateTokens vs EstimateHistoryTokens inconsistency in
end-of-run overhead calculation (loop_history.go), and updates stale
CLAUDE.md docs from 75% to 85%.
2026-03-29 10:14:52 +07:00
viettranx 8b75e095e9 feat(cancel): add run.cancelled event and fix stop across web UI, desktop, and channels
- Add distinct `run.cancelled` agent event to differentiate user-initiated
  cancellation from real errors (previously both emitted `run.failed`)
- Fix desktop stop button: wire onStop callback to InputBar, add abort()
  function in useChat hook, restore isRunning state on session switch
- Fix web UI: handle run.cancelled without showing error message, preserve
  partial streamed content on cancel
- Fix hanging chat.send promise: send {cancelled: true} OK response instead
  of silently dropping (was hanging 600s until timeout)
- Fix channel stream cleanup: handle run.cancelled for stream stop + run
  context deletion (prevents memory leak) + reaction status clear
- Fix child trace status: cancelled subagent runs now correctly get
  TraceStatusCancelled instead of TraceStatusError
- Add run.cancelled to all event consumers: query invalidation, session
  detail refresh, trace dialog refresh, gateway activity clearing
2026-03-29 09:00:06 +07:00
viettranx 243656a7b6 feat(mcp): add reconnect button to evict stale MCP connections
When MCP servers update their tools externally, the gateway keeps
stale cached connections. Add POST /v1/mcp/servers/{id}/reconnect
endpoint that evicts the pooled connection, forcing re-discovery
on next agent use. Exposed via reconnect button on both web and
desktop MCP management pages.
2026-03-29 07:15:03 +07:00
viettranx 040ced0a29 fix(desktop): allow scrolling long text in chat input
Change textarea overflow from hidden to auto so users can scroll
through long messages. Align attach/send buttons to bottom.
2026-03-28 15:08:49 +07:00
viettranx 407b2a3ed3 feat(desktop): add DB reset in About tab + complete onboarding i18n
About tab:
- Show clickable database path that opens data folder
- Add "Reset Database" button with RESET confirmation modal
- Reset deletes DB files and restarts app via RestartApp()
- Add GetDataDir/ResetDatabase Wails bindings

Onboarding i18n:
- Replace all hardcoded English strings in ProviderStep,
  ModelVerifyStep, and AgentStep with t() calls
- Add missing i18n keys to en/vi/zh common.json and desktop.json
2026-03-28 12:03:12 +07:00
viettranx ea60397754 fix(desktop): fix media chain settings not editable on reopen + add drag-drop
- Fix model Combobox permanently disabled on reopen: load models for
  existing chain entries once providers list is fetched
- Add drag-and-drop reordering via @dnd-kit/sortable (matching web UI)
- Use stable UUIDs as React keys instead of array indices
- Show model display names (e.g. "Gemini 2.0 Flash") instead of raw IDs
- Disable Save button when any entry is missing provider or model
- Extract MediaProviderChainForm into its own file
2026-03-28 09:19:00 +07:00
viettranx 8beb72039c feat(desktop): add language switcher to onboarding setup screen
Add centered language toggle (EN/VI/中文) at top of setup wizard.
Default language changed from English to Vietnamese.
2026-03-27 23:53:17 +07:00