* 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
* feat(telegram): use sendMessageDraft for streaming in private chats
Use Telegram Bot API 9.3's sendMessageDraft to stream partial messages
with smooth animation in private chats, replacing the sendMessage +
editMessageText approach. Group/channel chats keep the existing
edit-based streaming.
- Add sendTelegramDraft() for the sendMessageDraft API
- Detect private chats via conversation_type metadata in OpenStream
- Use 300ms throttle for drafts (vs 5s for edits)
- Send permanent messages at tool call boundaries and on final event
- Reset buffer atomically in StreamEventFinal to prevent duplicate
messages when multiple final events fire (one per assistant output)
* test(telegram): improve draft mode test assertions
Add sendTextForTest hook for sendTelegramTextReturnMessage to enable
direct assertion of send calls. Clean up residual unused variables
and replace indirect assertions with explicit mock-based verification.
cni.Remove() failure on stale iptables state blocked the retry
cni.Setup(), leaving bot containers without SNAT/MASQUERADE.
- Ignore cni.Remove() error so retry Setup always runs
- Add global MASQUERADE rule in entrypoints as belt-and-suspenders
Closes#161
- Fix DeleteContainer FAILED_PRECONDITION by cleaning up stopped task
entries before container deletion
- Fix CreateSnapshot leaving container in broken state: commit turns
the active snapshot read-only, so the full cycle (stop → commit →
prepare → delete → recreate → start) is now applied consistently
- Use context.WithoutCancel for atomic container replacement sequences
to prevent cancelled HTTP requests from corrupting container state
- Use dctx for DB operations (recordSnapshotVersion/insertEvent) to
avoid orphan snapshots in containerd without matching DB records
- Restart task + network after snapshot replacement, fixing Exec after
CreateVersion where the container had no running task
- Extract replaceContainerSnapshot helper to deduplicate the prepare →
delete → recreate → start pattern across three call sites
- Move snapshot list data fetching into Manager.ListBotSnapshotData to
encapsulate per-container locking; remove exported LockBot method
- Use UnixNano for snapshot names to avoid second-precision collisions
* fix(utils): preserve colon-containing values in tagsToRecord; align invalidFallback across date formatters; add formatRelativeTime
**key-value-tags: fix value truncation on tags with colons**
`tagsToRecord` used `tag.split(':')` with array destructuring, so any
value containing `:` (e.g. a webhook URL `https://example.com/hook`)
was silently truncated to just the scheme. Switch to `indexOf` so the
split happens only on the first colon, preserving the full value.
Example (before → after):
`tagsToRecord(['hook:https://api.example.com/cb'])`
before: `{ hook: 'https' }` ← bug
after: `{ hook: 'https://api.example.com/cb' }`
Add `key-value-tags.test.ts` covering: simple pairs, URL values,
multi-colon values, empty key/value, round-trip with `recordToTags`.
**date-time: honour `invalidFallback` consistently**
`FormatDateOptions` declares `invalidFallback` but only
`formatDateTimeSeconds` ever read it — `formatDateTime` and `formatDate`
both collapsed a present-but-invalid date string into `fallback ?? ''`,
making it impossible for callers to distinguish "nothing was passed" from
"a bad string was passed".
Extract a shared `resolveInvalid(value, options)` helper (prefers
`invalidFallback`, then `fallback`, then the raw value) and apply it
uniformly. Also refactor `formatDateTimeSeconds` to use the existing
`parseDate` helper, eliminating the duplicated `new Date` + `isNaN`
guard. No externally visible behaviour change for previously valid
combinations; callers that relied on invalid dates falling through to
`fallback` keep working since `resolveInvalid` falls through to
`fallback` when `invalidFallback` is absent.
**date-time: add `formatRelativeTime`**
Chat and notification UIs commonly need relative timestamps ("3 minutes
ago", "yesterday"). The utility file has no such function. Add
`formatRelativeTime(value, options?)` using `Intl.RelativeTimeFormat`
so the output respects the browser locale without hardcoded English
strings. Thresholds: seconds < 60 s, minutes < 1 h, hours < 24 h,
days < 7 d, beyond that falls back to `toLocaleDateString()`. Accepts
both ISO strings and `Date` objects.
Add `date-time.test.ts` covering all four exported functions including
`vi.useFakeTimers` assertions for `formatRelativeTime`.
* fix(utils): clean up formatRelativeTime after merge
Made-with: Cursor
- Add formatRelativeTime() to date-time utils (Intl.RelativeTimeFormat, locale-aware)
- Display relative time under each message in message-item.vue
- Show full datetime in title attribute on hover
Made-with: Cursor
Email is a supported channel (bindings, providers, outbox) but had no icon
and fell back to the generic comment icon. Use FontAwesome envelope.
Made-with: Cursor
Allow configuring a custom Telegram Bot API base URL (`apiBaseURL`) per
channel, enabling users behind restricted networks to route requests
through a reverse proxy (e.g. Nginx, Cloudflare Workers).
Both API calls and file downloads respect the configured endpoint.
When omitted, the official https://api.telegram.org is used.
Closes#159
Add mcp-build.sh that compiles the MCP binary and packages it as an
OCI image layer on top of the base rootfs, imported directly into
containerd. Air triggers rebuild on code changes, cleaning stale
containers automatically.
Consolidate dev-only files (Dockerfiles, entrypoint, config, build
script) into devenv/ to separate dev tooling from production artifacts.
Skip image pull when already imported to speed up dev startup.