Generic "operation requires admin approval" wording was over-applied
by the LLM to plain shell exec failures, causing unjustified pre-refusals.
Scope the section to commands returning the [CREDENTIALED EXEC] error
marker (already emitted at 7 sites in credentialed_exec.go).
Adds characterization test pinning the new wording and rejecting the
unqualified phrase.
Add TikTok account type dropdown (Livestream AIO, Business Messaging,
TikTok Shop) to Pancake channel config with prefix-aware webhook routing.
- Add tt, ttm, tts prefixes to platformPrefixes map for convID parsing
- Add TikTokType field to pancakeInstanceConfig (optional, backward compat)
- Add tiktok_type field to UI with showWhen platform=tiktok
- Add i18n labels for en/vi/zh locales
Co-authored-by: viettranx <viettranx@gmail.com>
* feat(tools): add vault tool group to policy
Add "vault" group containing vault_search and vault_read tools.
Include group:vault in messaging and coding profiles so agents
using these profiles can search and read vault documents.
Previously, vault tools were only in the goclaw composite group
but not exposed to profile-restricted agents like Telegram bots.
* fix(tests): use strings.Contains to avoid redeclaration
---------
Co-authored-by: viettranx <edu@200lab.io>
* feat(pancake): add Shopee platform support
- Fix Accept header for JSON response negotiation (Shopee GETs)
- Platform-prefix-aware pageID parsing (spo_ convID format)
- 500-char message limit for Shopee DMs
- Rune-safe truncation with slog.Warn on truncation
Phase 3 (integration smoke) pending: requires real credentials.
* fix(pancake): address PR review feedback
- Add RegisterPlatformPrefix() for extensibility (issue #1)
- Rename setCommonHeaders → setAcceptJSONHeader (issue #2)
- Add testdata fixture with page_id for priority testing (issue #3)
* chore: retrigger CI
* fix(permissions): classify sessions.compact in RBAC policy
Add MethodSessionsCompact to isWriteMethod to fix drift coverage test.
Method was added in #958 but missing from permissions classification.
* fix(tests): use strings.Contains to avoid redeclaration
Remove duplicate contains helper in tts_gemini_live_test.go that
conflicts with mcp_grant_revoke_test.go in same package.
* refactor(pancake): guard platformPrefixes with RWMutex for concurrent safety
RegisterPlatformPrefix is exported for future use (Lazada, Tokopedia) but
webhook handler reads the map concurrently. Protect with sync.RWMutex so
callers can register prefixes at any time without data races.
- Add platformPrefixesMu sync.RWMutex
- Wrap RegisterPlatformPrefix with write lock
- Extract isKnownPlatformPrefix helper with read lock
- Document that RegisterPlatformPrefix is currently unused (extension point)
---------
Co-authored-by: viettranx <viettranx@gmail.com>
* feat(pancake): private-reply funnel — 2 modes, scope filter, DB dedup, metrics
Refactor the pancake channel's one-time DM feature from the 5-day-old minimal
`first_inbox` toggle into a complete comment → DM funnel. Breaking rename
(first_inbox → private_reply) matches Pancake + Facebook Graph terminology.
Channel logic
- Two modes: `after_reply` (default — public reply then DM) and `standalone`
(DM-only; bypasses keyword filter, publishes synthetic outbound via
`ch.Bus().PublishOutbound` so the LLM pipeline is skipped for template DMs).
- Post-level scope filter (`private_reply_options.allow_post_ids` /
`deny_post_ids`, deny beats allow).
- Template vars `{{commenter_name}}` / `{{post_title}}` with pre-sanitized
literal-replace (strips `{{`/`}}` from values — no nested substitution).
- Locale-aware default text via `i18n.T(locale, MsgPancakePrivateReplyDefault)`
with en/vi/zh catalogs and English fallback.
DB dedup (replaces in-memory sync.Map)
- New `store.PancakePrivateReplyStore` (PG + SQLite impls), tenant-scoped via
`store.TenantIDFromContext`, fail-closed on missing tenant.
- Atomic `TryClaim` / `Unclaim` (INSERT ... ON CONFLICT DO UPDATE WHERE stale)
eliminates the concurrent-comment TOCTOU that would have fired duplicate
DMs — claim first, release on API failure so the next comment can retry.
- Configurable TTL (`private_reply_ttl_days`, default 7).
- Dual-DB: migration 000056 on PG; schema v25 on SQLite.
Bumps RequiredSchemaVersion 55 → 56.
Wiring
- `pancake.FactoryWithStores(ppReplyStore)` follows the existing
`FactoryWithStores*` pattern (telegram, discord, whatsapp) and replaces
`pancake.Factory` in `cmd/gateway.go`.
- `Channel.Send` injects `store.WithTenantID(ctx, ch.TenantID())` before
dispatch so the outbound path sees a non-nil tenant (regression guard:
without this the new store fails-closed and drops DMs in production — unit
fakes masked this by bypassing the tenant check).
- Routing metadata whitelist extended: `private_reply_mode`,
`private_reply_only`, `post_id`, `display_name` survive inbound → outbound.
Observability
- New `internal/metrics` package with `pancake_private_reply_total{
page_id, result, reason}` counter and `/metrics` endpoint mounted on the
gateway.
- Endpoint gated behind Bearer auth when `gateway.token` is configured; open
(with warning) when unset for local dev scraping. Operators should set a
token or bind `GOCLAW_HOST=127.0.0.1` to avoid leaking page identifiers.
UI
- 6 additive Pancake fields in `channel-schemas.ts` with `showWhen` gating on
`features.private_reply` (itself gated on platform = fb/ig).
- 10 schema tests; full web build passes.
- i18n entries in en/vi/zh `channels.json` + 2 new Go keys
(`MsgPancakePrivateReplyWindowExpired`, `MsgPancakePrivateReplyDefault`).
Tests
- 18 unit tests in pancake package (render/filter/mode switch/scope/dedup/
metrics/tenant-ctx regression); 6 store-level tests; 5 E2E tests against
live PG covering happy path, dedup-across-restart, TTL expiry via SQL,
scope filter, FB 7-day policy error → no claim retained.
- All green on both PG and SQLite build tags with race detector.
* refactor(pancake): simplify private-reply to stateless DM
Roll back the dedup funnel from PR 951 — no table, no in-memory state,
no metrics, no modes, no scope filter. Keep only feature flag + message
template.
Rationale: each platform owning its own dedup table is an anti-pattern
that inflates multi-tenant data footprint. Use the platform itself
(Facebook Graph /comment/private_replies is per-comment idempotent)
combined with the existing webhook comment_id dedup in
comment_handler.go. Zero GoClaw state required.
Removed:
- DB table pancake_private_reply_sent (PG migration 000056, SQLite v25)
- Store interface + PG + SQLite implementations + tests
- internal/metrics package + /metrics endpoint + prometheus dep
- Two modes (after_reply/standalone) + standalone fast-path
- Scope filter (allow/deny post IDs) + PrivateReplyOptions struct
- TTL config (private_reply_ttl_days)
- Locale-aware i18n default (MsgPancakePrivateReplyDefault,
MsgPancakePrivateReplyWindowExpired)
- FactoryWithStores wiring + tenant ctx injection in Send()
- SetHTTPClientForTest dead code
Kept:
- features.private_reply flag
- private_reply_message template with {{commenter_name}} and
{{post_title}} vars (literal-replace, injection-safe)
- After-reply flow: comment -> public reply -> DM
- Hard-coded English fallback when message is empty
Schema: RequiredSchemaVersion 56 -> 55; SQLite SchemaVersion 25 -> 24;
schema.sql DDL block removed.
Net delta: -2661 lines across 44 files. Build (PG + sqliteonly) +
vet + race tests clean.
* test(agent): bump none-mode prompt size budget to 3100
vault_read wiring (#948) added ~95 chars to read_file tool summary,
pushing none-mode prompt from <3000 to 3075 chars. Bump budget to
3100 (~775 tokens) to match the intentional addition.
* test(integration): ensure data_migrations table exists in reset helper
The reset helper runs before RunPendingHooks, but RunPendingHooks is
what normally creates data_migrations. On a fresh CI database the
DELETE fails with 'relation does not exist'. Create the table
defensively so reset works regardless of execution order.
* chore(sqlite): remove accidental trailing blank line in schema.sql
---------
Co-authored-by: viettranx <viettranx@gmail.com>
Previously `Registry.RoundRobinNext` used a single counter keyed by
`(tenantID, baseProviderName)`, shared across both ChatGPTOAuthRouter.Chat
and ChatGPTOAuthRouter.GenerateImage. On small pools with bursty mixed
traffic, chat calls would advance the counter and skew image-call
distribution over short windows.
Add a `modality` segment to the counter key so chat and image rotate
independently. ChatGPTOAuthRouter threads "chat" through Chat/ChatStream
and "image" through GenerateImage.
Tests (post red-team):
- ChatBurstDoesNotPerturbImageOrder — uses the issue's interleaved worked
example (5 chat, 1 image, 4 chat, 1 image, 3 chat, 1 image) which
produces skewed order [C, B, C] on the buggy path and even [A, B, C]
on the fixed path. Tautological set-membership assertions were
rejected: N consecutive image calls on an N-member pool always visit
every member once regardless of starting offset.
- IntrospectionDoesNotAdvanceCounter — guards that Name(), DefaultModel(),
HasAvailableProviders() call orderedProviders with advance=false.
- ChatAndImageConcurrent_CountersIndependent — 90 chat + 90 image
goroutines concurrently; assert even per-modality distribution under
-race.
Closes#1018
* refactor(codex-pool): remove redundant primary_first strategy
* test(tools): update tool schema fixtures to pointer form
* feat(create_image): route Codex pools through chain with member failover
Codex pool chain entries now iterate pool members per the pool's own
strategy (round_robin or priority_order) and fail over internally before
the outer chain advances to the next entry.
- ChatGPTOAuthRouter.GenerateImage implements NativeImageProvider:
iterates orderedProviders, tries each member, advances round-robin state
only on success, aggregates errors on pool exhaustion.
- media_provider_chain.wrapPoolProvider wraps a *CodexProvider in a router
when RoutingDefaults has extras or a non-primary-first strategy. Solo
Codex (no extras) stays unwrapped.
- Router exposes ProviderType() so chain telemetry records "chatgpt_oauth".
Closes#1008
* feat(ui/builtin-tools): pool badge on create_image chain entries
Chain entry card shows a read-only "Pool · <strategy>" badge when the
entry's provider is a Codex pool base. Strategy label translates in
en/vi/zh. No new toggle or selector — pool config lives on the provider
itself.
#1008
* refactor(providers): move Codex test helpers to providertest subpackage
Addresses code-review High finding: NewTestCodexProviderFast and its
staticTestTokenSource were exported from a non-_test.go file, compiling
into the production binary. Relocating to internal/providers/providertest/
keeps the helper importable from other packages' tests without leaking
test-only symbols into production.
Also tightens wrapPoolProvider — pools with zero extra members no longer
get wrapped in a router (KISS: nothing to rotate between).
- Added CodexProvider.WithRetryConfig as a legitimate fluent option (the
test helper now uses the public API).
- Dropped the internal-package test helper file.
#1008
* docs(pr-1006): add pool badge UI evidence
Force-UI style capture of the read-only "Pool · Round-robin" badge on
the create_image chain entry card when the selected provider carries
settings.codex_pool with extras. Captured against staging gateway.
* refactor(ui/builtin-tools): unify pool UX with Create Agent pattern
The create_image chain Provider dropdown was listing pool members alongside
pool owners, letting users accidentally bypass pool semantics by picking a
member directly. Matches the pattern already used by Create Agent: hide
pool members, show an inline "Pool" chip on owners.
- Filter pool members from the chain Provider dropdown via
getChatGPTOAuthPoolOwnership.ownerByMember.
- Inline "Pool" chip on owner options, reusing the existing
providers:list.poolBadge i18n key.
- Drop the separate card-level "Pool · <strategy>" badge — the dropdown chip
alone conveys the information without duplication.
- Remove the now-unused isPoolProvider / poolStrategyOf helpers and the
builtin.mediaChain.poolBadge* i18n keys we briefly introduced.
#1008
* docs(pr-1006): add pool-filtered dropdown screenshot
* fix(ui/builtin-tools): migrate stale pool-member chain entries on load
Red-team blocker: a chain saved before the pool-aware UI landed may
reference a pool member by name (e.g. openai-codex-2). After the
dropdown started hiding members, such an entry produced:
- empty Select trigger (no SelectItem matches the stored value)
- conflicting Row 1 label still showing the member name
- silent runtime misroute (bare solo call, no pool wrap)
parseInitialEntries now consults getChatGPTOAuthPoolOwnership and
rewrites any chain entry whose provider is a pool member to the owner's
name + id. The next save persists the migrated value. Safe no-op for
non-pool entries and for entries already pointing at an owner.
Also memoize enabledProviders in the parent form so the card's useMemo
boundaries actually hold (minor perf ding flagged by same review).
* docs(pr-1006): refresh HTML evidence to match filter-based UX
* fix(ui/agent-codex-pool): traffic policy reflects effective strategy under inherit
On the agent's OpenAI Account Pool page, when Agent routing mode is
Use Provider Defaults, the Traffic Policy buttons painted the draft's
placeholder strategy ("priority_order") as selected — contradicting the
top-of-page chip which already correctly shows the provider's effective
strategy. The removal of primary_first in this PR unmasked the latent
bug: the placeholder used to be a deprecated value that didn't match any
live button, so nothing appeared selected.
Derive selectedStrategy from defaultRouting when mode === "inherit":
the button highlight now mirrors what actually runs. Buttons remain
disabled in inherit mode (unchanged), but the displayed selection no
longer misleads the user.
* docs(pr-1006): add screenshot of inherit-mode Traffic Policy fix
* fix(permissions): remove duplicate MethodSessionsCompact entry
The writeExact slice listed MethodSessionsCompact twice. slices.Contains
still returned correct results so runtime behavior is unchanged, but the
duplicate entry was dead code.
Spotted during review of PR #1006.
---------
Co-authored-by: viettranx <edu@200lab.io>
handleMessage ran an extra IsAllowed(senderID) check after
checkDMPolicy/checkGroupPolicy, creating a double-gate. When DM/Group
policy was set to "pairing" or "allowlist" with an allowlist configured,
paired users were wrongly rejected because they were not individually
listed. Telegram has no such post-policy gate — CheckDMPolicy and
CheckGroupPolicy already handle allowlist + pairing correctly.
Removing the redundant check aligns Discord with the shared policy
semantics and fixes issue #985.
- config: align struct tag formatting in TelemetryConfig for readability
- audio tests: modernize test patterns
- cache, http, hooks, providers: small style and cleanup passes
- No functional changes
- Use maps.Copy in hooks dispatcher instead of manual map loop
- Remove implicit loop variable capture in router_abort_test (Go 1.26 semantics)
- Use range without index in http_test where index unused
- Use min() builtin in script_test instead of manual min computation
- edit tool now calls allowedWriteWithTeamWorkspace to prevent cross-chat writes
- shell tool upgraded to use write-allowed prefixes for cwd resolution
- Prevents shell commands from accessing/mutating peer chat workspaces via absolute paths
- Add clarifying comment on shell's write-safety model
- Add WithToolTeamRoot context key to allow agents in same team to read peer-chat files
- Split allowedWithTeamWorkspace into read (includes team root) and write (excludes it)
- Write operations now use allowedWriteWithTeamWorkspace to prevent cross-chat mutations
- Add comprehensive tests for team root merging, deduping, and cross-chat access control
- Refactor path merging into buildAllowedPrefixes helper to reduce duplication
Extend migration 000056 backfill-2 vocabulary from {telegram, ws, delegate}
to the full channel set the gateway supports: discord, zalo, feishu, lark,
whatsapp, slack, line, messenger, wechat, viber (plus api transport).
Verified against staging data: identical chat_id extraction (91 rows,
9 distinct chats) — no regression. Now safe for tenants using any
non-Telegram integration.
- Extend migration 000056 with legacy-docs backfill (telegram/<chat>,
ws/<chat>, delegate/<chat>, and agent/bot/<chat> path shapes) so
scope=shared/personal docs created before team-scope refactor also
get stamped with chat_id and stop leaking across chats
- rescan: inferOwnerFromPath now returns chat_id extracted from team
path third segment; stamp onto VaultDocument so admin /vault/rescan
preserves chat isolation
- HTTP vault search: accept optional chat_id body param, set
TeamIsolated=true when TeamID + ChatID both present (admin UI can
view vault from a specific chat perspective)
Applied to staging DB via MCP: 91 legacy rows backfilled.
- Add vault_documents.chat_id + composite index (migration 000056)
- Filter vault_search by chat_id when team.workspace_scope=isolated
- Stamp chat_id on AfterWrite/AfterWriteMedia for isolated teams
- Deny cross-chat vault_read in isolated teams (M2 fix)
- RunContext.TeamIsolated flag resolved once per run
- Fallback WorkspaceChatID → ChatID in loop_context for entry points
that don't set WorkspaceChatID explicitly (WS direct, HTTP, cron)
Fixes cross-chat doc leak where agent in chat A could see vault docs
from chat B within the same isolated team.
- Update docs/00-architecture-overview.md with TokenCounter and sessions.metadata details
- Update docs/codebase-summary.md with overhead accounting and compaction logic
- Update docs/project-changelog.md with v3.11.0 context-tokens accuracy fixes
After stripBotMention removes the bot's own @mention from content, the LLM
still lacks knowledge of its own platform handle. In multi-bot groups (e.g.
"@botA @botB do X") the other bot's mention remains in the content and the
agent incorrectly treats it as the intended target, replying NO_REPLY.
Capture the bot's first_name from GetMe during channel Start and expose it
alongside the username via a new MetaChannelSelfIdentity metadata key. The
consumer appends the formatted hint ("You are @{username} ({display_name})
on this Telegram channel.") to the agent's extraSystemPrompt so the LLM can
reliably identify itself across single- and multi-bot scenarios.
Falls back to "You are @{username} on this Telegram channel." when the
display name is not available, and no-ops when the username has not been
resolved yet (startup race).
Agents previously saw their own Telegram handle (e.g. "@viet_super_bot")
in user messages and mistook it for a different bot, replying NO_REPLY.
The username was only used for the mention gate, never removed from the
content passed to the LLM.
Slack and Feishu already strip their own bot mentions (handlers_mention.go
stripBotMention, bot_parse.go resolveMentions); Telegram was the odd one out.
Implementation:
- Add stripBotMention helper with leading/trailing word-boundary anchors so
inline matches inside words (e.g. contact@viet_super_bot.com) are not
falsely stripped.
- Apply in handleMessage right after the mention gate, before pairing/media
processing, so history recording for unmentioned messages keeps raw text.
- Restore "[empty message]" placeholder when a message consisting only of
"@botname" becomes empty after stripping.
* refactor(providers): migrate ToolDefinition.Function to pointer + add image response fields
ToolDefinition.Function becomes *ToolFunctionSchema with omitempty so native tool types (image_generation, web_search, etc.) can be declared without a function body. All 9 internal construction sites updated. CleanToolSchemas refactored — function-shape cleaning extracted into cleanFunctionSchema helper, outer pass-through handles native tools.
Added image response fields needed by the next commits: ChatResponse.Images, StreamChunk.Images, ImageContent.Partial (distinguishes partial frames from final images).
* feat(providers): native image_generation for Codex + OpenAI-compat tracks
Codex native (POST /codex/responses): emit image_generation tool object in request tools[] (type, action, model, output_format, partial_images). Handle SSE events response.image_generation_call.partial_image + response.output_item.done (type image_generation_call) + response.completed output[] walk for non-stream. Dedup per item_id. Extend codexSSEEvent/codexItem with output_format, result, partial_image_b64, partial_image_index.
OpenAI-compat (/v1/chat/completions): serialize ToolDefinition{Type:'image_generation'} as {type:'image_generation'} pass-through. Parse choices[0].message.images[] + delta.images[] (data URLs) via new parseDataURL helper; append to ChatResponse.Images.
ProviderCapabilities.ImageGeneration flag; Codex provider + adapter set true. Other providers default false.
* feat(agent,http,store): persist assistant images + tri-level image_generation gate
Agent loop tri-level gate: (provider capability) AND (AgentConfig.AllowImageGeneration, default true, stored in other_config.allow_image_generation) AND (request lacks x-goclaw-no-image-gen header). Gate in loop_tool_filter.go appends ToolDefinition{Type:'image_generation'} only when all three pass. Per-request opt-out parsed in chat_completions.go and propagated via RunRequest.NoImageGen.
Media persistence: persistAssistantImages writes final images (Partial:false) to {workspace}/media/{sha256}.{ext}, returns MediaRef entries, clears inline Images[] from the message. Idempotent on hash, traversal-safe, symlink-guarded. Invoked from pipeline.FinalizeStage via new Deps.PersistAssistantImages callback — covers both stream-final and non-stream paths.
Agent store reads AllowImageGeneration from other_config JSONB with absent/nil = true default (matches V3Flags pattern). No DB migration — code-only default.
* feat(ui/web): image_generation toggle + streaming placeholder + download filename
Composer chip 'Images' visible only when active agent's provider has ImageGeneration capability. Per-agent localStorage persistence via useImageGenToggle hook. When off, sends noImageGen:true to WS chat method (maps to x-goclaw-no-image-gen on upstream HTTP call path).
ActiveRunZone renders a skeleton placeholder while streaming partial_image frames arrive. MediaGallery assigns generated-YYYYMMDD-HHmmss.png as the download filename for hex/UUID PNGs.
i18n keys added to en/vi/zh chat.json: imageGenToggle, imageGenGenerating, imageGenDownloadName. 8 vitest tests for the toggle hook.
* docs: add Image Generation section to codebase-summary + changelog entry
Documents the new native image_generation pipeline across providers layer (Codex + OpenAI-compat), agent gate, media persistence, and web UI surface.
* fix(ui/web): match Codex-routed providers for image_generation toggle
Image-gen toggle visibility was hard-coded to provider id 'chatgpt_oauth' but real Codex-routed agents in production use provider ids like 'cliproxy-codex'. The toggle never rendered.
Replace the Set-has check with a small helper that accepts the literal ids plus any provider string containing 'codex' (case-insensitive). Same logic applied in both chat-input.tsx (composer chip) and chat-page.tsx (streaming placeholder gate).
Verified against a live Codex-routed agent: toggle now renders, noImageGen:true propagates on toggle-off.
* docs(pr-1002): targeted-mode UX evidence report
Captures the UI integration surface for native image_generation against a live Codex-routed agent on the remote dev backend.
Includes: composer toggle chip (rendered), streaming skeleton placeholder, and honest failure-path capture showing the legacy create_image builtin fallback. Self-contained HTML report in .github/pr-assets/1002/index.html.
* fix(permissions): classify sessions.compact as write method
CI RBAC-drift test (TestMethodRole_DriftCoverage_AllProtocolMethodsClassified) was failing because the new sessions.compact method added upstream was not classified in any of isPublicMethod / isAdminMethod / isWriteMethod / isReadMethod.
Sessions compaction mutates session history (compacts messages into summaries), so it belongs with the other sessions.* write methods.
* fix(tests): remove duplicate contains() in integration package
Both tts_gemini_live_test.go and mcp_grant_revoke_test.go declared a file-local func contains(s, substr string) bool with identical bodies, causing 'contains redeclared in this block' at compile time in the integration job.
Replace all call sites with strings.Contains (same semantics, stdlib) and drop the duplicates. No behavior change.
* feat(providers): NativeImageProvider interface + Codex implementation
Defines a provider-level contract (NativeImageProvider.GenerateImage) that OAuth-backed providers can implement to serve image generation without exposing static API credentials. Re-uses the PR's Track A native wire format (POST /codex/responses with image_generation tool, item.result decoding, SSE fallback).
CodexProvider + CodexAdapter implement it. Also adds MediaRef.Prompt field so downstream layers can propagate the generating prompt alongside the asset.
* feat(tools): route create_image through NativeImageProvider for OAuth providers
create_image.callProvider now checks for a NativeImageProvider implementation before the credentialProvider interface. When the provider chain points at a Codex-family provider (no static API key), the tool delegates to the provider's GenerateImage which executes the native ChatGPT Responses API image_generation flow.
Resolves 'provider X does not expose API credentials required for image generation' errors for openai-codex / cliproxy-codex chains. On success the tool embeds the user's prompt as a PNG tEXt 'Description' chunk (file-local pngEmbedPrompt helper to avoid tools→agent import cycle), writes the image to /tmp, and threads the prompt through result.MediaPrompts for downstream MediaRef propagation.
* feat(agent,pipeline): propagate image prompt through MediaRef + PNG tEXt embed helper
Adds EmbedPNGPrompt public helper in internal/agent/png_metadata.go that inserts a tEXt 'Description' chunk (plus 'Software: goclaw') into PNG byte streams before the IEND chunk. Non-PNG inputs are passed through unchanged — the helper never errors on unknown formats.
FinalizeStage wires MediaResult.Prompt (from create_image tool output) onto MediaRef.Prompt so the UI can render the generating prompt alongside the image. Per-image prompt list threaded via pipeline RunState.
* feat(ui/web): show generating prompt as caption under each image in MediaGallery
When a MediaRef carries a prompt, MediaGallery renders it as a muted italic caption (line-clamp-2) beneath the image with the full text in the title tooltip. Caption is hidden when the prompt is absent so non-assistant images (user uploads, legacy data) look unchanged.
MediaItem + session media_refs types extended with an optional prompt field; the chat-message adapter threads ref.prompt through when converting WS payloads to UI state.
* fix(providers/codex): stream:true + instructions for native image_generation
The ChatGPT Responses API on /codex/responses rejects two things hard:
- stream:false → HTTP 400 "Stream must be set to true"
- missing instructions → HTTP 400 "Instructions are required"
buildNativeImageRequestBody now sets stream:true and a purpose-specific instructions string ("Generate an image matching the user's description using the image_generation tool. Return only the image; do not describe it in text."). The existing parseNativeImageSSE path was already in place for stream parsing; routing changed from the non-stream branch to the SSE branch.
Regression assertions added to TestCodexGenerateImage_BuildsNativeRequest so these two fields can't silently regress.
* feat(providers,tools,ui): image_model whitelist (gpt-image-2 default, gpt-image-1.5 legacy)
Replaces the hardcoded "gpt-image-2" literal in buildNativeImageRequestBody with a user-configurable field threaded through NativeImageRequest.ImageModel. The whitelist is enforced by ValidateImageModel which rejects anything outside {gpt-image-2, gpt-image-1.5} with a clear error — prevents silent upstream 400s from model names the Responses API would reject.
create_image.callProvider reads params.image_model from the chain entry and threads it through. Empty / absent falls back to DefaultImageModel (gpt-image-2).
UI: added an 'Image model' select inside the existing openai-codex Settings panel on the Create Image Provider Chain dialog. Options: Default · gpt-image-2 (recommended) and Legacy · gpt-image-1.5. i18n keys in en/vi/zh tools.json under builtin.mediaChain.
Tests: TestCodexGenerateImage covers default/legacy/rejected model cases; TestCreateImageTool_ThreadsImageModel covers params→request threading with empty/legacy/explicit sub-cases.
* fix(tools): raise media chain default timeout to 600s/1 retry for image gen
gpt-image-2 on complex prompts (dense Vietnamese text, infographic layouts) legitimately takes 4–8 minutes to complete. The previous default of 120s × 2 retries routinely died mid-generation with 'context deadline exceeded' — the upstream run was still producing bytes when our ctx cancelled.
Default is now Timeout: 600 / MaxRetries: 1. Retries dropped to 1 because image generation is stateful per upstream run: a mid-flight timeout leaves orphan server work, and retrying a fresh generation doubles cost for no gain. Surface the failure fast so operators can widen the timeout instead.
Operators can still set a tighter value explicitly via the Chain dialog.
* refactor: remove user-facing Images toggle, keep admin-level AllowImageGeneration
The per-request opt-out toggle (composer chip + streaming placeholder + noImageGen header plumbing) was a support footgun — users toggle OFF, forget, then can't generate images and think it's broken. Removed in full.
Kept: AgentConfig.AllowImageGeneration (admin kill-switch, stored in other_config.allow_image_generation, default true). Tri-level gate simplifies to two tiers: provider capability AND agent config allows.
Removed: useImageGenToggle hook, IMAGE_GEN_PROVIDER_IDS set in chat-input, supportsImageGenProvider helper, agentProvider/agentKey props on ChatInput, showImageGenPlaceholder prop on MessageBubble/ActiveRunZone/ChatThread, noImageGen param on use-chat-send, parseNoImageGen in chat_completions.go, NoImageGen on RunRequest, no_image_gen_header_test.go, imageGenToggle/imageGenGenerating i18n keys. Kept imageGenDownloadName — used by MediaGallery for generated-\*.png filename resolution.
* docs(pr-1002): refreshed UX trace + updated codebase notes
Replaces the earlier stealth-state evidence with a clean three-capture trace from a real successful run: inline image + prompt caption, MediaGallery lightbox expansion, Chain dialog with the new Image model dropdown open. Skill-routing rows scrubbed from the capture — they reflect per-agent skill setup, not anything this PR introduces.
codebase-summary + changelog: reflect final state (toggle removed, image_model selector, 600s default chain timeout, gpt-image-2 as quality baseline).
* fix(pipeline): preserve mid-loop image_generation output across iterations
FinalizeStage previously read state.Think.LastResponse.Images, which holds
only the last iteration's response. If the LLM emitted image_generation_call
in iteration N alongside a function_call, then responded text-only in N+1,
the image from N was silently dropped on finalize.
Accumulate final (non-partial) images into state.Observe.AssistantImages
across every iteration via ObserveStage, and source FinalizeStage from the
accumulator instead of LastResponse. Partial streaming frames are filtered
defensively; response.Images is cleared on drain to prevent double-counting
on re-exec.
* test(pipeline): regression coverage for mid-loop image accumulation
Six ObserveStage cases covering image accumulation semantics:
- single-iter image-only, image+tool_call same iter, mid-loop image
surviving text-only final iter, multiple images across iters, partial
frame filtering, nil-response safety.
Two FinalizeStage cases verifying accumulator is the source of truth:
- PersistsFromObserveAccumulator: image in Observe + empty LastResponse
must still be persisted via PersistAssistantImages.
- NoPersistWhenAccumulatorEmpty: no call when no images were emitted.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
MiniMax /v1/get_voice does not return explicit gender/language fields,
but the system-voice naming convention exposes them:
- voice_id prefix `male-*` / `female-*` (legacy Chinese voices)
- voice_name suffix `*_Man` / `*_Boy` / `*_Lord` / `*_Speaker` → male
- voice_name suffix `*_Lady` / `*_Girl` / `*_Belle` / `*_Lass` → female
- voice_name prefix `English_*` / `Chinese_*` / `Japanese_*` /
`Korean_*` / `French_*` / etc. → language
`parseMinimaxLabels` is best-effort — returns nil for unparseable IDs
(e.g. `moss_audio_<uuid>` cloned voices), so the picker shows no badge
rather than a wrong one. The picker already renders `voice.labels` as
badges; nothing to wire on the FE side.
8-case unit test covers the matrix.
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.
Card primitive uses `flex flex-col gap-6` which adds 24px between
header and content regardless of `pb-3` on CardHeader. Override the
flex-gap to `gap-3` on each TTS section so the section number/title
visually attaches to its body.
Affects: provider-setup, credentials-section, voice/model card on
tts-page, test-playground, behavior-section.
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.
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.
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.
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).
- Change default mode from "off" to "cache-ttl" (enabled by default)
- Increase softTrimRatio from 0.25 to 0.3 to match TS defaults
- Update tests to reflect new default-enabled behavior
- Fix prose body citation in 15-core-skills-system (removed :410 line ref)
- Normalize File Reference schema in 08-scheduling-cron and 10-tracing-observability to Module/Path/Purpose format
- Add missing hint line to 03-tools-system File Reference section
- Clean Go method-call symbols from 01-agent-loop Mermaid diagrams (Router + Resolver)
- 01-agent-loop: replace Go function names in sanitize step details and
mermaid labels with behavioral descriptions; clean resolver resolved
properties, router cache/run-tracking, and team workspace context
variables of symbol references
- 11-agent-teams: compress mailbox 3-action table + Use Cases into
3 narrative paragraphs; replace WorkspaceDir Go code block with
prose description
Replace trailing File Reference sections in 7 docs with 3-4 row
module-level tables. Column schema: Module | Path | Purpose.
Adds 1-line grep hint at end of each section. Also fixes one
body .go:line citation in 05-channels-messaging.md.
Rewrote docs/03-tools-system.md from 987 lines to 519 lines.
Removed Go type/const definitions, function citations, 47-file
path listing, and static regex patterns. Replaced with concept
tables, complete tool inventory cross-checked against live
internal/tools/ registrations, and 3-row module table.
Root CHANGELOG.md is the user-facing single source of truth. docs/17-changelog.md
was an internal verbose log duplicating git history.
Also removed docs/multi-tenant-architecture.bk.md (gitignored, pre-v3 backup
superseded by 23-multi-tenant-architecture.md).
Phase 01 of plans/260419-1344-docs-audit-and-condense.
* fix(vault): prevent vault_read id-namespace collision
vault_search was leaking KG/episodic entity ids into result sets even when
narrow `types` were requested, and callers then passed those ids to
vault_read which returned a generic "document not found". The cause was
threefold:
1. `types` filter was only applied to the vault fan-out; KG and episodic
ran unconditionally. Now gated by shouldFanout(types, key).
2. vault_search output lacked a per-source tool hint. Each result now ends
with " → use <tool>" naming the correct follow-up (vault_read,
knowledge_graph_search, or memory_search).
3. vault_read miss returned "document not found" without checking whether
the id belonged to a foreign namespace. It now probes KG then episodic
and returns a namespace-specific redirect error. Stores are injected
via SetKGStore/SetEpisodicStore, nil-safe, tenant-scoped.
Adds red→green characterization tests plus an end-to-end integration
scenario seeding a vault doc + KG entity with identical basenames.
* test(agent): bump none-mode prompt size budget to 3100
vault_read wiring (#948) added ~95 chars to read_file tool summary,
pushing none-mode prompt from <3000 to 3075 chars. Bump budget to
3100 (~775 tokens) to match the intentional addition.
* test(integration): ensure data_migrations table exists in reset helper
The reset helper runs before RunPendingHooks, but RunPendingHooks is
what normally creates data_migrations. On a fresh CI database the
DELETE fails with 'relation does not exist'. Create the table
defensively so reset works regardless of execution order.
* refactor(vault): per-source id fields + wire episodic into search
Align vault_search output fields with downstream tool input params:
doc_id (vault_read), entity_id (knowledge_graph_search), episodic_id
(memory_expand). Prevents LLMs from pattern-matching a generic `id:`
and misrouting a foreign-namespace uuid into vault_read. Fallback
redirect in vault_read now quotes id + names the correct param so
the LLM can self-correct in one turn.
Also wire stores.Episodic into VaultSearchService (stale comment
claimed pending-impl; PGEpisodicStore has existed and been in use
since v3). Unifies search fan-out with vault_read namespace probe.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
- aria-haspopup/aria-expanded/aria-controls on trigger; role="listbox"
+ aria-label on dropdown; role="option" + aria-selected on rows. Listbox
id from useId() so multiple pickers don't collide.
- Clicking the trigger while open now closes it (toggle), not a no-op.
- Close on window scroll/resize: the fixed-position dropdown doesn't
reflow with the viewport, so pinning it in the wrong place is worse
than closing.
- shell.go: remove commented-out git conflict markers; restore load-bearing
rationale for plain exec.Command vs CommandContext (process-group kill).
- credentialed_exec.go: pass through SYSTEMROOT, TEMP, APPDATA and other
Windows runtime vars that native CLIs (gh, az, aws, npm) require. Missing
SYSTEMROOT breaks networking/registry APIs in most Win32 programs. Switch
to USERNAME (Windows) vs USER (POSIX) per platform convention.
* 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.
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.
* Fix: Destop Lite Windows edition exec failure
The executing of local commands use sh wrongly.