Skip to content

refactor: one way to patch a host method - #112

Open
viztor wants to merge 8 commits into
claude/vigilant-mendel-bctz9n-p-t1bfrom
claude/vigilant-mendel-bctz9n-p-h
Open

viztor wants to merge 8 commits into
claude/vigilant-mendel-bctz9n-p-t1bfrom
claude/vigilant-mendel-bctz9n-p-h

Conversation

@viztor

@viztor viztor commented Oct 24, 2026

Copy link
Copy Markdown
Owner

Phase P-H of docs/improvement-plan.md, stacked on #110.

What changed

Item Change
src/host/patch-method.ts (new) patchMethod is now the only way the plugin wraps a method on a host object. A dispose restores only while the own descriptor still holds its wrapper; otherwise the wrapper becomes a pass-through to its unpatched method. When the original was inherited, a dispose deletes the own property; when it was own, a dispose redefines its exact descriptor. A same-site reinstall (same id) wraps the true unpatched method, never the earlier install, so either disposal order ends at the exact original. Identity is read from own descriptors, never with === on a property read. 100% per-file coverage floor.
hideResponsesRoute listProviders, listConfigurableProviders and listModels on llm go through the helper.
decorateModelDiscovery llm.discoverModels goes through the helper.
decorateModelInfo llm.resolveModelInfo goes through the helper.
decoratePlaneAdapters Restructured into one patch per (adapter, method). pi-ai serves deepseek, opencode and opencode-go from one adapter instance, so each patch composes that adapter's plane routes in registry order. This reproduces today's nesting exactly, including the pi-ai-first [INTENDED_CHANGE] characterization row.
rewrapOnAdaptersUpdated Built and tested, wired to no site. Wiring it into decoratePlaneAdapters would flip the plugin-first [KNOWN_DEFECT] rows, and P-R1 deletes that site.
test/reload.test.ts (new) apply A, apply B, dispose A, B intact, dispose B, originals restored exactly, for every site, on own and inherited originals. Also runs the reverse disposal order, the real host (tier A), and the whole sequence through apply().
test/patch-method.test.ts (new) The helper's contract.
test/models-discovery.test.ts Today's per-site pins (receivers, own-identity restore, restore only while current, the frozen service, the shared-adapter nesting order) stay unchanged. Five new cases, marked "a deliberate change in P-H", record the edge cases on hosts that refuse the patch.
Docs docs/improvement-plan.md (P-H status and landed record; the global fetch stacking is left for P6), docs/engineering-notes.md (the measured identity trap), AGENTS.md, README.md, CONTRIBUTING.md.

Why: on the real host the three llm-level disposers never restored. ctx.llm is cordis's traceable proxy, and every read of a function-valued property returns a new shadow proxy, so llm.x === decorated was never true. Each reload stacked a wrapper that kept the previous config's decisions, and a disable left them installed. The plane-adapter disposer restored without checking, so disposing A wrote the prototype's method over B.

Out of scope: the globalThis.fetch swap in lifecycle.ts (a global, not a host service, now on P6's work list) and the llmFacade Proxy in responses-provider.ts (a view, not a patch).

Behaviour changes

None. No wrapper's answer changes on a host that accepts the patch. The docs/improvement-plan.md P-H entry records two things. First, three edge cases that the real LlmRuntime and pi-ai adapter cannot reach: a refused listing define, a refused or frozen plane-adapter method, and a reinstall's registeredRoutes. Second, that a reload or a disable on the real host now restores the methods instead of stacking wrappers.

Review

Round Found Upheld Fixed
1 15 15 15
2 9 9 9
3 6 6 6

Round 3's findings were all low severity and were fixed in tests, test comments and the plan of record, with no runtime change. Each new regression probe was checked by mutating the source, watching exactly the new cases fail, and restoring the source byte-for-byte.

Verification

Node Step Result
26.12.1 pnpm run check All 222 files correctly formatted; 0 errors, 18 warnings (limit 21)
26.12.1 vp test run 89 files, 1449 tests passed
26.12.1 shuffle, seeds 1, 42, 1234 89 files, 1449 tests passed, each seed
26.12.1 vp test run --config vitest.mock.config.ts 3 files, 51 tests passed
26.12.1 vp test run --coverage exit 0; statements 96.71, branches 92.74, functions 96.63, lines 96.7 (floors 95.9/91.3/95.9/95.9); src/host/patch-method.ts at 100/100/100/100
26.12.1 pnpm run build ESM 2 files, 346.58 kB; build complete
26.12.1 scripts/check.ts check passed, including "lib/ carries the current build"
24.22.1 shuffle, seed 7 89 files, 1449 tests passed
24.22.1 mock suite 3 files, 51 tests passed
n/a local-path and credential grep Grepped the full diff plus untracked files: no local paths and no credential-shaped strings. A canary path and a canary key each matched once, which shows the greps read their input.

Note: the catalog freshness check fails on every branch until the vendor-catalog refresh lands on main. That failure is unrelated to this change.


Generated by Claude Code

Add src/host/patch-method.ts (patchMethod), now the only way the
plugin wraps a method on a host object, and move the four instance
patch sites in src/models-discovery.ts onto it: the three listings
hidden by hideResponsesRoute, decorateModelDiscovery,
decorateModelInfo and decoratePlaneAdapters.

On the real host the three llm-level disposers never restored:
ctx.llm is cordis's traceable proxy, every method read returns a new
shadow proxy, so the `===` identity checks were never true and each
reload stacked a wrapper over the last. The helper reads identity
from own descriptors instead. A dispose restores only while its
wrapper is still current (otherwise it becomes a pass-through),
deletes the own property when the original was inherited, and a
same-site reinstall wraps the unpatched method rather than the
earlier install, so either disposal order ends at the exact
original.

decoratePlaneAdapters becomes one patch per (adapter, method) that
composes the adapter's plane routes in registry order, which keeps
today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is
built and tested but wired to no site yet.

test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and
the reverse order) for every site on own and inherited originals, on
the real host (tier A) and through apply(); test/patch-method.test.ts
covers the helper's contract. The existing per-site pins in
test/models-discovery.test.ts stay unchanged; five new cases there
record the deliberate edge changes on hosts that refuse the patch.

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

