GitHub
Revision 1latest

Artiflow CLI and API Reliability Investigation

A complete record of the CLI configuration trace, deployed API diagnosis, Effect runtime recovery, verification, merged work, and the remaining cleanup-error edge case.

Revision 1 · Published · Source Format 1

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:

  1. ARTIFLOW_BASE_URL
  2. base_url from ~/.artiflow/config.toml
  3. The built-in http://localhost:3000 default

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:

ProbeHistorical resultMeaning
GET /200DNS, TLS, Vercel, and the web deployment were reachable
GET /api/projects/prj_access_probe500Vercel matched the API route, but its application runtime was unhealthy
Invalid non-mutating POST /api/projects500Failure occurred before normal request validation completed
GET /projectsTimed out during one probeDatabase-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:

SurfaceRuntime and poolObserved behavior
Pages and server actionsShared ManagedRuntime from server/runtime.tsConnected successfully; UI reads and writes worked
/api/* routesSeparate HttpRouter.toWebHandler layer and PostgreSQL poolFirst connection timed out
CLIEffect 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:

  1. Layer.tap marks a generation initialized only after its full Effect layer builds successfully.
  2. A rejection before that marker replaces the failed generation with a fresh handler.
  3. The partially initialized generation is disposed.
  4. The original request still fails, avoiding an unsafe transparent replay of a write.
  5. The next request can initialize again; CLI create and publish operations already retry transient HTTP failures with stable idempotency keys.
  6. 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:write
  • bun run check-types
  • bun run lint
  • bun 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 failures
  • eb71cec style(web): widen project page layouts
  • e2478a5 chore(web): ignore local Prisma artifacts
  • 80259bc 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.

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.