Skip to content

refactor(nanoclaw): own one Harness client for the adapter's lifetime - #960

Closed
chughtapan wants to merge 2 commits into
impl/openclaw-harness-cutoverfrom
impl/nanoclaw-harness-cutover
Closed

refactor(nanoclaw): own one Harness client for the adapter's lifetime#960
chughtapan wants to merge 2 commits into
impl/openclaw-harness-cutoverfrom
impl/nanoclaw-harness-cutover

Conversation

@chughtapan

Copy link
Copy Markdown
Owner

Plan step 11. Stacked on #959. Completes the adapter half of the cutover — both runtimes now reach MoltZap only through HarnessClient.

Why NanoClaw is not a copy of the OpenClaw cutover

Two constraints OpenClaw does not have, and both fail silently if you get them wrong.

The factory takes no arguments. registerChannelAdapter("moltzap", { factory: () => makeMoltZapAdapter() }) runs at module import inside NanoClaw's own process, so there is no seam to inject a client through. fromHarnessClient documented "the caller owns the client's scope" — but there is no such caller. The adapter now owns the scope itself: acquire in setup(), release in teardown(). That inverts the borrowed-client contract, so the borrowing constructor is deleted rather than kept beside the owning one.

The channel is one file. copy-nanoclaw-assets.mjs installs exactly src/channels/moltzap.ts into a downloaded checkout. A sibling module compiles locally, passes every unit test, and breaks only in a real install. Verified the diff adds no new import — the only added specifier is @moltzap/client, and no new file appears under nanoclaw-channel/src.

The simulator does not start a daemon

The plan originally said it should. That instruction predates harnessClientForProfile, which is self-starting — a simulator-side daemon plus an adapter-side daemon would bind the slot's single mcpPort and fail EADDRINUSE.

The decisive reason is not symmetry with OpenClaw: a real NanoClaw checkout has no simulator, so an adapter that depends on one being started for it only works under test. The simulator's job is to write the slot (reserveSlotMcpPort + writeMoltZapProfileConfig) and pass MOLTZAP_PROFILE + MOLTZAP_CONFIG_HOME.

An acquire-without-spawn export (harnessClientForRunningProfile) was considered and rejected — a second acquisition path existing so one caller can skip spawning is the parallel-path shape this program deletes. One slot, one owner.

Gates

  • pnpm nx run workspace:precommitexit 0 (includes lint:sloppy-code-guard)
  • git grep 'MoltZapService|MoltZapChannelCore|fromProfile|fromService' -- 'packages/nanoclaw-channel/**' returns nothing, including AGENTS.md and generated MODULE.md
  • no new imports and no new files under nanoclaw-channel/src — the packaging constraint holds

Docs

packages/nanoclaw-channel/AGENTS.md no longer describes "an injected HarnessClient or the transitional MoltZapChannelCore path… profile/core-backed until profile-to-MCP acquisition is available." That sentence described the exact gap this stack closed.

packages/client/AGENTS.md also said adapters "wrap MoltZapChannelCore". That stopped being true two PRs ago; MoltZapChannelCore now sits behind the loopback boundary inside moltzapd.

ADR conformance (R-pass)

Governing outcome Record Owner Binds this diff? Verdict Evidence
Adapters use one local MCP client boundary and do not construct the daemon's backing services 20260801-harness-is-one-profile-slot-daemon v2 no — followed CONFORMS The adapter holds a HarnessClient only; the grep gate above is zero.
HarnessClient is the sole adapter-facing capability 20260801-harness-client-owns-runtime-context v2 (contested) no — followed CONFORMS One constructor, one capability. Eval wiring (MOLTZAP_EVAL_MODE) is retained — orthogonal to transport.
main No main-governing record covers adapter composition. Confirmed against the 5 main-governing records in the #926 baseline.

No new divergence; no ledger row changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P76aaa1STr3WPZ3nDascta

chughtapan and others added 2 commits August 5, 2026 11:22
The adapter carried two constructors and production only ever reached the
one that built MoltZapService and MoltZapChannelCore directly, because
nothing could resolve a daemon endpoint. The slot now carries its port, so
the profile name alone resolves a client.

NanoClaw registers its channel through a zero-argument factory that runs at
module import, so there is no seam to inject a client through. The adapter
therefore owns the scope itself: it acquires in setup and releases in
teardown. That inverts the borrowed-client contract, so the borrowing
constructor goes rather than surviving beside it.

The simulator writes the slot and stops there. It does not start a daemon:
the adapter starts its own, and a real nanoclaw checkout has no simulator to
start one for it. Two daemons would bind the same slot's single port.

The channel stays one file. The asset copier installs exactly this module
into a downloaded checkout, so acquisition lives here or behind a client
export, never in a sibling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P76aaa1STr3WPZ3nDascta
@chughtapan

Copy link
Copy Markdown
Owner Author

Superseded by #974, which collapses this stack into one PR against main. This branch's commits are all contained in it — nothing is lost, and this thread stays readable. Closing to leave one place to review.

@chughtapan chughtapan closed this Aug 6, 2026
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