Skip to content

refactor(dev): run the dev server's ordinary I/O on one session-scoped platform runtime (Effect FileSystem phase 2, PR 2) - #551

Merged
ScriptedAlchemy merged 7 commits into
mainfrom
feat/effect-filesystem-phase2-dev
Sep 4, 2026
Merged

refactor(dev): run the dev server's ordinary I/O on one session-scoped platform runtime (Effect FileSystem phase 2, PR 2)#551
ScriptedAlchemy merged 7 commits into
mainfrom
feat/effect-filesystem-phase2-dev

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Phase 2 of Effect FileSystem/Path adoption, second PR: the dev server. Behavior-preserving; follows the contract in docs/effect-conventions.md (FileSystem/Path section, keep-raw list binding). First PR: #540.

What changed

Layer wiring. startDevServer (src/dev/workbench-server.ts) creates one makeScopedEffectRuntime(platformLayer) per session via createDevPlatformRuntime() (src/dev/platform-run.ts) — inside the function, never at module top level (effect is a CLI cold-start cost, #530) — and releases it from the returned session's close after session.close() has closed every service that ran on it. A failed start closes the runtime and reports both errors through DevServerStartError if the cleanup also fails.

Every dev service takes the runtime as an optional platformRuntime?: DevPlatformRuntime constructor option. DevPlatformRuntime (src/dev/platform-runtime.ts) is a deliberately Effect-free handle (close() only): service option types sit on the package's public declaration graph, which public-api.test.ts ("keeps every public declaration graph free of effect") forbids from importing effect. Implementations resolve the handle to its PlatformRun edge with platformRunOf(options.platformRuntime)PlatformError unwrapped to the Node cause exactly like runWithPlatform; absent a handle it is runWithPlatform, so every service stays constructible on its own (and every existing service test keeps passing unchanged). Both modules live under dev/, not in src/effect/platform.ts, which is bundled into the emitted installers (kept byte-identical, see below).

Migrated to FileSystem on the session runtime (ordinary reads, temp directories, removals):

  • dev/project-service.ts — config identity read (readFileBytes)
  • dev/package-build-service.ts — stale output-file removal (fs.remove(..., { force: true }))
  • dev/host-install-manager.ts — MCP config document read (readFileString, ENOENT via isPlatformErrno) and writes (writeFileString)
  • dev/skill-document-service.ts — document body reads
  • dev/workbench-assets.ts — root realPath, asset stat + read; missing root/asset stays a miss
  • dev/runtime-provider-loader.ts — provider realPath containment + stat (resolveDevRuntimeProvider takes the handle as its fourth argument)
  • dev/eval/eval-service.ts, dev/playground/{hook,host-discovery,mcp-probe,native,script}-playground-service.ts — reads, makeTempDirectory, remove
  • dev/runtime-generation-store.ts — manifest/asset reads and makeDirectory through FileSystem, but on runWithPlatform: providers construct the store through the public createRuntimeGenerationStore factory whose effect-free options contract (runtime-store-contracts.ts, exported from agent-bundle/api, feat(api): export the dev.runtime.provider protocol types, errors, store and registry contracts from agent-bundle/api #528) has no session runtime to hand it. Adding one would put a dev-server handle on a provider-facing contract for no caller.

Two lifetimes made explicit (both preserved contracts):

  • dev/mcp-session/mcp-session-service.ts — the plugin-data directory is acquired into its own session-lifetime Scope (not a withTempDirectory bracket, since it outlives the call): the finalizer removes it; McpSession.close() closes that scope (releasePluginData), and when the open fails before a session exists the open path closes it instead. mcp-session-types.ts carries the platformRuntime option.
  • dev/playground/script-playground-service.ts — workspace creation (makeTempDirectory) and release (remove) stay separate steps so a removal failure lands in the result's cleanupFailures (workspace-release-failed) instead of replacing the script's outcome.

Kept raw, with reason (file:line on this head):

  • dev/project-service.ts:2 lstat/readdir/realpath — symlink identity and Dirent walk (keep-raw: lstat, link identity); readFile at :172 pairs with the lstat in the same Promise.all for one consistent snapshot.
  • dev/skill-document-service.ts:1 lstat/readdir/realpath — same symlink-aware walk.
  • dev/host-install-manager.ts:10 lstat rows — per the task ("keep its lstat rows raw").
  • dev/package-build-service.ts:183 rmdir — prune-if-empty relies on ENOTEMPTY semantics; FileSystem.remove has no non-recursive "only if empty" form with the same error.
  • dev/eval/eval-service.ts:2–3, dev/playground/native-playground-service.ts:1–2O_NOFOLLOW opens, link, rename, lstat, FileHandle (durable/identity protocols on the keep-raw list).
  • dev/playground/hook-playground-service.ts:1 cp — injectable test seam (options.copy, documented at :104 and :393); the temp directory around it is withTempDirectory.
  • dev/playground/mcp-probe-service.ts:328 rm with maxRetries/retryDelay — retrying removal of a directory a just-killed child may still hold; FileSystem.remove has no retry policy. The no-child paths use FileSystem.remove (removeUnusedPluginData).
  • dev/playground/lifecycle-replay-service.ts:199 existsSync — synchronous render-child path probe in the constructor path.
  • dev/runtime-generation-store.ts:10 lstat/open+sync/rename/writeFile(wx)/readdir — the durable staging→publish protocol (keep-raw: durable-fs).
  • Untouched by design: dev/watcher.ts (chokidar), dev/epoch-store.ts (durable protocol), dev/dev-lock.ts.

Tests

  • New tests/effect-filesystem-phase2-dev.test.ts (unit): layerNoop runtimes pin the call sequence for workbench-assets (realPathstatreadFile, containment miss, memoised second read) and the script playground (makeTempDirectory then remove as separate steps; a remove defect surfaces as cleanupFailures: [{ code: 'workspace-release-failed' }] with the script's exit code intact); real temp dirs pin the OS semantics (missing root/asset → miss like the former ENOENT catch; real workspace removed; MCP session plugin-data removed on close, service-owned release runs once and its failure is reported from close); platformRunOf resolves a session runtime, returns runWithPlatform for undefined, rejects a foreign handle, and unwraps PlatformError to { code: 'ENOENT' }.
  • pnpm typecheck 0 errors; pnpm lint clean; pnpm test:unit 229 files / 3316 passed.
  • Integration (prebuilt): cli, public-api, dev-server, dev-workbench, dev-host-install, eval-service, eval-workbench, hook-playground-*, host-discovery-dev-server, host-install-session, lifecycle-replay-dev-server, mcp-probe-dev-server, mcp-session-*, native-playground-service, playground-*, runtime-generation-store, runtime-provider, script-playground-service, skill-document-service, workbench-surface*, emitted-artifact-effect-surface — 165 passed, 0 failed. cli.test.ts confirms --version/--help still resolve no effect module.
  • Workbench browser acceptance pnpm test:examples:browser (examples-real.e2e, real Chrome, 1440×900, drives startDevServer from src/): 5/5 — populated state, hooks-and-scripts diagnostic-stale → diagnostic-repaired, skills-starter capability-stale → capability-repaired, audiobook-curator routes-catalog-stale → routes-catalog-repaired.
  • pnpm docs:site:build green (language parity ok, 1812 pages).

Timings

Same machine, same path, package built at each ref; medians.

origin/main this PR
agent-bundle --version (15 runs) 69 ms (min 60) 68 ms (min 61)
startDevServer on examples/host-test, start → ready (3 runs) 15.8 s (min 13.6) 11.0 s (min 9.9)
session.close() 15 ms 14 ms

Dev-server startup is dominated by the initial project build; the ~6 ms runtime construction is invisible against it, and the before/after gap is machine-load noise (the "before" runs overlapped a pnpm build), not a speedup claim.

Artifacts

examples/host-test (90 files) and examples/audiobook-curator (20 files) built at the same path from origin/main and this head: byte-identical (diff -r). The installer bundles src/effect/platform.ts; the runtime edge deliberately lives in dev/platform-run.ts so that module is unchanged.

Docs / changeset

docs/effect-conventions.md: phase table row and the FileSystem/Path section describe the session runtime, the Effect-free DevPlatformRuntime handle + platformRunOf split and why (public declaration graph), the runtime-generation-store exception, and the two explicit lifetimes. No website/ page changes: no CLI flag, config key, public export, diagnostic, or host behavior changed. Changeset: .changeset/effect-filesystem-phase2-dev-server.md (patch).

Self-review

Reviewer: change-risk-reviewer subagent on GPT-5.6 (sol, high), read-only, against this branch vs origin/main. No breaking-change, doc, or changeset findings. Two bug findings, one test finding:

  1. workbench-server.ts close: an in-flight asset / Skill / host-discovery handler can reach the runtime after platformRuntime.close(). Dismissed as benign. foreground.close() (inside session.close()) destroys every socket and awaits server.close() before the runtime is disposed, so any handler still running is writing to a destroyed response; its platform read now rejects (interrupted / disposed) instead of completing into a dead socket, and every handler rejection is already caught at foreground-server.ts:498 and turned into a diagnostic write that is a no-op on a destroyed response. No unhandled rejection, no observable difference to a client, no resource left behind (the runtime's Scope owns nothing of the handler's).
  2. workbench-server.ts close: the finally let a disposal failure replace the session's own close failure. Fixed: the session failure is rethrown as-is (disposal still runs, its rejection settled), and a disposal failure surfaces only after a clean session close.
  3. Missing test for a read racing shutdown / both closes failing. Not added: (1) is by design and not observable through the HTTP surface (the socket is gone), and (2) exercises ManagedRuntime.dispose() rejecting, which the platformLayer finalizers cannot produce; the ordering itself (services close, then runtime) is covered by every dev-server integration suite closing a live session, and effect-filesystem-phase2-dev.test.ts covers the runtime's own open/close and edge behavior.

Per this work's no-PR-comments rule, review notes live here.

…latform.ts (installer bundle stays byte-identical)
…; platformRunOf resolves the edge (public declaration graph stays free of effect)
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 581163b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T18:45:57.934832Z 4a6d39c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@551
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@551
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@551

commit: 581163b

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a6d39cc7b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +160 to +161
/** The dev server's session runtime; absent, each program runs on its own `platformLayer`. */
readonly platformRuntime?: DevPlatformRuntime;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the session runtime out of public service options

EvalServiceOptions is exported through agent-bundle/api, but this new structurally typed option cannot be supplied correctly by package consumers: platformRunOf accepts only object identities registered by the internal createDevPlatformRuntime factory and throws TypeError for every other conforming { close(): Promise<void> } value, while neither the factory nor runtime type is exported from a package entry. Thus consumer code can type-check and then fail in the EvalService constructor. Keep this plumbing in an internal options type, or expose and document a supported runtime API.

AGENTS.md reference: AGENTS.md:L71-L77

Useful? React with 👍 / 👎.

@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 4, 2026 19:00
@ScriptedAlchemy
ScriptedAlchemy merged commit 1a8a2f3 into main Sep 4, 2026
14 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the feat/effect-filesystem-phase2-dev branch September 4, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant