Skip to content

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

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

viztor wants to merge 11 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 and README.zh-CN.md (the paired "Changed in 1.x" bullet), 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

One, user-visible: disabling or reloading the plugin now restores the host methods it wrapped. As the README pair words it: the model listing, model discovery and model-info decorations, and the plane adapters' fill, come off on a disable; a live reload replaces them instead of stacking another layer, so a setting removed in the reload stops applying. Before this change, on the real host a reload stacked a wrapper that kept the previous config's decisions, and a disable left them installed.

  • No wrapper's answer changes on a host that accepts the patch.
  • Separately, three deliberate edge changes, each pinned in test/models-discovery.test.ts, on hosts the real LlmRuntime and pi-ai adapter never are: a refused listing define (the listing install is now all-or-nothing), a refused or frozen plane-adapter method (skipped), and a reinstall's registeredRoutes (read from the host's own listing).

The plan of record retitles P-H from refactor: to fix: disabling or reloading the plugin restores the host methods it wrapped, because main squash-merges, the pull request's title is the release signal, and release-please hides refactor. This pull request's title is not changed in this pass; the retitle is still owed.

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.

Restacked onto main 1.8.0

The branch now merges the restacked P-T1b (#110), which carries main 1.8.0 through its own parent chain; P-H merges no main itself.

Scope now: one way to patch a host method, on top of the restacked P-T1b (both harness channels, no pi patch).

  • src/host/patch-method.ts (patchMethod, rewrapOnAdaptersUpdated): a dispose restores only while its own descriptor still holds its wrapper, deletes an inherited original's own property, and a same-site reinstall wraps the unpatched method. 100% per-file coverage floor.
  • The four src/models-discovery.ts sites (the three listings, discovery, model-info, plane-adapter fill) migrated to it, with the three pinned edge changes above.
  • test/reload.test.ts: apply A, apply B, dispose A, dispose B for every site, in both orders, on own and inherited originals, and on tier A on both channels; also a reload with changed config and apply() twice.
  • The architecture tripwire for Reflect/Object writes and assignments onto llm, an adapter, settings and globalThis.
  • The README "Changed in 1.x" bullet in both languages (the Chinese one is new in the restack, d001586, for the pairing gate), and the plan and AGENTS.md map lines.

Dropped, because the restacked base now does it or the decision changed: the pinned-cell gate (the alpha cluster with pi 1.3.0 and the harness's patch) is superseded by the owner's no-patch decision; the alpha floor cell (pi 1.3.0 unpatched, cordis family at the record) and the rc floor cell (pi 1.1.0) stand in for it. Nothing in the helper, its sites or its tripwires was dropped.

Decisions applied: D14'' and D22'' (which replace D14' and D22'): tier A in test/reload.test.ts runs on whichever channel is installed, newest and floor, with no pi patch. D24 and D25 do not touch P-H's files.

Merge: the only conflict was docs/improvement-plan.md: P-T1b's plan, its D14''/D22'' rows and status table, plus P-H's own landed phase-6 record, its pinning-phase exception, its test-plan entries, the P6 fetch-restore item and "P-H before P-R1". The other P-H files merged clean and carry P-H's side unchanged.

Live re-measures CI must confirm: none specific to P-H. The e2e-host job runs the npm cell of both channels on this pull request (it touches src/models-discovery.ts and vite.config.ts); the alpha npm cell should show the inherited HOST_LIMITATION (the host's dsh-app-boot cannot save a config edit under the cordis npm now resolves), so its three save rows skip, as on P-T1b.

The Verification table above predates the restack; CI on the pushed head (ff2db1d) is the current record.


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

…t1b' into claude/vigilant-mendel-bctz9n-p-h

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The pairing gate main added (test/docs-pairing.test.ts) counts the
'Changed in 1.x' list items per language; P-H's English bullet about
disable and reload restoring the wrapped host methods had no Chinese
twin, so the merge left the pair at 46/45.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The plan's P-H status row and its section heading now carry a `fix:`
title, "disabling or reloading the plugin restores the host methods it
wrapped", and record why it is no longer `refactor:`: main squash-merges,
the pull request's title is the release signal, and release-please hides
`refactor`, so the changelog would never carry the restore on a disable
or a reload that the README's "Changed in 1.x" notes announce.

- fixed: rules-docs-3-PH-RULES-4 — status row and section heading
  retitled with the reason; the pull request's own title and its
  "Behaviour changes" section move at the post-restack PR-body pass.

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

viztor commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

Restacked onto main 1.8.0. This branch now merges the restacked P-T1b (#110) and so carries main 1.8.0 through its parent. It does not merge main directly. Pushed head: ff2db1dda13b3a7bb0ed6867791720540a483584.

Conflicts and how they were resolved

Real conflict:

  • docs/improvement-plan.md: I took P-T1b's plan, including its D14''/D22'' two-channel, no-patch rows and its status table, and kept P-H's own sections on top: the landed phase-6 record, the pinning-phase exception for P-H's deliberately red cases, the P-H and P6 test-plan entries, the P6 fetch-restore item, and "P-H before P-R1". The P-H status row now reads "merged P-T1b".

Merged clean, checked by hand:

  • src/models-discovery.ts, src/cordis-context.ts: P-T1b never touched them, so P-H's side is unchanged.
  • test/architecture.test.ts, test/models-discovery.test.ts, test/support/src.ts, vite.config.ts: P-T1b's changes sit alongside P-H's and add to them.
  • AGENTS.md, CONTRIBUTING.md: P-H's lines are unchanged, and the src/host/patch-method.ts map line is there.
  • README.md / README.zh-CN.md: the English reload bullet came through without its Chinese twin, so the pairing gate failed. d001586 adds the zh bullet under ## 1.x 变更.

Dropped

  • The pinned-cell gate (the alpha cluster with pi 1.3.0 plus the harness's patch) is gone because of the owner's no-patch decision (D14''). The alpha floor cell (pi 1.3.0 unpatched) and the rc floor cell (pi 1.1.0) cover it instead. Nothing in the helper, its sites or its tripwires was dropped.

Review rounds

Round Found Upheld Fixed
1 2 2 1: the body's "Behaviour changes: None" next to a README "Changed in 1.x" bullet, with the zh README missing. The body is corrected in this pass. Still open: the plan of record shows P0 as "in review" although #105 is merged. That belongs to re-planning the stack, not to P-H.
2 1 1 0: the README pair differs in meaning in three places inherited from main, which the pairing gate cannot detect. This is not fixed on this branch.
3 1 1 1 in the tree: ff2db1d retitles P-H in the plan as fix: disabling or reloading the plugin restores the host methods it wrapped. The PR title is still refactor: and needs the same retitle, or the squash commit hides the fix from the changelog.

No code changed after the merge. The two follow-up commits only touch docs.

Live re-measures still owed

  • None specific to P-H. On this PR the e2e-host job runs the npm cell of both channels. The alpha npm cell should show the inherited HOST_LIMITATION: the host's dsh-app-boot cannot save a config edit under the cordis npm now resolves, so its three save rows skip, as they do on P-T1b.

Reviews that ran on the general model because the dedicated reviewer was unavailable: all twelve passes. These were the resolutions, both-kept and rules-docs reviews in rounds 1 to 3, plus the verify pass after each of rounds 1, 2 and 3.


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