Artiflow CLI and API Reliability Investigation
Executive summary
The Artiflow CLI was correctly configured and could reach the deployed Vercel application, but project creation failed when the CLI-facing API runtime timed out while opening its own PostgreSQL connection. The deployed UI continued working because pages and server actions use a separate Effect runtime and database pool.
The investigation found a second-order lifecycle problem: Effect's Fetch handler caches its layer-build promise. If the first API-layer build rejects, a warm Vercel function keeps returning the cached rejection instead of attempting initialization again. A recovery change was implemented, tested, verified, committed, and merged into main.
A later review identified one remaining edge case: cleanup of the failed handler can itself reject and mask the original, actionable initialization failure. That follow-up is validated as real and should be addressed in a new PR.
What was investigated
CLI server configuration
The CLI talks to http://localhost:3000 by default. Server selection uses this precedence:
ARTIFLOW_BASE_URLbase_urlfrom~/.artiflow/config.toml- The built-in
http://localhost:3000default
ARTIFLOW_CONFIG_PATH selects an alternate TOML file. The resolved base URL is passed directly into the Effect HTTP API client, whose Artiflow routes live under /api/....
There is no current CLI command that initializes the config file. Former configuration commands were removed in favor of file- and environment-based configuration. Users must create the TOML file manually or set ARTIFLOW_BASE_URL.
Production failure
The configured CLI request reached:
POST https://artiflow.endalk200.com/api/projects
It returned HTTP 500. Server logs showed:
SqlError
ConnectionError
PgClient: Connection timed out
operation: connect
The installed @effect/sql-pg implementation performs a connection check while acquiring its pool and applies a five-second default connection timeout.
Evidence collected from the deployment
Read-only probes established that the deployment and API route were publicly reachable:
| Probe | Historical result | Meaning |
|---|---|---|
GET / | 200 | DNS, TLS, Vercel, and the web deployment were reachable |
GET /api/projects/prj_access_probe | 500 | Vercel matched the API route, but its application runtime was unhealthy |
Invalid non-mutating POST /api/projects | 500 | Failure occurred before normal request validation completed |
GET /projects | Timed out during one probe | Database-backed rendering was intermittently unavailable at that moment |
Vercel returned x-matched-path: /api/[...path], ruling out a missing route or reverse-proxy mismatch.
The deployed UI subsequently created and deleted a Project successfully. That observation was decisive: the production database, schema, credentials, and write permissions were functional. The failure was isolated to the CLI-facing API runtime rather than the database as a whole.
Root cause
The web UI and HTTP API construct independent Effect runtimes:
| Surface | Runtime and pool | Observed behavior |
|---|---|---|
| Pages and server actions | Shared ManagedRuntime from server/runtime.ts | Connected successfully; UI reads and writes worked |
/api/* routes | Separate HttpRouter.toWebHandler layer and PostgreSQL pool | First connection timed out |
| CLI | Effect HTTP client calling /api/* | Received the API runtime's 500 |
In Effect 4.0.0-beta.100, toWebHandlerLayerWith memoizes initialization with logic equivalent to:
handlerPromise ??= buildTheLayerAndCreateTheHandler();
The rejected promise is retained. A transient failure during the first API request therefore poisons that warm handler generation: later requests immediately receive the same rejection even after PostgreSQL becomes reachable.
This explains why the original request logged a five-second connection timeout while later API probes returned empty 500 responses in roughly half a second.
Recovery implemented and merged
The API handler now manages recoverable handler generations:
Layer.tapmarks a generation initialized only after its full Effect layer builds successfully.- A rejection before that marker replaces the failed generation with a fresh handler.
- The partially initialized generation is disposed.
- The original request still fails, avoiding an unsafe transparent replay of a write.
- The next request can initialize again; CLI create and publish operations already retry transient HTTP failures with stable idempotency keys.
- Rejections after successful initialization do not recycle a healthy pool.
A regression test makes repository initialization fail once, then verifies that the next request rebuilds the handler and creates the Project successfully.
Verification completed
The following checks passed after the recovery change:
bun run format:writebun run check-typesbun run lintbun run test- Production
next build
Lint completed successfully with four existing CSS specificity warnings unrelated to the API change.
Git work completed
The original recovery work was developed on fix/api-handler-recovery, committed as focused Conventional Commits, and later merged into main.
The corresponding commits visible on the updated main branch were:
1e4e9c8 fix(api): recover from handler initialization failureseb71cec style(web): widen project page layoutse2478a5 chore(web): ignore local Prisma artifacts80259bc chore(artiflow): update linked project
The project-page width, ignore rules, and linked Project update were pre-existing worktree changes that the user explicitly requested be committed alongside the recovery work. They were kept as separate focused commits.
Newly discovered cleanup edge case
Automated review correctly identified this sequence:
webHandler = makeWebHandler();
await currentHandler.dispose();
throw initializationCause;
Although Effect's scope-close error channel is typed as never, resource finalizers can still defect. dispose() uses Effect.runPromise(Scope.close(...)), and runPromise rejects when the Effect exits with a defect.
If disposal rejects, JavaScript exits the catch callback at the await. The later throw initializationCause is never reached, so the cleanup defect replaces the PostgreSQL initialization failure that triggered recovery.
Recommended follow-up
Preserve the initialization error as the caller-visible failure and report cleanup separately:
try {
await currentHandler.dispose();
} catch (disposalCause) {
console.error("Failed to dispose uninitialized API handler", disposalCause);
}
throw initializationCause;
Add a regression test in which layer initialization and cleanup both fail, then assert that the request still rejects with the original initialization error. Cleanup failure should remain observable in server diagnostics without replacing the primary cause.
The review's P2 priority is reasonable: simultaneous initialization and finalizer failures are uncommon, but masking the primary failure damages the diagnostics and retry behavior that the recovery change exists to protect.
Current status
- The primary poisoned-handler recovery is merged into
main. - The linked Artiflow Project resolves as
prj_e1beac91-fcd6-415c-87f2-8dac5cff1fb3. - The cleanup-error masking issue has been validated against the installed Effect source.
- The cleanup follow-up branch, commit, push, and PR were not completed before this report was requested.
Next action
Create a focused branch from current main, add the dual-failure regression test, preserve the original initialization error while reporting disposal failure, run the complete repository verification suite, then push and open a small follow-up PR.