Commit Graph
55 Commits
Author SHA1 Message Date
2fee42dc32 fix: handle ignored errors, unsafe type assertions, missing panic recovery (#854)
* fix: handle ignored errors, unsafe type assertions, missing panic recovery

- Cron scheduler (PG + SQLite): check all ExecContext errors in
  recomputeStaleJobs, run log insert, job delete, and post-run update.
  Previously these errors were silently discarded, which could leave
  job state inconsistent without any log trace.

- Discord: use comma-ok type assertions on sync.Map placeholder loads
  to prevent potential panics from bare type assertions.

- Slack: use comma-ok type assertions in sweepMaps for dedup and
  thread participation eviction to prevent potential panics.

- Feishu: add safego.Recover to WebSocket goroutine so a panic in
  the WS client doesn't silently kill the goroutine.

- Agent export: add tenant owner/admin permission check to canExport.
  Previously only agent owner and system owner could export — tenant
  admins were incorrectly denied.

- Channel health: use errors.Is/errors.As for context.DeadlineExceeded,
  net.DNSError, and net.OpError before falling back to string matching.
  DNS NXDOMAIN is now correctly classified as non-retryable.

* fix(review): revert export to system-only + add missing rows.Err check

- Revert canExport tenant role check — export/import is restricted to
  agent owner and system owner by design
- Add rows.Err() check after recomputeStaleJobs loop in PG cron
  (parity with SQLite implementation)

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-13 21:29:11 +07:00
c06d9d9705 fix: UI bug fixes — settings, cron, MCP, WS, events (#855)
* fix: UI bug fixes — team settings, cron, MCP, WS reconnect, events, config

Critical:
- Preserve team settings version field to prevent v2→v1 downgrade
- Add blocker_escalation + slow_tool to BE whitelist struct
- Cron update only sends enabled when actually changed
- MCP form no longer strips mcp_ prefix from tool_prefix on edit
- Prevent duplicate pairing modals on WS reconnect

Medium:
- Fix events category filter using wrong filtered source
- Fix config behavior section leaking masked "***" token
- Fix agent WS fallback mapping wrong status/type values
- Fix offset=0 silently dropped in activity/vault/episodic hooks
- Fix channel/provider/cron dialog useEffect missing deps
- Fix mobile sidebar not closing on programmatic navigation
- Fix WS role not cleared on disconnect, auth failure infinite loop
- Fix SearchInput spurious calls from unstable onChange ref
- Fix pending-messages poll/timeout not cleaned up on unmount
- Fix SummaryBlock "Show more" button never appearing
- Fix session delete double-navigate, channel delete not awaited
- Fix skill view passing slug instead of name
- Fix storage listFiles silently swallowing errors
- Fix graph search not early-terminating on large graphs

Low:
- Remove unused sessionKey from handleAgentChange deps
- Use ROUTES.CHAT constant in sidebar
- Fix reconnect backoff off-by-one
- Fix cron advanced dialog stale form on prop update
- Clear vault search results on dialog close

* fix: address review findings — pairing guard reset, stale closure, search cleanup

- Reset pairingInProgress on disconnect() to prevent stuck pairing state
- Add initial to handleSave deps to prevent stale version closure
- Use handleOpenChange in vault search result select to clear stale results

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-04-13 08:53:26 +07:00
Luan VuandLuvu182 50871eaaae fix(cron): prevent scheduler loop from blocking when a job hangs (#820)
* fix(cron): prevent scheduler loop from blocking when a job hangs

The cron scheduler's runLoop calls checkAndRunDueJobs() every second,
which previously used wg.Wait() to block until ALL claimed jobs complete.
If any single job hung (LLM provider timeout, agent loop stuck, network
issue), wg.Wait() would block indefinitely, preventing the scheduler
from ever checking for new due jobs — effectively killing all cron
scheduling until a container restart.

Changes:
- Remove wg.Wait() from both PG and SQLite cron schedulers — jobs now
  run as independent goroutines that don't block the check loop
- Add panic recovery to PG runLoop (safeCheckAndRunDueJobs wrapper)
  and per-job goroutines, matching the existing safego.Recover pattern
  in the SQLite scheduler
- Add 10-minute context timeout to the cron job handler so a hung
  agent run is cancelled instead of blocking forever
- Use select with context.Done() in the handler to respect the timeout
  when waiting for the scheduler outcome
- Invalidate PG job cache per-job on completion instead of after the
  (now-removed) batch wait

The SQLite scheduler already had safego.Recover on job goroutines but
still used wg.Wait() — this commit removes that blocking wait as well.

* fix(cron): make job timeout configurable + add SQLite runLoop panic recovery

- Add `cron.job_timeout` config field (Go duration string, default "10m")
  so operators can tune the per-job timeout for complex agent workloads
  without code changes
- Add `safeCheckJobs` panic recovery wrapper to SQLite cron runLoop,
  matching the PG scheduler's `safeCheckAndRunDueJobs` for consistency
- Use dynamic timeout string in error message for better diagnostics

* fix: remove unused "time" import from gateway_cron.go

* fix(cron): apply same fixes to SQLite DB scheduler (sqlitestore)

The SQLite DB-backed scheduler (used by desktop edition with SQLite
backend) had the exact same wg.Wait() blocking issue and missing
panic recovery as the PG scheduler. Apply identical fixes:

- Remove wg.Wait() — jobs run as independent goroutines
- Add safeCheckAndRunDueJobs panic recovery wrapper for runLoop
- Add per-job panic recovery and cache invalidation

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-04-10 19:50:49 +07:00
Luan VuandLuvu182 3703355b56 fix(sqlite): reset stale 'running' cron jobs to 'interrupted' on startup (#797)
PG version already resets jobs stuck in 'running' state on startup
(from a crash mid-execution), but SQLite was missing this step.
Desktop app crashes would leave cron jobs permanently stuck in
'running' status, never executing again.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-04-10 07:33:37 +07:00
Luan Vu 292e63dfa3 fix(cron): prevent job flood after restart, interval drift, and SQLite RunJob bug (#796)
## Summary

- **Flood after restart**: `recomputeStaleJobs()` now advances past-due jobs (not just NULL) to their next future run time, preventing all missed jobs from firing simultaneously on first tick
- **Interval drift**: Anchor-based scheduling computes next run from original scheduled time instead of execution end time, preserving per-job offsets
- **O(1) advance**: Replaced O(N) loop with modular arithmetic to prevent CPU starvation after prolonged downtime with short-interval jobs
- **SQLite RunJob fix**: Added `next_run_at = NULL` claim + `reloadClaimed` param to match PG store behavior — manual runs were silently skipped
- **Manual RunJob consistency**: Nil-anchor guard ensures manual triggers use `now + interval` (not anchor) across all stores
- **Zombie at-job fix**: JSON store Start() now disables past-due one-time `at` jobs instead of leaving them enabled with nil NextRunAtMS

All fixes applied across JSON, PostgreSQL, and SQLite store implementations.
4 new unit tests covering flood prevention, anchor arithmetic, RunJob scheduling, and at-job disabling.
2026-04-09 23:40:46 +07:00
Luan VuandLuvu182 0e2282be8f fix: use errors.Is() for sentinel comparisons + remove unused @xyflow/react (#727)
Replace direct sentinel error comparisons (==, !=) with errors.Is()
across the codebase. Direct comparison breaks when errors are wrapped
with fmt.Errorf("...: %w", err), which can cause missed matches for
sql.ErrNoRows, io.EOF, context.DeadlineExceeded, and context.Canceled.

Affected packages:
- store/pg: sql.ErrNoRows in secure_cli, agents_export_team_*, mcp_export_queries
- store/sqlitestore: sql.ErrNoRows in schema migration
- tools: context.DeadlineExceeded in shell, credentialed_exec
- mcp: context.DeadlineExceeded in bridge_tool
- providers: context.Canceled in acp_provider, anthropic_stream_test
- updater: io.EOF in tar extraction

Also removes unused @xyflow/react dependency from web UI — it was
replaced by react-force-graph-2d but never cleaned up from package.json.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-04-07 12:16:45 +07:00
Luan VuandLuvu182 97f4942943 fix(channels): add display_name to session metadata for Feishu, Zalo, Slack (#536)
Sessions page shows raw IDs instead of sender names because these channels
didn't include display_name in their message metadata. The gateway's
extractSessionMetadata() already looks for this key but it was missing
from Feishu, Zalo Personal, and Slack message payloads.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-29 11:25:05 +07:00
Luan VuandLuvu182 be9044ca8e fix(zalo): update API version, batch group fetch, fix WS decrypt (#516)
- Bump DefaultAPIVersion from 665 to 671 to match current Zalo web client
- Batch getmg-v2 requests (50 groups/batch) — Zalo rejects large payloads,
  causing error 114 for accounts with many groups (e.g. 250+)
- Fix encryptType=1 WebSocket event decryption: add gzip decompression
  after base64 decode (matches zca-js pako.inflate behavior)
- Split contacts picker into Friends/Groups tabs for better usability

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-27 21:50:01 +07:00
Luan VuandLuvu182 9e74974129 fix: feishu reply context truncation + task events NULL data crash (#508)
* fix(feishu): increase reply context max length from 500 to 2000

500 bytes is too short for CJK/Unicode text (each accented char = 2-3
bytes, so ~250 real characters). Increase to 2000 so reply context is
not aggressively truncated.

* fix(teams): handle NULL data column in task events queries

ListTaskEvents and ListTeamEvents crash with "unsupported Scan,
storing driver.Value type <nil> into type *json.RawMessage" when the
data column is NULL. Use COALESCE(data, '{}') to return an empty JSON
object instead.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-27 13:42:31 +07:00
Luan VuandLuvu182 95ce4200dd fix(ui): session detail missing tool calls, memory dialog toast errors (#458)
1. Session detail: isDisplayable() filtered out assistant messages with
   only tool_calls or thinking (no text content). Live chat renders these
   via MergedToolGroup but session history silently dropped them.

2. Memory document dialog: load error showed "Failed to create document"
   (wrong i18n key toast.failedCreate). Save error used the same wrong key.
   Fixed to use toast.docLoadFailed and toast.docUpdateFailed respectively.
   Added missing docLoadFailed key to en/vi/zh locale files.

3. Memory dialogs: all 5 dialogs missing aria-describedby={undefined} on
   DialogContent, causing Radix UI console warnings about missing
   Description for accessibility.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-25 08:26:35 +07:00
Luan VuandLuvu182 d63a7d4ced fix(docker): auto-sync host Claude CLI credentials and show Docker-aware login instructions (#398)
When running in Docker, the Claude CLI provider setup showed `claude auth login`
which doesn't work inside a container. This change:

- Mounts host ~/.claude as read-only into the container
- Entrypoint syncs credentials to a writable volume (respects cap_drop: ALL)
- Backend detects Docker via /.dockerenv and returns `in_docker` in auth-status API
- UI shows `docker compose exec goclaw claude auth login` for Docker deployments

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-25 08:19:52 +07:00
Luan VuandLuvu182 0881d7fc4e docs: use make up in Docker quick start, add optional service flags (#404)
- Update README Docker section to use `make up` instead of raw
  docker compose commands. `make up` handles network creation,
  version embedding from git tags, and database migrations.

- Add WITH_* flags to Makefile for optional compose services:
  WITH_BROWSER, WITH_OTEL, WITH_SANDBOX, WITH_TAILSCALE, WITH_REDIS.
  Uses WITH_ prefix to avoid conflicts with common env vars (e.g.
  BROWSER is set by many desktop environments).

- Document common make commands (up/down/logs/reset) and optional
  service flags with descriptions in a table format.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-24 10:00:13 +07:00
4868f6c4d8 feat(gateway): add version update checker (#374)
* feat(gateway): add version update checker

Check GitHub releases periodically (1h) and surface update availability
in the health endpoint + dashboard header. Version is auto-detected from
git tags via Makefile, falling back to VERSION file or build arg.

Backend: new UpdateChecker goroutine, health response includes
latestVersion/updateAvailable/updateUrl fields.

Frontend: version badge in overview header with clickable "available"
link when a newer release exists.

* fix(gateway): fix update checker compile errors and harden GitHub API call

- Export UpdateChecker/NewUpdateChecker to match server.go references
- Move StartUpdateChecker(ctx) after ctx declaration in gateway startup
- Add User-Agent header for GitHub API compliance
- Limit response body to 1MB via io.LimitReader
- Use strconv.Atoi instead of manual int parsing in parseSemver

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-23 17:37:19 +07:00
394ded7836 fix(skills): parse YAML multiline blocks and lists in skill frontmatter (#325)
* fix(skills): parse YAML multiline blocks and lists in skill frontmatter

parseSimpleYAML and ParseSkillFrontmatter only handled single-line
key: value pairs. YAML multiline block scalars (|, >) and list values
(- item) were parsed incorrectly:

  description: |
    Multi-line text here...

was stored as description="|" (literal pipe character), and indented
continuation lines were misinterpreted as new key-value pairs.

Fix: parseSimpleYAML now tracks indented continuation lines after
block scalar indicators (|, >, |-, >-) and empty-value keys (for
list blocks). Continuation lines are joined with spaces. List items
(- value) have the prefix stripped.

ParseSkillFrontmatter now delegates to parseSimpleYAML instead of
duplicating the parsing logic.

* fix(skills): preserve empty-value keys in parseSimpleYAML

When a key has no value and no indented continuation (e.g. "slug:"),
flushBlock now stores "" instead of silently dropping the key.
Maintains backward compatibility with the previous parser behavior.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-21 08:58:21 +07:00
Luan VuandLuvu182 f3b344d731 fix(pkg-helper): fix apk-packages persist file not writable in Docker (#324)
The .runtime directory on the data volume may be owned by goclaw:goclaw
(from older images or Docker volume initialization). pkg-helper runs as
root but without CAP_DAC_OVERRIDE, so it cannot create files in
goclaw-owned directories. This caused persistAdd() to fail silently —
runtime-installed system packages (bash, pandoc, etc.) were lost on
container recreate.

Fix: set .runtime directory ownership to root:goclaw (mode 0750) so
pkg-helper can write apk-packages while goclaw can still traverse.
Three layers for robustness:

- Dockerfile: pre-create .runtime with correct split ownership in image
- docker-entrypoint.sh: fix ownership on existing volumes (upgrade path)
- pkg-helper: self-healing ensurePersistDir() at startup as defense-in-depth

Subdirs (pip/, npm-global/, pip-cache/) remain goclaw-owned since those
are written by the app process, not pkg-helper.

Fixes #323

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-21 08:35:31 +07:00
ebc4a22fd2 fix(skills): use DB file_path for directory resolution + list archived skills (#322)
* fix(skills): use DB file_path for skill directory resolution + list archived skills

Two fixes:

1. Skills store methods (ListSkills, ListAllSkills, GetSkill, GetSkillByID,
   LoadSkill) constructed BaseDir from s.baseDir + slug + version, ignoring
   the file_path column stored in the database. When GOCLAW_DATA_DIR differs
   from the directory used during initial skill seeding, this caused
   dependency rescans to look in the wrong directory, find no scripts, and
   silently skip — leaving missing_deps stale in the database even after
   successful pip/npm installs.

   Fix: buildSkillInfo now accepts an optional filePath parameter from the
   DB and uses it as the authoritative BaseDir when set.

2. ListSkills only returned skills with status='active', hiding archived
   skills from the admin UI. Skills with missing dependencies are archived
   on seed, making them invisible — admins couldn't see them to install
   deps and reactivate. Now includes archived skills in the listing.

* fix(skills): use 'deleted' status for soft-delete, distinct from 'archived'

DeleteSkill previously set status='archived', which is also used for skills
with missing dependencies. Now that ListSkills includes archived skills
(so admins can see and fix missing deps), soft-deleted skills were
reappearing after page refresh.

Use status='deleted' for user-deleted skills to keep them hidden while
allowing archived (missing deps) skills to remain visible in the admin UI.

* fix(skills): complete file_path resolution + exclude deleted from rescan

- ListAllSkills: add `status != 'deleted'` to prevent rescan from
  resurrecting user-deleted skills
- ListAccessible: add file_path to SELECT so runtime skill resolution
  uses DB path instead of stale baseDir
- SearchByEmbedding: use DB file_path when available for path resolution
- CreateSkillManaged: ON CONFLICT visibility reset now matches both
  'archived' and 'deleted' statuses
- Update delete result message and status comment

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-21 08:00:39 +07:00
Luan VuandLuvu182 19d34aacec fix(mcp): add empty properties to object schemas for OpenAI compatibility (#321)
MCP tools that accept no parameters return {type: "object"} without a
properties field, which is valid per JSON Schema spec. However, OpenAI
API strictly requires "properties" on object schemas and rejects
requests with HTTP 400: "object schema missing properties".

Add default empty properties map when type is "object" and the MCP
server provides none.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-21 07:35:24 +07:00
c6bab9a059 fix(codex,memory): sanitize tool call IDs, fix memory delete, add GPT-5.4 (#313)
* fix(codex,memory): sanitize tool call IDs, fix memory delete, add GPT-5.4

- codex_build: sanitize colons in tool call IDs before sending to
  Responses API (OpenAI rejects non-alphanumeric chars except _ and -)
- memory_docs: check RowsAffected on delete — return error instead of
  silent 200 when no document matched
- memory UI: pass document's agent_id to delete call so deletion works
  from the "all agents" view (previously sent undefined as agent ID)
- codex models: add GPT-5.4 and GPT-5.4 Mini, update default model

* fix(codex): update default model assertion in test to gpt-5.4

* fix(codex): regex sanitize all invalid chars in tool call IDs, restore GPT-5.1 models

- toFcID: replace all non-alphanumeric/underscore/dash chars (not just
  colons) to prevent future Responses API rejections
- Restore GPT-5.1 Codex Max and Codex Mini in model dropdown to avoid
  breaking existing user configurations

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-20 21:57:30 +07:00
2c1efa29ec fix(memory): prevent silent data loss on memory file overwrites (#279)
* fix(memory): auto-backup + overwrite warning for memory files

When an agent writes to a memory file that already has content:
1. Old content is backed up to memory/.prev/{filename} (best-effort)
2. Write succeeds normally (no blocking)
3. Tool result includes warning with previous content (truncated to
   4000 chars) so the agent can notice and merge if needed

Backup files are hidden from list_files and not indexed/embedded.
No database migration needed - uses existing PutDocument.

* fix(memory): support append mode + fix UTF-8 truncation in overwrite warning

- Add appendMode parameter to MemoryInterceptor.WriteFile() so
  write_file(append=true) actually appends to memory files instead
  of silently replacing content. Uses "\n\n---\n\n" separator.
- Fix UTF-8 truncation in overwrite warning: use []rune instead of
  byte slicing to avoid cutting multi-byte characters.
- Fix char count in warning message: use len([]rune()) not len().
- Remove .prev/ backup mechanism (prevBackupPath, isPrevPath, backup
  writes, ListFiles filter) in favor of simpler approach: append
  mode prevents data loss, warning enables self-correction.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-19 17:42:03 +07:00
bb783b952c feat(feishu): add list_group_members tool + native @mention support (#280)
* feat(feishu): add list_group_members tool + native @mention support

Add agent tool to list all members in a Lark group chat via
GET /open-apis/im/v1/chats/{chat_id}/members with auto-pagination.

- New tool `list_group_members`: returns member IDs + display names
- Native @mention conversion: @ou_xxx in outbound messages becomes
  real Lark mentions (post: "at" element, card: <at> markdown tag)
- Auto-sync discovered members into ContactStore
- GroupMemberProvider interface on Channel for extensibility
- Add im:chat.members:read scope to channel setup UI

Requires Lark app scope: im:chat.members:read

* fix(feishu): URL-encode API params + channel-aware tool filtering

- URL-encode chatID (PathEscape) and pageToken (QueryEscape) in
  ListChatMembers to prevent path traversal and query corruption
- Add slog.Warn when ListGroupMembers fails for observability
- Add ChannelAware interface so tools can declare required channel types
- ListGroupMembersTool now declares RequiredChannelTypes=["feishu"]
- loop.go filters out channel-specific tools when channel type mismatches,
  so list_group_members only appears for Feishu conversations

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-19 17:21:38 +07:00
Luan VuandLuvu182 44197011f8 feat(feishu): include reply context when user replies to a message (#272)
When a Feishu/Lark user replies to an existing message, the agent now
sees the quoted message content via the GetMessage API, formatted as
[Replying to SenderName]...[/Replying] — matching the Telegram channel
behavior. Previously parent_id was parsed but the content was silently
discarded, so the agent had no idea what the user was replying to.

Requires im:message:readonly scope on the Lark app.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-19 14:03:09 +07:00
0dd4ebd6e6 fix(channels): collect contacts for DM and group-mentioned messages (#271)
* fix(channels): collect contacts for DM and group-mentioned messages

Previously, EnsureContact was only called inside the "not mentioned →
record history → return" branch, so contacts were never saved for
messages that the bot actually processed (DMs and group @mentions).
This caused the contacts list to show empty names ("—") for active
users across all channels.

Move contact collection to the main processing path (before
PublishInbound / HandleMessage) so every processed message upserts
the sender's display name. The existing ContactCollector 30-minute
cache prevents redundant DB writes.

Affected channels: Feishu, Telegram, Discord, Slack, Zalo, WhatsApp.

* fix(slack): use clean userID for EnsureContact cache key

The senderID param in HandleMessage is the compound "U123|DisplayName"
form, which produces a different cache key than the clean "U123" used
in the group-history path (handlers.go:178). Use userID (extracted
clean ID) to ensure cache dedup works correctly.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-19 14:02:19 +07:00
Luan VuandLuvu182 a38d972438 fix: agent update API missing is_default, budget, subagents fields (#270)
The HTTP allowlist (agentAllowedFields) was missing three fields that
the web UI sends when saving agent settings: is_default,
budget_monthly_cents, and subagents_config. These fields were silently
filtered out by filterAllowedKeys(), so saves appeared successful but
values never persisted to the database.

Also adds exclusive-default logic: when setting is_default=true on an
agent, any previously-default agent is automatically unset first.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-19 13:59:49 +07:00
Luan VuandLuvu182 73897b9481 refactor: replace magic numbers and hardcoded strings with named constants (#269)
Extract scattered magic numbers and hardcoded agent type strings into
centralized named constants to improve maintainability and prevent
silent inconsistencies when defaults need to change.

Changes:
- Add config/defaults.go: DefaultContextWindow, DefaultMaxTokens,
  DefaultMaxMessageChars, DefaultMaxIterations, DefaultTemperature,
  DefaultHistoryShare
- Add providers/defaults.go: DefaultHTTPTimeout, SSE/Stdio scanner
  buffer size constants
- Replace 6 hardcoded 200000 (context window) across agent/, http/,
  cmd/ with config.DefaultContextWindow
- Replace 4 hardcoded 32000/32_000 (max message chars) with constant
- Replace 3 hardcoded 20 (max iterations) with constant
- Replace 2 hardcoded 0.75 (history share) with constant
- Replace 3 hardcoded 300*time.Second (HTTP timeout) with constant
- Replace 5 hardcoded scanner buffer sizes with named constants
- Replace 10 hardcoded "predefined" string comparisons with
  store.AgentTypePredefined constant (already defined but unused)
- Use config.DefaultTemperature instead of hardcoded 0.7 in agent loop

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-19 13:59:32 +07:00
Luan VuandLuvu182 f21bf81280 fix: null-safe access on changes array in events page (#274)
Go can serialize empty slices as null in JSON. The events page
accessed p.changes.length without null checking, causing a crash
when viewing team.updated or agent_link.updated events with null
changes arrays.

Fixes #192

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-19 12:22:18 +07:00
Luan VuandLuvu182 ba9b5a6be3 feat(ui): show required API scopes for Feishu/Lark channels (#268)
* feat(ui): show required API scopes for Feishu/Lark channels

Add a collapsible info panel listing the required Lark/Feishu API
permissions (scopes) on the channel create/edit dialog and config
detail tab. Includes reminder about Contact Range and app publishing.

* fix(feishu): annotate DM messages with sender name

Feishu DMs were missing the [From: ...] annotation, so the agent
couldn't identify who was messaging. Group messages already had this.
Align with Telegram channel which annotates both DM and group messages.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-19 08:47:48 +07:00
Luan VuandLuvu182 b379c3ba30 fix(feishu): align WebSocket protocol with Lark SDK + fix incorrect UI labels (#263)
Backend — WSClient protocol fixes (larkws.go):
- Parse service_id from WS URL query params instead of hardcoding 0
- Update all 4 server config values from pong payload (PingInterval,
  ReconnectCount, ReconnectInterval, ReconnectNonce)
- Use server-configured reconnect params instead of hardcoded 120s wait
- Return HTTP 500 in ACK when event handler fails (enables Lark retry)
- Filter data frames by type header — only process "event" frames
- Report actual processing time in biz_rt header (was hardcoded "0")

Backend — event adapter (feishu.go):
- Return parse error from HandleEvent so ACK reflects failure status

UI — fix incorrect Feishu channel form labels:
- Remove "webhook only" from Lark Global domain label (WebSocket works
  on both Lark Global and Feishu China)
- Remove "Feishu only" from WebSocket option label
- Change default connection_mode from "webhook" to "websocket" (matches
  backend default)
- Add showWhen conditional field support to ChannelFields component
- Hide webhook_port, webhook_path, encrypt_key, verification_token when
  WebSocket mode is selected
- Update i18n labels in all 3 locales (en, vi, zh)

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-18 17:51:09 +07:00
Luan VuandLuvu182 a7f5acc1e3 fix(security): harden SQL injection, MCP prompt injection, sandbox fallback, and file serving (#246)
- execMapUpdate: validate column names with strict regex to prevent SQL injection
- HTTP update handlers: add field allowlists (agents, providers, custom_tools, mcp, channel_instances)
- pqStringArray: properly escape array elements to prevent PostgreSQL array literal injection
- scanStringArray: handle quoted elements in PostgreSQL array format
- MCP bridge: wrap tool results as external/untrusted content to prevent prompt injection
- File serving: block access to sensitive system directories (/etc, /proc, /sys, etc.)
- Sandbox: fail closed when Docker unavailable instead of silent fallback to host
- Shell deny: fix base64 --decode bypass, add host exec 1MB output limit
- ILIKE queries: escape % and _ wildcards in knowledge_graph, custom_tools, channel_instances

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-18 07:42:38 +07:00
Luan VuandLuvu182 8cbd30f1ab fix(channels): harden error handling and type safety across Telegram, Zalo, Feishu (#247)
Telegram:
- Fix context cancellation in reaction timer callbacks — use context.Background()
  so emoji reactions still work after request context is cancelled
- Add comma-ok safety on sync.Map type assertions in OnReactionEvent/ClearReaction

Zalo:
- Add comma-ok safety on sync.Map type assertions for typing controllers
- Validate non-empty senderID before processing text/image messages

Feishu/Lark:
- Handle json.Unmarshal errors in all 7 LarkClient messaging methods instead of
  silently returning empty values
- Add 30s HTTP timeout for WebSocket endpoint request (was using DefaultClient)
- Eliminate double decryption in webhook handler — cache first result
- Replace goroutine+sleep with time.AfterFunc for dedup cleanup
- Add comma-ok safety on pairing debounce type assertion
- Use json.Marshal instead of fmt.Sprintf for image/file key JSON construction

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-18 07:39:03 +07:00
Luan VuandLuvu182 b73f66d99b fix(tools): make MessageTool media path resolution workspace-aware (#169)
MessageTool.parseMediaPath() was hardcoded to only allow files in /tmp/,
while all other filesystem tools (read_file, write_file, edit, exec) use
workspace-aware resolvePath() with restrict_to_workspace enforcement.

This meant agents could create files in their workspace via write_file
but couldn't send them as attachments — only /tmp/ files from
create_image/create_audio worked.

Replace parseMediaPath() with resolveMediaPath() that:
- Reuses resolvePath() for consistent security (symlink, hardlink, traversal)
- Honors per-agent workspace + restrict_to_workspace from context
- Still allows /tmp/ as fallback (for create_image, create_audio, etc.)
- Supports relative paths resolved against workspace
- Updates tool description so LLM knows about MEDIA: prefix

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-12 20:43:52 +07:00
Luan VuandLuvu182 b488ef44d6 fix: media tag enrichment, Gemini file polling, credential merge (#158)
1. Media tag enrichment (audio/video/document):
   - Add enrichVideoIDs() — video media_id was never injected into
     <media:video> tags, causing LLM to hallucinate UUIDs
   - Fix all enrich functions to replace the LAST bare tag instead of
     the first. When group history prepends older media tags, the first
     occurrence belongs to history — injecting the current turn's ID
     there causes the LLM to reference the wrong file

2. Gemini File API polling:
   - Upload response returns fileURI immediately but file may still be
     in PROCESSING state. Check state field; only skip polling when
     file is already ACTIVE. Fixes "not in an ACTIVE state" errors

3. Channel instance credential merge:
   - Partial credential updates (e.g. updating just token) now merge
     with existing credentials instead of wiping other fields
   - Loads, decrypts, merges, re-encrypts in a single Update() call

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-12 09:35:04 +07:00
Luan VuandLuvu182 e5eca5d0f0 feat(telegram): support custom Bot API server URL (#157)
* feat(telegram): support custom Bot API server URL

Add api_server credential field for Telegram channels to allow
using a self-hosted Telegram Bot API server (e.g. for 2GB upload
limits). Uses telego.WithAPIServer() option.

* feat(telegram): local Bot API media download + config improvements

- Support local Bot API --local mode: detect absolute file paths and
  copy directly from filesystem (requires shared volume mount)
- Fall back to HTTP download via custom API server URL for non-local mode
- Move api_server/proxy from credentials to config (non-secret fields);
  credentials still accepted for backward compat
- Add media_max_mb config field (friendlier than media_max_bytes);
  deprecated media_max_bytes still works
- Default to 200 MB max when local Bot API is configured (vs 20 MB cloud)
- UI: move api_server/proxy to config section, use MB for media size

* fix: use 127.0.0.1 instead of localhost in API server placeholder

Docker containers cannot resolve localhost to the host machine.
127.0.0.1 works reliably with Docker's default bridge network.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-12 09:29:14 +07:00
Luan VuandLuvu182 7386fc8ad7 fix: read_audio fails when LLM passes literal <media:audio> as media_id (#156)
The read_audio tool errors with "audio not found" because:
1. BuildMediaTags() creates bare <media:audio> tags without IDs
2. persistMedia() generates UUIDs stored in context, not in message content
3. LLM sees <media:audio> and passes it literally as media_id parameter
4. resolveAudioFile() tries to match "<media:audio>" against UUIDs — fails

Two-part fix:
- enrichAudioIDs(): embed persisted UUIDs into <media:audio> tags (like
  enrichDocumentPaths does for documents), so LLM sees actual IDs
- resolveAudioFile(): sanitize tag-like media_id values and fallback to
  most recent audio instead of hard error on unmatched IDs

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-11 19:57:15 +07:00
Luan VuandLuvu182 b5b40e7d94 fix(cron): auto-default deliver=true when job created from channel chat (#155)
When users create cron jobs from channel chats (Zalo, Telegram, etc.),
the LLM often omits the `deliver` flag (defaults to false). This causes
cron jobs to run successfully but silently discard results — the response
never reaches the chat where the user requested it.

Auto-detect channel context and default deliver=true for real channels,
skipping internal channels (cli, system, subagent, cron, delegate).
The existing auto-fill logic then populates channel and chatID from
context, ensuring delivery info is always captured.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-11 19:56:46 +07:00
Luan VuandLuvu182 405a753239 fix: resolve media provider type from DB instead of guessing from name (#154)
Media tools (create_image, create_video, create_audio, read_audio,
read_video, read_document) routed API calls based on provider name
pattern matching (e.g. strings.HasPrefix(name, "gemini")). This breaks
when users give custom names to DB providers — a Gemini provider named
"chatgpt-sap-het" would be misrouted to the OpenAI-compat endpoint,
causing 404 errors.

Fix: carry the DB provider_type through OpenAIProvider, resolve it via
typedProvider interface in ExecuteWithChain, and inject as _provider_type
param for callProvider routing. Name-based heuristic kept as fallback
for config-file providers that don't have a DB type.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-11 18:32:51 +07:00
Luan VuandLuvu182 0592be359d fix: remove legacy per-agent imageGen/vision override from tools_config (#153)
The per-agent `imageGen` and `vision` fields in `ToolPolicySpec` (stored
in agents.tools_config JSONB) were added in d5cc5a7 (Feb 26) as the
original way to configure image/vision providers. When the media provider
chain system was introduced in 5815437 (Mar 8), these fields were kept
"for backward compat" but became dead code with no UI to manage them.

This causes a hard-to-debug issue: if an agent's tools_config contains
stale imageGen/vision data (set via API or leftover from DB), it silently
overrides the global provider chain configured in the builtin tools UI.
Users see the correct chain in the UI but the tool calls a completely
different provider/model, with no indication of why.

Changes:
- Remove Vision and ImageGen fields + struct definitions from ToolPolicySpec
- Remove associated context helpers (WithVisionConfig, WithImageGenConfig, etc.)
- Remove per-agent override injection in agent loop
- Simplify create_image and read_image to use chain as sole source of truth
- UI: whitelist known tools_config fields on save to clean stale DB data

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-11 17:37:55 +07:00
Luan VuandLuvu182 fa5f51e72e fix: allow OAuth providers in media tool chain (read_audio, read_image, etc.) (#150)
ExecuteWithChain previously required all providers to implement
credentialProvider (APIKey/APIBase). OAuth-based providers like
CodexProvider (ChatGPT OAuth) don't expose static credentials,
causing all media tools to fail with "does not expose API credentials".

Make credentialProvider optional (nil when unsupported). Each
callProvider gracefully falls back to the provider's Chat() API
when credentials are unavailable. Generation tools (create_image,
create_video, create_audio) return a clear error since they require
direct API access with no Chat fallback.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-11 16:40:35 +07:00
Luan VuandLuvu182 2fdb791802 fix: honor per-agent DB settings for restrict, subagents, memory, sandbox (#145)
Four per-agent settings stored in the database (and configurable via UI)
were silently ignored at runtime because the tool/system layer always
used the global config defaults instead.

**restrict_to_workspace**: Tools used the global config default baked at
startup. Fix: pass per-agent value through context; tools check context
override before falling back to constructor default.

**subagents_config**: ParseSubagentsConfig() existed but was never called.
All agents shared one SubagentManager with global limits. Fix: resolve
per-agent config in the agent resolver, store it on each spawned task,
and use it for limit checks, deny lists, and system prompt generation.

**memory_config**: Only the enabled toggle was read per-agent; search
weights (vector_weight, text_weight, max_results, min_score) were
hardcoded from PGMemoryStore defaults. Fix: extend MemorySearchOptions
with weight overrides, read per-agent config from context in the
memory_search tool.

**sandbox_config**: Only workspace_access was extracted per-agent; mode,
image, memory, CPU, timeout, network settings were discarded. Fix: pass
full sandbox.Config through context; Manager.Get() accepts an optional
config override for new containers.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-11 16:05:56 +07:00
Luan VuandLuvu182 1b99406012 fix: resolve embedding provider from DB registry + per-agent config (#134)
The embedding provider resolution only matched 3 hardcoded names
(openai, openrouter, gemini), silently failing for DB-stored providers
like "openai-embedding". This caused memory chunks to be stored
without vectors even when a valid embedding provider was configured.

Changes:
- resolveEmbeddingProvider: fallback to provider registry for DB-stored
  provider names when hardcoded match fails
- gateway startup: read per-agent memory config from DB (priority over
  config file defaults) for embedding provider resolution
- memory IndexDocument: log embedding errors instead of swallowing them
- memory admin ListChunks: return full chunk text instead of truncating
  to 200 chars, avoiding confusing partial content in the UI

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-11 14:31:00 +07:00
Luan VuandLuvu182 a4f2d02a80 fix(channels): annotate DM messages with sender identity (#120)
* fix(channels): annotate DM messages with sender identity

Telegram and Zalo group messages already include [From: sender] prefix
so the agent knows who is talking, but DM messages were sent without
any sender context — the agent had no way to address the user by name.

- Telegram DM: add [From: @username] (or FirstName if no username)
- Zalo DM: add [From: displayName] when dName is present in payload

* fix(tests): add missing EnsureUserProfile to test stubs

AgentStore interface gained EnsureUserProfile in 4fce731 but the test
stub implementations were not updated, breaking CI on main.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-10 19:25:55 +07:00
Luan VuandLuvu182 2495ff253c fix(ui): wire allowEmpty for embedding provider select (#112)
The `allowEmpty` prop on `ProviderModelSelect` was defined but the
Select dropdown still auto-selected the first provider when the value
was empty, and there was no way to reset back to empty (auto-detect).

- Map empty provider to a sentinel so Radix Select can represent it
- Show an "(auto)" option in the dropdown when `allowEmpty` is set
- Pass `allowEmpty` from both embedding config callers
- Skip clearing the model on provider change in allowEmpty mode to
  avoid a stale-closure bug where the second setState overwrites the
  provider update

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-10 18:45:09 +07:00
Luan VuandLuvu182 25fd9c9d6d feat(cron): configurable default timezone for cron expressions (#117)
* feat(cron): configurable default timezone for cron expressions

Cron expressions (e.g. "0 8 * * *") are evaluated relative to a timezone.
Without an explicit per-job timezone, they default to the server's system
timezone, which may not match the user's local time — especially in Docker
containers (default UTC) or multi-region deployments.

This adds a `default_timezone` setting to `CronConfig` (IANA format, e.g.
"Asia/Ho_Chi_Minh") that is applied as fallback when a cron job has no
explicit `schedule.tz`. The setting is configurable via the UI config page
(Integrations → Cron Scheduler) and hot-reloads on config changes.

Backend:
- Add `DefaultTimezone` field to `CronConfig`
- Add `SetDefaultTimezone()` to `CronStore` interface + PG implementation
- Apply default TZ in `AddJob()` when `schedule.TZ` is empty
- Wire at startup + subscribe to config change events for hot reload
- Update cron tool description so LLM knows about gateway default

Frontend:
- Add timezone dropdown (20 common IANA timezones) to Cron config section
- Add i18n keys for en, vi, zh

* fix(cron): apply default timezone to existing jobs via computeNextRun

Pass defaultTZ as fallback to computeNextRun so existing cron jobs
(with timezone = NULL in DB) also use the gateway's configured default
timezone when computing next_run_at. This ensures old jobs benefit
from the timezone setting without needing a DB migration or backfill.

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-10 18:44:28 +07:00
Luan VuandLuvu182 456e594b8f fix(config): treat GOCLAW_PROVIDER/MODEL env vars as fallback, not override (#119)
The onboard wizard sets GOCLAW_PROVIDER and GOCLAW_MODEL in .env for
initial bootstrap. Previously these env vars always overrode the config
file value via envStr(), making it impossible to change the default
provider/model through the Dashboard — every save was silently reverted
by ApplyEnvOverrides().

Change envStr to envFallback for these two fields: the env var is only
applied when the config file has no value (empty string). Once the user
saves a provider/model via the Dashboard, the config-file value wins.

Also:
- Stabilize ProviderModelSelect auto-select effect (useRef + useMemo)
- Add toast feedback on config save success/failure

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-10 18:41:39 +07:00
Luan VuandLuvu182 99110c4496 fix: memory flush UI default should match backend (ON, not OFF) (#115)
Backend defaults memoryFlush.enabled to true when config is nil
(memoryflush.go:39-46), but UI used `?? false` showing OFF.
Also fix sub-fields visibility to show when flush is defaulted ON.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-10 16:51:23 +07:00
Luan VuandLuvu182 aba5b42e3a fix: skip sender allowlist check for group messages in HandleMessage (#101)
HandleMessage() always checked IsAllowed(senderID), which re-gates
individual senders against the allowlist even after the channel-specific
group policy (checkGroupPolicy) already approved access.

This caused group members who weren't individually listed in AllowFrom
to be silently blocked — even when GroupPolicy was set to "open" or the
group ID was in the allowlist.

Affected channels: Zalo Personal, Slack (any channel using HandleMessage
with peerKind "group"). Telegram was unaffected because it publishes
directly to the bus.

Now HandleMessage only enforces the allowlist for DMs. Group access
control is fully delegated to each channel's checkGroupPolicy.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-09 18:55:56 +07:00
Luan VuandLuvu182 acf3db7bc6 docs: update README to reflect recent features and fixes (#98)
- Add knowledge graph (LLM extraction, traversal, visualization)
- Add memory management dashboard, persistent pending messages
- Add Slack channel (7 channels total), update architecture diagram
- Update hooks system from "Planned" to implemented
- Add Claude CLI and Codex as provider modes
- Replace non-existent tools (image, process, canvas) with actual
  media tools (create_image, create_audio, create_video, read_*)
  and announce_queue
- Add docker-compose.browser.yml, docker-compose.redis.yml
- Add Redis build tag option
- Update feature matrix, project status, and web dashboard description

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-09 17:30:21 +07:00
Luan VuandLuvu182 9249d9e358 fix(claude-cli): check scanner.Err() and increase stream buffer to 10MB (#96)
Fixes #94. The stream-json scanner never checked scanner.Err() after the
scan loop, so if a line exceeded the 1MB buffer limit the scanner would
stop with bufio.ErrTooLong and the response would be silently truncated.

- Check scanner.Err() after the loop and return an explicit error
- Increase max buffer from 1MB to 10MB to handle large tool outputs

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-09 15:49:25 +07:00
Luan VuandLuvu182 a321af8b04 feat(ui): add tool call details with arguments, results, and thinking display (#92)
- Enhance tool call cards with expandable arguments/result sections and inline summary
- Display thinking/reasoning blocks in chat messages (uses existing ThinkingBlock component)
- Reconstruct tool details from session history for persistent display
- Emit tool result content and truncated arguments in agent events
- Fix sanitize to always return cleaned content instead of empty string

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-09 12:57:55 +07:00
Luan VuandLuvu182 4721cf26bd fix: replace crypto.randomUUID() with fallback for older browsers (#93)
Fixes #87. crypto.randomUUID() is unavailable in browsers older than
Chrome 92 / Firefox 95 / Safari 15.4, causing an uncaught TypeError
that crashes the React tree and renders a blank page when opening
builtin-tool settings or MCP grant dialogs.

Add a uniqueId() helper that uses crypto.randomUUID() when available
and falls back to a Math.random-based UUID v4 generator otherwise.

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-09 12:56:35 +07:00
Luan Vu 7d744eb4f2 refactor(oauth): DB-backed token storage, split codex.go, remove file-based artifacts (#65)
Replace file-based OAuth token storage with DB-backed storage using
llm_providers (access token) + config_secrets (refresh token).

- Store: Add Settings JSONB field, chatgpt_oauth provider type
- OAuth: DBTokenSource backed by provider + secrets stores
- HTTP: oauth.go uses DB stores + registers provider in-memory
- Providers: chatgpt_oauth support in registerInMemory/registerProvidersFromDB
- Config: Remove HasOAuthToken, revert envFallback→envStr
- CLI: auth commands call HTTP API on running gateway
- Split codex.go (478→189 LOC) into codex.go + codex_build.go + codex_types.go
- Frontend: Remove fake OAUTH_PROVIDER_ID, use real DB-backed providers
- Tests: Rewrite with mock stores, fix SSE mock servers
2026-03-07 00:15:30 +07:00
Luan Vu 95885da14e feat(zalo): add file and image upload/send support (#62)
## Summary
- Implement UploadImage, SendImage, UploadFile, SendFile in Zalo Personal protocol
- Handle WebSocket control events (cmd=601) for async file upload callbacks
- Add media attachment routing in channel.Send() — auto-detects image vs file
- Add MEDIA: prefix support in message tool for agent file attachments
- Add group_id metadata routing for correct Zalo group API selection

## Review fixes (dc475bc)
- Fix CI: %s for json.Number (was %d)
- Fix path traversal: parseMediaPath restricted to os.TempDir()
- Add 25MB file size limit before upload reads
- Drain stale uploadCallbacks on Listener.reset()
- Split send.go (688 LOC) into send.go, send_image.go, send_file.go, send_helpers.go
- Add 12 unit tests for parseMediaPath
2026-03-06 23:20:50 +07:00
Luan VuandLuvu182 1ebbe53093 fix(ui): add missing delete buttons for agents and teams (#63)
The list pages had ConfirmDialog for deletion wired up but no UI
element to trigger it. Detail pages had no delete option at all.

- Add delete button with trash icon to agent and team cards
- Wire onDelete callbacks to existing setDeleteTarget state
- Add delete button + confirm dialog to agent detail page header
- Add delete button + confirm dialog to team detail page header

Closes #17

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-06 22:56:40 +07:00
Luvu182andLuvu182 62a6ca9ee5 feat(browser): add remote Chrome sidecar support for Docker deployments (#59)
* feat(browser): add remote Chrome sidecar support for Docker deployments

When running in Docker, Chrome is not installed in the runtime image.
This adds support for connecting to a remote Chrome via CDP (Chrome
DevTools Protocol) using a Docker Compose sidecar overlay, following
the existing pattern used by sandbox, OTel, and Tailscale overlays.

Changes:
- Add RemoteURL field to BrowserToolConfig
- Add GOCLAW_BROWSER_REMOTE_URL env var (auto-enables browser tool)
- Browser Manager: remote CDP connection with hostname-to-IP resolution
  (required by Chrome M113+ DNS rebinding protection), auto-reconnect
  on dead connections, disconnect-only on Stop (sidecar stays alive)
- Auto-start browser on first tool action (no explicit "start" needed)
- Add docker-compose.browser.yml overlay (zenika/alpine-chrome:124)
- Add unit tests for CDP resolution and Manager lifecycle

Usage:
  docker compose -f docker-compose.yml -f docker-compose.managed.yml \
    -f docker-compose.browser.yml up -d --build

Closes #56

* feat(browser): fix onboard summary and config serialization for remote mode

- onboard.go: show "remote: ws://..." instead of "headless" when RemoteURL is set
- onboard_auto.go: serialize remote_url field in generated config

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-05 10:09:08 +07:00
Luvu182andLuvu182 9cb73ac132 fix(dashboard): enrich overview with real stats, client tracking, and IP extraction (#51)
- Extract QueryTodaySummary to package-level so quota.usage returns
  requestsToday/tokensToday even when QuotaChecker is nil (standalone mode)
- Enrich health RPC: version, uptime, mode, DB ping, tool count,
  connected clients list with real IP and connectedAt
- Add handleStatus: sessions count, agentTotal from DB (managed mode)
- Extract client IP from X-Real-IP / X-Forwarded-For during WS upgrade
- Rebuild overview UI: live uptime, System Health panel, Connected Clients
  panel, inline channel status with dots, auto-refresh 30s

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-04 23:24:07 +07:00
Luvu182andLuvu182 ab9f582de6 fix: OpenAI-compat SSE streaming drops usage data (tokens always 0) (#52)
When stream_options.include_usage is enabled, the OpenAI API sends the
final usage chunk with an empty choices array. The streaming parser
skipped chunks with no choices before checking for usage data, causing
token counts to always be 0 in traces.

Fix: extract usage data before the empty-choices skip.

Also fixes two related SSE parsing issues:
- SSE data prefix now handles both "data: " and "data:" (some providers
  like Kimi omit the space after the colon, per SSE spec)
- Added scanner.Err() check to surface stream read errors (timeout,
  connection reset) instead of silently returning partial results

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
2026-03-04 23:20:27 +07:00