Run history and project memory#
The desktop records run requests, events, decisions, artifacts, outcomes, usage, and available settings/specification snapshots. Open a historical run to inspect its execution cards and timeline. Retry and Run again create additional attempts; see Running tasks.
Project memory stores durable entries such as decisions and contributes to the project timeline. It is not a full source index or a guarantee that every entry is inserted into every future model prompt.
Historical artifact actions operate on current files. Opening a path shows what is there now; a Delete action on an old artifact is not equivalent to reverting the exact historical write. Inspect the current content and action label.
Storage layout#
User-level data#
Path under %APPDATA%\Enactive |
Contents |
|---|---|
settings.json |
Desktop provider/team/phase configuration and other settings |
workspaces.json |
Workspace registry and remembered run preferences |
permissions.json |
Desktop workspace approvals keyed by workspace ID |
templates\ |
Global template files |
logs\ |
Daily application logs |
Workspace-level data#
Path under <workspace>\.enactive |
Contents |
|---|---|
templates\ |
Workspace template files |
enactive.db |
Default SQLite run, memory, and Inbox storage |
runs\ |
Run files when using the JSON backend |
memory.json |
JSON project-memory backend / possible legacy source |
inbox.json |
JSON Inbox backend / possible legacy source |
Additional artifact-store state supports tracked write recovery. Treat .enactive as application state rather than a folder the worker should edit. Built-in path handling reserves it.
Version workspace templates intentionally. Do not assume the whole .enactive directory should be committed: it can contain local history, tool outputs, and machine-specific state.
Storage backend selection#
ENACTIVE_STORE chooses SQLite (default), JSON, or MySQL for runs, memory, and Inbox together. MySQL requires ENACTIVE_MYSQL with a connection string.
SQLite imports existing legacy memory/Inbox JSON once and leaves those source files in place. This is not continuous synchronization between backends. MySQL does not automatically import local history; plan a migration explicitly.
Workspace identity is written into <workspace>/.enactive/workspace.json the first time a run takes the folder up, seeded with the id the workspace already had. In MySQL, records are scoped by that identity. Because the marker travels with the folder, renaming or moving a workspace keeps its history; before this it appeared as a different workspace and its records silently stopped matching.
Notes on that marker:
- It is created by a run, not by browsing. Opening a folder's history does not write anything into it.
- If it is missing or damaged, the workspace falls back to the path-derived id — the old behaviour — rather than becoming a new workspace with no history.
- A folder that cannot be written to still opens; the marker is best-effort.
- Copying a workspace copies its identity, so the copy and the original share a history. Renaming and copying are indistinguishable from inside one folder.
.enactive/is not tracked by git, so two clones of one repository remain two workspaces.- Remembered "Allow (workspace)" approvals are deliberately not keyed by this id — they stay keyed to the path. The marker sits in the folder the agent works in and can arrive inside a cloned repository, so it must never be able to carry permissions. Renaming a folder therefore asks for those approvals again, once per tool.
Logs and diagnosis#
The application has a global log with provider activity, tool calls, and execution events, mirrored to daily files. The desktop's Log window lets you inspect and export information; the per-run Log view focuses on the selected run.
ENACTIVE_LOG_LEVEL supports Trace, Debug, Info, Warn, and Error. Trace can capture raw model HTTP request/response bodies. Log exports can contain source code, command output, and task content; review an export before sharing it.
Log analysis can send an exported run to a model for diagnosis. The analyst treats the exported log as data rather than instructions. The analysis still consumes model work and may transmit log content to the configured provider.
For a useful issue report, include:
- Application/build version from About.
- Host OS and relevant provider kind/model ID.
- Workspace task/template ID and supplied parameters, with secrets removed.
- Expected outcome and actual terminal outcome.
- Relevant route, tool, error, and criterion events.
- Whether staging, background mode, or console mode was used.
Avoid using an old fixed test-count claim as a release check. Run the current test project and record its actual result.
MCP external tools#
Configure a server#
Open Settings → AI → MCP → Add. Configure the server and use Test connection, then accept the editor and Save the main Settings window.
| Field | Meaning |
|---|---|
| ID | Unique lowercase identifier, 1–24 characters, starting with a letter; digits/hyphens supported |
| Enable | Connect this server for each new desktop run; untick to stop using it without removing its configuration |
| Transport | Stdio local process or Http endpoint |
| Command / Arguments | Executable plus a JSON array of arguments for Stdio |
| Working directory | Process directory; blank uses the run workspace, while editor Test uses the app's current directory |
| Environment | JSON dictionary for the child process |
| URL / Headers | Endpoint and JSON header dictionary for Http |
| Timeout | 1–600 seconds for connection/discovery and individual calls |
| Ask before every tool call | Enabled by default; forces individual approval |
Test connection initializes the server and discovers tools; it does not invoke the discovered tools. Starting the configured program can itself have effects, for example a package launcher downloading and running a package.
Grant worker access#
In AI → Team, grant an appropriate pattern:
mcp__*
mcp__example__*Or use an exact tool name from discovery. Discovered names include the server ID and a stable suffix to distinguish tools. The worker must also satisfy normal role/policy checks; enabling a server does not automatically grant every worker access.
Runtime behavior#
Each desktop run gets its own server connections. Any enabled server that fails to connect/discover can stop launch. Disable unused broken configurations rather than waiting for a run to discover the same failure again.
Connections and child processes are released when the run ends or is cancelled. A transport error does not trigger automatic replay of a remote tool call: the external action may already have happened.
MCP environment values and headers are saved through encrypted secret storage. Arguments and URLs are not secret fields. If decryption is unavailable, the configuration requires attention rather than silently discarding the original protected data.
External tools operate with their process/account permissions. Enactive's workspace guard, staging, and rejected-step revert do not constrain or undo their external effects. Configure boundaries in the server itself when needed. The console does not connect these desktop MCP configurations.
Web tools and SearXNG#
Two built-in tools read the web. Both are off until a person turns them on, and neither is in a default role.
| Tool | What it does | Exists when |
|---|---|---|
fetch_url |
Reads one page or text file by its address. Returns the title and the text a reader sees, without markup, scripts, styles or comments | Let tasks read the web is on |
web_search |
Searches through a SearXNG server you run. Returns titles, addresses and short snippets, which fetch_url then reads |
It is on and a search server address is set |
They replace what a worker otherwise does with curl or Invoke-WebRequest through the shell: raw HTML, mostly markup, counted against the model's window, and impossible in a run whose shell is denied.
Turn them on#
- Settings → Web: tick Let tasks read the web. For search, enter the server's address, e.g.
http://localhost:8888(the next section sets one up). Decide on Use without asking each time (see below). Save. - Settings → AI → Team: tick
fetch_urlandweb_searchfor each role that should use them.*covers them too.
Until a role has them, every run warns that they are registered and named by no role, and no worker sees them. The bottom of the Web pane lists the roles that have them.
The console reads the same settings.json, so the Web settings apply to console runs too.
Run a SearXNG server#
SearXNG is a self-hosted metasearch engine: it asks public search engines and answers in JSON. It needs no account and no key, and the queries go where you run it. The simplest way is Docker.
Run it with its settings in a folder on this computer; on its first start it writes a default settings.yml there:
docker run -d --name searxng -p 127.0.0.1:8888:8080 -v C:/searxng:/etc/searxng --restart unless-stopped searxng/searxng:latest| Part | Meaning |
|---|---|
-p 127.0.0.1:8888:8080 |
Port 8888 on this computer leads to 8080 inside the container, where SearXNG listens. Change the left number for another port. 127.0.0.1 keeps it reachable from this computer only, not from the network |
-v C:/searxng:/etc/searxng |
settings.yml lives in C:\searxng, editable with any editor and kept when the container is recreated |
--restart unless-stopped |
It starts again with Docker |
Turn JSON on. SearXNG answers only HTML by default, and web_search reads JSON. In C:\searxng\settings.yml, under search:, list json among the formats:
search:
formats:
- html
- jsonThen restart it:
docker restart searxngCheck it before pointing Enactive at it:
curl "http://localhost:8888/search?q=test&format=json"JSON with a "results" array means it works. 403 Forbidden means JSON is still off.
Ports cannot be changed on an existing container. A container created without -p, or with the wrong port, has to be recreated: copy its settings out (docker cp searxng:/etc/searxng/. C:/searxng/), remove it (docker rm searxng), and run the command above. Then put the new address in Settings → Web.
Asking, and runs nobody watches#
With Use without asking each time off, every page and every search asks first, naming the address or the query. A run nobody can answer — a background or scheduled one, or a console run with --approve deny — is not offered the tools at all: a question nobody can answer would be a refusal on every call.
With it on, they are used without a question, by every run including scheduled ones. Asking for a page sends its address out of this computer, and a search sends its query; both are written by the model from the task. Turn the question off only for tasks whose requests you trust.
What comes back, and the limits#
| Limit | Value | Why |
|---|---|---|
| Addresses | http and https only, public addresses only |
A URL the model writes is checked by nobody. Allowed to reach this computer or its network, fetch_url would read a local admin page, a router or a cloud machine's metadata endpoint |
| Where the address check happens | At the connection, against what the name resolved to then | Checked only before the request, a name could resolve to a public address for the check and to this machine for the connection |
| Redirects | Followed one at a time, each checked, at most 5 | Followed automatically, a public page could send the request on to this machine |
| Proxy | None: fetch_url connects directly |
Through a proxy the check would be of the proxy, not of the page's address |
| Page read | Up to 2 MB | A larger page is read that far, and the result says so |
| Text returned | 12,000 characters by default; the call may ask for 500–50,000 with max_chars |
A cut always says how much was not shown |
| Content | Web pages and text (text/*, JSON, XML, YAML, JavaScript) |
A PDF or an image is refused, naming its type, rather than read as garbage |
| Time | 30 seconds per page or search | |
| Search results | 8 by default, at most 20; snippets cut to about 300 characters | |
| Search query | At most 300 characters | A longer query is text being sent out of the machine, not a search |
Everything the tools bring back is marked as text from the web — data to work with, not instructions. A page can say anything, including what to do next.
The search server's address is not limited to public ones: it is the server you configured, usually on this computer.
A search server that does not answer is reported as unavailable, never as an empty result: "no results" is a finding about the web, a stopped container is not. Search engines that SearXNG could not reach are listed under the results.
Requests carry the user agent Enactive/1.0. Sites behind bot protection may refuse them as they refuse curl; search usually still finds another copy of the page.
Filesystem and recovery boundaries#
Built-in path-based file tools reject absolute/escaping paths, inspect links that can lead outside the workspace, and protect reserved application-state paths. These checks are not a general sandbox for shell or external servers.
Tracked writes use canonical file identity and scoped sequence information. Revert can refuse when a later scope wrote the same file, the user edited it, or the operation cannot be represented safely. Read the refusal instead of assuming that Failed means all files were restored.
Use version control or an independent backup for recovery beyond the artifact journal. A tool-driven command can have effects that cannot be expressed as a text-file undo.
Backup and moving installations#
For a restorable desktop setup, preserve both user-level settings/templates and workspace state. Stop active work and use a consistent database backup approach; copying a live SQLite database without considering its journal/WAL is not a reliable backup procedure.
After restoring to another account or machine, verify provider and MCP credentials. DPAPI-protected material may need to be entered again. Recheck workspace paths, local model installation, and project dependencies.
The historical AIClient → Enactive rename changed names, environment-variable prefixes, state folders, and secret-protection entropy. There is no complete automatic migration from that product name. Preserve original data before making a manual migration, and re-enter keys rather than assuming copied encrypted values will decrypt. See the historical migration notes.
Troubleshooting reference#
| Symptom | Likely cause | What to check |
|---|---|---|
| Desktop says “No such folder” | Workspace path is absent or wrong | Select/create the intended folder before running |
| Console worked in an unexpected empty folder | Console created a misspelled workspace | Inspect the printed absolute workspace path |
| Provider cannot be reached | Service down, wrong URL, authentication, or network issue | Provider kind, API prefix, service availability, returned error |
| Model appears in a dropdown but calls fail | Catalog entry is not a compatibility test | Exact installed/available ID; endpoint/field/tool support |
| OpenAI-compatible request returns 400 | Unsupported request field or wrong API surface | Actual Chat Completions payload, especially temperature/token fields |
| Model describes JSON instead of doing work | Missing/unreliable structured tool calls | Use a tool-capable model; leave implicit calls off by default |
| Local model is very slow | Memory pressure, context size, queued requests | Reduce context/concurrency and inspect local provider utilization |
| Review does not run | Review binding is blank | AI → Phases; ReviewRequired alone does not enable it |
| Review rejects apparently correct work repeatedly | Reviewer misreads evidence or task/check mismatch | Read journal and feedback; qualify a stronger reviewer |
| Template vanished | Malformed/unreadable JSON or unsafe ID | File syntax, ID, scope folder, and reload |
| Global edit has no effect | Workspace definition shadows it | Origin metadata and matching IDs |
| Workspace edit seems unchanged | Editor saved to Global | Explicit Scope selection; remaining local override |
| Different commands run during final checks | Built-in criteria are literal .NET commands | Edit criterion Command fields as well as goal parameters |
| Template cannot run in console | Missing required inputs | Supply repeated --param "id=value" arguments |
| Scheduled build/test task is Incomplete | Console AskBefore plus unattended denial | Current console limitation; use interactive desktop or extend the host |
| Background task refuses an action | No approver in background handler | Open Inbox and re-run interactively |
| Background launch says Not started | Stage changes is enabled | Use foreground staging or turn staging off deliberately |
| Build passes while a proposed edit is wrong | Build saw disk, not unapplied staged content | Understand staging before trusting the check |
| Undo/revert leaves a file | Later write/user edit or unsupported effect | Read the conflict/revert event; inspect current content |
| Changing desktop settings does not change CLI | Separate composition roots | Console environment configuration |
| A custom WorkerId uses another role | Unknown ID falls back to default | Team IDs and console's built-in-only team |
| MCP failure prevents all work | An enabled server cannot initialize | Test, fix, or disable that server |
Run warns fetch_url, web_search are named by no role |
Web is on, but no role has the tools | Tick them for the role under AI → Team |
Worker uses curl instead of fetch_url |
Its role does not have fetch_url |
AI → Team, and the run's warnings |
web_search: Search is not available |
The SearXNG server is stopped, or the address/port is wrong | docker ps; the curl check in Web |
web_search: answered … but not with JSON |
JSON is off in SearXNG | search.formats in its settings.yml, then restart it |
fetch_url: not a public address |
The page is on this computer or its network | Intended; read local services with other tools |
| Web tools missing from a scheduled or background run | The question is on, and nobody can answer it | Settings → Web → Use without asking, if those tasks are trusted |
| App closes and background work stops | Process exited rather than hiding | Close-to-tray and explicit Quit behavior |
Remote access#
Starting tasks from a browser has its own chapter: Remote access, and what the service can and cannot see is in Remote security. The full server runbook is REMOTE_OPERATIONS; the parts below are the ones an operator reaches for most.
Two things belong here because they are troubleshooting rather than setup.
The panel shows an old build after a deploy. It should not: the page's stylesheets and scripts
are addressed by a fingerprint of their own contents, so a new build is a new URL that no cache has
an answer for. If it happens anyway, the deploy has not run or has not succeeded — check
journalctl -u enactive-deploy, and remember that the deploy only installs a build that is green
in CI for the branch named in /etc/enactive-remote/deploy.env.
A computer shows as Offline. The host syncs every 15 seconds, is shown offline after 45 without one, and backs off up to two minutes when the gateway is unreachable. Offline means the desktop application is not running, remote access is off in its settings, the computer was revoked under Computers, the account was disabled, or the computer is on a different gateway. Settings → Remote access → Test connection says which; it connects and publishes this computer's workspaces, so a success there is also what makes the workspaces selectable.
Running the remote gateway#
Settings. All are environment variables in /etc/enactive-remote/gateway.env (mode 0600),
which deploy/gateway.env.example lays out with every one named. Every value is checked at start, and
a bad one stops the gateway with a sentence naming it.
| Variable | Default | One line |
|---|---|---|
ENACTIVE_REMOTE_DB |
required | MySQL connection string for the protocol-2 database, enactive_remote_v2 |
ENACTIVE_PUBLIC_ORIGIN |
— | The address people use, scheme and host only; the providers send people back to it |
ENACTIVE_GITHUB_CLIENT_ID, ENACTIVE_GITHUB_CLIENT_SECRET |
— | A GitHub OAuth app, callback <origin>/auth/github/callback; both or neither |
ENACTIVE_GOOGLE_CLIENT_ID, ENACTIVE_GOOGLE_CLIENT_SECRET |
— | A Google OAuth client, redirect <origin>/auth/google/callback; both or neither |
ENACTIVE_ADMISSION |
list |
list: only approved identities may create an account; open: anyone who signs in |
ENACTIVE_LIMIT_HOSTS_PER_USER |
5 | Computers per account |
ENACTIVE_LIMIT_DEVICES_PER_USER |
10 | Devices per account |
ENACTIVE_LIMIT_ACTIVE_RUNS_PER_USER |
3 | Runs in progress per account |
ENACTIVE_LIMIT_QUEUED_COMMANDS_PER_HOST |
50 | Commands waiting for one computer; device removals and endorsements are counted apart, up to twice the devices limit |
ENACTIVE_LIMIT_TASKS_PER_DAY |
200 | Tasks per account in the last 24 hours |
ENACTIVE_LIMIT_OPEN_INVITES_PER_USER |
5 | Unused, unexpired invitations per account |
ENACTIVE_LIMIT_SEALED_BYTES_PER_USER |
209715200 | Bytes of encrypted content per account (200 MiB) |
ENACTIVE_RETENTION_DAYS |
30 | Days of history kept; older events, notices and ended runs are deleted hourly |
ENACTIVE_DEV_SIGNIN |
never set | A sign-in without a provider, for tests; refused outside Development |
ENACTIVE_GITHUB_BASE, ENACTIVE_GITHUB_API, ENACTIVE_GOOGLE_AUTHORITY |
never set | Move the providers to another host, for tests against a fake; outside Development they stop the start |
ENACTIVE_DATA (the Data Protection keys), ENACTIVE_BEHIND_TUNNEL and ASPNETCORE_URLS are set in
the systemd unit, and the backup's ENACTIVE_BACKUP_* settings on its cron line.
Admitting people. A command on the gateway's own binary, run on the server with the gateway's environment (how):
admin admissions the identities waiting for approval
admin approve <provider>:<id> let this identity create an account (e.g. github:12345)
admin refuse <provider>:<id> turn this identity away
admin disable <userId> stop an account: sign-ins, sessions and queued commands
admin enable <userId> let a disabled account sign in again
admin sessions revoke <userId> sign the account out everywhereEvery change is written to the audit trail as the operator. refuse does not stop an account that
already exists; disable does.
The cutover from protocol 1 is done by hand onto a new database, and the deploy timer refuses to do it: REMOTE_OPERATIONS, the cutover.
Restore. deploy/backup.sh keeps each night's dump with an archive of the Data Protection keys
beside it, and deploy/verify-restore.sh restores the newest dump into a scratch database and checks
it. A restore without the keys signs everyone out, and nothing else is lost. The procedure is in
REMOTE_OPERATIONS, backups.
Not automated, on purpose.
- Approving sign-ups and every other admission decision: a person runs
admin. - Installing a release that changes the schema or the protocol: the timer parks it.
- Comparing the served panel with the published fingerprints: the pipeline publishes them; nothing compares them unless a person does (how).
- Copying backups off the machine.
Maintaining this wiki#
Update the relevant page whenever a behavior changes in host composition, template resolution, permission matching, settings persistence, or model adapters. In particular, recheck the documented limitations before removing them: many of these behaviors cannot be inferred from UI labels or schema fields alone.
For documentation-only changes, validate relative links, headings/anchors, and executable example syntax. Use the engine's template validator/resolver for changed template examples. A full model-backed run is unnecessary unless the documentation claims live provider compatibility.