* refactor(agent): replace TypeScript agent gateway with in-process Go agent using twilight-ai SDK
- Remove apps/agent (Bun/Elysia gateway), packages/agent (@memoh/agent),
internal/bun runtime manager, and all embedded agent/bun assets
- Add internal/agent package powered by twilight-ai SDK for LLM calls,
tool execution, streaming, sential logic, tag extraction, and prompts
- Integrate ToolGatewayService in-process for both built-in and user MCP
tools, eliminating HTTP round-trips to the old gateway
- Update resolver to convert between sdk.Message and ModelMessage at the
boundary (resolver_messages.go), keeping agent package free of
persistence concerns
- Prepend user message before storeRound since SDK only returns output
messages (assistant + tool)
- Clean up all Docker configs, TOML configs, nginx proxy, Dockerfile.agent,
and Go config structs related to the removed agent gateway
- Update cmd/agent and cmd/memoh entry points with setter-based
ToolGateway injection to avoid FX dependency cycles
* fix(web): move form declaration before computed properties that reference it
The `form` reactive object was declared after computed properties like
`selectedMemoryProvider` and `isSelectedMemoryProviderPersisted` that
reference it, causing a TDZ ReferenceError during setup.
* fix: prevent UTF-8 character corruption in streaming text output
StreamTagExtractor.Push() used byte-level string slicing to hold back
buffer tails for tag detection, which could split multi-byte UTF-8
characters. After json.Marshal replaced invalid bytes with U+FFFD,
the corruption became permanent — causing garbled CJK characters (�)
in agent responses.
Add safeUTF8SplitIndex() to back up split points to valid character
boundaries. Also fix byte-level truncation in command/formatter.go
and command/fs.go to use rune-aware slicing.
* fix: add agent error logging and fix Gemini tool schema validation
- Log agent stream errors in both SSE and WebSocket paths with bot/model context
- Fix send tool `attachments` parameter: empty `items` schema rejected by
Google Gemini API (INVALID_ARGUMENT), now specifies `{"type": "string"}`
- Upgrade twilight-ai to d898f0b (includes raw body in API error messages)
* chore(ci): remove agent gateway from Docker build and release pipelines
Agent gateway has been replaced by in-process Go agent; remove the
obsolete Docker image matrix entry, Bun/UPX CI steps, and agent-binary
build logic from the release script.
* fix: preserve attachment filename, metadata, and container path through persistence
- Add `name` column to `bot_history_message_assets` (migration 0034) to
persist original filenames across page refreshes.
- Add `metadata` JSONB column (migration 0035) to store source_path,
source_url, and other context alongside each asset.
- Update SQL queries, sqlc-generated code, and all Go types (MessageAsset,
AssetRef, OutboundAssetRef, FileAttachment) to carry name and metadata
through the full lifecycle.
- Extract filenames from path/URL in AttachmentsResolver before clearing
raw paths; enrich streaming event metadata with name, source_path, and
source_url in both the WebSocket and channel inbound ingestion paths.
- Implement `LinkAssets` on message service and `LinkOutboundAssets` on
flow resolver so WebSocket-streamed bot attachments are persisted to the
correct assistant message after streaming completes.
- Frontend: update MessageAsset type with metadata field, pass metadata
through to attachment items, and reorder attachment-block.vue template
so container files (identified by metadata.source_path) open in the
sidebar file manager instead of triggering a download.
* refactor(agent): decouple built-in tools from MCP, load via ToolProvider interface
Migrate all 13 built-in tool providers from internal/mcp/providers/ to
internal/agent/tools/ using the twilight-ai sdk.Tool structure. The agent
now loads tools through a ToolProvider interface instead of the MCP
ToolGatewayService, which is simplified to only manage external federation
sources. This enables selective tool loading and removes the coupling
between business tools and the MCP protocol layer.
* refactor(flow): split monolithic resolver.go into focused modules
Break the 1959-line resolver.go into 12 files organized by concern:
- resolver.go: core orchestration (Resolver struct, resolve, Chat, prepareRunConfig)
- resolver_stream.go: streaming (StreamChat, StreamChatWS, tryStoreStream)
- resolver_trigger.go: schedule/heartbeat triggers
- resolver_attachments.go: attachment routing, inlining, encoding
- resolver_history.go: message loading, deduplication, token trimming
- resolver_store.go: persistence (storeRound, storeMessages, asset linking)
- resolver_memory.go: memory provider integration
- resolver_model_selection.go: model selection and candidate matching
- resolver_identity.go: display name and channel identity resolution
- resolver_settings.go: bot settings, loop detection, inbox
- user_header.go: YAML front-matter formatting
- resolver_util.go: shared utilities (sanitize, normalize, dedup, UUID)
* fix(agent): enable Anthropic extended thinking by passing ReasoningConfig to provider
Anthropic's thinking requires WithThinking() at provider creation time,
unlike OpenAI which uses per-request ReasoningEffort. The config was
never wired through, so Claude models could not trigger thinking.
* refactor(agent): extract prompts into embedded markdown templates
Move inline prompt strings from prompt.go into separate .md files under
internal/agent/prompts/, using {{key}} placeholders and a simple render
engine. Remove obsolete SystemPromptParams fields (Language,
MaxContextLoadTime, Channels, CurrentChannel) and their call-site usage.
* fix: lint
* feat(mcp): workspace container with bridge architecture
Migrate MCP containers to use UDS-based bridge communication instead of
TCP gRPC. Containers now mount runtime binaries and Unix domain sockets
from the host, eliminating the need for a dedicated MCP Docker image.
- Remove Dockerfile.mcp and entrypoint.sh in favor of standard base images
- Add toolkit Dockerfile for building MCP binary separately
- Containers use bind mounts for /opt/memoh (runtime) and /run/memoh (UDS)
- Update all config files with new runtime_path and socket_dir settings
- Support custom base images per bot (debian, alpine, ubuntu, etc.)
- Legacy container detection and TCP fallback for pre-bridge containers
- Frontend: add base image selector in container creation UI
* feat(container): SSE progress bar for container creation
Add real-time progress feedback during container image pull and creation
using Server-Sent Events, without breaking the existing synchronous JSON
API (content negotiation via Accept header).
Backend:
- Add PullProgress/LayerStatus types and OnProgress callback to
PullImageOptions (containerd service layer)
- DefaultService.PullImage polls ContentStore.ListStatuses every 500ms
when OnProgress is set; AppleService ignores it
- CreateContainer handler checks Accept: text/event-stream and switches
to SSE branch: pulling → pull_progress → creating → complete/error
Frontend:
- handleCreateContainer/handleRecreateContainer use fetch + SSE instead
of the SDK's synchronous postBotsByBotIdContainer
- Progress bar shows layer-level pull progress (offset/total) during
pulling phase and indeterminate animation during creating phase
- i18n keys added for pullingImage and creatingContainer (en/zh)
* fix(container): clear stale legacy route and type create SSE
* fix(ci): resolve lint errors and arm64 musl node.js download
- Fix unused-receiver lint: rename `s` to `_` on stub methods in
manager_legacy_test.go
- Fix sloglint: use slog.DiscardHandler instead of
slog.NewTextHandler(io.Discard, nil)
- Handle missing arm64 musl Node.js builds: unofficial-builds.nodejs.org
does not provide arm64 musl binaries, fall back to glibc build
* fix(lint): address errcheck, staticcheck, and gosec findings
- Discard os.Setenv/os.Remove return values explicitly with _
- Use omitted receiver name instead of _ (staticcheck ST1006)
- Tighten directory permissions from 0o755 to 0o750 (gosec G301)
* fix(lint): sanitize socket path to satisfy gosec G703
filepath.Clean the env-sourced socket path before os.Remove
to avoid path-traversal taint warning.
* fix(lint): use nolint directive for gosec G703 on socket path
filepath.Clean does not satisfy gosec's taint analysis. The socket
path comes from MCP_SOCKET_PATH env (operator-configured) or a
compiled-in default, not from end-user input.
* refactor: rename MCP container/bridge to workspace/bridge
Split internal/mcp/ to separate container lifecycle management from
Model Context Protocol connections, eliminating naming confusion:
- internal/mcp/ (container mgmt) → internal/workspace/
- internal/mcp/mcpclient/ → internal/workspace/bridge/
- internal/mcp/mcpcontainer/ → internal/workspace/bridgepb/
- cmd/mcp/ → cmd/bridge/
- config: MCPConfig → WorkspaceConfig, [mcp] → [workspace]
- container prefix: mcp-{id} → workspace-{id}
- labels: mcp.bot_id → memoh.bot_id, add memoh.workspace=v1
- socket: mcp.sock → bridge.sock, env BRIDGE_SOCKET_PATH
- runtime: /opt/memoh/runtime/mcp → /opt/memoh/runtime/bridge
- devenv: mcp-build.sh → bridge-build.sh
Legacy containers (mcp- prefix) detected by container name prefix
and handled via existing fallback path.
* fix(container): use memoh.workspace=v3 label value
* refactor(container): drop LegacyBotLabelKey, infer bot ID from container name
Legacy containers use mcp-{botID} naming, so bot ID can be derived
via TrimPrefix instead of looking up the mcp.bot_id label.
* fix(workspace): resolve containers via manager and drop gateway container ID
* docs: fix stale mcp references in AGENTS.md and DEPLOYMENT.md
* refactor(workspace): move container lifecycle ownership into manager
* dev: isolate local devenv from prod config
* toolkit: support musl node runtime
* containerd: fix fallback resolv.conf permissions
* web: preserve container create progress on completion
* web: add bot creation wait hint
* fix(workspace): preserve image selection across recreate
* feat(web): shorten default docker hub image refs
* fix(container): address code review findings
- Remove synchronous CreateContainer path (SSE-only now)
- Move flusher check before WriteHeader to avoid committed 200 on error
- Fix legacy container IP not cached via ensureContainerAndTask path
- Add atomic guard to prevent stale pull_progress after PullImage returns
- Defensive copy for tzEnv slice to avoid mutating shared backing array
- Restore network failure severity in restartContainer (return + Error)
- Extract duplicate progress bar into ContainerCreateProgress component
- Fix codesync comments to use repo-relative paths
- Add SaaS image validation note and kernel version comment on reaper
* refactor(devenv): extract toolkit install into shared script
Unify the Node.js + uv download logic into docker/toolkit/install.sh,
used by the production Dockerfile and runnable locally for dev.
Dev environment no longer bakes toolkit into the Docker image — it is
volume-mounted from .toolkit/ instead, so wrapper script changes take
effect immediately without rebuilding. The entrypoint checks for the
toolkit directory and prints a clear error if missing.
* fix(ci): address go ci failures
* chore(docker): remove unused containerd image
* refactor(config): rename workspace image key
* fix(workspace): fix legacy container data loss on migration and stop swallowing errors
Three root causes were identified and fixed:
1. Delete() used hardcoded "workspace-" prefix to look up legacy "mcp-"
containers, causing GetContainer to return NotFound. CleanupBotContainer
then silently skipped the error and deleted the DB record without ever
calling PreserveData. Fix: resolve the actual container ID via
ContainerID() (DB → label → scan) before operating.
2. Multiple restore error paths were silently swallowed (logged as Warn
but not returned), so the user saw HTTP 200/204 with no data and no
error. Fix: all errors in the preserve/restore chain now block the
workflow and propagate to the caller.
3. tarGzDir used cached DirEntry.Info() for tar header size, which on
overlayfs can differ from the actual file size, causing "archive/tar:
write too long". Fix: open the file first, Fstat the fd for a
race-free size, and use LimitReader as a safeguard.
Also adds a "restoring" SSE phase so the frontend shows a progress
indicator ("Restoring data, this may take a while...") during data
migration on container recreation.
* refactor(workspace): single-point container ID resolution
Replace the `containerID func(string) string` field with a single
`resolveContainerID(ctx, botID)` method that resolves the actual
container ID via DB → label → scan → fallback. All ~16 lookup
callsites across manager.go, dataio.go, versioning.go, and
manager_lifecycle.go now go through this single resolver, which
correctly handles both legacy "mcp-" and new "workspace-" containers.
Only `ensureBotWithImage` inlines `ContainerPrefix + botID` for
creating brand-new containers — every other path resolves dynamically.
* fix(web): show progress during data backup phase of container recreate
The recreate flow (delete with preserve_data + create with restore_data)
blocked on the DELETE call while backing up /data with no progress
indication. Add a 'preserving' phase to the progress component so
users see "正在备份数据..." instead of an unexplained hang.
* chore: remove [MYDEBUG] debug logging
Clean up all 112 temporary debug log statements added during the
legacy container migration investigation. Kept only meaningful
warn-level logs for non-fatal errors (network teardown, rename
failures).
* refactor(browser): split browser cores via build ARG, add core selector
- Replace playwright official image with ubuntu:noble base in both
docker/Dockerfile.browser and devenv/Dockerfile.browser; install
browsers at build time driven by ARG/ENV BROWSER_CORES
- Add GET /cores endpoint to Browser Gateway reporting available cores
- Proxy GET /browser-contexts/cores in Go handler to Browser Gateway
- Add `core` field to BrowserContextConfigModel and GatewayBrowserContext;
context creation selects the appropriate browser instance by core
- Frontend context-setting page fetches available cores and renders a
core selector; saves core as part of the config JSON
- install.sh prompts for browser core selection and writes BROWSER_CORES
to .env; builds the browser image locally before docker compose up
- Regenerate OpenAPI spec and TypeScript SDK
* fix: lint
* feat(terminal): add interactive web terminal for bot containers
Add WebSocket-based terminal endpoint (/container/terminal/ws) that
provides a full PTY shell session inside the bot's MCP container.
Extend the gRPC proto with pty and resize fields, implement PTY exec
on the container side using creack/pty, and add an xterm.js-based
terminal component in the frontend bot detail page.
* chore: add /mcp in .gitignore
* feat(terminal): add multi-tab support, localStorage cache, and reactivity fixes
- Support unlimited terminal tabs with add/close/switch
- Cache terminal content to localStorage via SerializeAddon for session persistence
- Use shallowReactive for tab objects to ensure status updates trigger UI reactivity
- Fix listener leak by tracking and disposing onData/onResize on reconnect
- Fix bottom clipping by using inset offsets instead of padding
Refactor the attachment tag extraction into a generic TagResolver/StreamTagExtractor
system that supports multiple custom tags. Implement <reactions> tag allowing the
agent to embed emoji reactions directly in text responses, dispatched as side-effects
through the channel reactor interface.
- Add TagResolver interface and StreamTagExtractor streaming state machine
- Refactor AttachmentsStreamExtractor as backward-compatible wrapper
- Add reactionsResolver and ReactionDeltaAction stream event
- Wire reaction dispatch in Go channel inbound processor
- Fix .gitignore to scope compiled binary patterns to repo root
Container file attachments (from agent <attachments> blocks) now render
as clickable buttons that open the right-side file manager preview.
Also broadens AttachmentItem type to cover both persisted media assets
and streaming container-file references.
Wrap the FileManager inside a relative container in the chat Sheet so
its absolute inset-0 positioning is scoped to the remaining space below
the SheetHeader, instead of covering the entire SheetContent.
Pass replyTarget through the full pipeline (ChatRequest → gateway
identity → agent headers → MCP session) so the send tool can detect
when the destination matches the current conversation and return an
error guiding the agent to reply directly instead.
- 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
* 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