304 Commits
Author SHA1 Message Date
fchengyan 4e99816a87 fix(channels): deliver NO_REPLY placeholder cleanup signal to channels (#1586)
The agent loop signals silent replies (NO_REPLY, cancelled runs, suppressed
errors) by publishing an outbound message with empty content plus the inbound
routing metadata (placeholder_key / local_key). Slack, Telegram and Discord
each implement an empty-content branch in Send() that deletes their streamed
'Thinking...' placeholder — the partial draft left visible in the thread when
delivery is suppressed.

deliverOutbound skipped every text-only empty outbound, so that cleanup
signal never reached channel.Send and the stray partial draft stayed
published (issue #1475). Pass the signal through when placeholder routing
metadata is present; keep skipping bare empty messages so channels without
an empty-content branch never render empty bubbles.

Fixes #1475
2026-09-29 07:07:12 +02:00
Clark Cant 0b1aeef52c Merge pull request #1573 from otrumb/fix/channel-health-failure-kind
fix(channels): clear failure kind after recovery
2026-09-26 04:02:42 +07:00
Ngoc TrungandSisyphus dcadb77548 fix(channels): clear failure kind after recovery
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-22 13:23:47 +07:00
yatulandClaude Opus 5 5379cc1163 feat(pairing): let an operator make a device pairing permanent
Every approved pairing expires 30 days after approval (pairedDeviceTTL),
and nothing lets an operator change that: owners of a personal setup have
to re-pair their own Telegram account and browser every month. The schema
already treats a NULL paired_devices.expires_at as "never expires" (IsPaired
and the prune both check for it), only no code path ever writes NULL.

- store: SetPairingPermanent(senderID, channel, permanent) on PairingStore,
  PG and SQLite. permanent=true clears expires_at, false restarts the
  default TTL from now. An already expired pairing is not revived.
  PairedDeviceData gains expires_at (Unix ms, null = never), returned by
  ApprovePairing and ListPaired.
- gateway: device.pair.approve accepts `permanent`; new admin RPC
  device.pair.update {senderId, channel, permanent} for existing pairings,
  classified in permissions/policy.go next to the other pairing methods.
- mcp: pairing approve tool accepts `permanent`.
- web UI (Nodes): "Never expires" switch in the approve dialog, an
  Expires column, and a Make permanent / Set expiry action per device.
  ConfirmDialog takes optional children for the switch.

The default stays 30 days; permanence is an explicit operator choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:12:23 +04:00
minhdang03 1d68a21f65 fix(channels): archive group messages before the pending buffer drops them (#1514)
channel_pending_messages is a buffer, not an archive. Rows were deleted
outright on two paths: the bot being mentioned hands the buffer to the
agent and clears the key, and LLM compaction replaces old rows with a
summary. For group capture that buffer held the only copy of the raw
text, so a single mention or compaction pass destroyed days of messages
nothing had read yet.

Copy every row into channel_message_archive inside the same transaction
as the delete, tagged with the reason (consumed, compacted, stale).
Archived rows keep their original id, so a replayed delete is a no-op.
Add ListArchivedByKey so consumers that need full history read the
archive instead of the buffer.
2026-08-15 23:48:15 +07:00
bilogic c09a72a3bf notify channel of pairing approval (#1508)
* notify channel of pairing approval

* purge cache when revoked
2026-08-07 14:35:59 +07:00
bilogic dacdddbfa1 don't react with 💔 on telegram (#1492) 2026-08-02 08:30:05 +07:00
Clark Cant 9f719b023a Merge pull request #1464 from connermo/feat/outbound-dispatch-sharding
perf(channels): shard outbound dispatch per conversation
2026-08-01 04:32:20 +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
ntduc b49c6abcc7 Merge remote-tracking branch 'upstream/dev' into dev 2026-07-22 20:24:10 +07:00
ntduc 51a27af707 fix(feishu): wire writer stores for database channels 2026-07-22 17:57:58 +07:00
Conner Mo 9996a16f83 perf(channels): shard outbound dispatch per conversation
A single dispatch goroutine delivered every reply in the process — every
channel, every user — each Send blocking the next. One media upload of a
few seconds froze all outbound traffic for that long, and sustained
throughput was capped at one Send round-trip regardless of lane capacity.

The ordering that loop actually protected is per-conversation: a run emits
block replies, retry notices and a final answer to one ChatID, and those
must arrive in that order. A global order was never required.

Dispatch now shards on channel+ChatID. A conversation always maps to the
same shard and is delivered serially there, so the ordering guarantee is
unchanged, while unrelated conversations proceed in parallel.
GOCLAW_OUTBOUND_SHARDS tunes the worker count (default 8).

Temp media needed a real fix to go with it. Serial dispatch deduped it
implicitly — the first delivery removed the file, and a later message
carrying the same path found os.Stat failing and skipped it. Across
concurrent shards that check alone is a TOCTOU race, so claims are now
taken atomically and released after the send.

Also makes the pool limits that bound this path configurable, defaults
unchanged: GOCLAW_PG_MAX_OPEN_CONNS / GOCLAW_PG_MAX_IDLE_CONNS, and
GOCLAW_HTTP_MAX_IDLE_CONNS / GOCLAW_HTTP_MAX_IDLE_CONNS_PER_HOST. The
per-host limit binds when one provider takes nearly all traffic: every
request past it pays a fresh TCP+TLS handshake, which a deployment
raising LaneMain needs to be able to lift.
2026-07-22 14:14:02 +08:00
ntduc e4e4bb33fb fix(feishu): preserve group context without sender name 2026-07-20 15:05:54 +07:00
SYNITY 21ad48de16 fix(bitrix24): preserve whisper visibility on intermediate replies [B24:2794] (#1449)
fix(bitrix24): preserve whisper visibility on intermediate replies (#1449)
2026-07-18 10:51:24 +07:00
SYNITYandDangTinh311 d07dca440a feat(bitrix24): status-reaction support via imbot.v2 reactions [B24:2794] (#1446)
Bitrix24 was the only channel that never showed the agent's progress as an
emoji reaction on the user's message (Telegram/Slack/Feishu/Discord/... all do).
Two things were missing: the channel didn't implement channels.ReactionChannel,
and its inbound metadata omitted the generic "message_id" key the gateway
consumer reads to thread status/streaming events back to a message.

- handle.go: set meta["message_id"] = MESSAGE_ID (every other channel does this;
  without it the run has no message id to react to). Kept the existing
  bitrix_message_id key for reply-link routing.
- reactions.go (new): implement OnReactionEvent + ClearReaction. Map agent
  status → a Bitrix v2 reaction code (thinkingFace / fire / whiteHeavyCheckMark /
  crossMark / ...), debounce intermediate updates (terminal states bypass), and
  replace the previous reaction via imbot.v2.Chat.Message.Reaction.delete +
  .add (Bitrix has no in-place replace). REACTION_ALREADY_SET is treated as
  success. ReactionLevel off/minimal/full already existed in the channel config.
- channel.go: add the per-message reactions state map + compile-time assertion.

Tests: full flow (add → delete+add on status change), minimal skips
intermediate, off/invalid/unknown are no-ops, ClearReaction removes current.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-16 18:54:40 +07:00
SYNITYandDangTinh311 6cf48a6b08 feat(bitrix24): send pairing reply with code instead of silent drop [B24:2794] (#1445)
When an agent's DM/Group policy is "pairing (require code)", an unpaired
Bitrix24 sender got silence: the channel only had a Phase 03 stub
(logPairingNeeded) that logged "pairing required (Phase 07 will send pairing
reply)" and dropped the message — no code was ever generated or sent, so an
admin had nothing to approve. Bitrix24 was the only channel missing this;
Discord/Zalo/WhatsApp/Slack/Feishu already send a pairing reply.

Replace the stub with sendPairingReply, mirroring the other channels:
- RequestPairing() to generate the code (keyed by senderID for DMs,
  "group:<chatID>" for groups so it matches CheckGroupPolicy.IsPaired).
- Render the shared systemmessages.KeyPairingAccountRequired message
  (code + how to approve).
- Send it back into the chat via the v2 API (imbot.v2.Chat.Message.send),
  consistent with the welcome / OAuth-invite messages — not the legacy
  imbot.message.add v1.
- Keep the per-sender debounce so a spammy sender can't flood the chat.

Tests: send fires on the v2 endpoint with the code, no-op without a pairing
service, and the second call within the debounce window is suppressed.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-16 17:25:59 +07:00
SYNITYandDangTinh311 aa27354d16 fix(bitrix24): label quoted-media reply note from downloaded MIME [B24:2794] (#1444)
Follow-up to #1443. For a media-only quoted message, im.dialog.messages.get
frequently returns a blank type/name (observed live for a voice note), so the
reply note fell back to the generic "một tệp" even though the file was correctly
downloaded and tagged <media:audio>.

Build the note AFTER downloading and derive the kind label from the downloaded
file's MIME (reusing classifyMediaType), so it reads "một tin nhắn thoại" /
"một hình ảnh" / "một video" to match the <media:*> tag the agent receives.
Falls back to the Bitrix file type, then "tệp", when no download is available
(e.g. oversized). Also adopts the downloaded filename when messages.get omitted it.

Adds tests for the MIME-preference path and mediaKindVN.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-16 11:25:20 +07:00
SYNITYandDangTinh311 2a23ad0c42 feat(bitrix24): recognize reply/quote context across all message types [B24:2794] (#1443)
Bitrix24 ships the quoted message inline on reply events via REPLY_MESSAGE,
but the channel parsed a non-existent REPLY_TO_MESSAGE_ID field, so the agent
never saw what a user was replying to. Recognize replies and feed their
context to the agent (Hybrid design):

- events.go: parse data[PARAMS][REPLY_MESSAGE][ID|AUTHOR_ID|MESSAGE] (form+json)
  with PARAMS[REPLY_ID] fallback into EventParams.ReplyMessage; drop the dead
  ReplyToMID field + bitrix_reply_to_mid meta.
- reply_context.go (new): resolveReplyContext prepends a short note and, for
  media-only originals, re-injects the quoted file(s) through the same download
  pipeline as a direct upload.
    text quote  -> sanitize BBCode + truncate, no API call.
    media quote -> im.dialog.messages.get (works for TYPE=bot bots, unlike the
      blocked imbot.v2.Chat.Message.get) to recover the attachment, bounded by
      media_max_mb; note-only fallback on any fetch/download failure.
- handle.go: wire resolveReplyContext into message assembly (prefix + extra media).

Covers text, image, audio, file, video quotes. Non-reply events are a no-op.
Adds unit tests for parse (form+json), sanitize, file-id extraction, result
parsing, the messages.get fetch, and both resolve branches.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-16 10:56:32 +07:00
Syed Osama Ali Shah 6f91b937c1 fix(slack): don't double-emit trailing text on odd number of ``` fences (#1436)
extractCodeBlocks split on "```" and treated every odd-indexed part as a
closed code block. With an odd number of fences (>=3), Split returns an even
number of parts, so the final segment -- which follows an unpaired opening
fence and must stay literal -- landed on an odd index and was both emitted as
a \x00CB\x00 placeholder (later restored as ```seg```) AND re-appended
verbatim by the trailing len(parts)%2==0 block. The segment was therefore
duplicated in the rendered Slack message (and blocks gained a spurious entry).
Unclosed trailing fences are common in streamed LLM output.

Exclude the final unpaired part from the paired-scan loop. Add a regression
test covering the closed-block-then-unpaired-fence case.
2026-07-15 00:54:17 +07:00
DangTinh311 af43c952ed feat(bitrix24): agent activity indicator via InputAction.notify [B24:2794]
Show an ephemeral "agent is working" indicator (thinking/searching/
generating/analyzing…) in Bitrix24 chat while the agent processes, so
users on this non-streaming channel aren't left staring at silence
until the final reply. Send behavior is unchanged; no LLM call, no DB.

Generic layer (internal/channels):
- New optional ActivityIndicatorChannel interface.
- HandleAgentEvent routes run.started->THINKING, tool.call->mapped
  status, tool.result->ANALYZING; static tool->status mapping.
- Conditional heartbeat ticker fills LLM-inference gaps (re-sends only
  when idle), 5s per-run throttle caps call rate; ticker stopped on
  terminal events AND in UnregisterRun (safety net for missed terminals).

Bitrix24 (internal/channels/bitrix24):
- OnActivityEvent calls imbot.v2.Chat.InputAction.notify best-effort,
  drop-on-limit (raw Client.Call, no retry) so cosmetic notifies never
  steal leaky-bucket capacity from real message sends.
- Per-channel activity_indicator toggle (default on).

Web UI + docs: dashboard toggle, i18n en/vi/zh, channels doc section.
Tests: 26 unit tests incl. UnregisterRun-stops-ticker regression.
2026-07-14 15:28:14 +07:00
SYNITYandDangTinh311 4ff7c56338 fix(bitrix24): normalize ws/wss scheme in public URL detection (#1425)
Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-11 18:54:06 +07:00
SYNITY bfd86091c0 fix(whatsapp): add missing protobuf import breaking dev build [B24:2794] (#1426)
fix(whatsapp): add missing protobuf import breaking dev build (#1426)
2026-07-11 18:24:34 +07:00
Reski Rukmantiyo 5773eec167 feat: implement slash command handling and text feedback for WhatsApp channel (#819) 2026-07-11 15:26:12 +07:00
ナムandCollective Developer 6b738924d5 fix(discord): qualify group titles across surfaces (#1420)
Co-authored-by: Collective Developer <man@collective.dev>
2026-07-10 22:25:45 +07:00
SYNITY 641d4f8756 feat(bitrix24): ship per-user OAuth re-authorization flow (#1417)
feat(bitrix24): ship per-user OAuth re-authorization flow (#1417)
2026-07-10 10:56:45 +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
2b7bfec504 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>
2026-07-09 16:55:54 +07:00
ba621391c2 feat(telegram): trigger-words, channel posts, message edit & topic posting (#1383)
- Fix image_generation nil-pointer crash on codex agents: the sentinel now
  carries a name-only Function so the many sites reading td.Function.Name never
  nil-deref; codex_build still branches on Type.
- Agent-declared trigger words in IDENTITY.md wake the bot in groups without an
  @mention (whole-word, Cyrillic-aware; text + caption), cached per-agent 60s.
- channel_post support with a synthetic sender + a recover() guard so a
  malformed update can't crash the gateway.
- message tool action=edit (editMessageText + editMessageCaption fallback),
  targeting the replied-to message via reply_to_message_id.
- message tool topic=<name> posts into a named forum topic; topics learned from
  forum_topic_created into channel_contacts and resolved by name.

Co-authored-by: skensel <skensel@MacBook-Pro-skensel.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 12:28:18 +07:00
Duc Nguyen ee37017e0e fix: warm Zalo group-approval cache + attach quoted-image content tag (#1402)
* fix: warm Zalo group-approval cache so forwards from non-group origins route correctly

send.go picks ThreadTypeGroup vs ThreadTypeUser for a target chat ID by
checking BaseChannel's approvedGroups cache (or an explicit "group_id"
metadata key). That cache is only ever populated from INBOUND group
traffic under the "pairing" group policy — under "allowlist" (this
integration's actual default) it stays empty for the whole process
lifetime unless something else populates it, and it's wiped on every
restart regardless (in-memory sync.Map, no persistence).

message.go's cross-target forward only attaches "group_id" metadata when
isGroupContext(ctx) is true, which reflects the ORIGIN session's peer
kind, not the destination target's. Forwarding from a DM (or any non-group
session) into a group therefore sends with no group_id metadata and an
empty approval cache, so Send() defaults to ThreadTypeUser — the message
goes out addressed as if to a user account, not the group, and is never
seen there. No error is returned anywhere in this path, so nothing in the
existing (or previously fixed) error-reporting surfaces it.

ListGroups now marks every returned group ID as approved via
MarkGroupApproved, and Channel.Start kicks off a best-effort ListGroups
call right after connecting so the cache is warm from process start, not
just after an explicit zalo_list_groups tool call.

* fix: expose a media tag for quote-forwarded images, not just vision access

extractQuoteMedia downloads the image attached to a quoted message so the
model can see it via vision, but the composed content only ever carried
the literal "[Quoted image]" placeholder text (from TQuote.Text()) — no
<media:image> tag was ever inserted for it. The later agent-side
enrichment (enrichImageIDs/enrichImagePaths) only fills id/path attributes
into a bare tag it finds already present in content; with no tag to find,
the downloaded file had no path reference the model could hand back to
message(MEDIA:<path>) to forward it elsewhere, even though the file was
sitting on disk the whole time. Symptom: asked to forward a quoted image
to another chat, the agent reported "the image is only a quote, no direct
file to attach" despite genuinely having already downloaded it.

buildQuoteMediaTag renders the same bare <media:image> tag a direct
(non-quoted) attachment gets, appended after the quote wrapper text in all
three call sites (handleDM, handleGroupMessage,
extractContentAndMediaWithQuote) — ordered after any of the current
message's own attachment tags to match the media slice's append order, so
positional id/path enrichment lines up correctly.
2026-07-09 07:55:02 +07:00
Duc Nguyen aa15317d5b fix: resolve Zalo group by name + notify origin on failed forward (#1395)
* fix: resolve Zalo group name to real chat ID, notify origin on failed forward

Two related root causes behind "forward message to group by name" silently
failing while the agent reports success:

1. No tool let the agent resolve a group's display name (e.g. "Ban Dieu
   Hanh") to the chat ID the message tool actually requires. sessions_list
   only exposes session keys (numeric group IDs), never human-readable
   names, so the agent had no reliable way to turn a name into a real
   target and ended up passing the display name itself as `target`.

   Adds zalo_list_groups, wrapping the already-used (dashboard picker)
   protocol.FetchGroups behind the same optional-interface pattern as
   list_group_members/GroupMemberProvider (GroupListProvider on
   channels.Manager, gated to zalo_personal via RequiredChannelTypes).

2. When the resulting send fails downstream (e.g. Zalo rejects a bad
   chat_id), dispatchOutbound only ever retried/notified media failures on
   the same (already-broken) destination, and dropped text-only failures
   entirely — even though message.go's own postCrossTargetNotice comment
   states forwards must never announce a fake delivery. Because the bus
   publish is fire-and-forget, the tool had already returned "sent" and
   announced success to the origin chat before the real send was even
   attempted.

   message.go now tags cross-target forwards with origin channel/chat in
   OutboundMessage.Metadata; dispatchOutbound uses it to notify the ORIGIN
   chat with the real failure instead of silently dropping it or retrying
   against the same invalid target.

* test: cover forward-origin metadata tagging and dispatch failure notice

Extracts dispatchOutbound's error branch into handleSendFailure so it can
be unit tested without driving the consumer loop/goroutine, and adds
coverage for: forward failures notifying the origin chat (not the broken
destination), pre-existing non-forward media/text-only behavior staying
unchanged, message.go tagging cross-target group forwards with origin
metadata alongside group_id, and the new zalo_list_groups tool/Manager
delegator.
2026-07-08 23:23:08 +07:00
ナムandCollective Developer fc7d183961 Enhance passive memory extraction tuning (#1393)
* feat(channelmemory): add passive extraction tuning

* feat(ui): expose passive memory tuning controls

* docs(memory): document passive extraction tuning

---------

Co-authored-by: Collective Developer <man@collective.dev>
2026-07-08 20:53:19 +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
ナムandCollective Developer 7263f771c8 fix(channelmemory): honor Discord parent channel excludes (#1390)
* fix(channelmemory): harden passive extraction

* fix(channelmemory): honor discord parent channel excludes

---------

Co-authored-by: Collective Developer <man@collective.dev>
2026-07-08 13:23:16 +07:00
ナムandCollective Developer 2a082f4edf fix(discord): preserve channel agent routing (#1380)
Co-authored-by: Collective Developer <man@collective.dev>
2026-07-07 09:53:02 +07:00
ナムandCollective Developer f31214f5ce feat(channel-memory): enhance passive extraction for Discord (#1361)
* fix(channels): persist pending history under instance names

* feat(channel-memory): add run-all extraction workflow

* feat(discord): resolve passive memory channel titles

* feat(web): enhance passive memory controls

* docs(channel-memory): document extraction endpoints

---------

Co-authored-by: Collective Developer <man@collective.dev>
2026-07-06 11:51:07 +07:00
Duc Nguyen 150c2b3c01 fix(zalo): correctly forward inbound reference images to the agent (#1353)
fix(zalo): correctly forward inbound reference images to the agent

Fixes #1352.

Three compounding bugs prevented zalo_personal from using photos as vision/image-generation references:
1. Zalo CDN URLs often lack usable extensions, so downloads landed as .bin and were misclassified as documents (not images). fixDownloadedExt sniffs content and renames correctly.
2. Photo captions (user's actual instruction) were dropped — now appended after the media tag.
3. Quoted photos were never downloaded — extractQuoteMedia + ParseAttachment now fetch the actual image.

Tests cover fixDownloadedExt and ParseAttachment helpers. Live-verified against production Zalo traffic.
2026-07-05 01:57:25 +07:00
nguyenha935andGoClaw Operator e1fcccea4f feat: add configurable system messages (#1343)
Co-authored-by: GoClaw Operator <operator@goclaw>
2026-07-04 21:57:14 +07:00
nguyenha935andGoClaw Operator 4d3c6bcb05 feat: add Telegram manager channel permissions (#1339)
Co-authored-by: GoClaw Operator <operator@goclaw>
2026-07-04 00:25:00 +07:00
90e7035100 fix(mcp): tenant isolation, tool policy engine, and prompt preview fixes (#1333)
* fix(prompt): remove duplicate Team Members section from system prompt

The TEAM.md context file already provides the Members section with better
formatting. The system prompt section was redundant and inconsistently
formatted.

- Remove buildTeamMembersSection() call from system prompt building
- Remove unused buildTeamMembersSection() function

* fix(mcp): wire store to manager for prompt preview tool visibility

MCP manager needs database access to query configured servers for tool visibility
in prompt preview.

- Add SetStore() method to MCP manager
- Wire pgStores.MCP to manager after initialization
- Add debug logging for MCP initialization flow

* fix(systemprompt): hide Tooling section when agent has no tools

The Tooling section header and boilerplate were displayed even when the
agent had no tools available. Add early return in buildToolingSection()
to skip the section entirely when toolNames is empty.

This reduces prompt noise for agents with no tool access.

* feat(mcp): cache tool descriptions for prompt preview visibility

MCP tools now show descriptions in prompt preview without requiring a live
server connection.

- Add CacheToolDescriptions() method to MCPServerStore (PG + SQLite)
- Cache tool descriptions in settings['tool_cache'] when server connects
- Use cached descriptions in ListToolsForAgent (fallback: hints → cache → global)
- Descriptions are auto-populated from live server manifest on connection
- Admins can still override via tool_hints in server settings

* test(mcp): add CacheToolDescriptions to MCPServerStore test fakes

Commit 5ce410b6 added CacheToolDescriptions() to the MCPServerStore
interface but missed updating test mock implementations, breaking
go vet across internal/mcp, internal/agent, internal/http, and
internal/channels/bitrix24.

Add no-op implementations matching each fake's existing style.

* fix(providers): enforce per-agent tool policy for Claude CLI provider

The Claude CLI provider (stdio+MCP bridge) was not enforcing per-agent
tool policy, unlike other providers where the policy-filtered tool list
already drives the system prompt's Tooling section.

Two gaps closed:

1. --disallowedTools was previously skipped entirely when no MCP config
   path was resolved, letting the CLI subprocess run with its full
   native toolset (Bash, Edit, Read, Write, Glob, Grep, WebFetch,
   WebSearch) regardless of agent policy. It's now unconditional and
   derived from the agent's actual allowed-tools list (state.Tool.AllowedTools),
   mapped to Claude CLI's native tool names.

2. The MCP bridge server executed any tool call without checking the
   calling agent's policy. It now resolves the agent's policy from
   context (via the existing HMAC-verified agent lookup) and denies
   calls to tools outside that agent's allowed set, logging
   security.mcp_bridge_denied on denial.

Both gaps were closed using existing plumbing (PolicyEngine.WouldAllow,
AgentData.ParseToolsConfig, the bridge context middleware) — no new
cross-cutting mechanism was introduced.

* feat(web): show MCP/tool schemas in system prompt preview dialog

The prompt-preview API response includes a separate `tools` field
(the actual JSON schemas sent to the LLM as the tools API parameter)
alongside `prompt` (the system prompt text), but the web UI only
rendered `prompt`, silently dropping the tools list.

Add a collapsible Tools section to both the full-screen System Prompt
dialog and the inline agent-detail preview, showing tool count, name,
description, and expandable parameter schema per tool. i18n keys added
to en/vi/zh locales.

* fix(prompt): render pinned skills on bootstrap turns

Pinned skills are documented (web UI copy) as "always inlined in the
system prompt", but the entire Skills section was gated behind
!cfg.IsBootstrap, so pinned skill XML never appeared on bootstrap
turns (first message of a session) despite the promise.

Separate pinned-skill rendering from bootstrap-suppressed guidance:
- Bootstrap + pinned skills present: render pinned XML only, no
  search/manage guidance (which stays suppressed as before)
- Non-bootstrap: unchanged behavior
- Minimal/none modes: pinned skills always render regardless of
  bootstrap state

Add regression tests covering all four prompt modes on bootstrap
turns, plus a non-bootstrap guard confirming existing behavior is
preserved.

* fix(skills): resolve managed skills directory per-tenant, not master-only

skills.Loader was wired at startup to scan a single fixed directory
(the master tenant's managed-skills dir), making any skill belonging
to a non-master tenant invisible to both pinned-skills prompt
resolution and skill_search/use_skill, regardless of DB visibility
settings.

- Loader now resolves the calling tenant's managed-skills directory
  per-call via context (store.TenantIDFromContext), never enumerating
  other tenants' directories
- Skill cache is now tenant-keyed to prevent slug collisions and
  cross-tenant cache leaks across tenants using the same skill slug
- gateway_setup.go passes the root data dir instead of a pre-resolved
  master-tenant path

Write-side tooling (skill_manage, publish_skill) was already correctly
tenant-scoped per-operation — no changes needed there.

Added TestLoader_ManagedSkills_TenantIsolation proving two tenants
with same-slug/different-content skills never see each other's
content, including after cache population from a different tenant's
lookup.

Known follow-up (not in this commit): skill_search's BM25 index is
still a single process-global index shared across tenants, which is
a related but separate cross-tenant search-result leak requiring its
own scoped fix (per-tenant index maps + threading tenant context
through ensureIndex/rebuildIndex).

* fix(tools): scope skill_search BM25 index per-tenant

SkillSearchTool held a single process-global BM25 index built once
from whichever tenant's context first triggered ensureIndex, then
reused for all subsequent Execute() calls regardless of caller —
leaking one tenant's skill search results into another's, the
search-path counterpart to the managed-directory bug fixed in
7b4668ad.

- index/lastVersion are now keyed per-tenant (map[uuid.UUID]*tenantIndexState)
- ensureIndex resolves the calling tenant from context and only
  builds/reads that tenant's index entry, never touching another
  tenant's cached state
- Builtin/bundled skills remain visible in every tenant's index
  (Loader already merges those tiers correctly per 7b4668ad)

Loader.Version() remains a single global counter — a version bump in
one tenant causes unnecessary rebuilds in others but does not cause
cross-tenant leakage, an acceptable tradeoff to avoid scope creep.

Added TestSkillSearchTool_TenantIsolation proving two tenants with
same-slug/different-content skills never see each other's search
results, including after cache population from a different tenant.

* fix(tools): fix group-spec expansion in tool policy engine

PolicyEngine.registry was only ever set via SetRegistry(), which was
never called in production (only in one test) — so pe.registry was
permanently nil in production. Every group-expansion helper
(applyProfile, intersectWithSpec, unionWithSpec, subtractSpec,
expandSpec, matchDenySpec, filterByCapability) silently dropped any
"group:*" spec entry instead of expanding it when registry was nil.

Concretely: "group:mcp" (auto-injected into agentToolPolicy.AlsoAllow
for any agent with MCP tools) never resolved to real tool names, so
MCP tools connected successfully and appeared in prompt text (which
reads the registry directly, bypassing PolicyEngine) but were never
included in the actual ChatRequest.Tools payload sent to the LLM —
confirmed live via mcp.agent.tools_loaded tools=6 immediately followed
by mcp.filtered_tools mcp_defs_count=0 in the same request. This
affects any agent relying on group-based grants, not just MCP.

PolicyEngine is a shared/global singleton used concurrently across
all agents (constructed once at gateway startup), so mutating a
registry field per-call would be a data race. Fix instead threads the
registry as an explicit parameter from FilterTools down through all
internal group-expansion helpers, and adds IsDenied/WouldAllow
registry parameters, removing the dead SetRegistry() mechanism
entirely.

Also fixes group expansion for the per-user-MCP-tools path: FilterTools
is sometimes called with a userToolOverlay wrapping a *Registry rather
than a *Registry directly; added Unwrap() to userToolOverlay so the
new registry-resolution logic works for both cases.

Added 4 tests proving group expansion works via the threaded parameter
alone (no SetRegistry): plain registry allow, userToolOverlay allow,
deny-side group expansion, and the WouldAllow bridge-server path.

Blast radius note: this restores intended access for every agent
configured with group:* specs (group:mcp, group:vault, group:goclaw,
group:coding, etc.) that were silently inert before. Existing agent
configs relying on group grants will gain the tool access they were
nominally already configured for.

* fix(mcp): cache tool descriptions from pool-connected servers too

5ce410b6 added tool-description caching (for prompt-preview visibility)
only inside connectServer. connectViaPool — the separate connect path
used when MCP connections go through the shared pool — never got the
same caching hook, even though it shares the same underlying
connectAndDiscover wire handshake.

Confirmed live: cloudflare/docker connected via connectViaPool and
received real descriptions over the wire, but prompt preview still
showed blank descriptions because this path never wrote to the cache.

Also removes the temporary mcp.connect.raw_tool debug log added
earlier this session for diagnosing the same issue — no longer needed
now that the root cause is fixed.

* chore(skills): remove temporary pinned-skills diagnostic logging

Confirmed live: pinned skills (caveman, infra-ansible-knowledge) now
resolve correctly end-to-end for tenant-scoped agents. Debug logging
added to trace the resolution chain is no longer needed.

* fix(tools): deny always wins over AlsoAllow group grants

AlsoAllow's unionWithSpec could reintroduce a tool explicitly listed
in Deny, since it added tools back from allTools without re-checking
deny specs. Previously masked because AlsoAllow's group-expansion was
also broken (fixed in f7af95de this session) — group specs silently
expanded to nothing, so this ordering bug never manifested. Now that
group expansion works, an admin-denied tool that's also reachable via
a group:* AlsoAllow entry (e.g. group:mcp) would silently reappear.

Re-apply deny-spec subtraction as a final step after AlsoAllow union,
for both global and per-agent policy, so deny always wins regardless
of which allow mechanism tries to add a tool back.

Added tests proving global and per-agent Deny correctly override an
overlapping AlsoAllow group grant, while sibling non-denied tools in
the same group remain allowed.

* fix(mcp): enumerate cached tools instead of wildcard placeholder in prompt preview

ListToolsForAgent (the prompt-preview path) collapsed any server with
an empty ToolAllow (unrestricted grant — the common case) into a
single "server__*" placeholder entry, even when tool_cache already
had every real tool name and description from connect time (5ce410b6,
8ffd67b9). This meant agents with unrestricted MCP server access never
saw individual tool names or descriptions in prompt preview, forcing
trial-and-error tool usage.

When ToolAllow is empty and tool_cache is populated, enumerate every
cached tool (skipping any explicitly denied) and emit one
MCPToolPreviewInfo per tool, matching the construction logic already
used for the ToolAllow-non-empty case. Falls back to the single
placeholder only when tool_cache is also empty (server never
connected).

The live (non-preview) conversation path, buildMCPToolDescs, does not
have this bug — it resolves tool identity from the live connected
registry, never from ToolAllow, so no placeholder shortcut exists
there.

Added tests covering both the cache-populated enumeration case and
the no-cache placeholder fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(prompt): filter alias re-injection and add MCP schemas to preview ToolDefs

Prompt-preview's tools: schema array (PreviewResult.ToolDefs) had two
bugs, both preview-only — confirmed live conversations already use
correctly-filtered tool payloads via buildToolsPayload/PolicyEngine.FilterTools,
unaffected by either bug:

1. Alias re-injection iterated ALL registry aliases globally with no
   check against the deny-filtered toolNames list, letting denied
   tools reappear via their alias name (e.g. a denied canonical tool
   still showing up as its Claude-Code-compat alias like Bash/Edit/Write/Read).
   Now skips any alias whose canonical tool isn't in the filtered set.

2. MCP tool descriptions (from the store-based, connection-free
   ListToolsForAgent path) only ever fed the prompt TEXT section,
   never got converted into ToolDefinition schema objects — so the
   tools: array never showed MCP tools at all, even when the MCP
   section of the prompt text correctly listed them. Now appends a
   ToolDefinition per MCP tool with name+description populated and a
   placeholder {"type":"object"} Parameters schema, documented as
   preview-only (real parameter schemas require a live MCP connection,
   only available during an actual conversation turn).

Added tests proving denied-tool-alias exclusion and MCP tool inclusion
in preview ToolDefs.

* feat(skills): inline full content for pinned skills instead of pointer-only

Web UI documented "Pinned skills are always inlined in the system
prompt", but BuildPinnedSummary was just BuildSummary with an
allowlist filter — same as the general searchable skill list:
name/description(truncated)/location pointer only, requiring
use_skill+read_file round trips to get actual content. No code path
inlined real SKILL.md content for pinned skills specifically.

BuildPinnedSummary now reads and inlines full SKILL.md content
(frontmatter stripped) per pinned skill inside <skill_instructions>
tags. Per-skill (10000 bytes) and total (30000 bytes) size caps fall
back to the original pointer-only format with a note when a skill is
too large to inline, so oversized skills degrade gracefully instead
of blowing the prompt budget.

Added tests: full-content inlining, size-cap fallback, and tenant
isolation for the new inline path (mirroring the existing managed-
skills tenant isolation test).

* fix(mcp): real parameter schemas and full policy enforcement in prompt preview

Three interconnected fixes to prompt preview, none affecting the live
conversation path (which was already correct):

1. Real MCP parameter schemas instead of a useless empty placeholder.
   tool_cache previously stored only name+description; extended to
   also capture the real JSON Schema from the MCP server's tools/list
   response (CachedToolInfo{Description, Parameters}) at connect time,
   for both direct-connect and pool-connect paths. Preview now shows
   complete, real input schemas instead of {"type":"object"} with no
   properties — verified against actual wire data from a live
   cloudflare MCP server showing genuinely rich schemas (zone_id,
   type, name, content, ttl, proxied, all typed with descriptions and
   correct required arrays) that were previously being discarded.
   Backward-compatible: old-shape cache entries degrade gracefully to
   description-only rather than crashing, self-healing on next
   connect.

2. Global tool deny now enforced in preview. BuildPreviewPrompt
   previously hand-rolled a partial policy reimplementation
   (per-agent deny only, explicitly skipping the full PolicyEngine
   "because runtime state isn't available in preview") — but
   PolicyEngine.WouldAllow already handles this per-tool-name without
   needing channel context. A tool denied via the global config (not
   per-agent) would appear in preview despite being correctly denied
   in every real conversation. Preview now calls WouldAllow per
   candidate tool, with a graceful per-agent-deny-only fallback when
   no PolicyEngine is wired (e.g. in tests).

3. MCP tools now also subject to the same policy check — previously
   the MCP tool supplement (store-based, connection-free tool listing)
   added MCP tool names unconditionally, bypassing WouldAllow entirely,
   so a denied MCP tool could still appear in preview.

Added tests for all three: real-schema presence, global-deny exclusion
for both core and MCP tools, and backward-compat cache handling.

* fix(prompt): remove redundant per-tool MCP enumeration from prompt text

Now that MCP tool schemas in the tools: API parameter are real and
complete (1290d4f1), the ## MCP Tools prompt-text section's per-tool
"- mcp_x__y: description" enumeration is pure duplication with zero
added value — the model already gets each tool's real schema
(including description) via tools:.

buildMCPToolsInlineSection now keeps only the behavioral instructions
that aren't expressible via JSON schema and thus aren't duplicated:
prefer-MCP-over-core-tools guidance, and the optional-parameter
guidance (don't guess/fill optional fields). The per-tool name+
description enumeration loop is removed. Section still only appears
when the agent has MCP tools (len(cfg.MCPToolDescs) > 0, unchanged
gate).

Updated tests to assert the enumeration is gone while the behavioral
instructions remain; ToolDefs assertions are now the authoritative
check for MCP tool allow/deny filtering behavior (prompt text no
longer enumerates names at all).

* test(agent): update TeamContextInjection test for removed Team Members section

TestBuildSystemPrompt_TeamContextInjection asserted the presence of a
'Team Members' prompt-text section that was intentionally removed in
71d33180 (duplicate of the canonical TEAM.md-context-file Members
section, which has better formatting). The test was never updated to
match, causing it to fail on every run since. Moved the assertion
from wantIn to wantNotIn for the 3 affected subtests -- BuildSystemPrompt
correctly no longer renders team-member roster info directly; that
info now comes exclusively from the TEAM.md context-file mechanism,
outside this test's isolated scope.

* fix(prompt): resolve real registry for WouldAllow calls in preview

BuildPreviewPrompt's two WouldAllow calls hardcoded reg=nil, silently
breaking group:* expansion (e.g. group:mcp) needed to resolve the
AlsoAllow grant production actually uses to grant MCP tool access
(resolver_helpers.go's agentToolPolicyWithMCP injects
AlsoAllow: ["group:mcp"]). With reg=nil, WouldAllow could match
literal tool names fine (the 18 core/static tools) but could never
resolve group-based grants, so every MCP tool silently failed
WouldAllow and was excluded from preview -- confirmed live via curl:
18 tools returned, zero mcp_* ones, for an agent with genuinely
working MCP access in real conversations.

Live conversations were never affected -- internal/mcp/bridge_server.go's
WouldAllow call already correctly passes a real registry.

Fix resolves a real *tools.Registry from deps.ToolLister via
tools.ResolveConcreteRegistry (the same helper used at the live call
site), passing it to both WouldAllow calls instead of nil. Falls back
to nil gracefully for test mocks that don't implement the full
ToolExecutor interface, preserving existing test behavior.

Added a test proving an MCP tool granted via the exact production
AlsoAllow: ["group:mcp"] pattern is now correctly included in preview
ToolDefs, where the old reg=nil bug would have silently excluded it.

* fix(prompt): use literal deny check for MCP tools in preview, not group expansion

The MCP-tools policy gate added in 1290d4f1 called WouldAllow with a
real registry (per 9b5fd2eb), which requires group:mcp expansion
against that registry to grant access via the production
AlsoAllow: ["group:mcp"] pattern. But MCP tools are only ever
registered into ephemeral per-agent registry clones at live connection
time (manager_connect.go) -- never into the shared/global registry
preview uses. group:mcp always resolved empty in preview's
connection-free context, so WouldAllow denied every MCP tool --
confirmed live via tool-name diff: live conversations correctly
included all 6 MCP tools, preview included zero.

MCP access-granting is already correctly handled by
ListToolsForAgent's own per-server tool_allow/tool_deny grant logic
(confirmed working correctly earlier this session). The preview gate
only needs to catch the narrower case of a literally-denied tool name
via global/per-agent policy config -- it never needed group
expansion. Replaced WouldAllow with IsDenied(nil, name, agentPolicy),
which forces a pure literal-name match with zero registry dependency,
matching the existing usage pattern already established elsewhere in
policy.go. This class of bug cannot recur: there's no registry-passing
code path left in this check to silently reintroduce group-expansion
dependence.

Added a test proving MCP tool inclusion in preview is independent of
group-expansion outcome (no AlsoAllow: group:mcp needed for a
non-denied tool to appear).

Also reverts the temporary loop.filtered_tool_names/
preview_prompt.filtered_tool_names diagnostic logging used to capture
the live-vs-preview tool-name comparison that diagnosed this bug.

* fix(http): preserve real MCP parameter schemas through HTTP preview adapter

mcpPreviewAdapter.ListToolsForAgent (the HTTP-layer glue converting
mcp.MCPToolPreviewInfo to agent.MCPToolPreviewInfo for BuildPreviewPrompt)
only copied RegisteredName and Description, silently dropping
Parameters -- a bug present since this adapter was introduced
(2499d0be/7e250244), unrelated to today's other MCP preview fixes.

This was masked until d5fc6344 fixed MCP tools being excluded from
preview entirely (a separate bug) -- once MCP tools started appearing
again, this pre-existing adapter gap became visible: tools showed up
correctly, but always with the bare {"type":"object"} placeholder
instead of their real cached schema (confirmed live: update_dns_record
missing its 7 real properties).

One-line fix: copy Parameters through in the adapter's struct literal.

Added a regression test constructing a real *mcp.Manager with
populated tool_cache, asserting the adapter's output preserves
specific real schema properties (not just non-nil Parameters) --
verified this test fails without the fix and passes with it.

---------

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 08:25:05 +07:00
Duc Nguyenandntduc c7652bce4d fix(zalo): preserve root quote in reply chains (#1331)
Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-07-03 06:53:46 +07:00
Duc Nguyenandntduc fb4dadce8b fix(zalo): preserve nested reply context (#1326)
Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-07-02 21:56:17 +07:00
SYNITYandDangTinh311 3f5e4f3cc3 feat(bitrix24): decode chat entity context into metadata [B24:2794] (#1315)
Give agents direct access to the CRM entity, Openline session, task,
and workgroup binding of an inbound chat without paying an extra RPC
per turn. Everything is derived from CHAT_ENTITY_* fields Bitrix
already ships in the ONIMBOTMESSAGEADD webhook.

New metadata keys emitted alongside the existing bitrix_chat_entity_*
pair (only present when non-empty, so DMs / plain groups pay no cost):

Every chat with a title/type:
  bitrix_chat_title, bitrix_chat_type

Openline (ChatEntityType=LINES), decoded from ENTITY_ID + DATA_1:
  bitrix_ol_connector_code   (facebook / synity_zalo_oa_chat / ...)
  bitrix_ol_line_id
  bitrix_ol_external_uid
  bitrix_ol_bitrix_user_id
  bitrix_ol_line_config_id
  bitrix_ol_session_started_at
  bitrix_ol_active_crm_type / bitrix_ol_active_crm_id

CRM linkage, decoded from CHAT_ENTITY_DATA_2 for Openline
(schema "LEAD|<id>|COMPANY|<id>|CONTACT|<id>|DEAL|<id>") or from
ENTITY_ID for native CRM-integrated chats ("CONTACT|1780"):
  bitrix_crm_lead_id / company_id / contact_id / deal_id
  bitrix_crm_type + bitrix_crm_id

Single-token ENTITY_ID cases:
  bitrix_task_id      (TASKS_TASK)
  bitrix_workgroup_id (SONET_GROUP)
  bitrix_mail_id      (MAIL)

Parsing is best-effort: malformed input (short ENTITY_ID, odd DATA_1
token count) logs a WARN and falls through to a partial map rather
than crashing the handler. Unknown ChatEntityType values pass raw
without noise so future surfaces (CALENDAR, LIVECHAT, ...) don't spam
logs before the parser knows how to decode them.

Verified live against tamgiac.bitrix24.com with three connectors
(Facebook, Zalo OA, Zalo personal) plus CRM/Task/Workgroup/Mail
sample events. Table-driven test covers 13 shapes including malformed
input and the NONE/0 edge cases.

Surface parity:
  - Gateway server: parser + wire in handle.go
  - API contract: N/A because the change adds string entries to the
    already-existing metadata map without a schema.
  - Web UI: N/A because the metadata is consumed by agents, not surfaced.
  - CLI/runtime: N/A because no CLI command reads these keys.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-01 15:29:00 +07:00
Duc Nguyenandntduc c02fb660bc fix(channels): honor paired DM policy before publish (#1313)
Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-07-01 10:24:48 +07:00
Aaron Vu a62980bf7c fix(telegram): honor per-group/topic overrides for DB instances
The dashboard persists per-group/per-topic overrides (enabled,
require_mention, skills, tools, system_prompt, ...) into the channel
instance config, but telegramInstanceConfig had no groups field, so the
payload was dropped on unmarshal. config.TelegramConfig.Groups stayed
nil, resolveTopicConfig fell back to global defaults, and the bot
replied in every forum topic regardless of the configured per-topic
gating.

Map the groups field through the DB-instance factory so per-topic
config takes effect for dashboard-created channels, matching the static
file-config path.
2026-06-29 23:23:02 +07:00
SYNITYandDangTinh311 23a4b7407c fix(bitrix24): OpenLine greeting and MCP provisioning (#1284)
* fix(bitrix24): skip join greeting in open channels [B24:2794]

* wip: rework bitrix24 mcp provisioning + skills prompt section (tam thoi, se lam lai) [B24:2794]

---------

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-06-29 08:54:00 +07:00
nguyenha935andGoClaw Operator 22efff8c0d fix: filter streamed thinking tags (#1300)
Co-authored-by: GoClaw Operator <operator@goclaw>
2026-06-29 03:54:20 +07:00
SYNITYandDangTinh311 4675aa3fd4 fix(bitrix24): skip native-group mention gate for Open Channel sessions [B24:2794] (#1296)
Follow-up to #1295. After scoping the Open Channel mention gate by
connector, customer messages on 1-to-1 connectors (Facebook Messenger,
Zalo OA, …) were STILL being silently dropped — by the second
mention-gate stanza further down in handleMessage, which checks the
channel-level c.RequireMention() flag without distinguishing Open
Channel traffic from native group chats (CRM deal/task chats).

For a channel like nguyen-dao-openline configured with RequireMention=true,
the flow was:

  1. isOpenChannel block → shouldRequireMentionForOpenline("facebook|...")
     returns false (facebook isn't in the whitelist) → no drop.
  2. isGroup block (isOpenChannel implies isGroup) → c.RequireMention()=true
     AND mentioned=false → DROP.

Net effect: the gate-by-connector decision was overridden one stanza
later, and every Facebook customer turn disappeared.

Fix: scope the drop check inside the isGroup block to native-group chats
only (!isOpenChannel). Open Channel sessions already own their mention
policy in the earlier isOpenChannel block; re-applying the channel-level
RequireMention flag here second-guesses that decision.

Importantly, the text-processing that follows the drop check
(MessageOriginal fallback, stripMention, bxConvertUserMentionsToReadable)
stays unconditional, so an Open Channel STAFF turn that does come in with
"[USER=<botID>]Bot[/USER]" BBCode still gets cleaned up the same way it
always did. The only thing the new guard suppresses is the drop itself.

Verified live: Facebook Messenger customer turn (IS_CONNECTOR=Y,
CHAT_ENTITY_ID=facebook|34|…) now reaches the agent loop and the bot
replies, matching the intent from #1295.

Surface parity: backend-only. No API contract / web UI / CLI change.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-06-28 13:54:19 +07:00
SYNITYandDangTinh311 164ea2ea4a feat(bitrix24): scope Open Channel mention gate to group-style connectors only [B24:2794] (#1295)
Before: every Open Channel message required a bot @mention before the agent
picked it up — including external-customer turns on 1-to-1 connectors
(Facebook Messenger, Zalo OA, …) where the customer has no syntactic way
to produce a mention. The gate, designed for group-style connectors
(Zalo personal pools multiple customers into one Open Channel chat),
was silently dropping every legitimate customer message on every other
connector.

This change scopes the gate by connector:

- Internal staff (IS_CONNECTOR=N) — STILL gate. Operators can @-mention
  and we don't want the bot jumping into operator-to-operator chatter
  in the same session.

- External customer (IS_CONNECTOR=Y) on a whitelisted connector
  (default: synity_zalo_personal) — STILL gate. Multiple customers
  share the chat, so untagged turns would otherwise spam them.

- External customer on any other connector (Facebook, Zalo OA, the long
  tail of 1-to-1 connectors, legacy payloads without CHAT_ENTITY_ID) —
  do NOT gate. Every message is addressed to the bot by construction.

The whitelist is overridable via BITRIX24_REQUIRE_MENTION_CONNECTORS
(comma-separated connector codes — the leading token of CHAT_ENTITY_ID).
Default keeps synity_zalo_personal gated so a fresh deployment is safe.
Adding a future group-style connector (e.g. a Zalo group flavour) is an
env edit + restart, not a code change.

- mention_gate.go (new):
    connectorCodeFromEntityID() parses "synity_zalo_personal|20|grp|960"
        into "synity_zalo_personal".
    requireMentionConnectorSet() reads the env once (sync.Once cache)
        and falls back to the hardcoded default {synity_zalo_personal}.
    shouldRequireMentionForOpenline() is the policy matrix.
- mention_gate_test.go (new): pins the parser, the env-override path,
    and every row of the policy matrix.
- handle.go: replaces the unconditional Open Channel mention gate with
    a call to shouldRequireMentionForOpenline(). Adds chat_entity_id to
    the drop log so operators can see which connector tripped the gate.
- handle_test.go:
    Update existing "ConnectorWithoutMentionDropped" test to set
    ChatEntityID to the whitelisted connector — that's what the gate now
    keys on.
    Add new "OneToOneConnectorWithoutMentionForwarded" test pinning the
    new behavior: facebook (not whitelisted) forwards every customer
    message without a mention.

Surface parity: backend-only. No API contract / web UI / CLI change.
The env var follows the same operational model as the other BITRIX24_*
vars in docker-compose.yml; documenting it there is a no-op until the
default needs overriding.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-06-28 11:54:57 +07:00
SYNITYandDangTinh311 abe80de946 feat(bitrix24): inject <media:*> tags into user message so the LLM sees attachments [B24:2794] (#1294)
Without these tags the Bitrix-borne PDF / audio / image arrive as bare text
to the agent loop: the LLM never realizes an attachment exists and degrades
to "no file attached" replies (or worse, hallucinates a CRM document id and
fires crm.documentgenerator at it). The other channels (Telegram, Slack,
Discord) all prepend <media:*> tags before publish via the shared
media.BuildMediaTags helper — Bitrix24 was the lone holdout.

The agent loop's enrichInputMedia REPLACES tags it finds (it does NOT
insert new ones), so adding the tag at the channel boundary is the fix
that aligns Bitrix with the other channels AND unlocks enrich's
read_document / read_audio / read_image routing downstream.

- media_tags.go: mediaFilesToInfos() adapter from bus.MediaFile to
  media.MediaInfo + classifyMediaType() MIME prefix → media.Type* mapping.
  Unknown MIMEs fall back to TypeDocument (read_document handles generic
  binaries safely; a bogus audio tag on a non-audio file would mislead the
  LLM). Bitrix ships voice notes as audio/* — they're classified as
  TypeAudio (read_audio handles both audio and voice identically).
- handle.go: prepend BuildMediaTags(...) output to text right before
  HandleMessageMedia. No-op when no media files.
- media_tags_test.go: pins the MIME map, the conversion, and the
  end-to-end tag shape through BuildMediaTags.

Surface parity: backend-only. No API / web UI / CLI impact.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-06-28 10:25:22 +07:00
Duc Nguyenandntduc d460c90403 fix(feishu): gate group pairing by target bot (#1291)
* fix(feishu): require exact bot_open_id match for mention detection

- Previously, when bot_open_id was empty, ALL mentions were treated as bot mentions
- This caused multiple agents in same group to all respond to any mention
- Now requires bot_open_id to be set AND match exactly for mentionedBot=true
- Fixes issue where CPPAI PM would respond even when other agent was mentioned

* fix(feishu): gate group pairing by target bot

---------

Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-06-28 00:25:49 +07:00