codex-copilot-dx 0.6.4 → 0.6.6

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.
@@ -273,9 +273,11 @@ export async function runWithConcurrency(taskFns, concurrency) {
273
273
  }));
274
274
  }
275
275
 
276
- function visitImageParts(parts, references, commit = null) {
276
+ function visitImageParts(parts, references, commit = null, assertActive) {
277
277
  if (!Array.isArray(parts)) return;
278
- for (const part of parts) {
278
+ for (let index = 0; index < parts.length; index += 1) {
279
+ if ((index & 63) === 0) assertActive?.();
280
+ const part = parts[index];
279
281
  const image = readResponsesImagePart(part);
280
282
  if (!image?.identity) continue;
281
283
  references.push({
@@ -286,22 +288,24 @@ function visitImageParts(parts, references, commit = null) {
286
288
  }
287
289
  }
288
290
 
289
- function collectImageReferences(reqBody) {
291
+ function collectImageReferences(reqBody, assertActive) {
290
292
  const references = [];
291
293
  const dirtyCommits = new Set();
292
294
  if (!Array.isArray(reqBody?.input)) {
293
295
  return { references, markDirty() {}, commitDirty() {} };
294
296
  }
295
297
 
296
- for (const item of reqBody.input) {
298
+ for (let index = 0; index < reqBody.input.length; index += 1) {
299
+ if ((index & 63) === 0) assertActive?.();
300
+ const item = reqBody.input[index];
297
301
  if (!item) continue;
298
- visitImageParts([item], references);
302
+ visitImageParts([item], references, null, assertActive);
299
303
  if (item.type === "message" && Array.isArray(item.content)) {
300
- visitImageParts(item.content, references);
304
+ visitImageParts(item.content, references, null, assertActive);
301
305
  }
302
306
  const toolOutput = readResponsesToolOutputParts(item);
303
307
  if (toolOutput) {
304
- visitImageParts(toolOutput.parts, references, toolOutput.commit);
308
+ visitImageParts(toolOutput.parts, references, toolOutput.commit, assertActive);
305
309
  }
306
310
  }
307
311
 
@@ -320,6 +324,7 @@ function collectImageReferences(reqBody) {
320
324
  }
321
325
 
322
326
  async function optimizeImageReferences(collection, {
327
+ assertActive,
323
328
  concurrency,
324
329
  model,
325
330
  optimizeImage,
@@ -334,7 +339,9 @@ async function optimizeImageReferences(collection, {
334
339
  return optimizedByDataUrl.get(identity);
335
340
  };
336
341
  const tasks = collection.references.map((reference) => async () => {
342
+ assertActive?.();
337
343
  if (reference.set(await optimizeOnce(reference.get()))) collection.markDirty([reference]);
344
+ assertActive?.();
338
345
  });
339
346
  if (tasks.length) await runWithConcurrency(tasks, concurrency);
340
347
  collection.commitDirty();
@@ -346,9 +353,11 @@ function imageInputBytes(dataUrl) {
346
353
  return match ? Buffer.byteLength(match[1], "base64") : 0;
347
354
  }
348
355
 
349
- function uniqueOriginalImages(references) {
356
+ function uniqueOriginalImages(references, assertActive) {
350
357
  const unique = new Map();
351
- for (const reference of references) {
358
+ for (let index = 0; index < references.length; index += 1) {
359
+ if ((index & 63) === 0) assertActive?.();
360
+ const reference = references[index];
352
361
  const dataUrl = reference.get();
353
362
  const identity = canonicalInlineImageIdentity(dataUrl);
354
363
  if (!identity) continue;
@@ -422,6 +431,7 @@ export async function optimizeImagesInBody(reqBody, {
422
431
  }
423
432
 
424
433
  export async function prepareResponsesPayload(reqBody, {
434
+ assertActive,
425
435
  currentInputStart = 0,
426
436
  maxBytes = MAX_UPSTREAM_BODY_BYTES,
427
437
  optimizeImage = optimizeImageDataUrl,
@@ -429,18 +439,22 @@ export async function prepareResponsesPayload(reqBody, {
429
439
  skipInitialOptimization = false,
430
440
  signal,
431
441
  } = {}) {
442
+ assertActive?.();
432
443
  const shouldOptimizeImages = !IMG_OPT_DISABLED && !skipInitialOptimization;
433
- const collection = shouldOptimizeImages ? collectImageReferences(reqBody) : null;
434
- const originalImages = collection ? uniqueOriginalImages(collection.references) : [];
444
+ const collection = shouldOptimizeImages ? collectImageReferences(reqBody, assertActive) : null;
445
+ const originalImages = collection ? uniqueOriginalImages(collection.references, assertActive) : [];
435
446
  if (collection) {
436
447
  await optimizeImageReferences(collection, {
448
+ assertActive,
437
449
  concurrency: IMG_CONCURRENCY,
438
450
  model: reqBody.model,
439
451
  optimizeImage: (dataUrl, options) => optimizeImage(dataUrl, { ...options, quality: IMG_QUALITY }),
440
452
  signal,
441
453
  });
442
454
  }
455
+ assertActive?.();
443
456
  let bodyText = JSON.stringify(reqBody);
457
+ assertActive?.();
444
458
  let bodyBytes = Buffer.byteLength(bodyText);
445
459
  const summary = summarizeReqBody(reqBody);
446
460
  const targetBytes = positiveInt(maxBytes, MAX_UPSTREAM_BODY_BYTES);
@@ -450,11 +464,13 @@ export async function prepareResponsesPayload(reqBody, {
450
464
  if (collection && originalImages.length > 0 && bodyBytes > targetBytes) {
451
465
  const batchSize = Math.max(1, IMG_CONCURRENCY);
452
466
  for (const profile of Array.isArray(profiles) ? profiles : []) {
467
+ assertActive?.();
453
468
  const beforeBytes = bodyBytes;
454
469
  let processed = 0;
455
470
  stage = `q${positiveInt(profile.quality, IMG_QUALITY, 100)}`;
456
471
  const orderedImages = orderImagesByCurrentWireBytes(originalImages);
457
472
  for (let index = 0; index < orderedImages.length && bodyBytes > targetBytes; index += batchSize) {
473
+ assertActive?.();
458
474
  const batch = orderedImages.slice(index, index + batchSize);
459
475
  const candidates = await Promise.all(batch.map((image) => optimizeImage(image.dataUrl, {
460
476
  ...profile,
@@ -462,6 +478,7 @@ export async function prepareResponsesPayload(reqBody, {
462
478
  model: reqBody.model,
463
479
  signal,
464
480
  })));
481
+ assertActive?.();
465
482
  for (let offset = 0; offset < batch.length && bodyBytes > targetBytes; offset += 1) {
466
483
  processed += 1;
467
484
  const applied = applyImageCandidate(collection, batch[offset], candidates[offset], bodyBytes);
@@ -469,19 +486,24 @@ export async function prepareResponsesPayload(reqBody, {
469
486
  adapted ||= applied.changed;
470
487
  if (bodyBytes <= targetBytes) {
471
488
  collection.commitDirty();
489
+ assertActive?.();
472
490
  bodyText = JSON.stringify(reqBody);
491
+ assertActive?.();
473
492
  bodyBytes = Buffer.byteLength(bodyText);
474
493
  }
475
494
  }
476
495
  }
477
496
  collection.commitDirty();
497
+ assertActive?.();
478
498
  bodyText = JSON.stringify(reqBody);
499
+ assertActive?.();
479
500
  bodyBytes = Buffer.byteLength(bodyText);
480
501
  console.warn(status("warn", `responses payload ${beforeBytes}b exceeds ${targetBytes}b; image profile max_dim=${profile.maxDim} quality=${profile.quality} processed=${processed}/${orderedImages.length} -> ${bodyBytes}b`));
481
502
  if (bodyBytes <= targetBytes) break;
482
503
  }
483
504
  }
484
505
 
506
+ assertActive?.();
485
507
  return {
486
508
  bodyText,
487
509
  bodyBytes,
package/src/launcher.mjs CHANGED
@@ -3,9 +3,8 @@ import { status } from "./status.mjs";
3
3
 
4
4
  export { ensureAuth } from "./auth.mjs";
5
5
 
6
- // The Codex desktop app was folded into the ChatGPT app, but it keeps the
7
- // original bundle identifier. Launching by bundle id works for both the old
8
- // Codex.app and the new ChatGPT.app regardless of where they are installed.
6
+ // Prefer the Codex bundle identifier, then fall back to the known app paths.
7
+ // This supports both Codex.app and ChatGPT.app regardless of installation path.
9
8
  const CODEX_BUNDLE_ID = "com.openai.codex";
10
9
  const APP_PATH_CANDIDATES = [
11
10
  "/Applications/Codex.app",
@@ -30,8 +30,9 @@ export function shouldShowLegacyCommandWarning({
30
30
  }
31
31
  try {
32
32
  atomicWriteFileSync(filePath, `${now}\n`, { mode: 0o600 });
33
+ return true;
33
34
  } catch {
34
- // The warning remains best effort when the cache directory is read-only.
35
+ // Stay silent when the throttle cannot persist, otherwise every invocation would warn.
36
+ return false;
35
37
  }
36
- return true;
37
38
  }
package/src/models.mjs CHANGED
@@ -95,11 +95,6 @@ function numericLimit(value, fallback) {
95
95
  return Number.isFinite(value) && value > 0 ? value : fallback;
96
96
  }
97
97
 
98
- function hasMessagesEndpoint(model) {
99
- const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
100
- return endpoints.includes("/v1/messages") || endpoints.includes("/chat/completions");
101
- }
102
-
103
98
  function hasOpenAIEndpoint(model) {
104
99
  const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
105
100
  return endpoints.includes("/responses") || endpoints.includes("/v1/responses") || endpoints.includes("/chat/completions");
@@ -140,13 +135,22 @@ function uniqueIds(models, predicate) {
140
135
  return ids;
141
136
  }
142
137
 
143
- function isClaudeCopilotModel(model) {
138
+ export function isClaudeCopilotCatalogEntry(model) {
144
139
  const id = String(model?.id || "").trim();
145
- const vendor = String(model?.vendor || "").toLowerCase();
146
- return id
147
- && (id.startsWith("claude-") || vendor === "anthropic")
140
+ const vendor = String(model?.vendor || model?.owned_by || "").trim().toLowerCase();
141
+ return Boolean(id) && (id.toLowerCase().startsWith("claude-") || vendor === "anthropic");
142
+ }
143
+
144
+ export function isClaudeCopilotModel(model) {
145
+ const policy = String(model?.policy?.state || "").trim().toLowerCase();
146
+ const declaredOwners = [model?.vendor, model?.owned_by]
147
+ .map((value) => String(value || "").trim().toLowerCase())
148
+ .filter(Boolean);
149
+ return isClaudeCopilotCatalogEntry(model)
150
+ && declaredOwners.every((owner) => owner === "anthropic")
148
151
  && model?.model_picker_enabled !== false
149
- && hasMessagesEndpoint(model);
152
+ && (!policy || policy === "enabled")
153
+ && modelSupportsChatCompletions(model);
150
154
  }
151
155
 
152
156
  export function gptModelIdsFromCopilotModels(models) {
@@ -150,6 +150,15 @@ function safeClientRuntimeStatus(client) {
150
150
  };
151
151
  }
152
152
 
153
+ function safeProfileCurrent(checkProfileCurrent) {
154
+ if (typeof checkProfileCurrent !== "function") return false;
155
+ try {
156
+ return checkProfileCurrent() === true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
153
162
  export function runtimeStatusPayload({
154
163
  metrics,
155
164
  streamPerformance,
@@ -161,6 +170,7 @@ export function runtimeStatusPayload({
161
170
  codexModelRegistry,
162
171
  claudeModelRegistry,
163
172
  claudeMode = "inherited",
173
+ isClaudeProfileCurrent,
164
174
  } = {}) {
165
175
  const memory = process.memoryUsage();
166
176
  const resolvedClaudeMode = claudeMode === "isolated" ? "isolated" : "inherited";
@@ -172,6 +182,8 @@ export function runtimeStatusPayload({
172
182
  const claudeModels = resolvedClaudeMode === "isolated"
173
183
  ? modelRegistryStatus(claudeModelRegistry)
174
184
  : codexModels;
185
+ const claudeProfileCurrent = resolvedClaudeMode === "isolated"
186
+ && safeProfileCurrent(isClaudeProfileCurrent);
175
187
  return {
176
188
  ...adapterHealthPayload(),
177
189
  uptime_ms: Math.round(process.uptime() * 1000),
@@ -192,7 +204,12 @@ export function runtimeStatusPayload({
192
204
  models: codexModels,
193
205
  profiles: {
194
206
  codex: { mode: "legacy", client: codexRuntime, models: codexModels },
195
- claude: { mode: resolvedClaudeMode, client: claudeRuntime, models: claudeModels },
207
+ claude: {
208
+ mode: resolvedClaudeMode,
209
+ profile_current: claudeProfileCurrent,
210
+ client: claudeRuntime,
211
+ models: claudeModels,
212
+ },
196
213
  },
197
214
  routing: profileRouting({ claudeMode: resolvedClaudeMode }),
198
215
  limits: {
@@ -396,6 +396,19 @@ function backupName(recipe) {
396
396
  return `${recipe.id}-${recipe.sourceAsarSha256.slice(0, 12)}`;
397
397
  }
398
398
 
399
+ export function pmStudioBackupRoot(home = os.homedir()) {
400
+ return path.join(home, ".local", "share", "codex-copilot-dx", "pm-studio-backups");
401
+ }
402
+
403
+ export function pmStudioPatchManifestPath({
404
+ home = os.homedir(),
405
+ backupRoot = pmStudioBackupRoot(home),
406
+ recipe,
407
+ } = {}) {
408
+ if (!recipe) throw new TypeError("PM Studio patch recipe is required");
409
+ return path.join(backupRoot, backupName(recipe), "manifest.json");
410
+ }
411
+
399
412
  function backupManifest(recipe, createdAt) {
400
413
  return {
401
414
  schema_version: 1,
@@ -512,13 +525,24 @@ function writePatchedBinaryRecord({ backup, inspection, operations }) {
512
525
  backup.manifest = manifest;
513
526
  }
514
527
 
515
- function assertPatchedBinaryRecord({ inspection, manifestPath }) {
528
+ export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe }) {
516
529
  let manifest;
517
530
  try {
518
531
  manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
519
532
  } catch {
520
533
  throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record is missing or invalid");
521
534
  }
535
+ if (recipe && (manifest?.schema_version !== 1
536
+ || manifest?.kind !== "ccdx-pm-studio-backup"
537
+ || manifest?.recipe_id !== recipe.id
538
+ || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
539
+ || manifest?.app?.version !== recipe.version
540
+ || manifest?.app?.build !== recipe.build
541
+ || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
542
+ || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
543
+ || !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256))) {
544
+ throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record does not match the installed patch recipe");
545
+ }
522
546
  const binaries = manifest?.patched?.binaries;
523
547
  if (!binaries
524
548
  || binaries.main_executable_sha256 !== inspection.executableIntegrity.executableSha256
@@ -593,7 +617,7 @@ export async function runPmStudioSetup({
593
617
  commandName = "ccdx",
594
618
  appPath = DEFAULT_PM_STUDIO_APP_PATH,
595
619
  home = os.homedir(),
596
- backupRoot = path.join(home, ".local", "share", "codex-copilot-dx", "pm-studio-backups"),
620
+ backupRoot = pmStudioBackupRoot(home),
597
621
  recipes = PM_STUDIO_RECIPES,
598
622
  operations: operationOverrides = {},
599
623
  logger = (line) => console.log(line),
@@ -629,7 +653,7 @@ export async function runPmStudioSetup({
629
653
  throw setupError("PM_STUDIO_BACKUP_INVALID", "Patched PM Studio has no matching verified backup; no files were changed");
630
654
  }
631
655
  const backup = validateBackup({ backupDir: finalBackupDir, recipe, operations });
632
- assertPatchedBinaryRecord({ inspection: source, manifestPath: backup.manifestPath });
656
+ assertPatchedBinaryRecord({ inspection: source, manifestPath: backup.manifestPath, recipe });
633
657
  emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already patched; no backup or signing was repeated.`);
634
658
  emit(`[OK] Verified backup: ${backup.backupAppPath}`);
635
659
  emit(`[OK] Backup manifest: ${backup.manifestPath}`);
@@ -700,7 +724,7 @@ export async function runPmStudioSetup({
700
724
  installed.issues);
701
725
  }
702
726
  try {
703
- assertPatchedBinaryRecord({ inspection: installed, manifestPath: backup.manifestPath });
727
+ assertPatchedBinaryRecord({ inspection: installed, manifestPath: backup.manifestPath, recipe });
704
728
  } catch (error) {
705
729
  throw setupError("PM_STUDIO_INSTALL_VERIFY_FAILED",
706
730
  `Installed PM Studio differs from the verified staging record; do not launch it. ${recoveryMessage(backup)}`,
@@ -7,16 +7,27 @@ import {
7
7
  } from "./auth-profile.mjs";
8
8
  import { PM_STUDIO_RECIPES } from "./pm-studio-asar.mjs";
9
9
  import {
10
+ assertPatchedBinaryRecord,
10
11
  createPmStudioSetupOperations,
11
12
  DEFAULT_PM_STUDIO_APP_PATH,
12
13
  inspectPmStudioApp,
13
14
  PM_STUDIO_CLAUDE_AUTH_COMMAND,
15
+ pmStudioBackupRoot,
16
+ pmStudioPatchManifestPath,
14
17
  } from "./pm-studio-setup.mjs";
18
+ import { readAdapterStatus } from "./cli-status.mjs";
15
19
  import { checkRunningAdapter } from "./running-adapter.mjs";
16
20
  import { status } from "./status.mjs";
17
21
 
18
22
  const PM_RELAY_HOST = "127.0.0.1";
19
23
  const PM_RELAY_PORT = 2026;
24
+ const PM_RELAY_CAPABILITY = "pm_studio_relay_v1";
25
+ const PM_RELAY_ROUTE_NAMES = Object.freeze([
26
+ "pm_models",
27
+ "pm_chat_completions",
28
+ "pm_responses",
29
+ "pm_embeddings",
30
+ ]);
20
31
 
21
32
  function selectRecipe(metadata, recipes) {
22
33
  return recipes.find((recipe) => recipe.version === String(metadata.version)
@@ -42,15 +53,52 @@ function inspectClaudeProfile(home, readCredentials) {
42
53
  }
43
54
  }
44
55
 
56
+ function inspectAdapterRuntime(adapter, runtimeStatus) {
57
+ const data = runtimeStatus?.data;
58
+ const health = adapter?.data;
59
+ const issues = [];
60
+ if (!data || typeof data !== "object") {
61
+ return { ok: false, baseUrl: runtimeStatus?.baseUrl || adapter?.baseUrl, data: null, issues: ["runtime status payload is missing"] };
62
+ }
63
+ if (data.version !== health?.version
64
+ || data.protocol_version !== health?.protocol_version
65
+ || data.instance_id !== health?.instance_id
66
+ || data.pid !== health?.pid) {
67
+ issues.push("health and runtime status do not describe the same adapter instance");
68
+ }
69
+ const capabilities = new Set(Array.isArray(data.capabilities) ? data.capabilities : []);
70
+ if (!capabilities.has(PM_RELAY_CAPABILITY)) issues.push("PM Studio relay capability is missing");
71
+ if (data.profiles?.claude?.mode !== "isolated") issues.push("Claude profile mode is not isolated");
72
+ if (data.profiles?.claude?.profile_current !== true) {
73
+ issues.push("running Claude credentials do not match the current isolated profile");
74
+ }
75
+ if (data.routing?.responses !== "codex") issues.push("/v1/responses is not routed to Codex");
76
+ if (data.routing?.messages !== "claude") issues.push("/v1/messages is not routed to Claude");
77
+ const routes = data.requests?.by_route;
78
+ for (const routeName of PM_RELAY_ROUTE_NAMES) {
79
+ if (!routes || typeof routes !== "object" || !Object.hasOwn(routes, routeName)) {
80
+ issues.push(`runtime status does not expose ${routeName}`);
81
+ }
82
+ }
83
+ return {
84
+ ...runtimeStatus,
85
+ ok: issues.length === 0,
86
+ issues,
87
+ };
88
+ }
89
+
45
90
  export async function inspectPmStudioStatus({
46
91
  appPath = DEFAULT_PM_STUDIO_APP_PATH,
47
92
  home = os.homedir(),
93
+ backupRoot = pmStudioBackupRoot(home),
48
94
  recipes = PM_STUDIO_RECIPES,
49
95
  operationOverrides = {},
50
96
  existsSync = fs.existsSync,
51
97
  inspectApp = inspectPmStudioApp,
98
+ verifyPatchRecord = assertPatchedBinaryRecord,
52
99
  readClaudeCredentials = readAuthProfileCredentials,
53
100
  checkRunningAdapterFn = checkRunningAdapter,
101
+ readAdapterStatusFn = readAdapterStatus,
54
102
  } = {}) {
55
103
  const operations = createPmStudioSetupOperations(operationOverrides);
56
104
  let app;
@@ -69,9 +117,24 @@ export async function inspectPmStudioStatus({
69
117
  bundleIdentifier: String(metadata.bundleIdentifier),
70
118
  };
71
119
  const recipe = selectRecipe(normalized, recipes);
72
- app = recipe
73
- ? { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id }
74
- : { state: "unsupported", metadata: normalized, issues: [], recipe: "" };
120
+ if (recipe) {
121
+ app = { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id };
122
+ if (app.state === "patched") {
123
+ const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
124
+ try {
125
+ verifyPatchRecord({ inspection: app, manifestPath, recipe });
126
+ app.patchRecord = { valid: true, manifestPath, reason: "" };
127
+ } catch (error) {
128
+ const reason = safeMessage(error);
129
+ app.patchRecipeMatched = true;
130
+ app.patchRecord = { valid: false, manifestPath, reason };
131
+ app.state = "drift";
132
+ app.issues = [...(Array.isArray(app.issues) ? app.issues : []), reason];
133
+ }
134
+ }
135
+ } else {
136
+ app = { state: "unsupported", metadata: normalized, issues: [], recipe: "" };
137
+ }
75
138
  } catch (error) {
76
139
  app = { state: "error", metadata: null, issues: [safeMessage(error)], recipe: "" };
77
140
  }
@@ -84,16 +147,40 @@ export async function inspectPmStudioStatus({
84
147
  } catch (error) {
85
148
  adapter = { ok: false, error };
86
149
  }
87
- const operational = app.state === "patched" && claude.valid && adapter?.ok === true;
88
- return { ok: operational, appPath, app, claude, adapter };
150
+ let runtime = { ok: false, baseUrl: adapter?.baseUrl, data: null, issues: ["adapter health check failed"] };
151
+ if (adapter?.ok === true) {
152
+ try {
153
+ runtime = inspectAdapterRuntime(adapter, await readAdapterStatusFn({
154
+ host: PM_RELAY_HOST,
155
+ port: PM_RELAY_PORT,
156
+ }));
157
+ } catch (error) {
158
+ runtime = { ok: false, baseUrl: adapter.baseUrl, data: null, issues: [safeMessage(error)] };
159
+ }
160
+ }
161
+ const operational = app.state === "patched"
162
+ && app.patchRecord?.valid === true
163
+ && claude.valid
164
+ && adapter?.ok === true
165
+ && runtime.ok === true;
166
+ return { ok: operational, appPath, app, claude, adapter, runtime };
89
167
  }
90
168
 
91
169
  function appStatusLine(result, commandName) {
92
170
  const { app } = result;
93
171
  const version = app.metadata ? `${app.metadata.version} build ${app.metadata.build}` : "";
94
- if (app.state === "patched") return status("ok", `PM Studio ${version} is patched and verified`);
172
+ if (app.state === "patched" && app.patchRecord?.valid === true) {
173
+ return status("ok", `PM Studio ${version} is patched and verified against its installed patch record`);
174
+ }
175
+ if (app.state === "patched") {
176
+ const reason = app.patchRecord?.reason || "installed patch record is missing";
177
+ return status("err", `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${reason}`);
178
+ }
95
179
  if (app.state === "clean") return status("warn", `PM Studio ${version} is supported but not patched; run ${commandName} pms setup`);
96
180
  if (app.state === "unsupported") return status("err", `PM Studio ${version} has no exact patch recipe; no files will be changed`);
181
+ if (app.state === "drift" && app.patchRecipeMatched) {
182
+ return status("err", `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${app.issues.join("; ")}`);
183
+ }
97
184
  if (app.state === "drift") return status("err", `PM Studio ${version} does not match the clean or patched recipe: ${app.issues.join("; ")}`);
98
185
  if (app.state === "not_installed") return status("warn", `PM Studio is not installed at ${result.appPath}`);
99
186
  return status("err", `PM Studio could not be inspected: ${app.issues.join("; ")}`);
@@ -111,14 +198,21 @@ export function formatPmStudioStatus(result, { commandName = "ccdx" } = {}) {
111
198
  const reason = result.claude.configured && result.claude.reason ? ` (${result.claude.reason})` : "";
112
199
  lines.push(status("err", `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}`));
113
200
  }
114
- if (result.adapter?.ok) {
115
- lines.push(status("ok", `PM relay is available at ${result.adapter.baseUrl}`));
201
+ if (result.adapter?.ok && result.runtime?.ok) {
202
+ lines.push(status("ok", `PM relay and isolated Claude routing are verified at ${result.adapter.baseUrl}`));
203
+ } else if (result.adapter?.ok) {
204
+ const reason = result.runtime?.issues?.join("; ") || "runtime status is unavailable";
205
+ lines.push(status("err", `The adapter is running at ${result.adapter.baseUrl}, but PM relay routing is not ready: ${reason}; stop it and run ${commandName} start`));
116
206
  } else if (result.adapter?.incompatible) {
117
207
  lines.push(status("err", `The adapter at ${result.adapter.baseUrl} is incompatible; stop it and run ${commandName} start`));
118
208
  } else {
119
209
  lines.push(status("warn", `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start`));
120
210
  }
121
- lines.push(status("info", "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
211
+ if (result.runtime?.ok) {
212
+ lines.push(status("info", "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
213
+ } else {
214
+ lines.push(status("info", "Expected routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
215
+ }
122
216
  return lines.join("\n");
123
217
  }
124
218
 
@@ -1,22 +1,13 @@
1
+ import {
2
+ isClaudeCopilotCatalogEntry,
3
+ isClaudeCopilotModel,
4
+ } from "./models.mjs";
5
+
1
6
  function catalogData(catalog) {
2
7
  if (Array.isArray(catalog)) return catalog;
3
8
  return Array.isArray(catalog?.data) ? catalog.data : [];
4
9
  }
5
10
 
6
- function isClaudeCatalogEntry(model) {
7
- const id = String(model?.id || "").trim();
8
- const vendor = String(model?.vendor || "").trim().toLowerCase();
9
- return Boolean(id) && (vendor === "anthropic" || id.toLowerCase().startsWith("claude-"));
10
- }
11
-
12
- function isAllowedClaudeModel(model) {
13
- return isClaudeCatalogEntry(model)
14
- && String(model?.vendor || "").trim().toLowerCase() === "anthropic"
15
- && model?.model_picker_enabled !== false
16
- && Array.isArray(model?.supported_endpoints)
17
- && model.supported_endpoints.includes("/chat/completions");
18
- }
19
-
20
11
  export function profileRouting({ claudeMode = "inherited", claudeConfigured = false } = {}) {
21
12
  const isolated = claudeMode === "isolated" || claudeConfigured === true;
22
13
  return Object.freeze({ responses: "codex", messages: isolated ? "claude" : "codex" });
@@ -44,9 +35,9 @@ export function createPmStudioModelRouter({ getCatalog, isClaudeEnabled } = {})
44
35
  const known = new Set();
45
36
  for (const model of catalogData(catalog)) {
46
37
  const id = String(model?.id || "").trim();
47
- if (!id || !isClaudeCatalogEntry(model)) continue;
38
+ if (!id || !isClaudeCopilotCatalogEntry(model)) continue;
48
39
  known.add(id);
49
- if (enabled && isAllowedClaudeModel(model) && !allowed.has(id)) allowed.set(id, model);
40
+ if (enabled && isClaudeCopilotModel(model) && !allowed.has(id)) allowed.set(id, model);
50
41
  }
51
42
  cachedCatalog = catalog;
52
43
  cachedEnabled = enabled;
@@ -46,6 +46,15 @@ export function createProfileRuntime({
46
46
  };
47
47
  const credentials = readCredentials();
48
48
  const claudeProfile = safeProfileStatus(credentials);
49
+ const claudeCredentialFingerprint = credentials.valid
50
+ ? githubTokenFingerprint(credentials.token)
51
+ : "";
52
+ const isClaudeProfileCurrent = () => {
53
+ if (!claudeCredentialFingerprint) return false;
54
+ const current = readCredentials();
55
+ return current.valid === true
56
+ && githubTokenFingerprint(current.token) === claudeCredentialFingerprint;
57
+ };
49
58
  let codexCredentialFingerprint = "";
50
59
  try {
51
60
  const codexCredentials = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
@@ -65,6 +74,7 @@ export function createProfileRuntime({
65
74
  claudeProfile,
66
75
  codexCredentialFingerprint,
67
76
  claudeCredentialFingerprint: "",
77
+ isClaudeProfileCurrent,
68
78
  });
69
79
  }
70
80
 
@@ -86,8 +96,7 @@ export function createProfileRuntime({
86
96
  claudeMode: "isolated",
87
97
  claudeProfile,
88
98
  codexCredentialFingerprint,
89
- claudeCredentialFingerprint: credentials.valid
90
- ? githubTokenFingerprint(credentials.token)
91
- : "",
99
+ claudeCredentialFingerprint,
100
+ isClaudeProfileCurrent,
92
101
  });
93
102
  }
@@ -1,4 +1,5 @@
1
1
  import { httpError } from "./http-transport.mjs";
2
+ import { clearResponsesToolOutputPartsCache } from "./responses-content.mjs";
2
3
  import { loadRuntimeConfig, parsePositiveInteger, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
3
4
 
4
5
  const DEFAULT_MAX_BYTES = RUNTIME_DEFAULTS.responseHistoryMaxBytes;
@@ -201,17 +202,26 @@ export function responseHistoryMaterializedBytes(responseId) {
201
202
  return chain.reduce((bytes, entry) => bytes + entry.bytes, 0);
202
203
  }
203
204
 
204
- export function materializeResponseHistory(responseId) {
205
+ export function materializeResponseHistory(responseId, { assertActive } = {}) {
206
+ assertActive?.();
205
207
  const { chain, rootId } = responseHistoryChain(responseId);
206
208
  const items = [];
207
- for (const entry of chain.reverse()) items.push(...entry.inputItems, ...entry.outputItems);
209
+ for (let index = 0; index < chain.length; index += 1) {
210
+ if ((index & 63) === 0) assertActive?.();
211
+ const entry = chain[chain.length - index - 1];
212
+ items.push(...entry.inputItems, ...entry.outputItems);
213
+ }
214
+ assertActive?.();
208
215
  const materialized = cloneJson(items);
216
+ assertActive?.();
209
217
  touchTree(rootId);
210
218
  return materialized;
211
219
  }
212
220
 
213
221
  export function rememberResponseHistoryNode({ id, parentId, inputItems, outputItems, takeOwnership = false }) {
214
222
  if (!id || !Array.isArray(inputItems)) return;
223
+ clearResponsesToolOutputPartsCache(inputItems);
224
+ clearResponsesToolOutputPartsCache(outputItems);
215
225
  const bytes = jsonByteLength([inputItems, outputItems]);
216
226
  if (bytes > maxBytes) {
217
227
  if (histories.has(id)) removeSubtree(id);