Commit Graph
2051 Commits
Author SHA1 Message Date
Vedant Madane f323c46a61 fix: Constrain traces table columns and show status text (#1526)
- Give Name/Status/Tokens/Spans/Time explicit widths so Name cannot shove others off-screen
- Show status label next to the status icon

Fixes #762

Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
2026-09-01 01:13:47 +07:00
Excelius b75753b787 fix: handle SQLite provider settings updates (#1531) 2026-09-01 00:44:27 +07:00
Clark Cant 753f583f24 Merge pull request #1541 from maitanloi/fix/onboarding-agent-name-slug
fix(onboarding): keep the chosen agent identity when the prompt is edited
2026-08-30 21:13:37 +07:00
Mai Tan Loi 517cf1e469 fix(onboarding): keep the chosen agent identity when the prompt is edited
Editing the description in onboarding step 3 deselected the preset, and the
display name, agent key and emoji were derived from that selection. Losing it
dropped them to a hardcoded "Fox Spirit" / "fox-spirit" / fox emoji, so picking
the Writer preset and rewriting its prompt created an agent named Fox Spirit.
The step also offered no way to name the agent at all.

Identity is now editable state that a preset only prefills:

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

Reuses the existing agents:create.* keys, present in en/vi/zh on both surfaces.
2026-08-30 16:54:25 +07:00
Clark Cant 645ab3329a Merge pull request #1540 from maitanloi/fix/deepseek-reasoning-content
fix(providers): carry reasoning_content on assistant turns without thinking
2026-08-29 17:15:17 +07:00
Mai Tan Loi a32e2f0d5d fix(providers): carry reasoning_content on assistant turns without thinking
DeepSeek rejects a request whose assistant history omits reasoning_content
once the conversation is in thinking mode:

  HTTP 400: The `reasoning_content` in the thinking mode must be passed
  back to the API.

A thinking-mode turn that only emits tool calls produces no reasoning of
its own, so Message.Thinking is empty and the wire field was dropped. The
next request then mixes assistant messages with and without the field and
upstream fails. Only kimi_coding had an empty-string fallback.

Detect thinking mode from the conversation itself: when any assistant
message carries reasoning, every replayed assistant message must carry the
field, empty string included. Narrower than matching on model name, which
would also inject the key for non-thinking models such as deepseek-chat.

Reproduces from a run where iteration 1 returned reasoning plus an edit
call, the edit failed, iteration 2 returned a bare read_file call, and
iteration 3 was rejected.
2026-08-29 14:24:15 +07:00
fchengyan be573f411b fix(vault): sanitize context window to prevent invalid UTF-8 (#1536)
ExtractWikilinks builds a ±25-byte context window around each wikilink
match. When the content contains multi-byte UTF-8 characters (Vietnamese,
CJK, emoji), the byte-offset start/end can fall on a continuation byte,
producing invalid UTF-8 that PostgreSQL rejects with SQLSTATE 22021
('invalid byte sequence for encoding UTF8').

Fix: wrap the byte-offset slice with strings.ToValidUTF8, which strips
any leading/trailing partial runes. This is a minimal one-line change
that preserves the existing context semantics while ensuring the output
is always valid UTF-8.

Available since Go 1.21 (this project uses Go 1.26).

Closes #1472
2026-08-27 19:12:48 +07:00
Clark Cant fc8a35eebd Merge pull request #1520 from cesc1802/dev
Embedding paths skip DockerLocalhost rewrite — Ollama "Verify Embedding" fails with connection refused in Docker
2026-08-22 20:57:05 +07:00
Cesc 42c2d78f86 Merge pull request #1 from cesc1802/fix/1519-docker-localhost-embedding
fix(memory): apply Docker localhost rewrite in embedding provider con…
2026-08-21 11:07:57 +07:00
Cesc 254081462c fix(memory): apply Docker localhost rewrite in embedding provider constructor
Model listing and chat provider construction rewrite localhost to
host.docker.internal via config.DockerLocalhost() when running inside
Docker, but all three memory.NewOpenAIEmbeddingProvider call sites
(verify-embedding handler, runtime memory embedding) passed the raw
api_base through. In Docker the container resolved localhost to itself,
so Ollama Verify Embedding and runtime embedding failed with connection
refused while the model dropdown loaded fine.

Centralize the rewrite inside the constructor so no call site can miss
it, add a SetInDockerForTest hook for environment-independent regression
tests, and pin Docker detection off in httptest-based suites.

Fixes #1519
2026-08-21 10:57:36 +07:00
Dessalines39394andDessalines39394 7c4716e021 docs: fix broken star history chart (#1517)
The star history chart in the README is currently broken because the upstream service can no longer render it reliably under GitHub's stargazer API restrictions. Point the chart at a working alternative so the project's growth remains visible.

Co-authored-by: Dessalines39394 <245616256+Dessalines39394@users.noreply.github.com>
2026-08-20 17:48:34 +07:00
Clark Cant 1869164a8c Merge pull request #1496 from georgeatparallel/parallel/search-mcp-2t120759941773z-47b475ff
feat(tools): add optional Parallel web search provider
2026-08-16 04:46:01 +07: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
Clark Cant bb0291294b Merge pull request #1515 from minhdang03/fix/duplicate-media-delivery
fix(pipeline): stop delivering the same generated image twice
2026-08-15 21:47:14 +07:00
Minh Dang 45680e9ff5 fix(pipeline): stop delivering the same generated image twice
An agent that generates an image and then attaches a copy of it under a
different name produced two media entries for one picture, and both were
sent. In production every image-edit reply arrived as two identical
attachments: the file create_image wrote (slug_timestamp_rand.png) plus
the path the agent passed to send_file, byte-identical but stored twice.

processMedia deduplicated by path only, so the pair survived. It also ran
before generated images were folded into MediaResults, leaving that
producer past the only dedupe pass entirely.

Fold persisted assistant images into MediaResults first, then deduplicate
once over the union, matching identical local files by content digest.
The assistant message is now built from that same deduplicated set, so
session history and outbound delivery cannot disagree. Media that cannot
be read (remote or already cleaned up) is left untouched.
2026-08-15 13:34:42 +00:00
Clark Cant fd532b4f8d Merge pull request #1510 from bilogic/dev
get refreshable tokens
2026-08-08 10:30:30 +07:00
bilogic 72c2c301a5 get refreshable tokens 2026-08-08 10:12:58 +08: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
Clark Cant 68a6d1c6c9 Merge pull request #1501 from justintruong29/fix/vault-read-utf8-rune-boundary
fix(vault): stop vault_read rejecting non-ASCII text at the sniff boundary
2026-08-04 12:29:17 +07:00
Clark Cant bc16352587 Merge pull request #1500 from justintruong29/fix/mcp-allcaps-business-codes-stripped
fix(mcp): stop stripping all-caps business identifiers from tool arguments
2026-08-04 12:29:14 +07:00
Clark Cant fdd0165208 Merge pull request #1499 from justintruong29/fix/vault-read-tenant-workspace
fix(vault): resolve vault_read paths under tenant workspace root
2026-08-04 12:29:09 +07:00
Clark Cant ae2b2adf53 Merge pull request #1498 from justintruong29/fix/gemini-tool-result-name
fix(providers): carry tool name on tool results for Gemini
2026-08-04 12:29:03 +07:00
Justin Truong eba1b4f600 fix(vault): stop vault_read rejecting non-ASCII text at the sniff boundary
The text-only gate sniffs the first 8192 bytes of a document and rejects
the read when they are not valid UTF-8. The window was cut at a plain byte
offset, so on non-ASCII text it regularly landed inside a multi-byte rune
and left a dangling prefix. The gate then reported the document as binary
even though the file was intact — measured on Vietnamese documents, about
one in four files above 8 KB was blocked, depending on where the accented
characters happen to fall.

Trim up to 3 bytes off the window when, and only when, the content was
actually cut there, then judge. A file that fits inside the window is left
alone, so genuinely broken trailing bytes are still caught, and binary
content stays blocked because its invalid bytes are spread over the whole
window. readCapped had the same byte-offset cut at max_bytes, which put a
broken character at the end of every truncated non-ASCII document.
2026-08-03 23:55:07 +07:00
Justin Truong 6666585b7b test(vault): move integration fixtures under the tenant workspace root
The vault_read fix changes what the injected workspace means: it is the
global root wired once at boot, not a per-tenant root, since one tool
instance serves every tenant. These tests seeded a non-master tenant but
wrote fixtures straight to the injected root, so they only passed while
reads ignored the tenant layer.

Write fixtures to config.TenantWorkspace(base, tenantID, slug) and carry
the slug in context, matching a real run. tenantSlug is now the single
source of truth shared with seedTenantAgent, so on-disk layout in a test
cannot drift from the seed.
2026-08-03 23:10:13 +07:00
Phan Thong IT 7dd0fc2308 fix(mcp): stop stripping all-caps business identifiers from tool arguments
isAllCapsPlaceholder() classified ANY string of >=3 chars made only of A-Z and
underscore as an LLM placeholder, and stripEmptyOptionalArgs() then removed it
from the outbound CallTool arguments. Real business identifiers match that shape:

  bank codes      BKASH, NAGAD, ROCKET, VCB
  currency codes  BDT, VND, USD, PKR, MMK
  enum values     SUCCESS, PENDING, ALL

Impact is silent and severe: dropping an optional filter turns a filtered query
into an unfiltered one. The MCP server still answers 200 with the full result set,
so nothing errors — the model reports totals for the entire dataset as if they were
the filtered result, with full confidence.

Observed in production: asking "how many BKASH proxies are active" returned 6,610
(all banks) instead of 1,263. Three different bank codes returned byte-identical
results, which also drove the model into a retry loop until the loop guard fired.

Diagnosis was slow because the removal is invisible: the outbound log records
args_len BEFORE stripping, and stripEmptyOptionalArgs logs nothing at all — while
normalizeArgsForSchema right below it does emit mcp.tool.args.coerced. Confirmed
the argument never reaches the server by echoing the raw JSON-RPC arguments from
RequestContext[CallToolRequestParams] on the server side.

Changes:
- isAllCapsPlaceholder: require multi-word (contains "_") or membership in a known
  single-word placeholder list. Keeps the original intent — the doc comment's own
  examples (SHOULD_NOT_BE_HERE, DO_NOT_SEND, NOT_APPLICABLE) are all multi-word —
  while letting business codes through.
- stripEmptyOptionalArgs: emit slog.Warn("mcp.tool.args.stripped") whenever an
  argument is dropped, so this class of bug is one grep away instead of a
  multi-hour investigation.
- Tests: guard both layers against regression.

Verified: go build ./... clean; go test ./internal/mcp/ passes.
2026-08-03 22:50:12 +07:00
Justin Truong 466ef577f7 fix(vault): resolve vault_read paths under tenant workspace root
vault_read is a singleton wired once at boot with the global workspace
root, then joins doc.Path (stored relative to the TENANT root) onto it.
For master tenant both roots coincide, so the bug stayed invisible; every
other tenant reads ENOENT because its files live under tenants/<slug>/.
vault_search is DB-only and keeps returning the doc, so the symptom is
"search finds it, read fails" rather than an obvious missing file.

Resolve the tenant-scoped root per request via TenantLayer, which is a
no-op for master and preserves existing single-tenant behaviour. Passing
the tenant root into resolvePath also narrows the boundary check, so it
now doubles as cross-tenant isolation.

Runs originating from channels or cron may not carry the tenant slug in
context; without it TenantLayer falls back to a UUID-named directory that
does not match the slug-named one the write path creates. Add a nil-safe
TenantStore lookup for that case only.
2026-08-03 22:50:12 +07:00
Justin Truong d13cc0804a fix(providers): carry tool name on tool results for Gemini
Gemini drops tool_call_id and pairs functionCall/functionResponse by
function name, so its OpenAI-compat shim requires a non-empty
FunctionResponse.name. The name was only recoverable through a reverse
id->name lookup over assistant tool_calls still present in the request
window, which fails after pruning, truncation or tool_call collapse and
produces HTTP 400 "Name cannot be empty" on any follow-up iteration.

Add Message.ToolName, set it at every tool-result creation site and
preserve it through context pruning, then prefer it when serializing.
Fall back to the existing index for history persisted before the field,
so old sessions keep working without a reset.

When neither source resolves a name, drop the unlabelled tool result
instead of emitting an empty one: an empty name is a guaranteed 400 and
a synthetic name would match no prior functionCall. Gated behind the
existing Gemini detection so other OpenAI-compat hosts, which pair by
tool_call_id, are unaffected.

Field is JSON-optional, so session history needs no migration and older
binaries ignore it on rollback.
2026-08-03 22:50:06 +07:00
georgeatparallel eef6f27775 feat(tools): add optional Parallel web search provider
Signed-off-by: georgeatparallel <george@parallel.ai>
2026-08-02 05:31:39 -07:00
Rayan Salhabandcyphercodes 149bb478ab fix(providers): omit temperature for Claude 5 (#1494)
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-08-02 13:59:22 +07:00
bilogic 6a316fc23d rescan filesystem for new skills when setup (#1493) 2026-08-02 12:30:08 +07:00
bilogic dacdddbfa1 don't react with 💔 on telegram (#1492) 2026-08-02 08:30:05 +07:00
bilogic 6e61bd1531 stop responding with "..." (#1491) 2026-08-02 07:29:30 +07:00
EddyandEddy Lockwood 89e4560f20 fix(ui): align workstation web contract with gateway API shape (#1461)
The workstations page was written against snake_case field names the
gateway never served. Go's json decoder drops unknown members silently,
so create requests arrived with an empty WorkstationKey and were rejected
with "workstationKey is required", while list rows read undefined and
rendered a blank key, "backend.undefined" and "Invalid Date".

Align the TypeScript contract on the camelCase shape that both the WS
handler and store model already use:

- Rename Workstation, CreateWorkstationParams and every field read on the
  workstations page to workstationKey/backendType/createdAt/updatedAt.
- Nest update params under `updates`; a flattened body decoded to an empty
  map and was rejected with "no updates provided".
- Replace the Identity File input, which mapped to a field SSHMetadata does
  not accept, with a private key or password credential path.
- Add the image field required by DockerMetadata and map the container name
  onto host and the daemon endpoint onto socketPath, so Docker workstations
  can be created at all.
- Extract the payload builder so the wire contract is unit-testable, and
  move validation strings into the en/vi/zh catalogs.

Covered by tests asserting the create payload shape and list rendering
against a verbatim gateway response body.

Co-authored-by: Eddy Lockwood <doakythanh@gmail.com>
2026-08-01 14:30:42 +07:00
Clark Cant d47cc51aa0 Merge pull request #1451 from binyangzhu000-sudo/codex/add-atlascloud-provider-goclaw
Add Atlas Cloud provider defaults
2026-08-01 05:39:42 +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
Clark Cant 4b865508da Merge pull request #1028 from krichprollsch/lightpanda
Add Lightpanda as alternative to Chrome for the browser automation tool
2026-08-01 03:59:31 +07:00
Floze 276b4bbab7 test(mcp): lock test and save SSRF parity (#1450) 2026-08-01 01:59:55 +07:00
Conner MoandConner Mo c21499a7f5 feat(security): let operators un-block CIDRs behind a transparent proxy (#1465)
SSRF protection resolves a hostname and judges the resulting IP. That
model assumes DNS resolution describes where the traffic actually goes,
which stops being true behind a TUN/fake-IP proxy: every query is answered
with a synthetic address out of a reserved range, and the proxy then
routes that address to the real public host. The IP is a handle, not a
destination.

In that environment web_fetch rejects ordinary public sites — observed
with news.sina.cn resolving to 198.18.0.236 — and no configuration can
fix it, because the block list is compiled in. The agent then burns
iterations retrying URLs that can never succeed.

GOCLAW_SSRF_ALLOWED_CIDRS lets an operator name the ranges their proxy
hands out. Empty by default, so nothing changes for deployments that do
not set it, and the accepted and refused entries are both logged at
startup — this widens what LLM- and admin-supplied URLs can reach, so it
should be visible.

Ranges an SSRF actually targets can never be allowlisted: link-local
(including cloud metadata at 169.254.169.254), multicast and unspecified
are refused at parse time, in either direction, so neither an exact entry
nor a wider range that swallows one gets through.

Applied inside isBlocked rather than only in validate() because
NewSafeClient re-checks the pinned IP at dial time through the same
function — relaxing just the pre-flight check would pass validation and
then fail to connect.

internal/tools carries its own private-range list for web_fetch and
web_search, separate from this package and not identical to it. It has to
consult the same setting, or relaxing one gate leaves the other rejecting
the very traffic the operator permitted. Unifying the two lists is left
alone here; it is a wider change than this one.

Co-authored-by: Conner Mo <connermo@ConnerdeMacBook-Pro.local>
2026-07-31 21:30:01 +07:00
bilogic 2e1b90ca35 mcp credentials over chat (#1490) 2026-07-31 19:34:40 +07:00
Leo225 bcd23c587b fix: handle input and database read errors in client and doctor commands (#1480)
Added error handling for input reading in the agent chat client and for database row reading in the doctor commands, ensuring that errors are reported clearly to the user.
2026-07-31 18:30:28 +07:00
Clark Cant 3f24337256 Merge pull request #1489 from itsddvn/fix/delegation-artifact-media-dedup
fix(collaboration): protect artifact and media delivery lifecycle
2026-07-31 09:00:41 +07:00
ntduc 4426f76140 fix(agent): deduplicate automatic media delivery 2026-07-31 08:40:02 +07:00
ntduc b6fb91b163 fix(collaboration): preserve active artifact exchanges 2026-07-31 08:39:50 +07:00
Clark Cant 9d5f9ce857 Merge pull request #1488 from bilogic/dev
consistent language
2026-07-31 06:30:51 +07:00
bilogic 8c564c0e9f consistent language 2026-07-31 06:30:07 +08: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
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