Skip to content

refactor: release replaced runtimes on switchModel; name service constructors - #65

Merged
K-Mistele merged 3 commits into
mainfrom
analyze-layer-vs-make-pattern-in-service-graph-m977n3
Oct 15, 2026
Merged

K-Mistele merged 3 commits into
mainfrom
analyze-layer-vs-make-pattern-in-service-graph-m977n3

Conversation

@K-Mistele

@K-Mistele K-Mistele commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follows a review of fold-core and fold-agent against the make / layer pattern from the RcMap post. It fixes one bug, removes two internal provides, and gives each service constructor a name.

Bug: switchModel kept replaced runtimes open (fold-core)

Root runtimes were built into the session scope, so every model switch left the old runtime's provider HTTP client and layers open until the session closed. A failed switch also left the new runtime open.

Now each root runtime gets its own child of the session scope (OpenSession.ts, provisionRootRuntime):

  • After a successful switch, the old runtime's scope closes. It's idle at that point, because root runs hold the same gate.
  • After a failed switch, the new runtime's scope closes.

New test: SwitchModel.vi.test.ts › switchModel releases the replaced runtime…. It fails on main (expected [] to deeply equal ['first']) and passes here.

Fewer internal provides (fold-agent)

  • SessionTitleGenerator service. It builds the title model once per session. Before, every title provided a fresh LanguageModel layer. The service is provided per session in Launch.ts, next to the output store and the JSONL log, and withGeneratedTitles yields it.
  • No Effect.context capture for titles. withGeneratedTitles used to capture FileSystem only to refresh the summary index after each title. listSessionSummaries already rebuilds any record whose log changed, and the title append changes the log, so that refresh and refreshSessionSummaryIndex are gone. The index now updates on the next listing, which the TUI runs after every durable change. The only Effect.context left in fold-core and fold-agent is Session.open's capture of the tools' generic R, whose services can't be known ahead of time.
  • readJsonlEntries(path). It yields FileSystem and reads the log. loadSessionSummary now uses it instead of building a whole JSONL EventLog layer (Ref, PubSub, Semaphore) just to read one file.

Cleanups

  • Removed the SkillSource service key (fold-core). Nothing provided or yielded it. The SkillSourceService type stays.
  • make inlined into every service class, following the post. Each service this PR touches is declared as class X extends Context.Service<X, Shape>()('id', { make: Effect.gen(...) }) with static readonly layer = Layer.effect(this, this.make) (or (args) => Layer.effect(this, this.make(args)) for services that take config). No separate makeX effects or liveXLayer constants remain.
    • fold-core: the ToolRuntime, AgentRuntime, Session, HookRunner, and AgentEvents implementations move into their *Service.ts modules, and the *Layer.ts files are deleted. ToolEventSink, SessionControls, and Profiles follow the same pattern. Callers now use X.layer (for example liveToolRuntimeLayer becomes ToolRuntime.layer and layerHookRunner(hooks) becomes HookRunner.layer(hooks)).
    • fold-agent: Photon, ManagedBinaries, OutputStore, and SessionTitleGenerator. layerOutputStore(opts) becomes OutputStore.layer(opts).
    • Moving the implementations re-indents large blocks, so review with whitespace hidden (git diff -w).

Left out on purpose

  • layerCodingToolServices and layerJsonlNode stay. They're documented in the README as helpers a host calls in its own startup code, which is where wiring belongs.
  • Launch.ts keeps Layer.build + Effect.provideContext(sessionServices). That provides a built layer; it doesn't capture the environment. Effect.provide(layer) would close the session's services when launchSession returns, but they must live as long as the session, as the README documents.
  • fold-cli is untouched.

Verification

  • bun run lint, bun run format:check, and bun run typecheck all pass.
  • bun run test passes everywhere except 2 live tests in fold-agent/test/Codex/CodexLive.vi.test.ts. These run against the real Codex backend with local ~/.fold/auth.json credentials, which reject the model ('gpt-5.5' model is not supported when using Codex with a ChatGPT account). They're skipped in CI and unrelated to this change.

🤖 Generated with Claude Code

…tructors

- fold-core: each root runtime gets its own child of the session scope. A
  successful switchModel closes the replaced runtime's scope (its provider
  client and layers); a failed one closes the new runtime's. Previously both
  stayed open until the session closed.
- fold-agent: SessionTitleGenerator service, provided per session with the
  output store and log, builds the title model once instead of providing a
  LanguageModel layer for every title.
- fold-agent: loadSessionSummary reads entries with readJsonlEntries instead
  of building a whole JSONL EventLog layer.
- fold-core: drop the SkillSource service key; nothing provided or yielded it.
- Name constructors: Photon, ManagedBinaries and OutputStore put make on the
  service key with layer = Layer.effect(this, this.make); fold-core layer
  files export makeX beside each live layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a14171-1b19-772e-812c-382bdc40b824
@humanlayer-channels-app humanlayer-channels-app Bot added the bug Something isn't working label Oct 15, 2026
Comment thread packages/fold-agent/src/Mode/Launch.ts Outdated
const titleServices = yield* Effect.context<FileSystem.FileSystem | HttpClient.HttpClient>()
const titles = yield* SessionTitleGenerator
// The summary index refresh runs after later sends, which run without this service, so take it now.
const indexServices = yield* Effect.context<FileSystem.FileSystem>()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Please yield this instead of using context.

K-Mistele and others added 2 commits October 15, 2026 15:35
…ration

withGeneratedTitles captured FileSystem with Effect.context only to refresh
the summary index right after each title. listSessionSummaries already
rebuilds any record whose log changed, and the title append changes the
log, so the eager refresh only saved one rebuild (which the next turn's
append would invalidate anyway). Remove it and the now-unused
refreshSessionSummaryIndex; withGeneratedTitles now needs only
SessionTitleGenerator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a14171-1b19-772e-812c-382bdc40b824
Follow the RcMap post's pattern: every service this PR touches declares its
constructor inline as the `make` option of `Context.Service`, with
`static readonly layer = Layer.effect(this, this.make)` (or a function of the
make's arguments). No separate makeX effects or liveXLayer constants remain.

- fold-core: the ToolRuntime, AgentRuntime, Session, HookRunner, and
  AgentEvents implementations move into their service modules; the
  *Layer.ts files are deleted. ToolEventSink, SessionControls, and Profiles
  inline make the same way. Callers use X.layer.
- fold-agent: ManagedBinaries, OutputStore, and SessionTitleGenerator inline
  make (Photon already did).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a14171-1b19-772e-812c-382bdc40b824
@K-Mistele
K-Mistele merged commit 4bd8c58 into main Oct 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant