Foxwarm is a self-hosted TypeScript runtime for AI agents. It provides persistent Sessions, Agent memory, tool calling, WebUI chat, optional messaging Channels, and execution Nodes.
Website · Documentation · GitHub
- WebUI + Channels: WebUI, Telegram, Matrix, WeChat Work, Weixin, and external trigger support
- Agents, Sessions, and Skills: Separate long-lived memory/workspaces from runnable conversation threads
- Persistent Memory: Agent memory files plus LanceDB-based searchable history
- Nodes: Optional remote/browser/CLI/sandbox tool hosts
- Layered-context Compaction: Automatic multi-level, traceable, archive-backed compaction and recall keep long conversations usable
The recommended first-time path is the installer script. It clones Foxwarm into ./foxwarm, stores runtime data/config in ./foxwarm-data, builds the app, starts it in the normal tmux mode, and prints a WebUI URL with token.
curl -fsSL https://foxwarm.550w.host/install-foxwarm.sh | bashTo inspect the script before running it, use a checkout:
git clone https://github.com/550W-HOST/foxwarm.git foxwarm
cd foxwarm
./install-foxwarm.shPrerequisites: Git, Node.js 20+, npm, and tmux. The installer validates these and prints clear instructions if anything is missing; it does not install system packages for you.
Useful overrides:
# CLI flags
curl -fsSL https://foxwarm.550w.host/install-foxwarm.sh | bash -s -- \
--dir "$PWD/foxwarm" \
--data-dir "$PWD/foxwarm-data"
# Environment variables
export FOXWARM_DIR="$PWD/foxwarm"
export FOXWARM_DATA_DIR="$PWD/foxwarm-data"
export FOXWARM_TMUX_SESSION=foxwarm
export FOXWARM_BRANCH=main
export FOXWARM_REPO=https://github.com/550W-HOST/foxwarm.git
curl -fsSL https://foxwarm.550w.host/install-foxwarm.sh | bashThe data directory contains runtime state/, agents/, tokens, logs, models, sessions, and channel config. Bundled skills stay in the program repo under skills/. Back up foxwarm-data/ to preserve your Foxwarm runtime state.
After startup, the installer prints a URL like:
http://localhost:3001/#token=...
The installer does not auto-attach. To view the running console/logs:
tmux attach -t foxwarmDetach without stopping Foxwarm:
Ctrl-b then d
Stop later:
cd foxwarm
npm run stopirm https://foxwarm.550w.host/install-foxwarm.ps1 | iexTo inspect the script before running it, use a checkout:
git clone https://github.com/550W-HOST/foxwarm.git foxwarm
cd foxwarm
.\install-foxwarm.ps1The Windows script checks for Git and Node.js 20+/npm, builds Foxwarm, stores data in ./foxwarm-data by default, writes a data_dir pointer into the program checkout, starts Foxwarm in the background, and opens the WebUI token URL when available. Rerunning the installer while that Foxwarm instance is active exits before dependency installation; run npm run stop:windows first so npm ci never replaces loaded native DLLs.
If local script execution is blocked, run:
powershell -ExecutionPolicy Bypass -File .\install-foxwarm.ps1Optional parameters:
.\install-foxwarm.ps1 -InstallDir .\foxwarm -DataDir .\foxwarm-data -BranchName mainUse this path if you are developing Foxwarm itself.
- Git
- Node.js 20+ and npm
- tmux for the normal background start mode on Linux/macOS
- (Optional) Ollama for embeddings / vector memory
- (Optional) Chromium or a browser node for browsing features
git clone https://github.com/550W-HOST/foxwarm.git foxwarm
cd foxwarm
npm run build-allFor local development test prerequisites, commands, suite ownership, and artifacts, see Local tests.
For a clean manual install with an external data directory:
mkdir -p ../foxwarm-data
printf "%s\n" "$PWD/../foxwarm-data" > data_dir
export FOXWARM_DATA_DIR="$PWD/../foxwarm-data"The installer creates this data_dir pointer automatically, so later npm start / npm run restart from a new shell continue using foxwarm-data.
Do not copy templates/models.example.yaml into $FOXWARM_DATA_DIR/state/models.yaml for first-time setup unless you want to configure it manually. If state/models.yaml is missing in the data directory, WebUI enters OOBE and helps create it.
Foreground:
npm run start:notmuxNormal tmux mode:
npm startThen open http://localhost:3001 and log in with the token from:
cat "$FOXWARM_DATA_DIR/state/token"Other tmux helpers:
tmux attach -t foxwarm
npm run restart
npm run stopOn Windows, use the PowerShell helpers or their equivalent npm commands:
.\scripts\start.ps1
.\scripts\status.ps1
.\scripts\restart.ps1
.\scripts\stop.ps1
npm run start:windows
npm run status:windows
npm run restart:windows
npm run stop:windowsThe Windows start and restart commands build first, then run Foxwarm in the background. Start performs a lockfile install while no instance is running; restart rebuilds against the installed dependency tree before gracefully replacing the running process, avoiding Windows locks on loaded native DLLs. Pass -SkipBuild to the PowerShell start/restart helper after a successful build. Status and stop use a local named pipe; stop requests the same graceful shutdown path used by SIGTERM. Standard output and errors are appended to state/logs/foxwarm.stdout.log and state/logs/foxwarm.stderr.log under the resolved data directory.
Logs are written under:
$FOXWARM_DATA_DIR/state/logs/
Docker Compose is still supported, but the recommended first-time UX is the installer + WebUI OOBE.
mkdir -p foxwarm-data/state foxwarm-data/agents
docker compose up -d --buildOpen http://localhost:3001 and use the token from:
cat foxwarm-data/state/tokenThe compose file mounts ./foxwarm-data to /data and sets FOXWARM_DATA_DIR=/data, so configuration and memory persist outside the program image.
Compose starts in OOBE by default when foxwarm-data/state/models.yaml is missing. To skip OOBE, create foxwarm-data/state/models.yaml before starting. You can also create foxwarm-data/state/config.yaml for app/channel settings. If changing bot.httpPort under Docker Compose, update docker-compose.yml ports and healthcheck to match; otherwise keep the default 3001.
Foxwarm enters OOBE (first-time setup) when:
state/models.yaml
does not exist.
After logging into WebUI, the OOBE page cannot be closed until models are configured. Once models are saved, you can use WebUI or any configured channel to ask the agent how to explore Foxwarm.
OOBE/Setup has three tabs:
- Appearance: manages browser-local themes, the instance name, and the tab icon
- Models: edits and validates
state/models.yamlas raw YAML - Config: edits
state/config.yaml, refreshes managed channels after save, shows their status, and includes Weixin QR login
You can later return to the same page from the WebUI setup/settings button. See WebUI Themes for the portable theme format and safety model.
Foxwarm's current primary configuration files live inside the data directory:
state/config.yaml— app settings and channelsstate/models.yaml— model providers, model list, and default modelagents/<agent>/memory/— long-term memory for each agentskills/<skill>/— bundled reusable skill documents in the program repostate/tokenandstate/node_token— WebUI and node pairing tokens
For installer-based setup, state/ and agents/ are under ./foxwarm-data/ by default, and the installer writes foxwarm/data_dir so later starts keep using that data directory. For Docker Compose, state/ and agents/ are under ./foxwarm-data/ on the host and /data/ in the container. Bundled skills remain in the program image/repo under skills/.
Back up and restore the whole data directory, not only configuration or individual SQLite files. The session and LLM archives are SQLite authorities; a live backup must use a SQLite-consistent online snapshot or a quiesced checkpoint/copy rather than copying the main database file without its WAL state. Session archive identity also depends on state/session-id-reservations.jsonl (committed move aliases), and an interrupted move may leave state/session-id-move-pending.json with explicit rollback/finish intent and target-directory ownership. Pending recovery is fail-closed before ordinary session loading. These files are durable/operator state, not disposable logs. Use foxwarm archive export-jsonl --output <directory> when an external workflow needs compatibility JSONL.
Example state/config.yaml app settings:
bot:
httpPort: 3001
enableWebUI: true
enableTrigger: true
llm:
compactKeepPercent: 0.3
compactThresholdPercent: 0.85
vector:
baseUrl: http://localhost:11434/v1
lexicalIndex: false
hybridSearch: false
vectorMaintenance:
enabled: true
retentionHours: 24
handoffConfirmation: falsellm.compactKeepPercent controls the fraction of recent rendered history kept
by default during compaction. llm.compactThresholdPercent controls the
automatic compaction trigger as a fraction of the resolved model context
window. Both values must be greater than 0 and at most 1; their defaults
are 0.3 and 0.85, respectively. A positive per-session threshold-token
override still takes precedence over the global threshold percentage.
Vector search is disabled by default. Omit vector or set vector: false to
keep semantic indexing and recall disabled. Supplying a vector object opts in
unless it sets enabled: false; enabled Vector requires an absolute HTTP(S)
OpenAI-compatible API base URL, including its version/custom API root. Foxwarm
rejects credentials, query strings, and fragments, and appends only
/embeddings, so a custom gateway may use a value such as
https://gateway.example/openai/v1. The legacy llm.ollamaBaseUrl field is
still read when top-level vector is absent, but new configuration should use
vector.baseUrl. Optional vector.lexicalIndex: true enables a dark,
exact-Vector-owner derived lexical indexing lane; it defaults off and is not
consumed by recall unless vector.hybridSearch: true is also set. Hybrid search
requires the lexical index and remains disabled by default. Vector, worker placement, maintenance, and handoff-confirmation settings are read
at process startup.
handoffConfirmation defaults to false. When set to true,
send_to_session and create_child_session require the structured confirmation
shown by their current tool schemas, including an original non-placeholder
review and final-property placement. This setting affects only inter-agent
handoff confirmation; model tool-call cancellation controls remain available in
both modes. Changing it requires a restart.
When explicitly enabled, the lexical derivative follows committed Session rename/fork boundaries and rebuilds incompatible derived schemas through a restart-resumable shadow SQLite file. These operations are best-effort derived maintenance: they do not make Archive commits or dense Vector availability depend on lexical health.
LanceDB maintenance is enabled by default. It compacts fragmented vector data
and removes table versions older than the configured positive whole-hour
retention window; the default is 24 hours. vectorMaintenance: true enables
the defaults, vectorMaintenance: false disables maintenance, and an object
enables it unless enabled: false while allowing retentionHours tuning.
Maintenance is a no-op while Vector itself is disabled.
Additional trusted Node providers are configured under nodeProviders. Use an
executable provider for a generic external adapter launched from a fixed
command; its contract is documented in
docs/executable-node-provider-protocol.md.
Use the first-party docker-worktree provider for resident Linux Docker
sandboxes bound to existing allowed Git worktrees; setup and limits are in
docs/docker-worktree-node-provider.md.
Provider configuration is startup-only and changes require a restart.
The WebUI OOBE page can create a basic model config. You can also edit state/models.yaml manually.
Preferred schema:
default: openai/gpt-5.6-sol
providers:
openai:
providerType: openai-completions
baseUrl: https://api.openai.com/v1
apiKey: your-openai-key
models:
- gpt-5.6-sol
- gpt-5.6-terra
- gpt-5.6-lunaProvider notes:
openai-completionsuses/chat/completionsopenaiandopenai-responsesuse/responsesopenai-wsuses the Responses request/event format over a provider WebSocketanthropicuses Anthropic-compatible requests- OpenAI-compatible local gateways can be configured by changing
baseUrland model ids.apiKeymay be left empty if your gateway does not require one.
openai-ws is an internal transport optimization. Foxwarm still constructs
the complete provider-visible request from canonical history on every turn.
Completed process-local connections may continue an exact matching Responses
prefix with previous_response_id; configuration or history changes naturally
start a full request. The optimization is not persisted and does not survive a
process restart. Request compression is not supported for this transport, and
extraFields may not set transport/envelope fields such as input,
previous_response_id, stream, type, background, context_management,
conversation, or stream_id.
Some Chat Completions-compatible providers return assistant thinking as
reasoning_content but require that history to be replayed under reasoning.
Configure that request dialect explicitly when needed; omission keeps the
standard Foxwarm reasoning_content behavior:
providers:
compatible-chat:
providerType: openai-completions
historyReasoningField: reasoning
models:
- compatible-modelhistoryReasoningField accepts only reasoning_content or reasoning, and is
valid only for concrete openai-completions providers. A model object may
override its provider value when models behind one endpoint use different
dialects.
Provider and model entries can declare first-class effort capabilities and a
default. When omitted, all six levels are allowed and high is the default:
providers:
openai:
providerType: openai-responses
effort:
allowed: [none, low, medium, high, xhigh, max]
default: high
models:
- gpt-5.6-sol
- id: gpt-5.6-terra
effort:
allowed: [low, medium, high, xhigh]
default: highA model-level allowed list replaces the provider list; omitted fields inherit
from the provider, and the resolved default must remain allowed. Requests use
the concrete model default when no effort is selected or when a virtual route
selects a leaf that does not allow the requested level.
Responses models can opt into OpenAI's hosted web search without a separate Foxwarm model request:
providers:
openai:
providerType: openai-responses
baseUrl: https://api.openai.com/v1
apiKey: your-openai-key
webSearch:
enabled: true
toolChoice: auto # or: required
searchContextSize: medium
models:
- gpt-5.6Foxwarm appends the hosted search tool beside its normal function tools. The
completed search item is kept only for replay to the same concrete model, and
the WebUI displays returned URL citations as clickable sources. Hosted search
is not enabled for compact planning or setup-test requests. webSearch: true
enables the default settings, webSearch: false disables it, and an object
enables it unless enabled: false; model-level values merge tuning fields from
their provider while overriding the inherited enabled state.
For stable session routing and ordered failover, see Virtual models.
The runtime resolves model definitions from state/models.yaml under the active Foxwarm data directory. If that file is missing, templates/models.example.yaml is a read-only fallback; Setup still edits the data-directory file and treats its absence as OOBE.
The template fallback is mainly a fallback for development and diagnostics; OOBE treats missing state/models.yaml as first-time setup.
After building Foxwarm, the repository includes a one-shot CLI that uses the same configuration loader and provider request stack as the server:
npm run build
node scripts/foxwarm.js model --list
echo "Summarize this text" | node scripts/foxwarm.js model --model openai/gpt-5.6-solThe package declares foxwarm as its executable, so an installed or npm linked checkout can use foxwarm model ... directly. The command does not start the Foxwarm server, but it does require lib/ build output and the normal runtime dependencies. It honors the normal data-root and model-config resolution, provider routing, retries, request compression, sanitization, and provider-specific response handling. Use foxwarm model --help for options.
Channels are configured under channels: in state/config.yaml.
Example Telegram channel:
channels:
telegram:
type: telegram
enabled: true
botToken: "123456:telegram-token"
# Optional best-effort ordinary-text tool progress for this channel instance.
# Omit it or set it to false to disable. Valid interval: 30000–1800000 ms.
channelProgress:
intervalMs: 60000
mainAttachUser: "your-telegram-user-id"
allowedUsers:
- "your-telegram-user-id"channelProgress is common to managed ordinary-text channels. It reports only
bounded top-level tool names/counts in transient messages; it never stores
progress in Session history or archives. Each attached channel target keeps an
independent timer and report baseline. WebUI is excluded, and an active WeWork
stream card continues to use its native progress instead of receiving a
duplicate text fallback.
Example Weixin channel:
channels:
weixin:
type: weixin
enabled: true
baseUrl: "https://ilinkai.weixin.qq.com"
token: "token-from-login"
allowAllUsers: falseYou can configure Weixin from WebUI Setup without manually editing the token: click Start Weixin login, scan the QR code or open the pairing link shown by Setup, then click Check login. On success, Setup writes the token to state/config.yaml and hot-reloads channels.
Example QQ Bot channel using the official QQ Open Platform gateway:
channels:
qq-primary:
type: qqbot
enabled: true
appId: "qq-bot-app-id"
clientSecret: "qq-bot-client-secret"
# QQ OpenIDs, not display names. Omit only when you intentionally use
# the normal per-attachment allow-all-users control.
allowedUsers:
- "qq-user-openid"Create a QQ Bot application in the QQ Open Platform, obtain its AppID and
ClientSecret, and enable the C2C, group @-message, guild @-message, and
guild-DM event permissions/intents that the application is eligible to use.
Add the bot to each target group or guild before expecting inbound events.
Foxwarm accepts only text from C2C_MESSAGE_CREATE, GROUP_AT_MESSAGE_CREATE,
AT_MESSAGE_CREATE, and DIRECT_MESSAGE_CREATE; media and unmentioned group
traffic are intentionally ignored. Its attachment conversation IDs are scoped
as c2c:<openid>, group:<group-openid>, guild:<channel-id>, or
dm:<guild-id>, so use those exact values with channel attachment tools.
For a source-bound message, Foxwarm follows the Tencent/OpenClaw local passive
reply policy: up to four successful passive text replies within one hour use
the inbound msg_id; later source replies make one proactive text send to the
same conversation. Enable QQ's active-message capability and make sure its
quota is suitable for that fallback. Unknown server failures never infer a
proactive fallback, and a failed proactive send is not retried.
The published @openclaw/qqbot package is a complete OpenClaw plugin and was
investigated as a protocol reference, but is not a Foxwarm dependency: it
bundles an UNLICENSED QR credential-provisioning connector. The native
adapter avoids both OpenClaw plugin coupling and that dependency risk because
official QQ credentials are configured directly.
Example WeWork/企业微信 intelligent bot channel with opt-in streaming aggregation:
channels:
wework-aibot:
type: wework
enabled: true
# Optional legacy/group-robot webhook used for proactive webhook sends and
# media upload/download by media_id. Intelligent-bot short-connection
# callbacks do not require this because replies use per-message response_url
# or passive stream replies.
webhookUrl: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..."
# Intelligent-bot short-connection callback listener. Configure this URL in
# WeWork with the matching Token / EncodingAESKey. This is sufficient for
# receiving intelligent-bot callbacks even when webhookUrl is omitted.
token: "callback-token"
encodingAESKey: "callback-encoding-aes-key"
listenPort: 3003
listenPath: "/wework/aibot"
# Optional bot/self display name. If inbound text starts with
# "@企业微信机器人" followed by whitespace, that prefix is stripped before
# slash-command parsing so Chinese WeWork mentions can still run commands.
selfName: "企业微信机器人"
aibot:
# When true, incoming intelligent-bot messages use WeWork stream replies:
# model/tool-loop broadcasts are aggregated into one stream card and the
# final assistant message marks the stream as finished.
stream: true
# Optional WebSocket/long-connection API mode. This can receive callbacks
# without a public webhook URL and pushes stream updates proactively.
websocket:
enabled: false
botId: "BOTID"
secret: "LONG_CONNECTION_SECRET"WebUI Setup can edit channels and then reload them without restarting Foxwarm. The reload flow stops registered managed channels and starts the enabled/configured channels again.
Managed channel types currently include:
telegrammatrixweworkweixinqqbot
Slash-command alternatives are available for runtime inspection and manual control:
/channel status
/channel start <channel-id>
/channel stop <channel-id>
/channel restart <channel-id>
Read the token directly from the data directory:
cat foxwarm-data/state/tokenThen open http://localhost:3001/#token=<token>.
tmux attach -t foxwarm # view Foxwarm console/logs
# detach without stopping: Ctrl-b then d
cd foxwarm && npm run stop
cd foxwarm && npm run restartIf you used a custom session name, pass FOXWARM_TMUX_SESSION=<name> for attach/start/restart/stop.
Return to WebUI Setup and check provider type, base URL, model id, and API key. Local OpenAI-compatible gateways may leave API key empty, but hosted providers usually require one. If a bad state/models.yaml was created manually, fix it or delete it to re-enter OOBE. Check logs under foxwarm-data/state/logs/.
Inside Docker, localhost means the container itself. To reach a model server on the host machine, use host.docker.internal where supported, for example in foxwarm-data/state/config.yaml or state/models.yaml base URLs.
For local/tmux installs, edit foxwarm-data/state/config.yaml and set bot.httpPort. For Docker Compose, also update docker-compose.yml ports and healthcheck to match the new port.
Open Setup → Config and save the configuration again, or inspect runtime state with /channel status. Channel reload errors are shown in Setup and logged under foxwarm-data/state/logs/.
A long-lived workspace + memory container. Agent memory lives under:
agents/<agent>/memory/
A runnable conversation thread bound to an agent. A single agent can have many sessions.
A reusable instruction/documentation pack under skills/<skill>/. The bundled about-foxwarm skill explains Foxwarm concepts after models are configured.
An action the assistant can call, such as file operations, shell commands, memory search, session management, or node operations.
An execution host for tools. master is the default local node; remote/browser/CLI/sandbox nodes can be paired and approved.
From a running Foxwarm WebUI or chat command, inspect node pairing help:
/node pair-help
Detailed docs are in:
docs/node-client.md
/session list, create, fork, move, archive, isolate, index
/agent list, create, inherit, delete
/model inspect or switch the current model
/skill list, attach, detach, inspect skills
/node list/switch nodes, approve pairings, show pair-help
/channel inspect and reload channel runtime state
/compact compact session history
foxwarm/
├── src/ # TypeScript backend source
├── lib/ # Compiled JavaScript
├── packages/webui/ # Browser frontend
├── packages/browser-node/# Browser node extension
├── packages/cli-node/ # CLI / interactive node client
├── agents/ # Agent workspaces and memory (runtime data)
├── skills/ # Skill definitions
├── state/ # Runtime state, config, tokens, sessions, logs, db
├── templates/ # Starter templates
├── scripts/ # Start/restart/stop helpers
├── docs/ # Detailed documentation
└── examples/ # Examples, including ToolScript
- User documentation
- Install guide
- Code Index
- Architecture
- Session Management
- Multi-Agent Guide
- Node Client
- Multica Bridge POC
- Vector Memory
- Development
- Multiprocess Session Workers
- WebUI Themes
- ToolScript examples
Foxwarm keeps a repository-local code index for both human contributors and coding agents. Before inspecting or modifying code:
- Read the code-index governance guide and architecture overview.
- Read the relevant module, thread, and unit documents as whole files.
- Verify important claims against current source and tests; the index is a map, while source and tests remain authoritative.
After changing source:
- Update the affected
docs/code-index/documents in the same branch and pull request. Refresh unit behavior, file ownership, and stable-symbol indexes as relevant; update parent navigation only when boundaries change. - Record each Design Decision at exactly one canonical owner: unit, module, thread, or overview. Other documents use a short summary and link. Repetition across modules is a signal to create or use a thread.
- Keep the index public-safe English. Do not add credentials, private paths, deployment runbooks, or private operational context.
- Run
npm run quality:code-indexbefore submitting.
npm run build # backend/shared/cli-node build
npm run build-all # install + build backend and WebUI
npm run dev # TypeScript watch mode
npm run start:notmux # build + foreground backend start
npm start # build + tmux startMIT