codex-copilot-dx 0.6.8 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,9 +112,11 @@ ccdx doctor --online --profile all
112
112
  ### PM Studio opt-in patch
113
113
 
114
114
  CCDX can add the isolated Claude catalog to PM Studio without switching its
115
- active account or profile. GPT requests continue to use the bearer supplied by
116
- the current PM profile; exact enabled Anthropic model IDs use the isolated
117
- CCDX Claude profile. Neither credential is copied into the other application.
115
+ active account or profile. GPT requests keep PM Studio's native official GitHub
116
+ Copilot transport and the bearer supplied by the current PM profile. Model
117
+ discovery tries the compatible local CCDX merge first, and only exact enabled
118
+ Anthropic model IDs use the isolated CCDX Claude profile. Neither credential is
119
+ copied into the other application.
118
120
 
119
121
  The current patch recipes support only the exact, unmodified PM Studio 2.9.7
120
122
  and 2.9.10 bundles on macOS. This is exact-version support, not a version range:
@@ -155,12 +157,21 @@ matches the frozen official fingerprint. The staged and installed CCDX patch
155
157
  must still pass strict ad-hoc signature verification and match the complete
156
158
  patched-tree record in the backup manifest.
157
159
 
158
- The patch is App-wide: every PM profile sends its Copilot API traffic through
159
- the loopback-only `/pm-ccdx/*` relay. Run ordinary `ccdx` before opening PM
160
- Studio; if port 2026 is unavailable, PM Copilot requests cannot connect. The
161
- relay explicitly supports only the Copilot paths discovered in the validated
162
- 2.9.7 and 2.9.10 bundles (`GET /models` and `POST /chat/completions`, `/responses`, and
163
- `/embeddings`) and is not a general-purpose proxy.
160
+ The patch uses split origins, not an App-wide proxy. GPT and other non-Claude
161
+ inference requests remain on PM Studio's native official GitHub Copilot origin;
162
+ they do not enter CCDX or depend on port 2026. Model discovery first tries the
163
+ loopback-only `GET /pm-ccdx/models` merge and accepts it only when the relay
164
+ returns the `X-CCDX-PM-Relay: split-origin-v1` compatibility marker. If the
165
+ relay is unavailable or incompatible, model discovery safely falls back to the
166
+ official directory and GPT remains available. Exact enabled Claude chat uses
167
+ `POST /pm-ccdx/chat/completions` and fails closed if CCDX is unavailable; it
168
+ never falls back to the PM enterprise account. The relay does not expose
169
+ `/responses` or `/embeddings` and is not a general-purpose proxy.
170
+
171
+ The loopback relay trusts local callers that present a valid Copilot bearer.
172
+ CCDX uses the PM bearer only to fetch or validate official model access; it is
173
+ never used as the credential for the isolated Claude upstream request. Keep
174
+ port 2026 local to the machine.
164
175
 
165
176
  Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
