Installation and startup failures
The process that will not start, the port that is taken, the store that degraded, and ignored environment values.
This page covers the office that never opens: a process that exits, a port that is taken, a database that quietly stopped being a database, and environment values that were read once and then never again. It does not cover a running office whose turns fail — that is Providers, models and routing.
What these failures have in common
Startup is the one moment when the environment is the authority. Everything the server needs to know about the machine is read exactly once, at module load, from real environment variables layered over .env. From the second boot onwards, eight of those values have been copied into the installation settings and the saved settings win instead.
Two consequences follow, and they explain most of this page. First, a running process is a snapshot: editing a file it has already read changes nothing until you restart, and the health endpoint will tell you the process has drifted. Second, the office is built to keep running rather than to refuse. A database it cannot open becomes an in-memory store, an unreadable skills directory becomes an empty catalogue, and a malformed number becomes its default. All of that is deliberate, and all of it means the symptom is usually an absence rather than an error.
Before anything else on this page: read the boot log, and read GET /api/health. Between them they answer the mode, the reason for the mode, the store backend and the drift state. See Installation for what a healthy boot looks like.
dev3d failed to start: <message>
What you see. The process prints dev3d failed to start: <message> on stderr and exits with code 1, with no server listening.
What it means
This is the outermost handler of the boot sequence. Anything thrown before the server begins listening is caught here, printed with its own message, and turned into exit code 1 — so the text after the colon is the real cause and is usually worth more than the rest of the line.
Because the deliberate degradations swallow so much, the causes that actually reach this handler are narrow. The most common is a failure to bind the socket, which has its own symptom below. A configuration value that resolves to a path the process cannot use will usually degrade rather than throw. If the message is unfamiliar, the value after the colon is the thing to search for rather than the phrase failed to start.
What to check
- Read the message after the colon, in full. It is produced by whatever threw, so it names the resource rather than the step.
- Confirm nothing is already listening on the configured address. See the port symptom below;
EADDRINUSEis the one error this path wraps in its own explanation. - Check the runtime version. The declared floor is
engines.node >= 24.0.0, and the store reaches for Node's own SQLite module. - Re-run with a debug log level.
DEV3D_LOG_LEVEL=debugin the environment for this start only, so you do not have to remember to undo a saved setting. - Start from a known-good environment. A throwaway database path (
DEV3D_DB=./data/probe.sqlite) andDEV3D_LLM_MODE=mocktogether rule out the database and every provider in one attempt.
Port 8787 on 127.0.0.1 is already in use
What you see. Port 8787 on 127.0.0.1 is already in use. Another dev3d orchestrator is probably still running - stop it, or set PORT to something else. The process then reports that it failed to start and exits 1.
What it means
The bind was attempted and the operating system refused it with EADDRINUSE, which is the one socket error the boot code translates into advice. The address in the message is the resolved bind pair — HOST and PORT, both defaulting to 127.0.0.1 and 8787 — and the message names them because a second copy is the overwhelmingly likely cause.
Note what is and is not happening here. This is not the office refusing to run twice; two orchestrators on two ports are perfectly possible and share nothing except the database file if you point them at the same one. The failure is the socket, nothing further along.
What to check
- Look for the previous process. An orchestrator left running in another terminal, or one whose window was closed rather than stopped, is the usual answer.
- Confirm which process holds the port. On Windows,
netstat -ano | findstr :8787gives a PID thattasklistresolves; on macOS and Linux,lsof -i :8787. A non-dev3d process means the port is simply taken. - Change the port deliberately.
PORTis read from the environment or.envat boot. There is no saved setting for it, so this one really is environment-only. - Check the web client too if you changed the port. The browser talks to the socket on the same origin; the console's own development settings use their own host and port variables, which are not read by the server.
- Confirm the new bind. The boot log prints the HTTP and WebSocket URLs on the first lines after listening succeeds.
The fix. Stop the other orchestrator, or set PORT to a free port before starting. If the port holder is not a dev3d process, changing PORT is the only answer; nothing in the office can take a port from another program.
The boot log says store: memory
What you see. A warning at scope store reading running without persistence (<reason>); history will be lost on exit, and a boot line reading store: memory (<reason>). The office works; it just forgets everything when the process stops.
What it means
Opening the SQLite store is wrapped in a fallback. If anything in that attempt throws — creating the directory, loading Node's SQLite module, opening the file, applying the schema — the store becomes an in-memory implementation carrying the reason in its own backend string. The reason is printed inside the parentheses, at boot and again on GET /api/health, which is why the two are worth reading together.
This is the single most misleading symptom in the project, because everything else continues to work. Runs, turns, approvals and the org chart are all held in memory and served normally. What you lose is the durable record: the event log that lets a reconnecting client replay, and every run you have ever taken, at the moment the process exits.
What to check
- Read the reason inside the parentheses. It is the message from the failed attempt, and it distinguishes a permissions problem from a missing module from a bad path.
- Resolve the database path the way the server does. Relative paths are resolved against the repository root, not your shell's working directory, so
./data/dev3d.sqlitemeans the checkout's owndatadirectory wherever you launched it from. - Check that the directory is writable. The path's parent directory is created if it is missing, so the failure here is usually permission rather than absence. Confirm with a write from the same account that runs the server.
- Check the runtime. Persistence is built on Node's own SQLite module with no driver package. A runtime that does not provide it produces exactly this fallback rather than a crash.
node --versionshould be 24 or newer. - Ask the health endpoint what is live.
curl -s localhost:8787/api/healthreports the same backend string;sqlite (path)means persistence is real andmemory (reason)means it is not. - Do not trust a green run in this state. Anything you wanted to keep was lost at the last restart, and there is no recovery path from a memory store — the events were never written.
The fix. Correct whatever the reason names, then restart. If the reason is an unusable database file rather than a path or permission problem, point DEV3D_DB at a fresh file and keep the old one: the office creates its schema on open and will not repair a damaged file. The database path is environment-only for the life of the process and can never be changed from the console.
I changed .env and nothing happened
What you see. The configuration file was edited and the running office behaves exactly as before. GET /api/health answers with configStale: true and a detail sentence ending The environment is read once at startup, so restart the orchestrator to apply it.
What it means
.env is a bootstrap, not a live configuration source. It is read once at module load, and a real environment variable always wins over it. The process then keeps the resolved values for its whole life, which is why editing the file behind a running server has no effect at all.
What the process can still do is notice. The health endpoint compares the file's modification time with the one it recorded at boot, and separately checks whether a provider key has appeared in the live environment since startup. Either of those flips configStale and produces a detail sentence naming the reason. A key that was removed is deliberately not reported, because a running process cannot observe that reliably.
What to check
- Read
configStaleDetail, not justconfigStale. It names the cause: either.env has been modified since this process startedor the specific variable that is now set for a named provider. - Restart the process. There is no reload command for the environment. The socket has a resynchronise command for client state, which is a different thing and will not help.
- Check whether a real environment variable is shadowing the file. A variable that is set in the shell wins over
.enveven when its value is empty, and an empty value counts as absent — so an exported empty key does not fall through to the file, it unsets the provider. - Remember which values were copied into settings on the first boot. Workspaces root, external-workspace permission, routing posture, concurrency, the soft-spend threshold, shell auto-approval, the approval timeout and the log level were written into the installation settings once. From then on those are read from settings, so editing them in
.envis ignored even after a restart. Change them in the console instead. - Confirm the value actually parsed. An empty string counts as absent, and a non-finite number falls back to its default rather than failing. A mistyped number is therefore indistinguishable from an absent one. See Environment for each variable's real default.
The fix. Restart after editing .env. For the eight values that were copied into settings, change them in the console rather than in the file, because that is where the engine now reads them from. Provider keys and the database path are the reverse case: they are environment-only and can never be moved into settings.
The office came up on scripted employees
What you see. The boot log reports mode: mock (scripted, no billing) and no provider keys found - the office runs its scripted employees end to end. GET /api/health returns llmMode: "mock" with a reason beginning DEV3D_LLM_MODE=auto, and no provider key or keyless base URL was found.
What it means
The default mode is auto, and it resolves to live if any provider is configured and to mock otherwise. The decision is made once, at boot, and the reason is recorded so the mode never has to be guessed at. Zero provider keys is a supported configuration rather than a degraded one: the whole pipeline runs with real stages, real tools and real files on disk, which is why it is a good first install and a good way to reproduce a bug without billing.
The trap is what makes a provider "configured". It is not only a key: the local provider is keyless, and its presence is what counts. Setting a local base URL to point at an OpenAI-compatible runtime moves the installation to live with no key at all, and does so silently.
What to check
- Read
llmModeReason. It distinguishes the three cases in as many words: auto with nothing configured, auto with providers named, or an explicit mode being forced. - Check the provider list in the boot log.
providers configured: noneis the line to look for, and it lists ids when there are any. - Check whether the key that should be there is empty or whitespace. Keys are trimmed, so a value of spaces counts as absent, and a shell variable set to empty shadows the value in
.env. - Check for a keyless local provider.
DEV3D_LOCAL_BASE_URLbeing set at all is enough to make the installation live. If you want a demonstration without billing, leave it unset or forceDEV3D_LLM_MODE=mock. - Check the mode is not being forced.
DEV3D_LLM_MODE=mockwith keys present is reported as exactly that, so it cannot be mistaken for a missing key.
The reverse case is the expensive one. Adding any key, or setting a local base URL, flips auto to live at the next boot and the office starts spending real money on real models. Nothing asks you to confirm. If you want to look around first, set DEV3D_LLM_MODE=mock before the key goes in. See Providers, models and routing for what a live mode with an unusable key looks like.
DEV3D_LLM_MODE=live, but no provider is configured
What you see. The health response carries the reason DEV3D_LLM_MODE=live, but no provider is configured — every turn will fail. The office starts, accepts a brief, and every turn fails.
What it means
live is an instruction rather than a preference: run on real providers whatever the situation. The server does not refuse to start, because refusing would make the misconfiguration harder to diagnose than reporting it; it starts, states the consequence in the reason string, and lets the first turn demonstrate it. This is the one startup symptom whose message is written for exactly this moment.
What to check
- Confirm which providers are configured. The boot log's provider line and
llmModeReasonagree; if both say none, the mode is the whole problem. - Decide which you meant. A real key, or
DEV3D_LLM_MODE=mock(orauto) for a scripted office. Leavinglivein place with no provider guarantees failed turns. - If the key should be there, treat it as a provider problem. Go to Providers, models and routing; the failure at turn time has its own set of messages.
First boot wrote settings I cannot change from .env
What you see. A value you set in .env was honoured once and then stopped having any effect, on this boot and every boot after it.
What it means
On the first boot, exactly eight engine-affecting values are copied into the installation settings: the workspaces root, whether external workspaces are allowed, the default routing posture, maximum concurrency, the soft-spend approval threshold, shell auto-approval, the approval timeout and the log level. From then on the saved settings win, because the engine is handed those values from the office document rather than from the environment.
This is not a bug, and it is not the same rule as the rest of the configuration. Provider keys, the database path, the plugin install gate and every cache path are environment-only for the life of the process and can never be moved into a settings document.
What to check
- Decide which of the two kinds of value you are changing. The list above is copied once; everything else is read from the environment at every boot.
- Change the copied values in the console. They are installation settings, and the settings screen is where they belong.
- Check the value was not already in the settings from a previous install. Reusing a database file from another installation carries its saved settings with it. If the behaviour is inexplicable, point
DEV3D_DBat a fresh file and see whether it disappears.
The console shows one floor named after an old chart
What you see. After upgrading an existing installation, the office opens on a single floor whose name came from the earlier organisation chart, or on one called Default project, instead of the floors you expect.
What it means
The office document is loaded from the store and passed through a tolerant migration before anything else uses it. There is no version field and no migration step to run: the loader recognises the current shape, and if it cannot, it recognises the older shape of a single organisation chart and converts it into one default floor, taking the name, path, colour and budget from the legacy record where they exist. Anything it does not recognise falls back to a fresh default office.
Two related facts are worth having beside this one. The SQLite schema is likewise created idempotently on every open rather than versioned: there is no migrations directory, and a database written by an older build is reused as it is. So "the schema is out of date" is not a step you forgot to run; it is a state you fix by starting a new database file.
What to check
- Look at the whole state, not the selected floor.
curl -s localhost:8787/api/statelists every floor the process actually loaded, which distinguishes a migration result from a display problem. - Confirm which database you are reading. The boot log prints the store backend with its path when persistence is real, and
/api/healthrepeats it. - Keep the old file. A migration is one-way and there is no downgrade path, so copy
DEV3D_DBbefore experimenting. - If the legacy shape is not what you want, start clean. Point
DEV3D_DBat a new file and create the floors you want. Nothing migrates automatically out of a memory store either — if the store degraded, see the symptom above first.
The runtime is older than Node 24
What you see. The office starts but the boot log's store line reads store: memory (...), and the test suites do not run at all. Or a suite refuses to start on a runtime the project declares it supports.
What it means
The repository declares engines.node as >= 24.0.0, and it is a real floor rather than a preference. Persistence is built on Node's own SQLite module with no driver package, reached through a lazy require precisely so that a runtime without it degrades instead of failing at load, and the suites run TypeScript directly under the built-in test runner rather than through a build step.
The result is the worst of both worlds if you do not notice: the server may well start, while the suites that would have told you something is wrong do not run. And because the store degrades rather than crashing, an unsupported runtime presents as a persistence problem rather than a version problem.
What to check
- Check the runtime.
node --version. The declared floor is 24 or newer. - Check the package manager the way the manifest states it. There is no
engines.pnpm; the floor is thepackageManagerfield, enforced through corepack. See Verification for why the manifest is the accurate statement. - Confirm which Node your shell is actually using. A version manager can leave a different runtime on the path than the one you installed.
- Re-check the store line after upgrading. A healthy boot reports
store: sqlite (path); if it still reports memory, the reason inside the parentheses is a different problem.
Every environment variable is optional
What you see. You are looking for the variable you must set before the server will boot — a session secret, a signing key, a required database URL — and cannot find one.
What it means
That is the actual state of the orchestrator: it ships with a default for every variable it reads, and there is no required secret anywhere in it. There is no user, no session and no token, which is why the default bind address is loopback and why the documentation keeps saying it belongs there. The nearest thing to a credential is a provider key, and zero keys is a supported configuration rather than a failure.
If you arrived here looking for SESSION_SECRET specifically, note that it belongs to the website you are reading rather than to the office. That site is a server-rendered application with accounts, and its environment loader throws at module load when its own database URL or session secret is missing — a genuine required-variable failure, in a different program, with a different symptom. Problems with the site go through contact.
What to check
- Confirm which program you are configuring. The orchestrator's variables all carry a
DEV3D_prefix or are the four provider keys and the bind pair; the website has its own set. - Check the resolved values rather than the file. The boot log lists providers, workspace, store and mode;
/api/healthadds the drift state. - Treat a silent default as a decision to review, not a failure. The routing posture falls back to balanced, the log level to info and the mode to auto when a value is unrecognised, so a typo is invisible unless you look at what was resolved.
What this means for a support request. Because nothing is required, a startup problem is almost never "a variable is missing". It is a variable that is set and resolved to something other than what you meant — which is why the boot log and the health endpoint, not the file, are the first two things to read.
The skills count is not what you expect
What you see. The boot line reads skills: 0 | tools: 9 | models: ... on an installation that should have shipped with fifteen skills, and nothing in the log explains the missing ones.
What it means
The skills directory is loaded before the server starts listening, and the loader is deliberately forgiving in two different ways. A single malformed markdown file costs you that file and nothing else: it is skipped with its reason, a warning is logged at scope skills, and the rest load normally. But a directory that cannot be created or read at all is handled more quietly than that — the loader returns an empty catalogue without a warning, because the alternative would be a typo in a markdown file taking the whole boot down with it.
So a count of zero on an installation that expects fifteen means the directory itself, not the files inside it. A count of fourteen means one file, and the log line naming it is the answer. See Writing a skill for the front-matter rules a file has to satisfy.
What to check
- Resolve the configured directory the way the server does.
DEV3D_SKILLS_DIRdefaults to./skillsrelative to the repository root, not your shell's directory. - Search the log for skipped files. A skipped skill logs
skipped <file> — <reason>at scopeskills. A missing required front-matter key is the common reason. - Check the directory is readable by the account running the server. This is the case that produces no log line at all.
- Compare against what the office enables per floor. The boot count is the catalogue; a floor can be configured to enable only some of it, and a skill that is loaded need not be offered to a turn.
Nobody is being asked before shell commands run
What you see. A warning at boot: DEV3D_AUTO_APPROVE_SHELL is on: employees may run shell commands without asking.
What it means
Shell commands are the only action in the system that goes through a human approval round trip, and this switch removes it. That is a deliberate setting for an unattended installation, and it is also the only control on the most dangerous tool in the office, so it is worth being certain it is what you meant. The boot warning exists because the switch is otherwise invisible from the outside.
What to check
- Decide whether the installation is unattended. If a person is watching the console, the approval round trip is cheap and the switch should be off.
- Check where the value actually came from. It is copied into the installation settings on the first boot, so after that the console is the authority, not the file.
- Remember what is still gated. Turning this on removes the approval for shell commands only. The soft-spend gate is a separate setting with its own approval.
When it is not in this list
Startup problems that resist the above are almost always a resolved value that differs from the intended one, so the useful report is the resolved state rather than the configured one. Include:
- The full boot log from the first line, with the log level at debug for that start.
- The complete health response: mode, mode reason, drift flag and detail, store backend, uptime, active runs and pending approvals.
- The runtime version and the package manager version.
- The exact command you started the server with, and the directory you started it from — both matter, because relative paths resolve against the repository root.
- Whether the problem survives a fresh database at a throwaway path with mock mode forced. That single experiment separates configuration from persistence from providers.
Send it through contact. If the office starts and the problem is what happens next, the page you want is probably Providers, models and routing or Budget, cost and stuck runs; Quick start is the shortest path to a known-good run to compare against.
Where to go next
Linked from
Did this page answer your question?