Commit Graph
2067 Commits
Author SHA1 Message Date
yatulandClaude Opus 5 305bd7c50d fix(teams): grant a delegated lead read access to its own team (#1537)
* fix(teams): scope team tasks to the delegation origin, not the delivery channel

A delegated run is delivered on the internal "delegate" channel while its real
origin is preserved separately — buildAgentLinkRunRequest is explicit about it:

    // preserves the origin's authorization-bearing identity while keeping
    // delegation on its internal delivery channel.
    Channel:          "delegate",
    WorkspaceChannel: req.Channel,
    WorkspaceChatID:  req.ChatID,

The team tools did not consult that origin, so a task created by a lead reached
through delegate was stamped with the delivery channel. Nothing is registered
for it, so every notification about that task — completion, failure, blocker
escalation, ask_user — was dropped:

    unknown channel for outbound message channel=delegate

The delegatee's first answer still arrived, because it travels back as the
delegation result rather than through a channel; everything the lead said after
the delegation closed was lost. One session produced 16 such drops, including a
blocker escalation the user needed to see.

Add OriginChannelFromCtx / OriginChatIDFromCtx next to the existing workspace
scope propagation helpers, and use them where team scoping and notification
routing are decided: task records, list/search scoping, dispatch fallbacks,
event payloads, escalation tasks, ask_user and leader notifications. The
resolution is the identity when no delegation origin is present, so
non-delegated flows are unchanged.

Deliberately not touched: the channel used in authorization decisions
(checkTeamAccess, requireLead, approve/reject lead bypass). Those ask "how did
this call arrive", not "where should the answer go", and widening them is a
separate question.

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

* test(teams): cover delegated-lead completion routing and origin isolation

Triage on #1529 named two gates this PR had not met: regression coverage for
delegated lead task completion, and a guard that unrelated origins cannot
receive the notification. The existing tests only covered what create persists.

TestDelegatedLeadCompletionNotifiesOrigin completes a task raised in a delegated
context and asserts the completion event is addressed to the caller's origin.
Reverting team_event_helpers.go to the delivery channel fails it with
"addressed to delegate/system".

TestCompletionNotificationStaysWithinItsOwnOrigin puts two tasks from different
origins on one board, completes one, and asserts no completion notification
carries the other origin.

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

* fix(teams): grant a delegated lead read access to its own team

Fixes #1535. Second attempt: the first one set RunRequest.TeamWorkspace on the
delegated run, which loop_context.go rejects outright and deliberately so — that
field also becomes ToolWorkspace, which would displace the exchange outputs
directory. Every delegation then died at setup with "invalid delegation artifact
workspace". This does not go near that field.

The separation the lead needs already exists in the code. A lead addressed
directly resolves its team at loop_context.go:285-321 and gets

    ctx = tools.WithToolTeamWorkspace(ctx, wsDir)
    ctx = tools.WithToolTeamRoot(ctx, teamRoot)

with no WithToolWorkspace call — a read allowance, not an override. Only the
!isArtifactDelegation gate keeps a delegated lead out of it. So the grant is
made inside the artifact branch instead, from the same inputs, and the guard on
req.TeamWorkspace stays exactly as strict as it was.

Both paths are needed. The workspace alone covers only the lead's own chat leaf;
the deliverable in the report sits at teams/<id>/system/review-....md, written by
a member under a different chat scope. buildAllowedPrefixes adds the team root
for reads and not for writes (filesystem.go:366-370), which is precisely the
asymmetry this case wants: the lead reads the team's output, per-chat write
isolation is untouched.

Team ID is deliberately not set. It switches on the workspace interceptor's
write validation, file-change broadcast and task attachment (workspace_
interceptor.go:36,114,203) — none of which a delegated run should trigger, and
none of which reading needs.

Ambiguity is not resolved silently, as triage asked: an agent leading more than
one active team gets nothing. store.GetTeamForAgent would answer in one call but
it is ORDER BY (lead_agent_id = $1) DESC LIMIT 1 and would quietly pick a team.

Hermeticity is unchanged: send_file and message still refuse inside an artifact
run, publication still happens through delegation outputs on completion.

Tests: TestInjectContext_DelegatedLeadReadsTeamWithoutDisplacingOutputs drives
injectContext through the guard that broke the first attempt and pins both
halves — the run is not refused, and ToolWorkspace stays the outputs directory.
Resolution rules, the shared/isolated path shapes, read-not-write on the team
root, and send_file staying blocked are covered alongside.

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

* test(teams): pin that the delegated lead resolver does not re-scope by tenant

l.dataDir arrives already tenant-scoped from resolver.go (config.TenantDataDir),
which is why resolveDelegatedLeadTeamRead must not apply TenantLayer itself.
That was carried only by a comment, here and in the sibling branch of
injectContext, and nothing failed if someone added the layer back.

The failure it guards against is quiet: the path stays plausible, it just gains
a second tenant segment — teams/<id> under tenants/<slug>/tenants/<slug> — so
the lead is handed a directory its own tasks never write to, and reads come back
empty rather than denied.

Verified the assertion has teeth by reintroducing the layer: the subtest fails
with "tenant segment appears 2 times". Prompted by #1546 in this same area,
where a contract that lived outside the unit under test went unpinned and the
feature shipped green and did nothing.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 21:05:49 +07:00
yatulandClaude Fable 5.1 60cf79de51 feat(teams): let a human cancel and retry stuck team tasks from the dashboard (#1559)
* feat(teams): let a human cancel and retry stuck team tasks from the dashboard

A task that ended up blocked, stale, failed or cancelled could not be
recovered from the UI: the dashboard only deletes terminal tasks and
approves/rejects in_review ones. CancelTask and ResetTaskStatus existed
in the store but were reachable only through the lead agent's team_tasks
tool, which needs the full task UUID the dashboard never shows (#506).

Two WS RPCs, wired to Retry / Cancel buttons in the task detail dialog:

- teams.tasks.cancel — any task not yet completed/cancelled. Optional
  reason is stored as the result and posted as a comment so the lead
  sees it on the board. Dependents are unblocked by the store; they are
  not dispatched here because the dashboard has no agent turn.
- teams.tasks.retry — stale / failed / cancelled / in_review, plus
  blocked tasks that nothing blocks any more. A human comment is
  required: it is posted on the task and appended to the assignment
  prompt, so the assignee gets the missing answer in the same message.
  Optional agentId reassigns; the lead is refused as assignee (same
  guard as teams.tasks.assign).

ResetTaskStatus (pg + sqlite) now also accepts blocked, guarded on the
handler side by an empty blocked_by. Handler tests cover both actions
with a stub store: reason/comment persistence, required comment,
status gating, blocked_by guard, lead guard, cross-team IDOR.

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

* fix(permissions): classify teams.tasks.cancel and teams.tasks.retry as write methods

Every WS method must be classified in internal/permissions/policy.go,
otherwise the router answers UNAUTHORIZED for every role. Caught on a
live gateway (the drift test TestMethodRole_DriftCoverage… flags it, I
had not run that package). Both sit next to approve/reject/assign as
operator-level write methods; the write-methods test now pins them.

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

* feat(teams): let the retry dialog pick the assignee

A cancelled or stale task may have no owner (created unassigned, or the
member was removed). teams.tasks.retry then needs agentId, which the
dialog did not send, so Retry would fail with "task has no assignee".
The retry dialog now shows an assignee select (team members minus the
lead, defaulting to the current owner) and passes agentId only when it
differs from the owner. Retry is offered only when there is someone to
pick.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 13:30:48 +07:00
yatulandClaude Opus 5 34cc6e011d test(teams): fix the data races in the teammate streaming test (#1556)
TestHandleTeammateMessageSchedulesStreamedRun fails CI intermittently under
-race. Two separate races, both in the test rather than in what it exercises:

  1. It shared `gotReq` between the scheduler's RunFunc and the assertions. The
     RunFunc runs on a scheduler goroutine, and the announce loop schedules a
     second run after the teammate one, so the write could land while the test
     was reading — and the second run also closed an already-closed channel.
     Requests now arrive over a buffered channel: no shared state, and a second
     run cannot clobber the first.

  2. The deferred sched.Stop() ran while handleTeammateMessage's background
     goroutine was still calling Schedule, so Lane.Submit's wg.Add raced
     Lane.Stop's wg.Wait. The gateway drains BgWg before stopping the scheduler
     (gateway_consumer.go waits on it; sched.Stop is an outer defer in
     gateway.go); the test skipped that step. It now drains too, which also makes
     the test match the shutdown order it is meant to represent.

Reproduced before the fix with `-race -count=60 -cpu=1,4` (fails within a few
iterations) and clean afterwards over `-count=200 -cpu=1,2,4`, plus the whole
cmd package under -race.

Nothing in production changes; `Stream: true` and the channel-manager assertion
are untouched.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 09:01:31 +07:00
yatulandClaude Fable 5.1 d98ea89e3b feat(teams): show the full task UUID in the task detail dialog with one-click copy (#1560)
The short identifier (T-015-cc8e) carries only the last four hex chars
of the UUID, while every agent-facing surface — the team_tasks tool
(get / retry / cancel / comment) and the teams.tasks.* RPCs — takes the
full UUID. A human looking at the dashboard therefore had no way to name
a task to the lead in chat. Show the UUID next to the identifier and
status badges, copy it to the clipboard on click, with a short "copied"
confirmation.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 00:00:47 +07:00
fchengyan 0fef6ff596 fix(providers): skip temperature for base gpt-5 and gpt-5-chat models (#1555)
OpenAI rejects non-default temperature on gpt-5 / gpt-5-chat with
HTTP 400 ("Unsupported value: 'temperature' does not support 0.7
with this model. Only the default (1) value is supported.").

skipTemp in buildRequestBody only covered gpt-5-mini / gpt-5-nano and
the o-series, so base gpt-5 and gpt-5-chat fell through and received
config.DefaultTemperature (0.7). On channel integrations the inbound
error is suppressed, leaving the agent silently dead.

The gpt-5.X flagship variants (gpt-5.1, gpt-5.4, gpt-5.5) keep their
temperature — only the base gpt-5 and gpt-5-chat aliases are locked
to the default.

Regression cases added for gpt-5, gpt-5-chat, gpt-5-chat-latest and
provider-prefixed forms; gpt-5 moved to the locked list in
TestBuildRequestBody_TemperatureKeptForNonReasoningModels per the
maintainer triage on #1460.

Fixes #1460
2026-09-08 10:31:37 +07:00
yatulandClaude Opus 5 0e3eb57317 fix(delegate): add action=list so a lost delegation ID is recoverable (#1546)
* feat(delegate): add action=list, scoped to the originating chat

Fixes #1545. A delegation result was addressable only by the UUID returned once
in a tool result, which the calling model had to carry forward by hand. One
mistyped character orphaned a completed, durably stored result with no way back:
`get` answers "delegation result not found", and there was nothing else to ask.
Observed in production with a 31B-class caller — one flipped character, and
separately a splice of the previous delegation's tail onto the next one's prefix.

`spawn`, the sibling async mechanism over the same table, has had list/wait/cancel
all along; `delegate` had delegate/get.

Scope is tenant and calling agent, as get already resolves, plus the origin chat.
The chat rather than the session, for three reasons:

  - It survives a session reset. Deferring long work, clearing the context and
    coming back to ask for status is ordinary use; a SessionKey predicate would
    return nothing exactly then — when the handle is most likely already lost.
  - It keeps chats apart, which is the enumeration boundary #1525 is about: there
    spawn's list filters on the parent agent key alone and ignores the session,
    so one chat reads another chat's task text.
  - It does not carry a conversation between chats. A delegation raised in a team
    chat stays visible in that team chat and does not surface in someone's DM
    with the same agent. History stays where it began.

In a direct chat that separates users as well, since the chat ID is per person.
Group chats deliberately show the group what the group started.

get is left as it was, deliberately. #1525 is an enumeration defect — no prior
knowledge needed and task text is disclosed. get is access through an unguessable
handle, and adding a predicate there would break fetching a result by an ID kept
across a reset, which is the very failure this fixes.

No schema change: the origin fields are already persisted by
createDelegateCompletion. ListByParent is filtered in Go behind a cap of 20,
which suits handle recovery; a dedicated predicate would be the next step if this
ever needs to page.

Tests pin the chat boundary, the session-reset case, refusal when there is no
chat to scope to (without querying the store), and the cap. The fake store leaves
ListBySession embedded and nil, so a refactor back to session scoping panics
rather than passing quietly.

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

* fix(delegate): list needs its own store query — ListByParent excludes delegations

The action shipped in 506ecba4 always returned an empty list. It read through
SubagentTaskStore.ListByParent, whose SQL carries

    AND COALESCE(metadata->>'completion_kind', 'subagent') <> 'delegate'

ListBySession carries the same clause. Both serve spawn and filter delegations
out on purpose, so no listing in the store could return one — only Get by ID
reaches a delegation. The feature was a no-op in production while its unit tests
were green, because the fake store returned whatever rows the fixture supplied
and never reproduced the predicate that does the damage.

Found by running it against a live cluster: an async delegation was created,
`get` returned it completed with its result, and `list` reported zero.

Adds ListDelegationsByChat to the interface and to both implementations, with
the inverse predicate plus origin_chat_id, and points the tool at it. Chat scope
and delegate-only selection now live in the query rather than in a Go filter over
whatever the store happened to return; an empty chat yields no rows instead of
falling back to everything.

Tests are where the fix matters most:

  - internal/store/sqlitestore exercises the real SQL. It pins that
    ListDelegationsByChat returns the chat's delegation, that a spawn in the same
    chat is not one, that another tenant's identically named chat stays invisible,
    and — the part that would have caught this — that ListByParent and
    ListBySession still do not return delegations, so the complementarity is
    documented rather than assumed.
  - the tool's fake now panics if ListByParent or ListBySession is called, so a
    regression to either fails loudly instead of quietly listing nothing, and it
    applies the chat and kind predicates itself so fixtures behave like the store.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 00:31:01 +07:00
Clark Cant ef4a265832 Merge pull request #1553 from aikiata/fix/openai-stream-toolcall-index-panic
fix(providers): iterate tool-call accumulators by map, not index
2026-09-07 07:10:14 +07:00
aikiata db9600054c fix(providers): iterate tool-call accumulators by map, not index
ChatStream parsed the tool-call accumulator map with a positional loop
(for i := 0; i < len(accumulators); i++ { acc := accumulators[i] }),
assuming OpenAI-compatible providers always emit contiguous zero-based
tc.Index values in streamed delta.tool_calls.

Some OpenAI-compatible gateways (observed with a self-hosted point-p1
proxy) emit non-contiguous or non-zero-based indices. When that
happens accumulators[i] misses a populated key and returns a nil
*toolCallAccumulator, and the next line dereferences acc.rawArgs on
that nil pointer, causing a SIGSEGV panic. The panic kills the
in-flight chat run's goroutine; the gateway's tracing safety-net logs
'finalizing orphan trace' and the user's message goes unanswered.

Fix: range over the accumulators map directly instead of indexing by
position, and skip nil entries defensively.

Verified: built a full backend image (make build-full equivalent,
CGO_ENABLED=0 go build -tags embedui) from this branch, deployed it
against a real point-p1-backed agent, and confirmed multiple
multi-tool-call turns (use_skill, read_file, exec x3) that previously
triggered the panic now complete via v3.run.completed with no panic
and no safety-net trace.
2026-09-06 23:20:17 +07:00
isaacgao4396andisaacgao4396 169e0bafaf fix(tools): use_skill inlines content when read_file isn't granted (#1547)
* feat(store): add context helper for per-iteration tool allowlist

* feat(pipeline): surface resolved tool allowlist to tool dispatch via context

* fix(tools): use_skill inlines content when read_file isn't granted

Fixes #1477

---------

Co-authored-by: isaacgao4396 <gaoyuan4396@gmail.com>
2026-09-03 18:13:56 +07:00
Clark Cant e0792f56e0 Merge pull request #1549 from fcy222fcy/fix/pip-bsp-flag-956
fix(skills): retry pip installs without --break-system-packages when unsupported
2026-09-03 13:16:06 +07:00
fchengyan a1e550f0f0 fix(skills): retry pip installs without --break-system-packages when unsupported
pip >= 23.0 added the PEP 668 --break-system-packages flag. On older pip
builds (e.g. macOS Command Line Tools Python) and on the pip `list`
subcommand of every version, the flag is rejected with
"no such option: --break-system-packages", breaking skill dependency
install/check paths entirely.

- dep_installer: route pip installs through pipRunInstall, which retries
  once without the flag when pip rejects it (issue #956)
- pip_update_executor: same retry for the upgrade path
- pip_update_checker: drop the flag from `pip3 list --outdated` - list is
  read-only, PEP 668 never applies, and no pip accepts the flag there
- add pip_flags.go helpers + unit tests, and legacy-pip integration tests
  reproducing the original failure

Fixes #956
2026-09-03 10:18:12 +08:00
Clark Cant 8331211abf Merge pull request #1533 from yatul/fix/teammate-run-streaming
fix(teams): stream teammate runs so long generations keep the connection alive
2026-09-01 06:41:54 +07:00
yatulandClaude Opus 5 e1674b0249 fix(teams): dispatch team tasks created by detached child runs (#1528)
Team tasks are not dispatched inline: the turn's PendingTeamDispatch
collects them and the post-turn drain assigns and dispatches them once
the turn ends. That same tracker also owns the per-(team, chat) create
lock taken by team_tasks list/search.

Async delegations and async spawns keep the caller's context values
(context.WithoutCancel) but run detached — after the caller's turn has
already ended and drained. A team lead reached through delegate therefore
adds every task it creates to a tracker nobody will drain again, and
takes a create lock nobody will release. Two symptoms follow:

- tasks stay pending forever and are never dispatched, so the team never
  starts work. The task ticker does not cover this: it never dispatches
  pending tasks, it only marks them stale after 2h and asks the lead to
  retry by hand.
- the next team_tasks list/search for the same (team, chat) blocks on the
  never-released mutex for the remaining lifetime of the process.

Give each detached child run its own tracker through the existing
InjectTeamDispatch helper, drained when that run ends. The synchronous
delegate/spawn paths are deliberately left alone: they execute inside the
caller's turn, where the caller's tracker is the correct owner.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 03:43:55 +07:00
HaiDuongandmor-phongdt b39f0decb9 fix(skills): five defects in slash activation, grants, subagent tool policy, preview and import (#1534)
* fix(agent): filter skill slash commands by the agent's grants

The inline <available_skills> block was already filtered by visibility and
agent grants, but slash activation read the loader's full skill list. A
/<slug> command could therefore activate a skill the agent was never granted,
and the not-found suggestions disclosed that such skills existed.

Resolve slash commands against the agent's allow list via FilterSkills. The
list is filtered before matching, so /list-skills, /help and the near-match
suggestions are all covered by the same change.

The allow-list convention is unchanged: nil means every skill (the fallback
when the access store errors), an empty slice means none, and a populated
slice is an explicit set of slugs.

* fix(agent): split slash commands on any whitespace

The tokenizer separated the skill name from the rest of the message with
strings.Cut(after, " ") — a literal space. A user who typed the command and
pressed Enter before the rest of the message sent "/ck:git\nreview the diff",
which parsed to the target "ck:git\nreview" and matched no skill. The reply
was "skill not found" followed by near-matches that included the skill they
had just named, because the similarity fallback searched the mangled string
and still landed beside it. Multi-line messages are ordinary in Slack and
Telegram, so this was reachable in normal use.

Introduce cutFirstField, which splits on the first run of whitespace, and use
it everywhere the literal-space split appeared. The match loop had the same
assumption in strings.HasPrefix(raw, value+" "); hasFieldPrefix replaces it
and decodes the following rune properly, so a multi-byte skill name is not
truncated. That also stops a plain string prefix from matching: a command for
"frontend-design-extra" no longer resolves to "frontend-design".

* fix(tools): inherit the parent agent's tool policy in spawned subagents

buildSubagentToolsRegistry clones the parent registry, then overwrites exec,
read_file, write_file and list_files with freshly constructed tools. A fresh
tool carries none of the hardening the gateway applies to the parent's
instances at startup: exec path denials and their exemptions, the shell
deny-group toggles, the command keyword allowlist, and the read/write/list
deny prefixes covering config.json, the internal databases and delegate/.

Spawning a subagent therefore widened what an agent could reach — the parent
was blocked from the data dir, the subagent was not. Verified by disabling
the new call: the subagent's exec read config.json out of the denied data
dir, read_file did the same, and write_file overwrote it.

Copy the policy from the live parent instances rather than re-deriving it, so
there is one source of truth and a deny path reloaded later through config
pub/sub reaches subagents without a second wiring site that can drift.

Allow-prefixes are inherited alongside the denials: they are what make the
denied roots usable at all, since the skills store sits under the denied data
dir. Copying denials without them would leave a subagent unable to read the
skills it is told to use.

* fix(agent): use one inline-vs-search decision for prompt and preview

The system-prompt preview exists to show the prompt an agent actually gets,
but it decided between inline skills and search mode on its own terms: it
counted tokens with the fallback counter over the fully rendered XML — tags
and <location> paths included, at roughly runes/2 — while the prompt builder
estimated name+description at chars/4. On the same eighteen skills that read
3087 against 944, so the preview reported search mode for an agent that was
running inline.

Extract shouldInlineSkills and call it from both paths. The estimate keeps
the prompt builder's rule, mirroring BuildSummary's 200-rune description
truncation, so the decision still costs no rendering.

The preview's loader interface gains FilterSkills to feed it. Its behaviour
on an access-store error is unchanged: an empty allow list yields no skills
rather than falling back to showing every skill.

* fix(http): keep reference files when importing skills

Export archives the whole skill directory, but import recognised only
metadata.json, SKILL.md and grants.jsonl. The switch had no default, so every
other file was discarded without a log line. A skill whose SKILL.md cites
references/*.md arrived without them, the import reported success, and the
skill failed at the first read.

Collect the remaining entries and write them under the skill directory with
their structure intact. Archive entry names are attacker-controlled, so each
path goes through sanitizeRelPath: a traversal attempt collapses to a
relative path and the write stays inside the skill directory.

* test(agent): make the inline-decision table exercise both gates

The "100 skills, 200-char descriptions" case claimed to cover the token
ceiling, but 100 exceeds the count ceiling so the count check short-circuited
and the token branch never ran — deleting that branch would not have failed
the test.

Split the table so each case isolates one gate: tiny descriptions for the
count rows, a count safely under the ceiling for the token rows. Removing the
token comparison now fails the over-the-ceiling case and nothing else.

* fix(agent): gate slash commands on the managed tier only

The allow list comes from a query over the `skills` table, so filesystem-tier
skills — the workspace, .agents and ~/.agents directories of the five-tier
loader — have no row in it. Filtering every skill against the list made those
four tiers unreachable by slash while skill_search still found them
unfiltered, which turned a security fix into a functional regression.

Apply the list to managed skills only. Builtins are seeded with is_system and
returned unconditionally, so they stay reachable either way.

This closes slash activation of ungranted managed skills. It does not close
skill_search and use_skill, which still read the loader's full list; that is a
wider change and wants its own review.

* fix(http): stop imported skill files from replacing guarded ones

Two ways the auxiliary write could go wrong.

GuardSkillContent scans SKILL.md before anything reaches disk, but the switch
that routes archive entries compares the raw path. An entry named
"./SKILL.md" does not match it, so it fell through to the auxiliary set —
where sanitizeRelPath collapses it back to "SKILL.md" and the write replaced
the file that had just been scanned. Reject any auxiliary path that resolves
to a name the import handles by itself, and log the attempt.

Tar directory entries carry a trailing separator and no content. Written as
files they took the name a real directory needed, so everything beneath was
dropped — and whether that happened depended on map iteration order, making
it intermittent. Skip them; MkdirAll creates what is needed.

Also cap the number of auxiliary files per skill and log when the cap trims
an archive, so a truncated import is visible rather than silent.

---------

Co-authored-by: mor-phongdt <phong.dangtuan@mor.com.vn>
2026-09-01 02:42:47 +07:00
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
yatulandClaude Opus 5 e7e3f25eeb test(teams): pin the teammate streaming contract
Review on #1533 asked for coverage of the behavioural contract rather than
the field assignment, and that is the right call — the PR text arguing a test
would only restate the assignment was wrong, because the contract has two
halves that a refactor could break independently.

The test drives handleTeammateMessage through a real scheduler and asserts
both: the scheduled run requests streaming, and the run is not registered with
the channel manager, so its chunks cannot be routed to a user. It fails with
Stream reverted to false.

The remaining half — a streamed call still yielding the complete final response
— is already covered in internal/agent/loop_pipeline_callbacks_test.go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:09:12 +04:00
yatulandClaude Opus 5 34a1540c18 fix(teams): stream teammate runs so long generations keep the connection alive
handleTeammateMessage pinned Stream: false, so every team member run — coder,
reviewer, researcher — made a non-streamed provider call. On a slow reasoning
model that means a silent connection for the whole generation, and
ResponseHeaderTimeout eventually kills it:

    iter 0 think: llm call: litellm: request failed:
      Post ".../v1/chat/completions": http2: timeout awaiting response headers

Observed on a member asked to write a large single-file page: 15 minutes, zero
output tokens, run failed. The same model over the same provider is fine on an
ordinary channel run, which streams by default.

Streaming here is for connection liveness, not delivery. A teammate run is never
registered with the channel manager, so HandleAgentEvent returns on its first
line and the chunks are dropped; nothing reaches a user incrementally. The task
result is unchanged — it still comes from the final RunResult, since ChatStream
returns the complete response once the stream ends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 22:03:41 +04: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