166
177
  updater is not disabled; an official update may overwrite the patch or may
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -73,7 +73,7 @@ function routingTarget(value) {
73
73
  function pmRouteSummary(requests = {}) {
74
74
  const routes = requests.by_route;
75
75
  if (!routes || typeof routes !== "object") return null;
76
- const names = ["pm_models", "pm_chat_completions", "pm_responses", "pm_embeddings"];
76
+ const names = ["pm_models", "pm_chat_completions"];
77
77
  const totals = names.reduce((summary, name) => {
78
78
  const route = routes[name] || {};
79
79
  summary.total += finiteNumber(route.total) || 0;
@@ -81,7 +81,7 @@ function pmRouteSummary(requests = {}) {
81
81
  summary.errors += finiteNumber(route.errors) || 0;
82
82
  return summary;
83
83
  }, { total: 0, active: 0, errors: 0 });
84
- return `PM relay: ${count(totals.total)} requests, ${count(totals.active)} active, ${count(totals.errors)} errors; models ${count(routes.pm_models?.total)}, chat ${count(routes.pm_chat_completions?.total)}, responses ${count(routes.pm_responses?.total)}, embeddings ${count(routes.pm_embeddings?.total)}`;
84
+ return `PM relay: ${count(totals.total)} requests, ${count(totals.active)} active, ${count(totals.errors)} errors; models ${count(routes.pm_models?.total)}, chat ${count(routes.pm_chat_completions?.total)}`;
85
85
  }
86
86
 
87
87
  function errorCode(error) {
package/src/doctor.mjs CHANGED
@@ -590,6 +590,12 @@ export async function collectDoctorChecks({
590
590
  fix: "stop the running adapter, then run ccdx start",
591
591
  });
592
592
  }
593
+ } else if (pm.app.state === "predecessor") {
594
+ checks.push({
595
+ kind: "warn",
596
+ message: `PM Studio ${version} has a verified predecessor split patch`,
597
+ fix: "ccdx pms setup",
598
+ });
593
599
  } else if (pm.app.state === "clean") {
594
600
  checks.push({
595
601
  kind: "warn",
@@ -17,8 +17,6 @@ const ROUTE_NAMES = Object.freeze([
17
17
  "messages_count_tokens",
18
18
  "pm_models",
19
19
  "pm_chat_completions",
20
- "pm_responses",
21
- "pm_embeddings",
22
20
  "not_found",
23
21
  ]);
24
22
 
@@ -104,8 +102,6 @@ export function classifyAdapterRoute(method, pathname) {
104
102
  if (method === "POST" && pathname === "/v1/messages/count_tokens") return "messages_count_tokens";
105
103
  if (method === "GET" && pathname === "/pm-ccdx/models") return "pm_models";
106
104
  if (method === "POST" && pathname === "/pm-ccdx/chat/completions") return "pm_chat_completions";
107
- if (method === "POST" && pathname === "/pm-ccdx/responses") return "pm_responses";
108
- if (method === "POST" && pathname === "/pm-ccdx/embeddings") return "pm_embeddings";
109
105
  return "not_found";
110
106
  }
111
107
 
@@ -2,8 +2,22 @@ import { createHash } from "node:crypto";
2
2
 
3
3
  export const PM_STUDIO_ORIGIN = "https://api.githubcopilot.com";
4
4
  export const CCDX_PM_STUDIO_ORIGIN = "http://127.0.0.1:2026/pm-ccdx";
5
+ export const PM_STUDIO_SPLIT_ORIGIN_MARKER = "split-origin-v1";
5
6
  export const ELECTRON_ASAR_INTEGRITY_SENTINEL = "AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A";
6
7
 
8
+ const PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH = 2_121;
9
+ const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256 = "3da6245d676015ab6cd26e4d6a0d2f54894a687c370aa419c5ad170533d086aa";
10
+
11
+ function splitOriginConfigModule() {
12
+ const source = `47024(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"${PM_STUDIO_ORIGIN}",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>\`req_\${Date.now()}_\${Math.random().toString(36).substr(2,9)}\`}};const c=fetch.bind(globalThis),n="${CCDX_PM_STUDIO_ORIGIN}",a="${PM_STUDIO_SPLIT_ORIGIN_MARKER}",h="X-CCDX-PM-Relay",d=new Set,m=I=>I.headers.get(h)===a,q=I=>AbortSignal.any([I,AbortSignal.timeout(750)].filter(Boolean)),f=(I,e=400)=>Response.json({error:{code:I}},{status:e,headers:{[h]:a}});globalThis.fetch=async(I,e)=>{const t=I.url||I+"";if(t===\`\${l.API_ENDPOINT}/models\`){try{const t=await c(\`\${n}/models\`,e);if(t.ok&&m(t))try{let I=await t.clone().json(),e=I.data||I;if(Array.isArray(e)){d.clear();for(const I of e)/anthropic/i.test(I.vendor||I.owned_by)&&d.add(I.id);return t}}catch{}t.body?.cancel?.()?.catch?.(()=>{})}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I}return c(I,e)}const s=t.startsWith(l.API_ENDPOINT)?t.slice(l.API_ENDPOINT.length):"";if("POST"===e?.method&&["/chat/completions","/responses","/embeddings"].includes(s)){let t="";try{t=(JSON.parse(e.body).model||"").trim()}catch{}if(d.has(t)||/^claude-/i.test(t)){if("/chat/completions"!==s)return f("model_not_supported");try{const I=await c(\`\${n}/models\`,{signal:q(e?.signal)}),t=401===I.status&&m(I);await I.text();if(!t)throw 0;const l=await c(\`\${n}\${s}\`,e);if(!m(l)){await l.body?.cancel?.();throw 0}return l}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I;return f("relay_incompatible",503)}}}return c(I,e)}}`;
13
+ if (source.length > PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH) {
14
+ throw new Error("PM Studio split-origin module exceeds its exact source window");
15
+ }
16
+ return `${source.slice(0, -1)}${" ".repeat(PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH - source.length)}}`;
17
+ }
18
+
19
+ export const PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE = splitOriginConfigModule();
20
+
7
21
  export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
8
22
  id: "pm-studio-2.9.7-build-2.9.7",
9
23
  version: "2.9.7",
@@ -21,50 +35,62 @@ export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
21
35
  dataOffset: 3_832_764,
22
36
  sourceAsarSha256: "36fb8bf2fc9326ddf49baab5cc0b54b38751a7db041bda3c42a3b01107b54118",
23
37
  sourceHeaderSha256: "87746d92e00db2d40c03830ea06de22ec7cba112d07729801ccd02e226ffd961",
24
- patchedAsarSha256: "3dd0f53cdaa35a644d2cf56e4fc2dd20f5c90dc2989b6d81a467ef00ebb620a7",
25
- patchedHeaderSha256: "86ed1113a563b82ecec05de89aab87a5fd1666f35bee76e2d76e2ed5f6dcb3fa",
26
- sourceSentinel: PM_STUDIO_ORIGIN,
27
- patchedSentinel: CCDX_PM_STUDIO_ORIGIN,
38
+ patchedAsarSha256: "799dc18103a4553daefcaaddbe3410a1bfaf68d84407f4c10ef3d996afd71ad0",
39
+ patchedHeaderSha256: "08253795176d5471c6f406a29ccb134875d503bb55bca909b2cd131cd7708510",
40
+ predecessor: Object.freeze({
41
+ id: "split-origin-v1-model-discovery-750ms",
42
+ kind: "split-origin-predecessor",
43
+ asarSha256: "d520d115604225c1a3feb749dbe29ad0d2cd175c5233c010de0e1fade527fa0b",
44
+ headerSha256: "83e5fba3b6751b3098d8f6901db361a9b620ab3b924e0e551d3df612ff412642",
45
+ signingMetadata: Object.freeze({
46
+ identifier: "com.pm-studio.app",
47
+ flags: "0x10002(adhoc,runtime)",
48
+ runtime_version: "26.0.0",
49
+ entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
50
+ }),
51
+ }),
28
52
  targets: Object.freeze([
29
53
  Object.freeze({
30
54
  path: "dist/main/main.js",
31
55
  offset: 101_621_627,
32
56
  size: 9_909_974,
33
- sentinelOffset: 1_120_480,
34
- absoluteSentinelOffset: 106_574_871,
35
- sentinelCount: 1,
36
57
  blockSize: 4_194_304,
37
58
  sourceSha256: "58383be112886becdb8d8e7cce1b3efce07be6716190aa4940627f456eb8edb2",
38
- patchedSha256: "24f1c47831c30f59e8464a66237c6bc9ed771d79f3a6889019dfdf53bc20fe42",
59
+ patchedSha256: "b219d08290b208fe41fa29b1e8ac4a1dee789e15f68fd4ff01eb5cd21249099b",
60
+ predecessorSha256: "69156040fb233a835b7be2f86fbe2f653380f03379adfedf46c63ab45de9d39d",
39
61
  sourceBlocks: Object.freeze([
40
62
  "3ce05a21e8992ccf12966cf9b78fcd28b2f66e6786489b5bb0a06f1d4b2a0c1e",
41
63
  "0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
42
64
  "bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
43
65
  ]),
44
66
  patchedBlocks: Object.freeze([
45
- "afc18cc7d56cce6dd6f3bade8661268a534f2de93de5ba638f6609d2638c7436",
67
+ "dcbffb0f3aaa79a5ca8f1ee9170826b874d9db237c230abcdc0490335b2dc6a8",
46
68
  "0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
47
69
  "bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
48
70
  ]),
49
- }),
50
- Object.freeze({
51
- path: "dist/renderer/js/main.09f18d95.js",
52
- offset: 137_002_230,
53
- size: 2_469_533,
54
- sentinelOffset: 489_935,
55
- absoluteSentinelOffset: 141_324_929,
56
- sentinelCount: 1,
57
- blockSize: 4_194_304,
58
- sourceSha256: "eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
59
- patchedSha256: "89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
60
- sourceBlocks: Object.freeze([
61
- "eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
62
- ]),
63
- patchedBlocks: Object.freeze([
64
- "89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
71
+ predecessorBlocks: Object.freeze([
72
+ "f0b5585d2ff0d403e5c4d35e9bd0b8cbfec5c41a9b9057b8cc808107e889cf46",
73
+ "0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
74
+ "bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
65
75
  ]),
76
+ edit: Object.freeze({
77
+ offset: 1_120_359,
78
+ absoluteOffset: 106_574_750,
79
+ length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
80
+ anchor: "47024(I,e,t){",
81
+ anchorCount: 1,
82
+ sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
83
+ patchedSha256: "873d2dd276bd12a7dc2876f5eaf8c67d4ae34749ead8911d0588badf4e9fc0cf",
84
+ predecessorSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
85
+ replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
86
+ }),
66
87
  }),
67
88
  ]),
89
+ legacy: Object.freeze({
90
+ kind: "global-origin-replacement",
91
+ asarSha256: "3dd0f53cdaa35a644d2cf56e4fc2dd20f5c90dc2989b6d81a467ef00ebb620a7",
92
+ headerSha256: "86ed1113a563b82ecec05de89aab87a5fd1666f35bee76e2d76e2ed5f6dcb3fa",
93
+ }),
68
94
  });
69
95
 
70
96
  export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
@@ -117,50 +143,62 @@ export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
117
143
  dataOffset: 3_833_292,
118
144
  sourceAsarSha256: "d243860770e8b1d8044213924f9704d1fc52f900d6c33461eff6358962330b78",
119
145
  sourceHeaderSha256: "5fc77d5b61e9647fba10fd2bd5752d0889c14da5a00b21d31bc48fdcccf74a69",
120
- patchedAsarSha256: "49f72d999a6085102341c2d551577e164db6f22e4707d2112efa3fd280e7315e",
121
- patchedHeaderSha256: "b264ece5b8e469cd3885a7a91f904f1902e1991f214ecbf228dfe91059ac7008",
122
- sourceSentinel: PM_STUDIO_ORIGIN,
123
- patchedSentinel: CCDX_PM_STUDIO_ORIGIN,
146
+ patchedAsarSha256: "f309fc7e86bb4edaf1fecd9061fb4ddc634357c0ba9299f1d13f8b2ff33efd90",
147
+ patchedHeaderSha256: "98c4af8656e61cc1a1a8b2188592a499a987136f8e6b763b0e83abda788e798d",
148
+ predecessor: Object.freeze({
149
+ id: "split-origin-v1-model-discovery-750ms",
150
+ kind: "split-origin-predecessor",
151
+ asarSha256: "ea28d998056b32ca4115d208a4d81ce629c83f3f5f89c6a66d50749849beb6fc",
152
+ headerSha256: "348d80f5f2d1ebb92e6089195b1703b87467678f80d8946b8ab83b3ead03e5be",
153
+ signingMetadata: Object.freeze({
154
+ identifier: "com.pm-studio.app",
155
+ flags: "0x10002(adhoc,runtime)",
156
+ runtime_version: "26.0.0",
157
+ entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
158
+ }),
159
+ }),
124
160
  targets: Object.freeze([
125
161
  Object.freeze({
126
162
  path: "dist/main/main.js",
127
163
  offset: 101_622_115,
128
164
  size: 10_150_157,
129
- sentinelOffset: 1_241_344,
130
- absoluteSentinelOffset: 106_696_751,
131
- sentinelCount: 1,
132
165
  blockSize: 4_194_304,
133
166
  sourceSha256: "03607bb1edc068396b99f02b9c9e0d8e10388fb658afc112e06e875c123dbc27",
134
- patchedSha256: "e400300575e2d1c5ad089613f14e138612114547ff5060e94cb81e8de587a75a",
167
+ patchedSha256: "71e936c79c3560ab5bd9813479f8a66f4a7b06629edca74953feaaf7904a6564",
168
+ predecessorSha256: "b5b5e37af3d06ffc603bdef620fc1e0254580f0ee325f1a77096af6c78379b79",
135
169
  sourceBlocks: Object.freeze([
136
170
  "3910a9bc5459f493956aafee7b3644e8d242027758f0dcdea97d7c10a324c6bf",
137
171
  "49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
138
172
  "bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
139
173
  ]),
140
174
  patchedBlocks: Object.freeze([
141
- "74022f5ad06c7879b37875efe0a090b23ccfbcf269afd26b10b77690413dca63",
175
+ "203ad1efbdcd869314510086632fdb0089196eb25179c46fb4108133cc65412f",
142
176
  "49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
143
177
  "bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
144
178
  ]),
145
- }),
146
- Object.freeze({
147
- path: "dist/renderer/js/main.5ad30c24.js",
148
- offset: 137_266_992,
149
- size: 2_520_547,
150
- sentinelOffset: 528_919,
151
- absoluteSentinelOffset: 141_629_203,
152
- sentinelCount: 1,
153
- blockSize: 4_194_304,
154
- sourceSha256: "3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
155
- patchedSha256: "dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
156
- sourceBlocks: Object.freeze([
157
- "3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
158
- ]),
159
- patchedBlocks: Object.freeze([
160
- "dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
179
+ predecessorBlocks: Object.freeze([
180
+ "63aa15d8c44614a7b2d9656ddba23109d40bab66b66b36339154953bd164da75",
181
+ "49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
182
+ "bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
161
183
  ]),
184
+ edit: Object.freeze({
185
+ offset: 1_241_223,
186
+ absoluteOffset: 106_696_630,
187
+ length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
188
+ anchor: "47024(I,e,t){",
189
+ anchorCount: 1,
190
+ sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
191
+ patchedSha256: "873d2dd276bd12a7dc2876f5eaf8c67d4ae34749ead8911d0588badf4e9fc0cf",
192
+ predecessorSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
193
+ replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
194
+ }),
162
195
  }),
163
196
  ]),
197
+ legacy: Object.freeze({
198
+ kind: "global-origin-replacement",
199
+ asarSha256: "49f72d999a6085102341c2d551577e164db6f22e4707d2112efa3fd280e7315e",
200
+ headerSha256: "b264ece5b8e469cd3885a7a91f904f1902e1991f214ecbf228dfe91059ac7008",
201
+ }),
164
202
  });
165
203
 
166
204
  export const PM_STUDIO_RECIPES = Object.freeze([
@@ -334,7 +372,7 @@ function addMismatch(issues, label, actual, expected) {
334
372
  if (actual !== expected) issues.push(`${label}: expected ${expected}, got ${actual}`);
335
373
  }
336
374
 
337
- function inspectTarget(parsed, target, recipe) {
375
+ function inspectTarget(parsed, target) {
338
376
  const entry = findAsarEntry(parsed.header, target.path);
339
377
  if (!entry) return { path: target.path, missing: true };
340
378
 
@@ -352,8 +390,28 @@ function inspectTarget(parsed, target, recipe) {
352
390
  }
353
391
 
354
392
  const bytes = parsed.buffer.subarray(absoluteOffset, absoluteOffset + size);
355
- const sourceSentinel = Buffer.from(recipe.sourceSentinel);
356
- const patchedSentinel = Buffer.from(recipe.patchedSentinel);
393
+ let edit = null;
394
+ if (target.edit) {
395
+ let editOffset;
396
+ let editLength;
397
+ try {
398
+ editOffset = integer(target.edit.offset, `${target.path} edit offset`);
399
+ editLength = integer(target.edit.length, `${target.path} edit length`);
400
+ } catch (error) {
401
+ return { path: target.path, offset, size, absoluteOffset, malformed: error.message };
402
+ }
403
+ if (editLength === 0 || editOffset + editLength > bytes.length) {
404
+ return { path: target.path, offset, size, absoluteOffset, malformed: "edit window is outside the target" };
405
+ }
406
+ const anchor = Buffer.from(target.edit.anchor || "");
407
+ edit = {
408
+ offset: editOffset,
409
+ absoluteOffset: absoluteOffset + editOffset,
410
+ length: editLength,
411
+ sha256: sha256Hex(bytes.subarray(editOffset, editOffset + editLength)),
412
+ anchorPositions: occurrencePositions(bytes, anchor),
413
+ };
414
+ }
357
415
  return {
358
416
  path: target.path,
359
417
  offset,
@@ -363,41 +421,48 @@ function inspectTarget(parsed, target, recipe) {
363
421
  blockSize: entry.integrity?.blockSize,
364
422
  blocks: blockSha256(bytes, target.blockSize),
365
423
  integrity: entry.integrity || null,
366
- sourceSentinelPositions: occurrencePositions(bytes, sourceSentinel),
367
- patchedSentinelPositions: occurrencePositions(bytes, patchedSentinel),
424
+ edit,
368
425
  };
369
426
  }
370
427
 
371
- function targetStateIssues(detail, target, recipe, state) {
428
+ function targetStateIssues(detail, target, state) {
372
429
  const issues = [];
373
430
  if (detail.missing) return [`${target.path}: entry is missing`];
374
431
  if (detail.malformed) return [`${target.path}: ${detail.malformed}`];
375
432
 
376
433
  addMismatch(issues, `${target.path} offset`, detail.offset, target.offset);
377
434
  addMismatch(issues, `${target.path} size`, detail.size, target.size);
378
- addMismatch(issues, `${target.path} absolute sentinel offset`,
379
- detail.absoluteOffset + target.sentinelOffset, target.absoluteSentinelOffset);
380
435
  addMismatch(issues, `${target.path} integrity algorithm`,
381
436
  detail.integrity?.algorithm, "SHA256");
382
437
  addMismatch(issues, `${target.path} block size`, detail.integrity?.blockSize, target.blockSize);
383
438
 
384
439
  const patched = state === "patched";
385
- const expectedHash = patched ? target.patchedSha256 : target.sourceSha256;
386
- const expectedBlocks = patched ? target.patchedBlocks : target.sourceBlocks;
387
- const expectedSentinelPositions = [target.sentinelOffset];
388
- const presentPositions = patched ? detail.patchedSentinelPositions : detail.sourceSentinelPositions;
389
- const absentPositions = patched ? detail.sourceSentinelPositions : detail.patchedSentinelPositions;
440
+ const predecessor = state === "predecessor";
441
+ const expectedHash = patched
442
+ ? target.patchedSha256
443
+ : predecessor ? target.predecessorSha256 : target.sourceSha256;
444
+ const expectedBlocks = patched
445
+ ? target.patchedBlocks
446
+ : predecessor ? target.predecessorBlocks : target.sourceBlocks;
390
447
 
391
448
  addMismatch(issues, `${target.path} SHA-256`, detail.sha256, expectedHash);
392
449
  addMismatch(issues, `${target.path} integrity hash`, detail.integrity?.hash, expectedHash);
393
450
  if (!sameArray(detail.blocks, expectedBlocks)) issues.push(`${target.path} computed block hashes do not match ${state} recipe`);
394
451
  if (!sameArray(detail.integrity?.blocks, expectedBlocks)) issues.push(`${target.path} header block hashes do not match ${state} recipe`);
395
- if (!sameArray(presentPositions, expectedSentinelPositions)) {
396
- issues.push(`${target.path} ${state} sentinel position/count does not match recipe`);
397
- }
398
- if (absentPositions.length !== 0) issues.push(`${target.path} contains the opposite-state sentinel`);
399
- if (presentPositions.length !== target.sentinelCount) {
400
- issues.push(`${target.path} ${state} sentinel count is ${presentPositions.length}, expected ${target.sentinelCount}`);
452
+ if (target.edit) {
453
+ addMismatch(issues, `${target.path} edit offset`, detail.edit?.offset, target.edit.offset);
454
+ addMismatch(issues, `${target.path} absolute edit offset`, detail.edit?.absoluteOffset, target.edit.absoluteOffset);
455
+ addMismatch(issues, `${target.path} edit length`, detail.edit?.length, target.edit.length);
456
+ addMismatch(issues, `${target.path} edit SHA-256`, detail.edit?.sha256,
457
+ patched
458
+ ? target.edit.patchedSha256
459
+ : predecessor ? target.edit.predecessorSha256 : target.edit.sourceSha256);
460
+ if (!sameArray(detail.edit?.anchorPositions, [target.edit.offset])) {
461
+ issues.push(`${target.path} module anchor position/count does not match recipe`);
462
+ }
463
+ if (detail.edit?.anchorPositions?.length !== target.edit.anchorCount) {
464
+ issues.push(`${target.path} module anchor count is ${detail.edit?.anchorPositions?.length || 0}, expected ${target.edit.anchorCount}`);
465
+ }
401
466
  }
402
467
  return issues;
403
468
  }
@@ -406,26 +471,44 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
406
471
  const parsed = parseAsarBuffer(value);
407
472
  const asarSha256 = sha256Hex(parsed.buffer);
408
473
  const headerSha256 = sha256Hex(parsed.headerBytes);
409
- const targets = recipe.targets.map((target) => inspectTarget(parsed, target, recipe));
474
+ const targets = recipe.targets.map((target) => inspectTarget(parsed, target));
410
475
  const commonIssues = [];
411
476
  addMismatch(commonIssues, "ASAR data offset", parsed.dataOffset, recipe.dataOffset);
412
477
 
413
478
  const issuesFor = (state) => {
414
479
  const patched = state === "patched";
480
+ const predecessor = state === "predecessor";
481
+ const expected = predecessor ? recipe.predecessor : null;
415
482
  const issues = [...commonIssues];
416
483
  addMismatch(issues, "ASAR SHA-256", asarSha256,
417
- patched ? recipe.patchedAsarSha256 : recipe.sourceAsarSha256);
484
+ patched ? recipe.patchedAsarSha256 : predecessor ? expected?.asarSha256 : recipe.sourceAsarSha256);
418
485
  addMismatch(issues, "ASAR header SHA-256", headerSha256,
419
- patched ? recipe.patchedHeaderSha256 : recipe.sourceHeaderSha256);
486
+ patched ? recipe.patchedHeaderSha256 : predecessor ? expected?.headerSha256 : recipe.sourceHeaderSha256);
420
487
  for (let index = 0; index < recipe.targets.length; index += 1) {
421
- issues.push(...targetStateIssues(targets[index], recipe.targets[index], recipe, state));
488
+ issues.push(...targetStateIssues(targets[index], recipe.targets[index], state));
422
489
  }
423
490
  return issues;
424
491
  };
425
492
 
426
493
  const cleanIssues = issuesFor("clean");
427
494
  const patchedIssues = issuesFor("patched");
428
- const state = cleanIssues.length === 0 ? "clean" : patchedIssues.length === 0 ? "patched" : "drift";
495
+ const predecessorIssues = recipe.predecessor
496
+ ? issuesFor("predecessor")
497
+ : ["recipe has no recognized predecessor patch"];
498
+ const legacyIssues = [...commonIssues];
499
+ if (!recipe.legacy) {
500
+ legacyIssues.push("recipe has no recognized legacy patch");
501
+ } else {
502
+ addMismatch(legacyIssues, "ASAR SHA-256", asarSha256, recipe.legacy.asarSha256);
503
+ addMismatch(legacyIssues, "ASAR header SHA-256", headerSha256, recipe.legacy.headerSha256);
504
+ }
505
+ const state = cleanIssues.length === 0
506
+ ? "clean"
507
+ : patchedIssues.length === 0
508
+ ? "patched"
509
+ : predecessorIssues.length === 0
510
+ ? "predecessor"
511
+ : legacyIssues.length === 0 ? "legacy" : "drift";
429
512
  return {
430
513
  recipeId: recipe.id,
431
514
  state,
@@ -435,19 +518,30 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
435
518
  targets,
436
519
  cleanIssues,
437
520
  patchedIssues,
521
+ predecessorIssues,
522
+ legacyIssues,
438
523
  };
439
524
  }
440
525
 
441
526
  export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
442
- if (Buffer.byteLength(recipe.sourceSentinel) !== Buffer.byteLength(recipe.patchedSentinel)) {
443
- throw new Error("PM Studio patch requires equal-length origin sentinels");
444
- }
445
527
  const before = inspectAsarBuffer(value, recipe);
446
528
  if (before.state === "patched") {
447
529
  return { changed: false, buffer: Buffer.from(value), before, after: before };
448
530
  }
531
+ if (before.state === "predecessor") {
532
+ const error = new Error("Predecessor PM Studio split-origin patch must be migrated from a verified clean backup");
533
+ error.code = "PM_STUDIO_ASAR_PREDECESSOR";
534
+ error.inspection = before;
535
+ throw error;
536
+ }
537
+ if (before.state === "legacy") {
538
+ const error = new Error("Legacy PM Studio global-origin patch must be migrated from a verified clean backup");
539
+ error.code = "PM_STUDIO_ASAR_LEGACY";
540
+ error.inspection = before;
541
+ throw error;
542
+ }
449
543
  if (before.state !== "clean") {
450
- const error = new Error("PM Studio ASAR does not match the clean or patched recipe");
544
+ const error = new Error("PM Studio ASAR does not match the clean, current split-origin, predecessor, or legacy recipe");
451
545
  error.code = "PM_STUDIO_ASAR_DRIFT";
452
546
  error.inspection = before;
453
547
  throw error;
@@ -455,16 +549,25 @@ export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
455
549
 
456
550
  const output = Buffer.from(value);
457
551
  const parsed = parseAsarBuffer(output);
458
- const sourceSentinel = Buffer.from(recipe.sourceSentinel);
459
- const patchedSentinel = Buffer.from(recipe.patchedSentinel);
460
552
 
461
553
  for (const target of recipe.targets) {
462
554
  const entry = findAsarEntry(parsed.header, target.path);
463
555
  const offset = entryOffset(entry.offset, `${target.path} offset`);
464
- const absoluteSentinelOffset = parsed.dataOffset + offset + target.sentinelOffset;
465
- const existing = output.subarray(absoluteSentinelOffset, absoluteSentinelOffset + sourceSentinel.length);
466
- if (!existing.equals(sourceSentinel)) throw new Error(`${target.path} source sentinel moved before patching`);
467
- patchedSentinel.copy(output, absoluteSentinelOffset);
556
+ const edit = target.edit;
557
+ if (!edit) throw new Error(`${target.path} has no split-origin edit recipe`);
558
+ const replacement = Buffer.from(edit.replacement);
559
+ if (replacement.length !== edit.length || sha256Hex(replacement) !== edit.patchedSha256) {
560
+ throw new Error(`${target.path} split-origin replacement does not match its recipe`);
561
+ }
562
+ const absoluteEditOffset = parsed.dataOffset + offset + edit.offset;
563
+ if (absoluteEditOffset !== edit.absoluteOffset) {
564
+ throw new Error(`${target.path} split-origin edit moved before patching`);
565
+ }
566
+ const existing = output.subarray(absoluteEditOffset, absoluteEditOffset + edit.length);
567
+ if (sha256Hex(existing) !== edit.sourceSha256) {
568
+ throw new Error(`${target.path} source edit window does not match its recipe`);
569
+ }
570
+ replacement.copy(output, absoluteEditOffset);
468
571
  entry.integrity = {
469
572
  ...entry.integrity,
470
573
  algorithm: "SHA256",
@@ -16,6 +16,8 @@ import { requireUpstreamEventStream } from "./stream-contract.mjs";
16
16
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
17
17
 
18
18
  export const PM_STUDIO_RELAY_PREFIX = "/pm-ccdx";
19
+ export const PM_STUDIO_RELAY_MARKER_HEADER = "X-CCDX-PM-Relay";
20
+ export const PM_STUDIO_RELAY_MARKER_VALUE = "split-origin-v1";
19
21
  const COPILOT_ORIGIN = "https://api.githubcopilot.com";
20
22
  const DEFAULT_VALIDATION_TTL_MS = 5 * 60 * 1000;
21
23
  const DEFAULT_VALIDATION_CACHE_SIZE = 128;
@@ -32,11 +34,7 @@ const HOP_BY_HOP_HEADERS = new Set([
32
34
  "transfer-encoding",
33
35
  "upgrade",
34
36
  ]);
35
- const POST_PATHS = new Set([
36
- "/chat/completions",
37
- "/responses",
38
- "/embeddings",
39
- ]);
37
+ const CHAT_COMPLETIONS_PATH = "/chat/completions";
40
38
 
41
39
  function localError(statusCode, code, message) {
42
40
  const error = httpError(message, statusCode);
@@ -46,7 +44,11 @@ function localError(statusCode, code, message) {
46
44
 
47
45
  function sendLocalError(res, statusCode, code, message, headers = {}) {
48
46
  if (res.destroyed || res.writableEnded) return;
49
- res.writeHead(statusCode, { "Content-Type": "application/json", ...headers });
47
+ res.writeHead(statusCode, {
48
+ "Content-Type": "application/json",
49
+ [PM_STUDIO_RELAY_MARKER_HEADER]: PM_STUDIO_RELAY_MARKER_VALUE,
50
+ ...headers,
51
+ });
50
52
  res.end(JSON.stringify({ error: { message, type: "invalid_request_error", code } }));
51
53
  }
52
54
 
@@ -160,6 +162,7 @@ async function responseSnapshot(response) {
160
162
  function responseHeaders(headers, body, options) {
161
163
  return {
162
164
  ...safeUpstreamResponseHeaders(headers, options),
165
+ [PM_STUDIO_RELAY_MARKER_HEADER]: PM_STUDIO_RELAY_MARKER_VALUE,
163
166
  "Content-Length": String(body.length),
164
167
  };
165
168
  }
@@ -179,8 +182,6 @@ function rejectUpstreamRedirect(response) {
179
182
 
180
183
  function mergeModelCatalog(snapshot, modelRouter) {
181
184
  if (!snapshot.ok) return null;
182
- const allowed = modelRouter.allowedModels();
183
- if (!allowed.length) return null;
184
185
  let enterprise;
185
186
  try {
186
187
  enterprise = JSON.parse(snapshot.body.toString("utf8"));
@@ -189,6 +190,8 @@ function mergeModelCatalog(snapshot, modelRouter) {
189
190
  }
190
191
  const data = catalogData(enterprise);
191
192
  if (!data) return null;
193
+ const allowed = modelRouter.allowedModels();
194
+ if (!allowed.length) return snapshot.body;
192
195
  const seen = new Set(data.map((model) => String(model?.id || "").trim()).filter(Boolean));
193
196
  const additions = [];
194
197
  for (const model of allowed) {
@@ -197,7 +200,7 @@ function mergeModelCatalog(snapshot, modelRouter) {
197
200
  seen.add(id);
198
201
  additions.push(model);
199
202
  }
200
- if (!additions.length) return null;
203
+ if (!additions.length) return snapshot.body;
201
204
  return Buffer.from(JSON.stringify(Array.isArray(enterprise)
202
205
  ? [...enterprise, ...additions]
203
206
  : { ...enterprise, data: [...data, ...additions] }));
@@ -215,6 +218,7 @@ async function relayUpstreamResponse(response, reqBody, res, abort, streamIdleTi
215
218
  abort.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
216
219
  res.writeHead(response.status, {
217
220
  ...safeUpstreamResponseHeaders(response.headers, { contentType: "text/event-stream" }),
221
+ [PM_STUDIO_RELAY_MARKER_HEADER]: PM_STUDIO_RELAY_MARKER_VALUE,
218
222
  "Cache-Control": "no-cache",
219
223
  });
220
224
  const reader = response.body.getReader();
@@ -424,6 +428,9 @@ export function createPmStudioRelayHandler(options = {}) {
424
428
  }
425
429
  const merged = mergeModelCatalog(snapshot, modelRouter);
426
430
  if (!merged) {
431
+ throw localError(502, "invalid_upstream", "PM Studio upstream model catalog has an invalid JSON shape");
432
+ }
433
+ if (merged === snapshot.body) {
427
434
  sendSnapshot(res, snapshot);
428
435
  return;
429
436
  }
@@ -431,15 +438,26 @@ export function createPmStudioRelayHandler(options = {}) {
431
438
  res.end(merged);
432
439
  }
433
440
 
434
- async function handlePost(req, res, pathname, token, abort) {
441
+ async function handleChatCompletions(req, res, token, abort) {
435
442
  let releaseRequest = releaseOnce();
436
443
  try {
437
444
  const admission = await acquireRequest(req, { signal: abort.signal });
438
445
  releaseRequest = releaseOnce(admission);
439
446
  abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
440
447
  const body = await readJsonBody(req, { admission, signal: abort.signal });
441
- const supportsStream = pathname === "/chat/completions" || pathname === "/responses";
442
- const streaming = supportsStream && body?.stream === true;
448
+ const modelType = modelRouter.classify(body?.model);
449
+ if (modelType === "unsupported_claude") {
450
+ throw localError(400, "model_not_supported", "The requested Claude model is not enabled for PM Studio");
451
+ }
452
+ if (modelType !== "claude") {
453
+ throw localError(
454
+ 400,
455
+ "native_route_required",
456
+ "Non-Claude PM Studio requests must use the native GitHub Copilot origin",
457
+ );
458
+ }
459
+
460
+ const streaming = body?.stream === true;
443
461
  const requestTimeoutMs = streaming ? streamHandshakeTimeoutMs : upstreamTimeoutMs;
444
462
  const timeoutReason = streaming ? "stream_handshake_timeout" : "upstream_timeout";
445
463
  abort.setTimeout(requestTimeoutMs, timeoutReason);
@@ -452,23 +470,12 @@ export function createPmStudioRelayHandler(options = {}) {
452
470
  }
453
471
  abort.setTimeout(requestTimeoutMs, timeoutReason);
454
472
 
455
- const modelType = modelRouter.classify(body?.model);
456
- if (modelType === "unsupported_claude" || (modelType === "claude" && pathname !== "/chat/completions")) {
457
- throw localError(400, "model_not_supported", "The requested Claude model is not available for this endpoint");
458
- }
459
-
460
- let upstream;
461
- if (modelType === "claude") {
462
- upstream = await options.claudeClient.chatCompletions(body, {
463
- signal: abort.signal,
464
- fetchImpl: fetchClaudeUpstream,
465
- });
466
- } else {
467
- const bodyText = JSON.stringify(body);
468
- upstream = await fetchEnterprise(pathname, "POST", req.headers, token, bodyText, abort.signal);
469
- }
473
+ const upstream = await options.claudeClient.chatCompletions(body, {
474
+ signal: abort.signal,
475
+ fetchImpl: fetchClaudeUpstream,
476
+ });
470
477
  releaseRequest();
471
- await relayUpstreamResponse(upstream, supportsStream ? body : null, res, abort, streamIdleTimeoutMs);
478
+ await relayUpstreamResponse(upstream, body, res, abort, streamIdleTimeoutMs);
472
479
  } finally {
473
480
  releaseRequest();
474
481
  }
@@ -494,7 +501,9 @@ export function createPmStudioRelayHandler(options = {}) {
494
501
  }
495
502
 
496
503
  const upstreamPath = pathname.slice(PM_STUDIO_RELAY_PREFIX.length);
497
- const allowedMethod = upstreamPath === "/models" ? "GET" : POST_PATHS.has(upstreamPath) ? "POST" : null;
504
+ const allowedMethod = upstreamPath === "/models"
505
+ ? "GET"
506
+ : upstreamPath === CHAT_COMPLETIONS_PATH ? "POST" : null;
498
507
  if (!allowedMethod) {
499
508
  sendLocalError(res, 404, "route_not_found", "PM Studio relay route not found");
500
509
  return true;
@@ -512,13 +521,14 @@ export function createPmStudioRelayHandler(options = {}) {
512
521
  abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
513
522
  await handleModels(req, res, token, abort);
514
523
  } else {
515
- await handlePost(req, res, upstreamPath, token, abort);
524
+ await handleChatCompletions(req, res, token, abort);
516
525
  }
517
526
  } catch (error) {
518
527
  const safe = safeFailure(error, abort);
519
528
  if (res.headersSent) {
520
529
  if (!res.destroyed) res.destroy();
521
530
  } else {
531
+ res.setHeader(PM_STUDIO_RELAY_MARKER_HEADER, PM_STUDIO_RELAY_MARKER_VALUE);
522
532
  sendJsonError(res, safe, safe.statusCode || 502);
523
533
  }
524
534
  } finally {
@@ -519,6 +519,10 @@ export function inspectPmStudioApp({
519
519
  && codeSign?.adHoc === true
520
520
  && (!recipe.patchedSigningMetadata
521
521
  || JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
522
+ const predecessorSignatureAccepted = codeSign?.valid === true
523
+ && codeSign?.adHoc === true
524
+ && Boolean(recipe.predecessor?.signingMetadata)
525
+ && JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.predecessor.signingMetadata);
522
526
  const clean = issues.length === 0
523
527
  && asar.state === "clean"
524
528
  && integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
@@ -530,8 +534,16 @@ export function inspectPmStudioApp({
530
534
  && asar.state === "patched"
531
535
  && integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
532
536
  && patchedSignatureAccepted;
537
+ const predecessor = issues.length === 0
538
+ && asar.state === "predecessor"
539
+ && integrityMatches(plistIntegrity, "SHA256", recipe.predecessor?.headerSha256)
540
+ && predecessorSignatureAccepted;
541
+ const legacy = issues.length === 0
542
+ && asar.state === "legacy"
543
+ && integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)
544
+ && patchedSignatureAccepted;
533
545
 
534
- if (!clean && !patched) {
546
+ if (!clean && !patched && !predecessor && !legacy) {
535
547
  if (asar.state === "drift") issues.push("app.asar is unknown drift");
536
548
  else if (asar.state === "clean"
537
549
  && !integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)) {
@@ -539,6 +551,12 @@ export function inspectPmStudioApp({
539
551
  } else if (asar.state === "patched"
540
552
  && !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
541
553
  issues.push("patched ASAR does not match ElectronAsarIntegrity");
554
+ } else if (asar.state === "predecessor"
555
+ && !integrityMatches(plistIntegrity, "SHA256", recipe.predecessor?.headerSha256)) {
556
+ issues.push("predecessor ASAR does not match ElectronAsarIntegrity");
557
+ } else if (asar.state === "legacy"
558
+ && !integrityMatches(plistIntegrity, "SHA256", recipe.legacy?.headerSha256)) {
559
+ issues.push("legacy ASAR does not match ElectronAsarIntegrity");
542
560
  }
543
561
  if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
544
562
  issues.push("bundle content does not match the exact clean recipe");
@@ -547,7 +565,9 @@ export function inspectPmStudioApp({
547
565
  issues.push("codesign verification failed");
548
566
  }
549
567
  else if (asar.state === "clean" && codeSign.adHoc) issues.push("clean bundle has an unexpected ad-hoc signature");
550
- else if (asar.state === "patched" && !codeSign.adHoc) issues.push("patched bundle is not ad-hoc signed");
568
+ else if (["patched", "predecessor", "legacy"].includes(asar.state) && !codeSign.adHoc) {
569
+ issues.push(`${asar.state} bundle is not ad-hoc signed`);
570
+ }
551
571
  else if (asar.state === "clean" && codeSign.teamIdentifier !== recipe.sourceTeamIdentifier) {
552
572
  issues.push(`clean bundle TeamIdentifier is ${codeSign.teamIdentifier || "missing"}, expected ${recipe.sourceTeamIdentifier}`);
553
573
  }
@@ -559,15 +579,21 @@ export function inspectPmStudioApp({
559
579
  && executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
560
580
  issues.push("Electron Framework hash does not match the clean recipe");
561
581
  }
562
- if (asar.state === "patched" && codeSign?.valid && codeSign?.adHoc
582
+ if (["patched", "legacy"].includes(asar.state) && codeSign?.valid && codeSign?.adHoc
563
583
  && recipe.patchedSigningMetadata
564
584
  && !patchedSignatureAccepted) {
565
- issues.push("patched bundle signing metadata does not match the recipe");
585
+ issues.push(`${asar.state} bundle signing metadata does not match the recipe`);
586
+ }
587
+ if (asar.state === "predecessor" && codeSign?.valid && codeSign?.adHoc
588
+ && !predecessorSignatureAccepted) {
589
+ issues.push("predecessor bundle signing metadata does not match the recipe");
566
590
  }
567
591
  }
568
592
 
569
593
  return {
570
- state: clean ? "clean" : patched ? "patched" : "drift",
594
+ state: clean
595
+ ? "clean"
596
+ : patched ? "patched" : predecessor ? "predecessor" : legacy ? "legacy" : "drift",
571
597
  metadata: {
572
598
  version: String(metadata.version),
573
599
  build: String(metadata.build),
@@ -676,6 +702,28 @@ function backupManifest(recipe, createdAt) {
676
702
  };
677
703
  }
678
704
 
705
+ function patchRecordMatches(record, recipe, state) {
706
+ const expected = state === "legacy"
707
+ ? recipe.legacy
708
+ : state === "predecessor"
709
+ ? recipe.predecessor
710
+ : { asarSha256: recipe.patchedAsarSha256, headerSha256: recipe.patchedHeaderSha256 };
711
+ return Boolean(expected)
712
+ && record?.asar_sha256 === expected.asarSha256
713
+ && record?.asar_header_sha256 === expected.headerSha256
714
+ && integrityMatches(record?.electron_asar_integrity, "SHA256", expected.headerSha256);
715
+ }
716
+
717
+ function patchRecordForState(manifest, recipe, state) {
718
+ const historicalKey = state === "legacy"
719
+ ? "legacy_patched"
720
+ : state === "predecessor" ? "predecessor_patched" : "";
721
+ if (historicalKey && patchRecordMatches(manifest?.[historicalKey], recipe, state)) {
722
+ return manifest[historicalKey];
723
+ }
724
+ return patchRecordMatches(manifest?.patched, recipe, state) ? manifest.patched : null;
725
+ }
726
+
679
727
  function validateBackup({ backupDir, recipe, operations }) {
680
728
  const manifestPath = path.join(backupDir, "manifest.json");
681
729
  const backupAppPath = path.join(backupDir, "PM Studio.app");
@@ -702,9 +750,11 @@ function validateBackup({ backupDir, recipe, operations }) {
702
750
  && JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
703
751
  || (recipe.sourceArtifact
704
752
  && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
705
- || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
706
- || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
707
- || !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256)) {
753
+ || (manifest?.predecessor_patched !== undefined
754
+ && !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor"))
755
+ || (!patchRecordForState(manifest, recipe, "patched")
756
+ && !patchRecordForState(manifest, recipe, "legacy")
757
+ && !patchRecordForState(manifest, recipe, "predecessor"))) {
708
758
  throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup does not match recipe ${recipe.id}`);
709
759
  }
710
760
  const inspection = inspectPmStudioApp({ appPath: backupAppPath, recipe, operations });
@@ -750,12 +800,29 @@ function ensureBackup({ appPath, backupRoot, recipe, operations }) {
750
800
 
751
801
  function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
752
802
  const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
753
- manifest.patched.binaries = {
754
- main_executable_sha256: inspection.executableIntegrity.executableSha256,
755
- electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
803
+ if (manifest.predecessor_patched !== undefined
804
+ && !patchRecordMatches(manifest.predecessor_patched, recipe, "predecessor")) {
805
+ throw setupError("PM_STUDIO_BACKUP_INVALID", "Existing PM Studio predecessor record does not match the patch recipe");
806
+ }
807
+ if (manifest.predecessor_patched === undefined
808
+ && patchRecordMatches(manifest.patched, recipe, "predecessor")) {
809
+ manifest.predecessor_patched = manifest.patched;
810
+ }
811
+ if (!patchRecordMatches(manifest.legacy_patched, recipe, "legacy")
812
+ && patchRecordMatches(manifest.patched, recipe, "legacy")) {
813
+ manifest.legacy_patched = manifest.patched;
814
+ }
815
+ manifest.patched = {
816
+ asar_sha256: recipe.patchedAsarSha256,
817
+ asar_header_sha256: recipe.patchedHeaderSha256,
818
+ electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
819
+ binaries: {
820
+ main_executable_sha256: inspection.executableIntegrity.executableSha256,
821
+ electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
822
+ },
823
+ signing_metadata: signingMetadata(inspection.codeSign),
824
+ ...(recipe.sourceBundleContent ? { bundle_content: inspection.bundleContent } : {}),
756
825
  };
757
- manifest.patched.signing_metadata = signingMetadata(inspection.codeSign);
758
- if (recipe.sourceBundleContent) manifest.patched.bundle_content = inspection.bundleContent;
759
826
  const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
760
827
  try {
761
828
  fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
@@ -766,7 +833,14 @@ function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
766
833
  backup.manifest = manifest;
767
834
  }
768
835
 
769
- export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe }) {
836
+ export function assertPatchedBinaryRecord({
837
+ inspection,
838
+ manifestPath,
839
+ recipe,
840
+ recordState = ["legacy", "predecessor"].includes(inspection?.state)
841
+ ? inspection.state
842
+ : "patched",
843
+ }) {
770
844
  let manifest;
771
845
  try {
772
846
  manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
@@ -790,23 +864,24 @@ export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe })
790
864
  || JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
791
865
  ))
792
866
  || (recipe.sourceArtifact
793
- && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
794
- || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
795
- || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
796
- || !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256))) {
867
+ && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact)))) {
797
868
  throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
798
869
  }
799
- const binaries = manifest?.patched?.binaries;
870
+ const record = patchRecordForState(manifest, recipe, recordState);
871
+ if (!record) {
872
+ throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", `PM Studio ${recordState} record does not match the installed patch recipe`);
873
+ }
874
+ const binaries = record.binaries;
800
875
  if (!binaries
801
876
  || binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
802
877
  || binaries.electron_framework_sha256 !== inspection.executableIntegrity.frameworkSha256) {
803
878
  throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio executable or Electron Framework differs from the installed patch record");
804
879
  }
805
- if (JSON.stringify(manifest?.patched?.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
880
+ if (JSON.stringify(record.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
806
881
  throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
807
882
  }
808
883
  if (recipe?.sourceBundleContent
809
- && JSON.stringify(manifest?.patched?.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
884
+ && JSON.stringify(record.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
810
885
  throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
811
886
  }
812
887
  }
@@ -922,12 +997,15 @@ export async function runPmStudioSetup({
922
997
  emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
923
998
  emit(`[OK] Verified backup: ${backup.backupAppPath}`);
924
999
  emit(`[OK] Backup manifest: ${backup.manifestPath}`);
925
- emit(`[INFO] Start ${commandName} before opening PM Studio.`);
1000
+ emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
926
1001
  return { status: "already_patched", changed: false, recipe: recipe.id, backup, inspection: source, messages };
927
1002
  }
928
- if (source.state !== "clean") {
1003
+ const migratingLegacy = source.state === "legacy";
1004
+ const migratingPredecessor = source.state === "predecessor";
1005
+ const migratingExistingPatch = migratingLegacy || migratingPredecessor;
1006
+ if (source.state !== "clean" && !migratingExistingPatch) {
929
1007
  throw setupError("PM_STUDIO_BUNDLE_DRIFT",
930
- `PM Studio ${recipe.version} build ${recipe.build} does not match the clean or patched recipe; no files were changed`, source.issues);
1008
+ `PM Studio ${recipe.version} build ${recipe.build} does not match the clean, current split-origin, predecessor, or legacy recipe; no files were changed`, source.issues);
931
1009
  }
932
1010
 
933
1011
  if (source.sourceVerification === "exact-bundle-content") {
@@ -935,17 +1013,38 @@ export async function runPmStudioSetup({
935
1013
  }
936
1014
 
937
1015
  const backupExists = fs.existsSync(finalBackupDir);
1016
+ let verifiedBackup = null;
1017
+ if (migratingExistingPatch) {
1018
+ if (!backupExists) {
1019
+ throw setupError("PM_STUDIO_BACKUP_INVALID",
1020
+ `${migratingLegacy ? "Legacy global-origin" : "Predecessor split-origin"} PM Studio patch has no matching verified clean backup; no files were changed`);
1021
+ }
1022
+ verifiedBackup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
1023
+ assertPatchedBinaryRecord({
1024
+ inspection: source,
1025
+ manifestPath: verifiedBackup.manifestPath,
1026
+ recipe,
1027
+ recordState: source.state,
1028
+ });
1029
+ emit(migratingLegacy
1030
+ ? "[WARN] Legacy global-origin patch detected; setup will rebuild from the verified clean backup so GPT and enterprise authentication return to PM Studio's native origin."
1031
+ : "[WARN] Predecessor split-origin patch detected; setup will rebuild from the verified clean backup so delayed Claude model discovery no longer falls back to the native catalog.");
1032
+ }
938
1033
  assertSpace({ appPath, backupRoot, backupExists, operations });
939
1034
 
940
1035
  emit("[WARN] PM Studio's vendor signature will be replaced with an ad-hoc signature; an official update or reinstall may overwrite this patch.");
941
- const backup = ensureBackup({ appPath, backupRoot, recipe, operations });
1036
+ const backup = verifiedBackup || ensureBackup({ appPath, backupRoot, recipe, operations });
942
1037
  const stagePath = path.join(path.dirname(appPath), `.${path.basename(appPath)}.ccdx-stage-${operations.uuid()}`);
943
1038
  let preserveStage = false;
944
1039
  try {
945
- operations.copyBundle({ source: appPath, destination: stagePath, processRunner: operations.processRunner });
1040
+ operations.copyBundle({
1041
+ source: migratingExistingPatch ? backup.backupAppPath : appPath,
1042
+ destination: stagePath,
1043
+ processRunner: operations.processRunner,
1044
+ });
946
1045
  const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
947
1046
  if (stagedSource.state !== "clean"
948
- || (recipe.sourceBundleContent
1047
+ || (!migratingExistingPatch && recipe.sourceBundleContent
949
1048
  && stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
950
1049
  throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
951
1050
  }
@@ -966,12 +1065,12 @@ export async function runPmStudioSetup({
966
1065
  if (staged.state !== "patched") {
967
1066
  throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
968
1067
  }
969
- assertSigningMetadataPreserved(source, staged, recipe);
1068
+ assertSigningMetadataPreserved(stagedSource, staged, recipe);
970
1069
  writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
971
1070
 
972
1071
  assertNoBlockingProcesses(operations, appPath);
973
1072
  const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
974
- if (unchangedSource.state !== "clean"
1073
+ if (unchangedSource.state !== source.state
975
1074
  || unchangedSource.asar.asarSha256 !== source.asar.asarSha256
976
1075
  || unchangedSource.asar.headerSha256 !== source.asar.headerSha256
977
1076
  || (recipe.sourceBundleContent
@@ -989,12 +1088,17 @@ export async function runPmStudioSetup({
989
1088
  `PM Studio replacement final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
990
1089
  }
991
1090
  if (installed.state !== "patched") {
992
- preserveStage = installed.state !== "clean" && fs.existsSync(stagePath);
993
- const code = installed.state === "clean"
1091
+ const originalRetained = installed.state === source.state
1092
+ && installed.asar.asarSha256 === source.asar.asarSha256
1093
+ && installed.asar.headerSha256 === source.asar.headerSha256
1094
+ && (!recipe.sourceBundleContent
1095
+ || installed.bundleContent?.sha256 === source.bundleContent?.sha256);
1096
+ preserveStage = !originalRetained && fs.existsSync(stagePath);
1097
+ const code = originalRetained
994
1098
  ? "PM_STUDIO_REPLACE_FAILED"
995
1099
  : "PM_STUDIO_INSTALL_VERIFY_FAILED";
996
- const stateMessage = installed.state === "clean"
997
- ? "The installed PM Studio remains the verified clean bundle"
1100
+ const stateMessage = originalRetained
1101
+ ? `The installed PM Studio remains the verified ${source.state} bundle`
998
1102
  : "The installed PM Studio did not retain the verified patched state; do not launch it";
999
1103
  throw setupError(code,
1000
1104
  `${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
@@ -1011,7 +1115,11 @@ export async function runPmStudioSetup({
1011
1115
  emit("[WARN] Atomic replacement reported an error after the exact patched bundle became installed; the final bundle was independently verified.");
1012
1116
  }
1013
1117
 
1014
- emit(`[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
1118
+ emit(migratingLegacy
1119
+ ? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the legacy global-origin patch to the split-origin patch.`
1120
+ : migratingPredecessor
1121
+ ? `[OK] Migrated PM Studio ${recipe.version} build ${recipe.build} from the predecessor split-origin patch to the current split-origin patch.`
1122
+ : `[OK] Patched PM Studio ${recipe.version} build ${recipe.build}.`);
1015
1123
  emit(`[OK] app.asar SHA-256: ${installed.asar.asarSha256}`);
1016
1124
  emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
1017
1125
  emit(`[OK] Backup manifest: ${backup.manifestPath}`);
@@ -1019,9 +1127,9 @@ export async function runPmStudioSetup({
1019
1127
  emit(`[INFO] Restore step 1/3: quit PM Studio and its updater.`);
1020
1128
  emit(`[INFO] Restore step 2/3: confirm the installed Info.plist still reports version/build ${recipe.version}/${recipe.build}; never restore this backup over another version.`);
1021
1129
  emit(`[INFO] Restore step 3/3: move ${appPath} aside, copy ${backup.backupAppPath} to that exact path, and verify its signature before launch.`);
1022
- emit(`[INFO] Start ${commandName} before opening PM Studio.`);
1130
+ emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
1023
1131
  return {
1024
- status: "patched",
1132
+ status: migratingExistingPatch ? "migrated" : "patched",
1025
1133
  changed: true,
1026
1134
  recipe: recipe.id,
1027
1135
  backup,
@@ -16,6 +16,11 @@ import {
16
16
  pmStudioPatchManifestPath,
17
17
  } from "./pm-studio-setup.mjs";
18
18
  import { readAdapterStatus } from "./cli-status.mjs";
19
+ import {
20
+ PM_STUDIO_RELAY_MARKER_HEADER,
21
+ PM_STUDIO_RELAY_MARKER_VALUE,
22
+ PM_STUDIO_RELAY_PREFIX,
23
+ } from "./pm-studio-relay.mjs";
19
24
  import { checkRunningAdapter } from "./running-adapter.mjs";
20
25
  import { status } from "./status.mjs";
21
26
  import {
@@ -27,12 +32,11 @@ import {
27
32
 
28
33
  const PM_RELAY_HOST = "127.0.0.1";
29
34
  const PM_RELAY_PORT = 2026;
30
- const PM_RELAY_CAPABILITY = "pm_studio_relay_v1";
35
+ const PM_RELAY_PROBE_TIMEOUT_MS = 500;
36
+ const PM_RELAY_CAPABILITY = "pm_studio_split_origin_v1";
31
37
  const PM_RELAY_ROUTE_NAMES = Object.freeze([
32
38
  "pm_models",
33
39
  "pm_chat_completions",
34
- "pm_responses",
35
- "pm_embeddings",
36
40
  ]);
37
41
 
38
42
  function selectRecipe(metadata, recipes) {
@@ -45,6 +49,31 @@ function safeMessage(error) {
45
49
  return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
46
50
  }
47
51
 
52
+ export async function probePmStudioRelay({
53
+ baseUrl,
54
+ fetchImpl = fetch,
55
+ timeoutMs = PM_RELAY_PROBE_TIMEOUT_MS,
56
+ } = {}) {
57
+ const controller = new AbortController();
58
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
59
+ try {
60
+ const response = await fetchImpl(new URL(`${PM_STUDIO_RELAY_PREFIX}/models`, baseUrl).href, {
61
+ method: "GET",
62
+ headers: { Accept: "application/json" },
63
+ redirect: "error",
64
+ signal: controller.signal,
65
+ });
66
+ const result = {
67
+ status: response.status,
68
+ marker: response.headers.get(PM_STUDIO_RELAY_MARKER_HEADER) || "",
69
+ };
70
+ await response.body?.cancel().catch(() => {});
71
+ return result;
72
+ } finally {
73
+ clearTimeout(timer);
74
+ }
75
+ }
76
+
48
77
  function inspectClaudeProfile(home, readCredentials) {
49
78
  try {
50
79
  const credentials = readCredentials(AUTH_PROFILE_CLAUDE, { home });
@@ -73,7 +102,7 @@ function inspectAdapterRuntime(adapter, runtimeStatus) {
73
102
  issues.push("health and runtime status do not describe the same adapter instance");
74
103
  }
75
104
  const capabilities = new Set(Array.isArray(data.capabilities) ? data.capabilities : []);
76
- if (!capabilities.has(PM_RELAY_CAPABILITY)) issues.push("PM Studio relay capability is missing");
105
+ if (!capabilities.has(PM_RELAY_CAPABILITY)) issues.push("PM Studio split-origin relay capability is missing");
77
106
  if (data.profiles?.claude?.mode !== "isolated") issues.push("Claude profile mode is not isolated");
78
107
  if (data.profiles?.claude?.profile_current !== true) {
79
108
  issues.push("running Claude credentials do not match the current isolated profile");
@@ -93,6 +122,21 @@ function inspectAdapterRuntime(adapter, runtimeStatus) {
93
122
  };
94
123
  }
95
124
 
125
+ function inspectRelayProbe(runtime, probe) {
126
+ const relayProbe = {
127
+ status: probe?.status,
128
+ marker: String(probe?.marker || ""),
129
+ };
130
+ const issues = [...runtime.issues];
131
+ if (relayProbe.status !== 401) {
132
+ issues.push(`PM Studio relay probe returned HTTP ${relayProbe.status ?? "unknown"}; expected 401`);
133
+ }
134
+ if (relayProbe.marker !== PM_STUDIO_RELAY_MARKER_VALUE) {
135
+ issues.push("PM Studio relay compatibility marker is missing or incompatible");
136
+ }
137
+ return { ...runtime, relayProbe, ok: issues.length === 0, issues };
138
+ }
139
+
96
140
  export async function inspectPmStudioStatus({
97
141
  appPath = DEFAULT_PM_STUDIO_APP_PATH,
98
142
  home = os.homedir(),
@@ -105,6 +149,7 @@ export async function inspectPmStudioStatus({
105
149
  readClaudeCredentials = readAuthProfileCredentials,
106
150
  checkRunningAdapterFn = checkRunningAdapter,
107
151
  readAdapterStatusFn = readAdapterStatus,
152
+ probePmStudioRelayFn = probePmStudioRelay,
108
153
  } = {}) {
109
154
  const operations = createPmStudioSetupOperations(operationOverrides);
110
155
  let app;
@@ -125,10 +170,15 @@ export async function inspectPmStudioStatus({
125
170
  const recipe = selectRecipe(normalized, recipes);
126
171
  if (recipe) {
127
172
  app = { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id };
128
- if (app.state === "patched") {
173
+ if (["patched", "predecessor"].includes(app.state)) {
129
174
  const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
130
175
  try {
131
- verifyPatchRecord({ inspection: app, manifestPath, recipe });
176
+ verifyPatchRecord({
177
+ inspection: app,
178
+ manifestPath,
179
+ recipe,
180
+ recordState: app.state,
181
+ });
132
182
  app.patchRecord = { valid: true, manifestPath, reason: "" };
133
183
  } catch (error) {
134
184
  const reason = safeMessage(error);
@@ -163,6 +213,18 @@ export async function inspectPmStudioStatus({
163
213
  } catch (error) {
164
214
  runtime = { ok: false, baseUrl: adapter.baseUrl, data: null, issues: [safeMessage(error)] };
165
215
  }
216
+ if (runtime.data) {
217
+ try {
218
+ runtime = inspectRelayProbe(runtime, await probePmStudioRelayFn({ baseUrl: adapter.baseUrl }));
219
+ } catch (error) {
220
+ runtime = {
221
+ ...runtime,
222
+ ok: false,
223
+ relayProbe: null,
224
+ issues: [...runtime.issues, `PM Studio relay probe failed: ${safeMessage(error)}`],
225
+ };
226
+ }
227
+ }
166
228
  }
167
229
  const operational = app.state === "patched"
168
230
  && app.patchRecord?.valid === true
@@ -182,6 +244,12 @@ function appStatusItem(result, commandName) {
182
244
  const reason = app.patchRecord?.reason || "installed patch record is missing";
183
245
  return { kind: "err", component: "App patch", detail: `${version}; patch record not verified`, message: `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${reason}` };
184
246
  }
247
+ if (app.state === "predecessor" && app.patchRecord?.valid === true) {
248
+ return { kind: "warn", component: "App patch", detail: `${version}; verified predecessor split patch`, message: `PM Studio ${version} has a verified predecessor split patch; run ${commandName} pms setup to migrate` };
249
+ }
250
+ if (app.state === "legacy") {
251
+ return { kind: "err", component: "App patch", detail: `${version}; legacy global-origin patch`, message: `PM Studio ${version} has a legacy global-origin patch; run ${commandName} pms setup to migrate` };
252
+ }
185
253
  if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version}; supported, not patched`, message: `PM Studio ${version} is supported but not patched; run ${commandName} pms setup` };
186
254
  if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; unsupported version`, message: `PM Studio ${version} has no exact patch recipe; no files will be changed` };
187
255
  if (app.state === "drift" && app.patchRecipeMatched) {
@@ -203,7 +271,7 @@ function pmStudioStatusItems(result, commandName) {
203
271
  items.push({ kind: "err", component: "Claude profile", detail: "not ready", message: `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}` });
204
272
  }
205
273
  if (result.adapter?.ok && result.runtime?.ok) {
206
- items.push({ kind: "ok", component: "PM relay", detail: `verified at ${result.adapter.baseUrl}`, message: `PM relay and isolated Claude routing are verified at ${result.adapter.baseUrl}` });
274
+ items.push({ kind: "ok", component: "PM relay", detail: `verified at ${result.adapter.baseUrl}`, message: `PM model discovery and isolated Claude routing are verified at ${result.adapter.baseUrl}` });
207
275
  } else if (result.adapter?.ok) {
208
276
  const reason = result.runtime?.issues?.join("; ") || "runtime status is unavailable";
209
277
  items.push({ kind: "err", component: "PM relay", detail: "running but routing not ready", message: `The adapter is running at ${result.adapter.baseUrl}, but PM relay routing is not ready: ${reason}; stop it and run ${commandName} start` });
@@ -213,9 +281,9 @@ function pmStudioStatusItems(result, commandName) {
213
281
  items.push({ kind: "warn", component: "PM relay", detail: `not running at ${PM_RELAY_HOST}:${PM_RELAY_PORT}`, message: `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start` });
214
282
  }
215
283
  if (result.runtime?.ok) {
216
- items.push({ kind: "info", component: "Routing", detail: "GPT -> PM bearer; Claude -> isolated profile", message: "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile" });
284
+ items.push({ kind: "info", component: "Routing", detail: "GPT -> native official; models/Claude -> local", message: "Routing: PM GPT uses its native official GitHub Copilot path; model discovery and eligible Claude chat use the local CCDX relay" });
217
285
  } else {
218
- items.push({ kind: "info", component: "Routing", detail: "expected: GPT -> PM bearer; Claude -> isolated profile", message: "Expected routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile" });
286
+ items.push({ kind: "info", component: "Routing", detail: "expected: GPT -> native official; models/Claude -> local", message: "Expected routing: PM GPT uses its native official GitHub Copilot path; model discovery and eligible Claude chat use the local CCDX relay" });
219
287
  }
220
288
  return items;
221
289
  }
@@ -65,9 +65,21 @@ function replayableResponsesOutputItems(output) {
65
65
  return output.filter((item) => item && typeof item === "object");
66
66
  }
67
67
 
68
+ function isEncryptedContentPart(value) {
69
+ return value && typeof value === "object" && value.type === "encrypted_content";
70
+ }
71
+
68
72
  function stripEncryptedReasoningValue(value, state) {
69
73
  if (Array.isArray(value)) {
70
- return value.map((item) => stripEncryptedReasoningValue(item, state));
74
+ const out = [];
75
+ for (const item of value) {
76
+ if (isEncryptedContentPart(item)) {
77
+ state.changed = true;
78
+ continue;
79
+ }
80
+ out.push(stripEncryptedReasoningValue(item, state));
81
+ }
82
+ return out;
71
83
  }
72
84
 
73
85
  if (value && typeof value === "object") {
@@ -87,7 +99,9 @@ function stripEncryptedReasoningValue(value, state) {
87
99
 
88
100
  function isEncryptedReasoningInputItem(item) {
89
101
  return item && typeof item === "object"
90
- && (item.type === "reasoning" || Object.prototype.hasOwnProperty.call(item, "encrypted_content"));
102
+ && (item.type === "reasoning"
103
+ || item.type === "encrypted_content"
104
+ || Object.prototype.hasOwnProperty.call(item, "encrypted_content"));
91
105
  }
92
106
 
93
107
  export function sanitizeEncryptedReasoningRequest(reqContext) {
@@ -5,7 +5,9 @@ export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
5
5
  export const ADAPTER_STATUS_PATH = "/_ccdx/status";
6
6
  export const ADAPTER_PROTOCOL_VERSION = 2;
7
7
  export const ADAPTER_VERSION = localPackageVersion();
8
- export const ADAPTER_CAPABILITIES = Object.freeze(["pm_studio_relay_v1"]);
8
+ export const ADAPTER_CAPABILITIES = Object.freeze([
9
+ "pm_studio_split_origin_v1",
10
+ ]);
9
11
  const ADAPTER_INSTANCE_ID = randomUUID();
10
12
 
11
13
  export function adapterHealthPayload() {