viztor commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

catalog freshness is red on aec8c1a because the vendor changed its catalog. This PR didn't cause it. The Go plane renamed gemini-4-argon-contributor to gemini-4-argon, added iceman-contributor-free, and its limits page changed with it, so every branch that predates the regenerated tables is stale.

The fix exists. 2ebaffc on #105 takes the regenerated tables unchanged and moves the fixtures and counts with them. d51a3e6 makes the live suite skip free models the gateway has withdrawn. Both are being merged up the stack branch by branch. #106–#110 are syncing now. This branch gets them in the sync pass that follows the review now running on it, and that push re-runs this check.


Generated by Claude Code

What the review found, and what changed:

- rewrapOnAdaptersUpdated reinstalls its site inside the plugin's own
  teardown and on every foreign registry change. Its JSDoc in
  src/host/patch-method.ts now says so, counts the reinstalls a release
  causes, says a caller disposes it before releasing its adapters, and
  notes that a throw from a reinstall is logged by the host.
- The plan of record said all three P-H edge changes are unreachable on
  the real host for one reason; the third (the live-reload listing) is
  unreachable for a different one. docs/improvement-plan.md now gives
  each its reason.
- The tier-A reload cases for the listing and discovery sites proved the
  row only structurally. test/reload.test.ts now gives every site a
  tier-A probe whose decorated answer must differ from the host's own,
  so "survivor still decorates" and "last dispose restores" can fail.
- "patchMethod is the only way the plugin wraps a host method" was
  asserted in docs and enforced by nothing. test/architecture.test.ts
  now has a textual tripwire over everything apply() reaches, with a
  self-check of planted violations, and the apply()-twice reload case
  requires a patch record on every own function apply() leaves on the
  stand-in.
- The tripwire missed several write forms (Object.assign,
  Object.defineProperty/defineProperties, Reflect.setPrototypeOf,
  Object.setPrototypeOf, and an Object.* target reached through a
  property path such as registration?.adapter or this.llm). The
  patterns now catch them, and the self-check plants each one (17
  planted, up from 13). Nothing new in src/ matches.
- The docs said "any Reflect.* write" where the pattern matches a fixed
  set. The test docstring, AGENTS.md and the plan's P-H section now
  name the four Reflect and four Object methods matched, and why a
  prototype swap is listed.

No runtime behaviour changed.

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

viztor commented Oct 26, 2026

Copy link
Copy Markdown
Owner Author

Second review of P-H

This phase's first reviews ran on the general model because the dedicated reviewer was unavailable at the time. It has now had the dedicated review.

Rounds (lenses: correctness, rules, test matrix)

round found upheld fixed upheld by lens
1 5 4 4 correctness 1, rules 1, matrix 2
2 3 3 3 correctness 1, matrix 2
3 2 2 2 correctness 1, matrix 1

All findings were low severity.

Fixed

  • src/host/patch-method.ts: the rewrapOnAdaptersUpdated JSDoc now says the listener reinstalls the site inside the plugin's own teardown and on every registry change. It counts the reinstalls a release causes, says the caller must dispose it before releasing adapters, and notes that a throw from a reinstall is logged by the host.
  • docs/improvement-plan.md: the plan said all three P-H edge changes are unreachable on the real host for one reason. The third one, the live-reload listing, is unreachable for a different reason, and each change now states its own.
  • test/reload.test.ts: every site's tier-A reload case now has a probe whose decorated answer must differ from the host's own. Before, the listing and discovery rows only proved the structure, so "survivor still decorates" and "last dispose restores" could not fail. Now they can.
  • test/reload.test.ts: the apply()-twice case now requires a patchMethod record on every own function apply() leaves on the stand-in llm and adapter.
  • test/architecture.test.ts: a new textual tripwire checks that "patchMethod is the only way to wrap a host method" across everything apply() reaches, with a self-check of planted violations. Over rounds 2 and 3 it was widened to also catch Object.assign/defineProperty/defineProperties/setPrototypeOf, Reflect.setPrototypeOf, and a target reached through a property path (registration?.adapter, this.llm). The self-check now plants 17 violations, up from 13. Nothing new in src/ matches.
  • AGENTS.md and docs/improvement-plan.md: they now list the exact four Reflect and four Object methods the rule matches, replacing "any Reflect.* write", and say why a prototype swap is included.

Declined

Verification did not uphold one round-1 finding. Every upheld finding was fixed, and none is left open. There is no runtime behaviour change; the only stricter thing is the architecture tripwire.

The full gate passes: check, unit suite, mock suite, build and bundle gate.

Commit: 3cfacd9


Generated by Claude Code

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.

2 participants