Commit Graph
1996 Commits
Author SHA1 Message Date
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
nguyenha935andnguyenha935 496b7ffce6 fix(providers): raise ResponseHeaderTimeout so reasoning models can answer (#1483)
A reasoning model emits nothing at all while thinking, so the wait for the
response's FIRST byte — not any streaming timeout — decides whether its answer
is reachable. At 180s a large-prompt reasoning request routinely lost that race:
the gateway reported "http2: timeout awaiting response headers" while the
router's own usage log showed the same request completing normally moments later
(promptTokens=77194 -> completionTokens=2789). The request was healthy; only the
deadline was short.

Raise it to 300s. The value is deliberately modest rather than generous because
it is multiplied twice downstream: RetryDo wraps every provider call with
Attempts: 3, and a workflow step requeues up to MaxTaskDispatches times on top
of that, so 300s is already ~45min of worst-case wall clock per step.

Removing the timeout entirely was considered and rejected. Agent run contexts
come from scheduler queue.go via context.WithCancel, so they carry no deadline —
this transport timeout is the only thing bounding a request that never answers.
Without it a silently dead connection would hold its scheduler lane until the OS
TCP keepalive expired, and workflow requeue could not rescue it because requeue
only fires when a run ends. The doc comment claiming the design could "rely on
ctx deadlines" was wrong and is corrected here.

Tests lock the sizing from both sides (must exceed the latency that failed, must
not exceed the ceiling the retry math was sized against) and assert the retry
multiplier itself has not drifted.

Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
2026-07-29 05:00:29 +07:00
nguyenha935andnguyenha935 2c7903e7ea fix(agent): make the extractive memory fallback work on non-English sessions (#1484)
The extractive fallback runs when an LLM memory flush fails, so it is the last
thing standing between a session and losing its accumulated decisions. Its cue
patterns were English-only ("decided to", "prefer", "the API is"), which makes it
effectively inert for a session conducted in another language: it still returns a
non-empty result, so nothing looks broken, but the result is noise.

Measured on 195k characters of real Vietnamese session text: 9 decision matches
(all of them stray English fragments), 0 preferences, 0 technical facts — about
97% of what it "saved" was incidental file paths. After adding Vietnamese cues the
same corpus yields 21 decisions and 6 preferences on the largest session.

Adds parallel Vietnamese patterns for the same three categories and merges them
into the existing results with deduplication, so English extraction is unchanged
and mixed-language sessions get both. Tests cover Vietnamese-only,
English-only, and mixed histories, plus a guard that ordinary Vietnamese
narration is not misread as a decision or a preference.

Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
2026-07-29 03:30:42 +07:00
bilogic e71d6a5519 in groups, resolve group credentials first, then sender credentials (#1482) 2026-07-28 14:31:14 +07:00
nguyenha935andnguyenha935 31f973477d test(pipeline): lock mid-loop compaction pressure flag end-to-end (#1479)
FinalizeStage forwards state.Prune.MidLoopCompacted into MaybeSummarize so a
mid-loop compaction gets persisted (TruncateHistory + IncrementCompaction)
instead of being thrown away and redone next turn. Nothing asserted the flag
value: TestFinalizeStage_MaybeSummarize_Called binds it as `_ bool`, so
hardcoding `false` at the call site keeps the whole suite green while silently
restoring the per-turn re-compaction loop and stalling the cumulative
compaction count episodic depends on.

Adds two cases driving the real NewDefaultPipeline().Run():
- a history large enough to force prune-stage compaction must reach
  MaybeSummarize with midLoopCompacted=true
- a small history must compact nothing and report false, so the flag cannot
  simply be pinned true

Mutation-verified: with the finalize call site hardcoded to false the new test
fails while the pre-existing pipeline and agent suites still pass.

Test-only change.

Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
2026-07-25 15:02:08 +07:00
Clark Cant 93e1331c57 Merge pull request #1478 from nguyenha935/fix/context-compaction-episodic
fix: persist mid-loop compaction to stop re-compaction loop and revive episodic memory
2026-07-25 03:03:18 +07:00
Clark Cant 5ca433b8ac fix: persist mid-loop compaction to stop re-compaction loop and revive episodic
The v3 pipeline compacts session history mid-loop (prune_stage +
final-request guard) but only mutates the run's message buffer, never the
session store. Each turn reloads full history and re-compacts from scratch:
message_tokens climb 129k->156k across turns while every turn compacts back
down to ~60k. The lossy compaction differs per run, degrading the agent.

The same missing persistence stalls episodic memory: the cumulative
compaction count never advances, so the episodic worker's idempotency key
(sessionKey:count) is pinned and every cycle after the first is skipped.
Observed on live traffic: 8 run.completed since deploy, 0 new episodic.

Fixes, all reusing existing machinery (no new store methods, no migrations):

- Bug A: emitSessionCompleted reads cumulative GetCompactionCount (matching
  the legacy v2 path) instead of the per-run counter that resets to 0.
- Bug B/anti-loop: finalize passes state.Prune.MidLoopCompacted into
  maybeSummarize; under pressure it lowers the trigger to a unit-aligned
  threshold (compactionInputCap - overhead, same MaxRequestShare the guard
  uses) so the compaction is PERSISTED via the existing TruncateHistory +
  IncrementCompaction path. Defensive floor prevents over-compaction on
  pathological config; tool-result-only bloat still skips (history-only).
- Bug C: SourceID embeds the count (sessionKey:count) so the eventbus dedup
  key advances per compaction cycle instead of swallowing rapid same-session
  turns within the 5m TTL.

Tests: episodic compaction, maybe_summarize pressure, request budget.
go build (PG + sqliteonly), go vet, go test -race all green.
2026-07-25 00:39:02 +07:00
Clark Cant 9957a94868 Merge pull request #1469 from thotam/fix/session-context-usage-tokens
fix(sessions): store last-call context tokens instead of run-cumulative total
2026-07-23 11:02:08 +07:00
thotam 503909d3b6 fix(sessions): store last-call context tokens instead of run-cumulative total
SetLastPromptTokens received TotalUsage.PromptTokens — the sum of prompt
tokens across every think→act→observe iteration of a run — instead of the
final iteration's own usage. A tool-heavy run inflated the stored value by
the iteration count, so the sessions list showed e.g. 901K "context used"
against a 258K window, and maybeSummarize over-triggered compaction once
the inflation exceeded the 40% overhead clamp (observed: 83 compactions
on a 4-message session).

Track the final iteration's usage separately (ThinkState.LastUsage) and
persist its actual context size: prompt tokens + cached segments (Anthropic
reports input_tokens excluding cache read/creation) + output tokens (the
final reply joins history, so it occupies the next request's prompt). The
run-cumulative total still feeds AccumulateTokens for billing.
2026-07-23 10:37:25 +07:00
Duc Nguyenandntduc 08e8040c18 fix(tools): return authoritative datetime fields (#1468)
Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-07-23 10:30:42 +07:00
Clark Cant 696efa28db Merge pull request #1467 from itsddvn/fix/audio-provider-resolution-timeout
feat(runtime): strengthen hooks, channels, embeddings, and audio
2026-07-22 21:00:37 +07:00
ntduc bb58e03352 fix(audio): resolve provider options and honor STT timeout 2026-07-22 20:48:12 +07:00
ntduc b49c6abcc7 Merge remote-tracking branch 'upstream/dev' into dev 2026-07-22 20:24:10 +07:00
ntduc fe5a3c7f92 fix(hooks): select prompt provider and model 2026-07-22 19:49:15 +07:00
ntduc 876104950b fix: recover missing embeddings safely 2026-07-22 19:01:16 +07:00
ntduc 51a27af707 fix(feishu): wire writer stores for database channels 2026-07-22 17:57:58 +07:00
ntduc b428baf70b docs(hooks): document script context injection 2026-07-22 14:13:30 +07:00
ntduc 03a196e613 feat(hooks): add script context injection 2026-07-22 14:13:23 +07:00
bilogic c1fb41254b Initialize selectedLocale based on resolved language (#1459) 2026-07-20 22:51:16 +07:00
ntduc e4e4bb33fb fix(feishu): preserve group context without sender name 2026-07-20 15:05:54 +07:00
minhdang03 d844795e07 fix(teams): recover task locks for all active teams, not only v2 (#1448)
Task locking (AssignTask/ClaimTask) applies to every active team with no
version check, but the recovery/stale/followup queries only ran for teams
whose settings.version >= 2. Any team without that flag accumulated
permanently-stuck in_progress tasks: locks were set on dispatch but never
reclaimed once they expired, so the task ticker skipped them forever
(including the startup force-recover).

Align the maintenance scope with the locking scope by filtering recovery,
stale-marking, and follow-ups to active teams regardless of settings
version. Applies to both the Postgres and SQLite backends; renames the
shared JOIN constant v2ActiveTeamJoin -> activeTeamJoin to match.
2026-07-18 11:49:27 +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
Duc Nguyen e3dad3c60c fix(tts): unify voice resolution — dashboard settings affect tts tool (#1175)
The dashboard /tts page writes to system_configs[tts.<provider>.voice],
but the LLM-invoked tts tool was only checking args > agent OtherConfig
> builtin_tool_tenant_configs[tts].default_voice_id. Two different
storage locations → the user's chosen voice was ignored, Edge defaulted
to en-US-AriaNeural even when "HoaiMy" (vi-VN-HoaiMyNeural) was set
correctly in the dashboard.

Add system_configs as a 4th-level fallback so the dashboard becomes
the single source of truth.

- TtsTool gains SetSystemConfigStore(s) setter
- resolveVoiceAndModel takes providerName + looks up
  tts.<provider>.voice/model when no higher-precedence source set
- effectiveProvider resolved BEFORE voice/model so the right key is hit
- Wired at gateway boot in cmd/gateway.go (after pgStores ready)
- 4 unit tests covering: fallback, arg precedence, no-store, empty provider

Verified against production trace 019e6036-44bb-703b-85fa-dee34f7ab2c0
where the tts tool was called with provider=edge, no voice arg, and
defaulted to English instead of the configured Vietnamese voice.
2026-07-18 00:22:48 +07:00
Bruno ClermontandBruno Clermont cc9956c1f9 feat(audio): openai_compat TTS/STT provider for self-hosted endpoints (#1447)
* feat(audio): add openai_compat TTS/STT provider for self-hosted endpoints

Adds a provider pair speaking the OpenAI audio wire format against any
compatible endpoint (gpu-manager, Speaches, vLLM, LocalAI, llama.cpp,
Ollama), so a self-hosted engine no longer needs a bespoke HTTP shim
implementing goclaw's proprietary /transcribe_audio contract.

Kept distinct from the openai package on purpose. Both speak the same
wire format, but "openai" carries api.openai.com semantics, and
audio.IsVoiceCompatible applies OpenAI's voice allowlist to any provider
by that name — FilterVoiceForProvider then silently substitutes "alloy"
for anything outside it. A self-hosted voice ID such as Piper's
"fr_FR-gilles-low" would be replaced with no error and no log line, and
the caller would hear the wrong language. Providers without validation
rules pass through untouched, which is the correct behaviour for an
engine whose voice namespace is its own.

Other deliberate departures from the openai package:

- api_base is required. There is no public endpoint to fall back to, and
  defaulting to api.openai.com would send self-hosted traffic to a vendor.
- An empty api_key omits the Authorization header rather than sending an
  empty Bearer token; self-hosted engines commonly have no auth.
- Empty voice/model are omitted from the request instead of sent blank,
  since some engines resolve the model from the requested voice.
- The multipart filename is never blank: several engines type the audio
  by extension rather than Content-Type and reject what they cannot type.

Endpoint error bodies are preserved in the returned error so a format
mismatch (e.g. an engine that cannot encode mp3) is diagnosable.

* feat(audio): wire openai_compat TTS/STT provider into gateway config

Adds tts.openai_compat config block and registers both providers at
startup. api_base is the enable switch rather than an API key, since
self-hosted endpoints commonly have no auth and there is no vendor
default to fall back to.

The STT chain is now built from the providers that actually registered
instead of being hardcoded to {elevenlabs, proxy}. Transcribe skips
unregistered names with a warning on every call, so a static chain
naming absent providers is log noise on the hot path. openai_compat goes
first when present: it is an explicit operator choice and keeps audio on
the local network. "proxy" stays last, as BridgeLegacySTT registers it
later per channel.

Behaviour is unchanged when tts.openai_compat.api_base is unset: the
chain resolves to {elevenlabs, proxy} exactly as before.

---------

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
2026-07-17 22:51:16 +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
dc605b911e feat(mcp): expand CRUD server to near-complete CLI parity (#1440)
* feat(mcp): expand CRUD MCP server to near-complete CLI parity

Closes the CLI-vs-MCP tool coverage gap identified by auditing every
`goclaw` CLI command against the existing goclaw_* MCP tool set. Adds
skill grant/revoke, agent skill pin/unpin, and full CRUD/inspection
surfaces for memory, knowledge graph (including dedup/merge/prune),
tenants, providers, LLM traces, channel contacts, pending messages,
audit activity, system config, tenant storage (list/size/delete/move),
scoped agent config export/import, secure-CLI binary registry, and a
DB-backed health check.

Deliberately out of scope, documented inline where relevant:
- `goclaw credentials`: confirmed CLI-local (~/.goclaw/config.yaml +
  keychain), no server resource to wrap. goclaw_secure_cli_binaries_*
  covers the closest real, previously-uncovered server resource instead.
- Full tar-archive agent export/import (KG + workspace files): the CLI's
  version streams a multi-section archive with progress events, a shape
  that doesn't map to a single MCP tool call. Config + context files
  (the portable "brain") is covered.
- `kg extract` (LLM-driven text extraction): goclaw_kg_ingest accepts
  the same Entity/Relation shapes the extractor produces, so a caller
  can run extraction itself and hand off the result.

Wires 8 new store dependencies (Memory, KnowledgeGraph, Tracing,
Contacts, PendingMessages, Activity, SystemConfigs, SecureCLI) through
gateway.Server setters -> cmd/gateway.go -> CRUDDeps, following the
existing Providers/Tenants pattern. Storage and secure-CLI-binary
handlers duplicate internal/http's path-escape/symlink-hiding
validation logic (documented inline) since internal/http already
imports internal/mcp and the reverse would cycle.

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

* fix(mcp): return sessionKey in goclaw_chat_send response for persistent conversations

The goclaw_chat_send tool creates a new session internally when sessionKey is
empty, but never returned the key to the caller. This prevented using
goclaw_chat_history to fetch previous messages in the session.

Add SessionKey field to ChatSendResult so callers can:
1. Start a new agent chat without providing sessionKey
2. Receive the sessionKey back in the response
3. Use that sessionKey for follow-up messages and history queries

Fixes the training loop pattern: start chat → get sessionKey → call
goclaw_chat_history with that key → iterate skill based on actual failures.

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

* feat(mcp): add timing diagnostics to goclaw_chat_send for timeout troubleshooting

Log request arrival, processing duration, and errors with millisecond precision.
Helps identify whether timeouts occur at MCP client→goclaw, goclaw→ollama, or
during agent execution. Critical for production debugging.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(gateway): add http.Server timeouts for defensive timeout handling

Set explicit timeouts in http.Server:
- ReadTimeout: 1h (allow large uploads, long-running agent operations)
- WriteTimeout: 1h (allow streaming responses to slow clients)
- IdleTimeout: 30s (close idle keep-alive connections quickly)

Provides defense-in-depth when Nginx/Traefik timeouts are misconfigured.
Matches Nginx timeout (3600s) to prevent race conditions.

Timeout chain: traefik 3600s = nginx 3600s = goclaw 3600s

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 01:56:03 +07:00
SYNITYandDangTinh311 02249593a2 fix(packages): make .runtime group-writable for goclaw self-update [B24:2794] (#1439)
The entrypoint set /app/data/.runtime to root:goclaw 0750 so the root
pkg-helper can write apk-packages. But the gateway runs as goclaw and the
github-package self-update flow (running in-process as goclaw) must write
into .runtime: the scratch dir (.runtime/tmp/<pkg>-<tag>-<nanos>) and the
manifest (.runtime/github-packages.json[.tmp], updates-cache.json).

With 0750 the group has no write bit, so updating a github binary package
(e.g. goclaw v0.6.2 -> v0.8.0) fails with:
  create scratch dir: ... mkdir /app/data/.runtime/tmp: permission denied
and, if the scratch is worked around, later:
  manifest save failed: open .../github-packages.json.tmp: permission denied

Change the mode to 0770 so root keeps ownership (apk-packages still root)
while the goclaw group can create the scratch dir and rewrite the manifest.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-15 17:55:27 +07:00
SYNITYandDangTinh311 c203d45811 fix(cron): guard nil job and tenant-consistent readback in cron create [B24:2794] (#1438)
Creating a cron job over a connection with no tenant in context
(gateway-token / CLI / master-scope UI session) panicked with a
nil-pointer dereference.

Root cause — tenant asymmetry between insert and readback:
- AddJob INSERT uses tenantIDForInsert(ctx), which falls back to
  MasterTenantID when the context has no tenant (row inserted OK).
- The readback (scanJob / GetJob) rejects a nil-tenant context with
  "tenant_id required", so GetJob returned (nil, false) and AddJob
  swallowed it as (nil, nil).
- handleCreate then dereferenced job.ID (UpdateJob patch) → panic.

Fix (defense-in-depth):
- handleCreate (internal/gateway/methods/cron.go): add a nil-job guard
  before any job.ID use; return ErrInternal instead of panicking. Apply
  the post-create stateless/command patch under a tenant-consistent
  context (WithTenantID(ctx, job.TenantID)) so lockCronJobForMutation
  finds the row for nil-tenant callers.
- AddJob (store/pg + store/sqlitestore cron_crud.go): read back under
  WithTenantID(ctx, tenantIDForInsert(ctx)) so insert and readback share
  the same tenant; return an explicit error if readback fails instead of
  swallowing it.

Tests:
- internal/store/sqlitestore/cron_crud_test.go: AddJob with a nil-tenant
  context returns a non-nil job + nil error (real SQLite DB).
- internal/gateway/methods/cron_test.go: handleCreate does not panic and
  skips UpdateJob when the store returns (nil, nil).

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-15 16:54:49 +07:00
293aa2a5e1 (feat) MCP Server (#1365)
* feat(hooks/observe): add structured output validation hook (ObserveHook)

- hooks/types.go: Add ObserveHook interface + BuiltInHookType enum
- hooks/dispatcher.go: HookDispatcher emits ObserveHook with PhaseResult payload
- pipeline/observe_stage.go: ObserveStage emits hook after ObserveResult built
- pipeline/substates.go: ObserveStageResult carries hook results + validation errors
- hooks/config.go: BuiltInHookTypeObserve added to BuiltInHookType enum

PhaseResult carries structured output, token usage, tool calls + validation errors
emitted post-ObserveStage. ObserveHook implementations can validate structured
output against schemas, detect tool-call loops, enforce token budgets, etc.

Hook fires after ObserveStage produces ObserveResult, before results propagate
to next stage. ValidationError returned by hook halts pipeline and propagates
error to caller without further stage execution.

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

* ui(hooks): add post_model_response event to web UI

- Add event to Zod schema, filter dropdown, and form dialog
- Implement conditional test panel UI for model response payload
- Add translations (en/zh/vi) for new test panel fields
- Updated beta description to reference the new event

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

* feat(mcp): add MCP CRUD server exposing goclaw resources at /api/mcp/ with Bearer token auth
and X-GoClaw-Tenant-Id header, default to master tenant

* feat(mcp): add goclaw_skills_write_file tool to edit skill files on disk

The CRUD MCP server's goclaw_skills_update only touched skill DB metadata,
with no way to edit a skill's SKILL.md/file content on the filesystem. Extract
the versioned write logic from the web UI's skill file editor
(SkillsHandler.handleWriteFile) into skills.WriteVersionedFile so both
surfaces share identical validation and versioning, and expose it as a new
MCP tool.

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

---------

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-15 09:56:43 +07:00
4f558e2c3b fix(ollama): resolve context window per model via POST /api/show (#1437)
The Ollama integration never applied a correct context window, so agents
with real prompts (20k-100k tokens) were rejected with HTTP 400
exceed_context_size_error against a 4096-token default.

Three root causes fixed:

1. FetchOllamaModelContext issued a GET to /api/show, which Ollama answers
   with 405 (the endpoint is POST-only). Now POSTs {"model": "<name>"}.

2. The response parser expected a flat model_info.context_length, but a real
   Ollama server namespaces the key by architecture (gemma4.context_length,
   qwen3.context_length, ...). extractContextLength now matches "context_length"
   or any "*.context_length" key.

3. num_ctx was resolved once at startup for a hardcoded "llama3.3" model and
   never for the model an agent actually uses. Resolution now happens per
   request for the real model inside OllamaProvider.resolveNumCtx, cached under
   an RWMutex, with an explicit settings override winning and the fetched value
   bounded by OllamaDefaultNumCtx so an enormous advertised window (Qwen3.5
   reports 262144) cannot balloon the KV cache beyond VRAM.

Also classify Ollama's "exceed_context_size" 400 as a context-overflow error so
the pipeline's emergency-compaction+retry path (Issue 958) engages gracefully
instead of surfacing a raw error.

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:24:31 +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
Duc Nguyenandntduc 27f4b7743b fix(skills): preserve custom skills during bundled seeding (#1435)
Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-07-15 00:25:07 +07:00
Clark Cant 1f5a6f0ef6 Merge pull request #1434 from tech-synity/b24-2794-agent-activity-indicator
feat(bitrix24): agent activity indicator via InputAction.notify
2026-07-14 17:24:05 +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 6167a53bbe fix(bitrix24): mount shared webhook route independent of bot readiness [B24:2794] (#1432)
/bitrix24/* only mounted when a bitrix24 Channel loaded successfully via
WebhookHandlers(), which requires portal + bot_code + bot_name already
set. But completing portal OAuth requires /bitrix24/install to be
reachable, and the UI won't let you pick an uninstalled portal when
creating a bot -- deadlock on any fresh deployment: no bot can load
until OAuth completes, OAuth can't complete without the route mounted.

Claim and mount the shared bitrix24.WebhookRouter() singleton directly
at boot, independent of whether any Channel loaded. ClaimWebhookRoute
is idempotent (first-claim-wins via CompareAndSwap), so this is a
no-op once a real Channel has already claimed the route.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-13 08:25:59 +07:00
SYNITYandDangTinh311 143ed4a8a4 fix(bitrix24): make pending-portal delete button clickable in Radix Select [B24:2794] (#1431)
The delete affordance next to each pending portal in the channel-create
form was wired to onClick, but Radix Select closes the popover on the
SelectItem's own pointerdown handler. By the time the click event would
fire on the nested button, the popover — and the button — are gone.
onPointerDown on the button additionally called e.preventDefault(),
which further blocked the browser from synthesising the click at all.

Result: clicking the trash icon looked completely dead. Keyboard users
were already covered by the Delete/Backspace onKeyDown handler on the
SelectItem itself; only mouse users were locked out.

Fire setPendingDeleteName from onPointerDown (with stopPropagation +
preventDefault so the parent SelectItem doesn't also select the pending
portal), and keep the onClick handler so the button stays activatable by
Enter/Space when it takes focus. This mirrors the pattern Radix uses for
its own nested interactive elements (e.g. DropdownMenu.CheckboxItem).

Surface parity:
  - Gateway server: N/A because the change is UI-only.
  - API contract:  N/A because bitrix.portals.delete already exists.
  - Web UI:        the single component that owned the broken handler.
  - CLI/runtime:   N/A because no CLI reads this UI state.

Co-authored-by: DangTinh311 <dangtinh31193@gmail.com>
2026-07-13 07:23:49 +07:00
Huy Tran fec70a7910 📝 docs: prioritize Vietnamese language in README (#767) 2026-07-13 01:56:16 +07:00
Matt Van HornandMatt Van Horn e5b5d03e3d fix: persist explicit None TTS provider instead of dropping it (#1424)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 12:56:59 +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
61993b4387 feat(i18n): add Russian (ru) language — full parity across backend, web & desktop (#1391)
* docs(i18n): design spec for Russian (ru) language support

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: skensel <skensel@MacBook-Pro-skensel.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: ナム <namnhfreelancer@gmail.com>
Co-authored-by: Collective Developer <man@collective.dev>
2026-07-11 12:25:38 +07:00
Zezae Oh 2a6872ac84 fix: honor model override for sync subagents (#1423) 2026-07-11 11:54:18 +07:00
ナムandCollective Developer 0d588e05c1 fix(discord): qualify group titles across surfaces (#1422)
* fix(discord): qualify group titles across surfaces

* feat(ui): add searchable delivery target selectors

---------

Co-authored-by: Collective Developer <man@collective.dev>
2026-07-11 11:25:23 +07:00
thotam 0c1ededc92 feat(webhooks): return per-call usage breakdown with provider/model/cost (#1421)
feat(webhooks): per-call usage breakdown with provider/model/cost (#1421)
2026-07-10 22:56:05 +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
Vũ Thanh Thiênandalethienvu2 244197c2db feat: add config cloudflared, env for db port (#708)
Co-authored-by: alethienvu2 <ale.thienvu@setel.com>
2026-07-10 21:26:34 +07:00