- attachment-block: use loading=eager with explicit dimensions to prevent Chromium lazy-load intervention from freezing the page
- router: add error handler for chunk load failures to auto-reload
- vite: pre-bundle route page dependencies to improve initial load speed
- Extract ContainsMarkdown to shared channel package
- Auto-detect markdown in normalizeOutboundMessage and MCP send tool
- Apply markdown-to-HTML conversion during streaming deltas, not just
on the final message
- Remove resolveTelegramParseMode which incorrectly returned Telegram's
native "Markdown" mode instead of converting to HTML
- Fix all 14 Telegram send/edit paths for consistent parse mode handling
- Reset parseMode for plain-text error messages to avoid HTML corruption
- Treat maxTokens=0 as "unconfigured/unlimited" instead of disabling
trimming for any non-positive value (which masked exhausted budgets)
- Set historyBudget=1 when maxTokens>0 but overhead exceeds the limit,
ensuring aggressive trimming instead of no trimming
- Estimate token cost for messages without usage data (len/4 fallback)
so user/tool messages are not free-passed during budget accounting
Use rune-aware truncation for user-facing text and log previews so multibyte content is not corrupted in memory context, Telegram messages, or diagnostics.
MCP containers spawning child processes (npx, uvx, etc.) left zombies
because the Go binary running as PID 1 does not reap orphaned children.
Add dumb-init as the entrypoint init process.
Use rune-aware truncation for user-facing text and log previews so multibyte content is not corrupted in memory context, Telegram messages, or diagnostics.
truncateSnippet sliced bytes directly, which could split multi-byte
UTF-8 characters. Switch to []rune so truncation always respects
character boundaries.
- Extract parseTelegramTarget helper to consolidate duplicated @username
vs numeric chat ID parsing from 6+ locations (builder functions,
sendTelegramTextReturnMessage, sendTelegramAttachmentImpl)
- Extract Config.baseURL() to eliminate duplicate base URL resolution
between apiEndpoint() and fileEndpoint()
- Refactor stream.go Push method: extract resetStreamState(),
deliverFinalText(), and per-event-type sub-methods (pushDelta,
pushFinal, pushToolCallStart, pushAttachment, pushPhaseEnd,
pushError), reducing the 200-line switch-case to a clean dispatcher
- Use pushFinal's existing getBot() instead of duplicating parseConfig +
getOrCreateBot
- Replace sort.SliceStable with slices.SortStableFunc + cmp.Compare
- Replace strings.Index + manual slicing with strings.Cut in
decodeDataURLBytes, ResolveAttachment, and parseTelegramUserInput
* fix(containerd): prevent silent network failures from leaving containers unreachable (#202)
* fix(containerd): prevent silent network failures from leaving containers unreachable
Container network setup failures were silently swallowed at multiple
points in the call chain, leaving containers in a "running but
unreachable" ghost state. This patch closes every silent-failure path:
- setupCNINetwork: return error when CNI yields no usable IP
- Manager.Start: roll back container when IP is empty instead of
returning success
- ensureContainerAndTask: extract setupNetworkOrFail with 1 retry,
propagate error to callers
- ReconcileContainers: stop reporting "healthy" when network setup fails
- recoverContainerIP: retry up to 2 times with backoff for transient
CNI failures (IPAM lock contention, etc.)
- gRPC Pool: evict connections stuck in Connecting state for >30s
* fix(containerd): clean stale cni0 bridge on startup to prevent MAC error
After a Docker container restart, the cni0 bridge interface can linger
with a zeroed MAC (00:00:00:00:00:00) and DOWN state. The CNI bridge
plugin then fails with "could not set bridge's mac: invalid argument",
making all MCP containers unreachable.
Two-layer fix:
- Entrypoint: delete cni0 and flush IPAM state before starting containerd
- Go: detect bridge MAC errors in setupCNINetwork and auto-delete cni0
before retrying, as defense-in-depth for runtime recovery
* fix(containerd): use exec.CommandContext to satisfy noctx linter
* fix(mcp): propagate network errors from replaceContainerSnapshot
Network setup failure after snapshot replace (rollback/commit) was
silently swallowed — the container would start but remain unreachable
via gRPC. Return the error so callers (CreateSnapshot, RollbackVersion,
etc.) surface the failure instead of reporting success.
* fix(containerd): prevent silent network failures from leaving containers unreachable
Container network setup failures were silently swallowed at multiple
points in the call chain, leaving containers in a "running but
unreachable" ghost state. This patch closes every silent-failure path:
- setupCNINetwork: return error when CNI yields no usable IP
- Manager.Start: roll back container when IP is empty instead of
returning success
- ensureContainerAndTask: extract setupNetworkOrFail with 1 retry,
propagate error to callers
- ReconcileContainers: stop reporting "healthy" when network setup fails
- recoverContainerIP: retry up to 2 times with backoff for transient
CNI failures (IPAM lock contention, etc.)
- gRPC Pool: evict connections stuck in Connecting state for >30s
* fix(containerd): clean stale cni0 bridge on startup to prevent MAC error
After a Docker container restart, the cni0 bridge interface can linger
with a zeroed MAC (00:00:00:00:00:00) and DOWN state. The CNI bridge
plugin then fails with "could not set bridge's mac: invalid argument",
making all MCP containers unreachable.
Two-layer fix:
- Entrypoint: delete cni0 and flush IPAM state before starting containerd
- Go: detect bridge MAC errors in setupCNINetwork and auto-delete cni0
before retrying, as defense-in-depth for runtime recovery
* fix(containerd): use exec.CommandContext to satisfy noctx linter
* feat(channel): add qq adapter and outbound delivery
* feat(channel): ingest inbound qq messages
* feat(web): expose qq channel in management ui
* feat(channel): support qq attachment ingestion
* fix(mcp): fail read raw immediately for missing files
* fix(agent): parse inline image data into native image parts
* test(agent): align read_media tool tests with SDK options
* fix(channel): harden qq image delivery and reconnect loop
Avoid data URLs for qq channel images, reset reconnect backoff after healthy sessions, and fall back gracefully for malformed public image URLs.
* fix(channel): restore qq media delivery and target resolution
* fix(qq,mcp,agent): fix message/qq regressions and pass go lint
* fix(qq,agent): validate inline base64 and sync heartbeat seq
* fix(qq): validate remote voice mime for upload checks
* fix(qq): fall back intents and restore adapter wiring
* fix(qq): prevent final text leakage and dedupe persisted inbound query
* feat(container): add explicit data workflows and snapshot rollback
Make container upgrades and recreation data-safe by adding explicit preserve, export, import, restore, and rollback flows across the backend, SDK, and web UI.
* fix(container): resolve go lint issues
Fix formatting and lint violations introduced by the container data workflow changes so the Go CI lint job passes cleanly.
jsdom relies on Node.js-specific APIs that Bun cannot properly resolve
when running a bundled artifact. This caused an EISDIR error in Docker
containers (Bun tried to read the jsdom directory as a file).
Replace jsdom with linkedom, a lightweight pure-JS DOM implementation
that is fully compatible with Bun and @mozilla/readability. Also remove
the --external jsdom build flag since linkedom bundles cleanly.
Closes#181
Replace the host bind-mount + containerd exec approach with a per-bot
in-container gRPC server (ContainerService, port 9090). All file I/O,
exec, and MCP stdio sessions now go through gRPC instead of running
shell commands or reading host-mounted directories.
Architecture changes:
- cmd/mcp: rewritten as a gRPC server (ContainerService) with full
file and exec API (ReadFile, WriteFile, ListDir, ReadRaw, WriteRaw,
Exec, Stat, Mkdir, Rename, DeleteFile)
- internal/mcp/mcpcontainer: protobuf definitions and generated stubs
- internal/mcp/mcpclient: gRPC client wrapper with connection pool
(Pool) and Provider interface for dependency injection
- mcp.Manager: add per-bot IP cache, gRPC connection pool, and
SetContainerIP/MCPClient methods; remove DataDir/Exec helpers
- containerd.Service: remove ExecTask/ExecTaskStreaming; network setup
now returns NetworkResult{IP} for pool routing
- internal/fs/service.go: deleted (replaced by mcpclient)
- handlers/fs.go: deleted; MCP stdio session logic moved to mcp_stdio.go
- container provider Executor: all tools (read/write/list/edit/exec)
now call gRPC client instead of running shell via exec
- storefs, containerfs, media, skills, memory: all I/O ported to
mcpclient.Provider
Database:
- migration 0022: drop host_path column from containers table
One-time data migration:
- migrateBindMountData: on first Start() after upgrade, copies old
bind-mount data into the container via gRPC, then renames src dir
to prevent re-migration; runs in background goroutine
Bug fixes:
- mcp_stdio: callRaw now returns full JSON-RPC envelope
{"jsonrpc","id","result"|"error"} matching protocol spec;
explicit "initialize" call now advances session init state to
prevent duplicate handshake on next non-initialize call
- mcpclient Pool: properly evict stale gRPC connection after snapshot
replace (container process recreated); use SetContainerIP instead
of direct map write so IP changes always evict pool entry
- migrateBindMountData: walkErr on directories now counted as failure
so partially-walked trees don't get incorrectly marked as migrated
- cmd/mcp/Dockerfile: removed dead file (docker/Dockerfile.mcp is the
canonical production build)
Tests:
- provider_test.go: restored with bufconn in-process gRPC mock
(fakeContainerService + staticProvider), 14 cases covering all 5
tools plus edge cases
- mcp_session_test.go: new, covers JSON-RPC envelope, init state
machine, pending cleanup on cancel/close, readLoop cancel
- storefs/service_test.go: restored (pure function roundtrip tests)
Split long AI responses into multiple platform messages during streaming
instead of truncating them. The manager counts accumulated delta runes
and opens a new stream when approaching the platform's TextChunkLimit.
Uses a soft/hard limit strategy that prefers splitting at sentence ends
or line breaks over cutting mid-sentence.
- Add pushDelta with soft (75%) / hard (100%) limit and natural break
point detection
- Add splitStream, pushFinalAfterSplit, pushFinalWithChunking helpers
- Fix Discord adapter to use RuneCount Message Length
- Add tests for delta splitting, natural breaks, and final handling