codex-copilot-dx 0.6.15 → 0.6.18

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.
@@ -12,7 +12,10 @@ import {
12
12
  PM_STUDIO_2_9_10_RECIPE,
13
13
  PM_STUDIO_RECIPES,
14
14
  createCompatiblePmStudioRecipe,
15
+ createCompatiblePmStudioRecipeFromFile,
16
+ inspectAsarFile,
15
17
  inspectElectronAsarIntegritySlots,
18
+ inspectElectronAsarIntegrityFile,
16
19
  inspectAsarBuffer,
17
20
  patchAsarBuffer,
18
21
  sha256Hex,
@@ -57,10 +60,10 @@ function runChecked(processRunner, command, args, label) {
57
60
  return result;
58
61
  }
59
62
 
60
- function hashRegularFile(filePath, expected) {
63
+ function hashRegularFile(filePath, expected, readBuffer) {
61
64
  const hash = createHash("sha256");
62
65
  const file = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
63
- const buffer = Buffer.allocUnsafe(1024 * 1024);
66
+ const buffer = readBuffer || Buffer.allocUnsafe(1024 * 1024);
64
67
  try {
65
68
  const before = fs.fstatSync(file, { bigint: true });
66
69
  if (before.dev !== BigInt(expected.dev)
@@ -136,6 +139,7 @@ function parseXattrHex(output, root, ignoredXattrs) {
136
139
  export function inspectBundleContent(appPath, {
137
140
  xattrOutput = "",
138
141
  ignoredXattrs = [],
142
+ readBuffer,
139
143
  } = {}) {
140
144
  const root = path.resolve(appPath);
141
145
  const rootStat = fs.lstatSync(root);
@@ -186,7 +190,7 @@ export function inspectBundleContent(appPath, {
186
190
  }
187
191
  regularFileCount += 1;
188
192
  regularBytes += stat.size;
189
- records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat)]);
193
+ records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat, readBuffer)]);
190
194
  } else {
191
195
  throw setupError("PM_STUDIO_BUNDLE_ENTRY_INVALID", `PM Studio contains an unsupported entry: ${relative}`);
192
196
  }
@@ -208,12 +212,13 @@ export function inspectBundleContent(appPath, {
208
212
  };
209
213
  }
210
214
 
211
- function defaultInspectBundleContent({ appPath, recipe, processRunner }) {
215
+ function defaultInspectBundleContent({ appPath, recipe, processRunner, readBuffer }) {
212
216
  const xattrs = runChecked(processRunner, "/usr/bin/xattr", ["-r", "-s", "-x", "-l", appPath],
213
217
  "Reading PM Studio extended attributes");
214
218
  return inspectBundleContent(appPath, {
215
219
  xattrOutput: xattrs.stdout,
216
220
  ignoredXattrs: recipe.sourceBundleContent?.ignoredXattrs || [],
221
+ readBuffer,
217
222
  });
218
223
  }
219
224
 
@@ -316,6 +321,55 @@ function defaultInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, pro
316
321
  };
317
322
  }
318
323
 
324
+ function statusInspectExecutableIntegrity({ appPath, infoPlistPath, recipe, processRunner, evidenceCache }) {
325
+ const executableName = plistRead(processRunner, infoPlistPath, "CFBundleExecutable");
326
+ if (!executableName || path.basename(executableName) !== executableName) {
327
+ throw setupError("PM_STUDIO_EXECUTABLE_INVALID", "PM Studio CFBundleExecutable is invalid");
328
+ }
329
+ const executablePath = path.join(appPath, "Contents", "MacOS", executableName);
330
+ const frameworkPath = path.join(appPath, recipe.electronFrameworkPath);
331
+ const executable = inspectElectronAsarIntegrityFile(executablePath, { cache: evidenceCache });
332
+ const framework = inspectElectronAsarIntegrityFile(frameworkPath, { cache: evidenceCache });
333
+ return {
334
+ executableName,
335
+ executablePath,
336
+ executableSha256: executable.sha256,
337
+ frameworkPath,
338
+ frameworkSha256: framework.sha256,
339
+ embeddedIntegrity: {
340
+ state: executable.integrity.slots.length === 0 && framework.integrity.slots.length === 0 ? "absent" : "present",
341
+ supported: executable.integrity.slots.length === 0 && framework.integrity.slots.length === 0,
342
+ executable: executable.integrity,
343
+ framework: framework.integrity,
344
+ },
345
+ matchesRecipeName: executableName === recipe.executable,
346
+ };
347
+ }
348
+
349
+ export function createPmStudioStatusOperationOverrides() {
350
+ const evidenceCache = new Map();
351
+ const bundleReadBuffer = Buffer.allocUnsafe(1024 * 1024);
352
+ return {
353
+ inspectExecutableIntegrity: (args) => statusInspectExecutableIntegrity({
354
+ ...args,
355
+ evidenceCache,
356
+ }),
357
+ inspectAsarFile: ({ asarPath, recipe }) => inspectAsarFile(asarPath, recipe, {
358
+ cache: evidenceCache,
359
+ releaseTargets: true,
360
+ }),
361
+ createCompatibleRecipeFromFile: ({ asarPath, options }) => createCompatiblePmStudioRecipeFromFile(
362
+ asarPath,
363
+ options,
364
+ { cache: evidenceCache, releaseTargets: true },
365
+ ),
366
+ inspectBundleContent: (args) => defaultInspectBundleContent({
367
+ ...args,
368
+ readBuffer: bundleReadBuffer,
369
+ }),
370
+ };
371
+ }
372
+
319
373
  function defaultSignApp({ appPath, source, processRunner }) {
320
374
  const preserve = [];
321
375
  if (source?.codeSign?.displayValid === true) {
@@ -411,6 +465,17 @@ function defaultAssertPermissions({ appPath, backupRoot }) {
411
465
  }
412
466
  }
413
467
 
468
+ function defaultAssertRestorePermissions({ appPath, backupAppPath }) {
469
+ try {
470
+ fs.accessSync(appPath, fs.constants.R_OK | fs.constants.X_OK);
471
+ fs.accessSync(path.dirname(appPath), fs.constants.W_OK | fs.constants.X_OK);
472
+ fs.accessSync(backupAppPath, fs.constants.R_OK | fs.constants.X_OK);
473
+ } catch {
474
+ throw setupError("PM_STUDIO_PERMISSION_DENIED",
475
+ "PM Studio or its verified backup is not accessible to the current user; restore will not use sudo");
476
+ }
477
+ }
478
+
414
479
  export function createPmStudioSetupOperations(overrides = {}) {
415
480
  const processRunner = overrides.processRunner || defaultProcessRunner;
416
481
  return {
@@ -432,6 +497,7 @@ export function createPmStudioSetupOperations(overrides = {}) {
432
497
  availableBytes: defaultAvailableBytes,
433
498
  volumeId: defaultVolumeId,
434
499
  assertPermissions: defaultAssertPermissions,
500
+ assertRestorePermissions: defaultAssertRestorePermissions,
435
501
  readClaudeCredentials: ({ home }) => readAuthProfileCredentials(AUTH_PROFILE_CLAUDE, { home }),
436
502
  now: () => new Date(),
437
503
  uuid: () => randomUUID(),
@@ -470,7 +536,9 @@ export function inspectPmStudioApp({
470
536
  const paths = appPaths(appPath, recipe);
471
537
  const issues = [];
472
538
  const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
473
- const asar = inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
539
+ const asar = operations.inspectAsarFile
540
+ ? operations.inspectAsarFile({ ...paths, recipe, processRunner: operations.processRunner })
541
+ : inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
474
542
  const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
475
543
  const executableIntegrity = operations.inspectExecutableIntegrity({
476
544
  ...paths, recipe, processRunner: operations.processRunner,
@@ -679,11 +747,14 @@ function compatibleSourceStructure({ appPath, metadata, operations }) {
679
747
  throw setupError("PM_STUDIO_ASAR_INCOMPATIBLE",
680
748
  "PM Studio compatible-version discovery found an unsupported executable or embedded ASAR integrity policy");
681
749
  }
682
- const compatibleRecipe = createCompatiblePmStudioRecipe(fs.readFileSync(paths.asarPath), {
750
+ const recipeOptions = {
683
751
  ...compatibleStaticOptions(metadata, executableIntegrity.executableName),
684
752
  sourceExecutableSha256: executableIntegrity.executableSha256,
685
753
  sourceElectronFrameworkSha256: executableIntegrity.frameworkSha256,
686
- });
754
+ };
755
+ const compatibleRecipe = operations.createCompatibleRecipeFromFile
756
+ ? operations.createCompatibleRecipeFromFile({ asarPath: paths.asarPath, options: recipeOptions })
757
+ : createCompatiblePmStudioRecipe(fs.readFileSync(paths.asarPath), recipeOptions);
687
758
  const plistIntegrity = operations.readAsarIntegrity({
688
759
  ...paths,
689
760
  recipe: compatibleRecipe,
@@ -737,11 +808,14 @@ function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operation
737
808
  recipe: PM_STUDIO_2_9_10_RECIPE,
738
809
  processRunner: operations.processRunner,
739
810
  });
740
- const compatibleRecipe = createCompatiblePmStudioRecipe(fs.readFileSync(sourceAsarPath), {
811
+ const recipeOptions = {
741
812
  ...compatibleStaticOptions(metadata, executableIntegrity.executableName),
742
813
  sourceExecutableSha256: manifest?.source?.binaries?.main_executable_sha256,
743
814
  sourceElectronFrameworkSha256: manifest?.source?.binaries?.electron_framework_sha256,
744
- });
815
+ };
816
+ const compatibleRecipe = operations.createCompatibleRecipeFromFile
817
+ ? operations.createCompatibleRecipeFromFile({ asarPath: sourceAsarPath, options: recipeOptions })
818
+ : createCompatiblePmStudioRecipe(fs.readFileSync(sourceAsarPath), recipeOptions);
745
819
  const recipe = Object.freeze({
746
820
  ...compatibleRecipe,
747
821
  sourceBundleContent: frozenBundleContent(manifest.source.bundle_content),
@@ -766,7 +840,8 @@ function compatibleRecipeFromManifest({ backupDir, manifest, metadata, operation
766
840
 
767
841
  function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations }) {
768
842
  if (!fs.existsSync(backupRoot)) return null;
769
- const installedAsar = fs.readFileSync(path.join(appPath, PM_STUDIO_2_9_10_RECIPE.asarPath));
843
+ const installedAsarPath = path.join(appPath, PM_STUDIO_2_9_10_RECIPE.asarPath);
844
+ const installedAsar = operations.inspectAsarFile ? null : fs.readFileSync(installedAsarPath);
770
845
  let candidates = [];
771
846
  const invalidCandidates = [];
772
847
  for (const entry of fs.readdirSync(backupRoot, { withFileTypes: true })) {
@@ -788,7 +863,9 @@ function compatibleRecipeFromBackup({ appPath, backupRoot, metadata, operations
788
863
  continue;
789
864
  }
790
865
  if (!recipe) continue;
791
- const asarState = inspectAsarBuffer(installedAsar, recipe).state;
866
+ const asarState = operations.inspectAsarFile
867
+ ? operations.inspectAsarFile({ asarPath: installedAsarPath, recipe }).state
868
+ : inspectAsarBuffer(installedAsar, recipe).state;
792
869
  if (asarState === "drift") continue;
793
870
  candidates.push({ recipe, manifest, asarState });
794
871
  }
@@ -1188,6 +1265,74 @@ function assertSpace({ appPath, backupRoot, backupExists, operations }) {
1188
1265
  }
1189
1266
  }
1190
1267
 
1268
+ function assertRestoreSpace({ appPath, backupAppPath, operations }) {
1269
+ const bundleBytes = operations.bundleSize(backupAppPath);
1270
+ if (!Number.isFinite(bundleBytes) || bundleBytes <= 0) {
1271
+ throw setupError("PM_STUDIO_SIZE_INVALID", "Could not determine verified PM Studio backup size");
1272
+ }
1273
+ if (operations.availableBytes(path.dirname(appPath)) < bundleBytes + MIN_FREE_MARGIN_BYTES) {
1274
+ throw setupError("PM_STUDIO_SPACE_INSUFFICIENT", "Not enough free space for PM Studio restore staging");
1275
+ }
1276
+ }
1277
+
1278
+ function codeSignFingerprint(codeSign) {
1279
+ return {
1280
+ valid: codeSign?.valid === true,
1281
+ verifyValid: codeSign?.verifyValid === true,
1282
+ displayValid: codeSign?.displayValid === true,
1283
+ entitlementsState: codeSign?.entitlementsState || "",
1284
+ adHoc: codeSign?.adHoc === true,
1285
+ identifier: codeSign?.identifier || "",
1286
+ teamIdentifier: codeSign?.teamIdentifier || "",
1287
+ flags: codeSign?.flags || "",
1288
+ runtimeVersion: codeSign?.runtimeVersion || "",
1289
+ cdHashFull: codeSign?.cdHashFull || "",
1290
+ notarizationTicket: codeSign?.notarizationTicket || "",
1291
+ entitlementsSha256: codeSign?.entitlementsSha256 || "",
1292
+ };
1293
+ }
1294
+
1295
+ function restoreStateFingerprint(inspection, bundleContent) {
1296
+ return JSON.stringify({
1297
+ state: inspection.state,
1298
+ metadata: inspection.metadata,
1299
+ asar: {
1300
+ state: inspection.asar?.state,
1301
+ asarSha256: inspection.asar?.asarSha256,
1302
+ headerSha256: inspection.asar?.headerSha256,
1303
+ },
1304
+ plistIntegrity: inspection.plistIntegrity,
1305
+ executableIntegrity: {
1306
+ executableName: inspection.executableIntegrity?.executableName,
1307
+ executableSha256: inspection.executableIntegrity?.executableSha256,
1308
+ frameworkSha256: inspection.executableIntegrity?.frameworkSha256,
1309
+ embeddedIntegrity: inspection.executableIntegrity?.embeddedIntegrity,
1310
+ matchesRecipeName: inspection.executableIntegrity?.matchesRecipeName,
1311
+ },
1312
+ codeSign: codeSignFingerprint(inspection.codeSign),
1313
+ bundleContent,
1314
+ });
1315
+ }
1316
+
1317
+ function inspectRestoreState({ appPath, recipe, operations }) {
1318
+ const inspection = inspectPmStudioApp({ appPath, recipe, operations });
1319
+ const bundleContent = inspection.bundleContent || operations.inspectBundleContent({
1320
+ ...appPaths(appPath, recipe),
1321
+ recipe,
1322
+ processRunner: operations.processRunner,
1323
+ });
1324
+ return {
1325
+ inspection,
1326
+ fingerprint: restoreStateFingerprint(inspection, bundleContent),
1327
+ };
1328
+ }
1329
+
1330
+ function assertRestoreStateMatches(actual, expected, code, message) {
1331
+ if (actual.fingerprint !== expected.fingerprint) {
1332
+ throw setupError(code, message, actual.inspection.issues);
1333
+ }
1334
+ }
1335
+
1191
1336
  function replaceVerifiedStage({ appPath, stagePath, operations }) {
1192
1337
  try {
1193
1338
  operations.replaceApp({ appPath, stagePath, processRunner: operations.processRunner });
@@ -1376,9 +1521,8 @@ export async function runPmStudioSetup({
1376
1521
  emit(`[OK] Verified backup: ${backup.backupAppPath}${backup.reused ? " (reused)" : ""}`);
1377
1522
  emit(`[OK] Backup manifest: ${backup.manifestPath}`);
1378
1523
  emit("[INFO] The verified staging bundle was installed with Foundation's atomic item replacement.");
1379
- emit(`[INFO] Restore step 1/3: quit PM Studio and its updater.`);
1380
- 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.`);
1381
- emit(`[INFO] Restore step 3/3: move ${appPath} aside, copy ${backup.backupAppPath} to that exact path, and verify its signature before launch.`);
1524
+ emit(`[INFO] To restore, quit PM Studio and its updater, then run ${commandName} pms restore.`);
1525
+ emit("[INFO] Restore uses exact source-content verification; the original source signature is inspected and reported but is not an admission gate.");
1382
1526
  emit(`[INFO] Start ${commandName} before using Claude in PM Studio; GPT remains on PM Studio's native GitHub Copilot path.`);
1383
1527
  return {
1384
1528
  status: migratingExistingPatch ? "migrated" : "patched",
@@ -1394,3 +1538,164 @@ export async function runPmStudioSetup({
1394
1538
  }
1395
1539
  });
1396
1540
  }
1541
+
1542
+ export async function runPmStudioRestore({
1543
+ commandName = "ccdx",
1544
+ appPath = DEFAULT_PM_STUDIO_APP_PATH,
1545
+ home = os.homedir(),
1546
+ backupRoot = pmStudioBackupRoot(home),
1547
+ recipes = PM_STUDIO_RECIPES,
1548
+ operations: operationOverrides = {},
1549
+ logger = (line) => console.log(line),
1550
+ } = {}) {
1551
+ const operations = createPmStudioSetupOperations(operationOverrides);
1552
+ return operations.withSetupLock({ appPath }, async () => {
1553
+ const messages = [];
1554
+ const emit = (message) => {
1555
+ messages.push(message);
1556
+ logger(message);
1557
+ };
1558
+
1559
+ assertInstalledAppPath(appPath);
1560
+ assertNoBlockingProcesses(operations, appPath);
1561
+ const metadata = operations.readBundleMetadata({
1562
+ appPath,
1563
+ infoPlistPath: path.join(appPath, "Contents/Info.plist"),
1564
+ processRunner: operations.processRunner,
1565
+ });
1566
+ const recipe = resolvePmStudioCompatibleRecipe({
1567
+ appPath,
1568
+ home,
1569
+ backupRoot,
1570
+ metadata,
1571
+ recipes,
1572
+ operations,
1573
+ });
1574
+ const source = inspectRestoreState({ appPath, recipe, operations });
1575
+ if (source.inspection.state === "clean") {
1576
+ emit(`[OK] PM Studio ${recipe.version} build ${recipe.build} is already the exact clean local bundle; no files were changed.`);
1577
+ return {
1578
+ status: "already_clean",
1579
+ changed: false,
1580
+ recipe: recipe.id,
1581
+ inspection: source.inspection,
1582
+ messages,
1583
+ };
1584
+ }
1585
+ if (!["patched", "predecessor", "legacy"].includes(source.inspection.state)) {
1586
+ throw setupError("PM_STUDIO_BUNDLE_DRIFT",
1587
+ `PM Studio ${recipe.version} build ${recipe.build} does not match a recorded patch state; no files were changed`,
1588
+ source.inspection.issues);
1589
+ }
1590
+
1591
+ const manifestPath = pmStudioPatchManifestPath({ home, backupRoot, recipe });
1592
+ const backupDir = path.dirname(manifestPath);
1593
+ const backup = validateBackup({ backupDir, recipe, operations });
1594
+ assertPatchedBinaryRecord({
1595
+ inspection: source.inspection,
1596
+ manifestPath: backup.manifestPath,
1597
+ recipe,
1598
+ recordState: source.inspection.state,
1599
+ });
1600
+ operations.assertRestorePermissions({
1601
+ appPath,
1602
+ backupAppPath: backup.backupAppPath,
1603
+ processRunner: operations.processRunner,
1604
+ });
1605
+ assertRestoreSpace({ appPath, backupAppPath: backup.backupAppPath, operations });
1606
+ const manifestSnapshot = fs.readFileSync(backup.manifestPath);
1607
+ const backupSource = inspectRestoreState({ appPath: backup.backupAppPath, recipe, operations });
1608
+ if (backupSource.inspection.state !== "clean") {
1609
+ throw setupError("PM_STUDIO_BACKUP_INVALID", "Verified PM Studio backup is not an exact clean bundle",
1610
+ backupSource.inspection.issues);
1611
+ }
1612
+
1613
+ const stagePath = path.join(path.dirname(appPath),
1614
+ `.${path.basename(appPath)}.ccdx-restore-stage-${operations.uuid()}`);
1615
+ let preserveStage = false;
1616
+ try {
1617
+ operations.copyBundle({
1618
+ source: backup.backupAppPath,
1619
+ destination: stagePath,
1620
+ processRunner: operations.processRunner,
1621
+ });
1622
+ if (!fs.readFileSync(backup.manifestPath).equals(manifestSnapshot)) {
1623
+ throw setupError("PM_STUDIO_BACKUP_CHANGED",
1624
+ "PM Studio backup manifest changed during restore; the verified staging bundle was not installed");
1625
+ }
1626
+ const currentBackup = validateBackup({ backupDir, recipe, operations });
1627
+ const unchangedBackup = inspectRestoreState({
1628
+ appPath: currentBackup.backupAppPath,
1629
+ recipe,
1630
+ operations,
1631
+ });
1632
+ assertRestoreStateMatches(unchangedBackup, backupSource, "PM_STUDIO_BACKUP_CHANGED",
1633
+ "PM Studio backup changed during restore; the verified staging bundle was not installed");
1634
+ const unchangedSource = inspectRestoreState({ appPath, recipe, operations });
1635
+ assertRestoreStateMatches(unchangedSource, source, "PM_STUDIO_SOURCE_CHANGED",
1636
+ "PM Studio changed during restore; the verified staging bundle was not installed");
1637
+ assertPatchedBinaryRecord({
1638
+ inspection: unchangedSource.inspection,
1639
+ manifestPath: backup.manifestPath,
1640
+ recipe,
1641
+ recordState: source.inspection.state,
1642
+ });
1643
+
1644
+ const staged = inspectRestoreState({ appPath: stagePath, recipe, operations });
1645
+ if (staged.inspection.state !== "clean") {
1646
+ throw setupError("PM_STUDIO_RESTORE_STAGE_INVALID",
1647
+ "PM Studio restore staging copy is not an exact clean bundle", staged.inspection.issues);
1648
+ }
1649
+ assertRestoreStateMatches(staged, backupSource, "PM_STUDIO_RESTORE_STAGE_INVALID",
1650
+ "PM Studio restore staging copy differs from the verified clean backup");
1651
+ assertNoBlockingProcesses(operations, appPath);
1652
+
1653
+ const replacementError = replaceVerifiedStage({ appPath, stagePath, operations });
1654
+ let installed;
1655
+ try {
1656
+ installed = inspectRestoreState({ appPath, recipe, operations });
1657
+ } catch {
1658
+ preserveStage = fs.existsSync(stagePath);
1659
+ throw setupError("PM_STUDIO_RESTORE_STATE_UNKNOWN",
1660
+ `PM Studio restore final state could not be verified. Do not launch it. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`);
1661
+ }
1662
+ if (installed.inspection.state !== "clean" || installed.fingerprint !== staged.fingerprint) {
1663
+ const originalRetained = installed.fingerprint === source.fingerprint;
1664
+ preserveStage = !originalRetained && fs.existsSync(stagePath);
1665
+ const code = originalRetained
1666
+ ? "PM_STUDIO_RESTORE_REPLACE_FAILED"
1667
+ : "PM_STUDIO_RESTORE_INSTALL_VERIFY_FAILED";
1668
+ const stateMessage = originalRetained
1669
+ ? `The installed PM Studio remains the verified ${source.inspection.state} bundle`
1670
+ : "The installed PM Studio does not match the verified clean backup; do not launch it";
1671
+ throw setupError(code,
1672
+ `${stateMessage}. ${recoveryMessage(backup)}${preserveStage ? ` Verified staging retained: ${stagePath}.` : ""}`,
1673
+ installed.inspection.issues);
1674
+ }
1675
+ if (replacementError) {
1676
+ emit("[WARN] Atomic replacement reported an error after the exact clean bundle became installed; the final bundle was independently verified.");
1677
+ }
1678
+ emit(`[OK] Restored PM Studio ${recipe.version} build ${recipe.build} from the exact verified clean backup.`);
1679
+ emit(`[OK] Verified backup retained: ${backup.backupAppPath}`);
1680
+ emit(`[OK] Backup manifest retained: ${backup.manifestPath}`);
1681
+ emit("[INFO] The verified clean staging bundle was installed with Foundation's atomic item replacement.");
1682
+ if (installed.inspection.codeSign?.valid === true) {
1683
+ emit("[OK] The restored source bundle's original signature verifies successfully.");
1684
+ } else {
1685
+ emit("[WARN] The restored bundle exactly matches its recorded local source, whose original signature does not currently verify; reinstall PM Studio if a vendor-valid signature is required.");
1686
+ }
1687
+ emit(`[INFO] Run ${commandName} pms setup to apply the split-origin patch again.`);
1688
+ return {
1689
+ status: "restored",
1690
+ changed: true,
1691
+ recipe: recipe.id,
1692
+ backup,
1693
+ inspection: installed.inspection,
1694
+ replacementMode: "foundation-atomic-replace",
1695
+ messages,
1696
+ };
1697
+ } finally {
1698
+ if (!preserveStage && fs.existsSync(stagePath)) operations.removePath({ target: stagePath });
1699
+ }
1700
+ });
1701
+ }
@@ -8,6 +8,7 @@ import {
8
8
  import { PM_STUDIO_RECIPES } from "./pm-studio-asar.mjs";
9
9
  import {
10
10
  assertPatchedBinaryRecord,
11
+ createPmStudioStatusOperationOverrides,
11
12
  createPmStudioSetupOperations,
12
13
  DEFAULT_PM_STUDIO_APP_PATH,
13
14
  inspectPmStudioApp,
@@ -147,7 +148,10 @@ export async function inspectPmStudioStatus({
147
148
  readAdapterStatusFn = readAdapterStatus,
148
149
  probePmStudioRelayFn = probePmStudioRelay,
149
150
  } = {}) {
150
- const operations = createPmStudioSetupOperations(operationOverrides);
151
+ const operations = createPmStudioSetupOperations({
152
+ ...createPmStudioStatusOperationOverrides(),
153
+ ...operationOverrides,
154
+ });
151
155
  let app;
152
156
  if (!existsSync(appPath)) {
153
157
  app = { state: "not_installed", metadata: null, issues: [] };
@@ -71,6 +71,7 @@ function jsonByteLength(value, seen = new Set()) {
71
71
  const histories = new Map();
72
72
  const childrenById = new Map();
73
73
  const treeLru = new Map();
74
+ const pinnedTrees = new Map();
74
75
  const evictedIds = new Set();
75
76
  let totalBytes = 0;
76
77
  const HISTORY_RUNTIME_CONFIG = loadRuntimeConfig();
@@ -88,6 +89,21 @@ function touchTree(rootId) {
88
89
  treeLru.set(rootId, true);
89
90
  }
90
91
 
92
+ function pinTree(rootId) {
93
+ if (!rootId) return;
94
+ pinnedTrees.set(rootId, (pinnedTrees.get(rootId) || 0) + 1);
95
+ }
96
+
97
+ function unpinTree(rootId) {
98
+ const count = pinnedTrees.get(rootId) || 0;
99
+ if (count <= 1) pinnedTrees.delete(rootId);
100
+ else pinnedTrees.set(rootId, count - 1);
101
+ }
102
+
103
+ function treeIsPinned(rootId) {
104
+ return (pinnedTrees.get(rootId) || 0) > 0;
105
+ }
106
+
91
107
  function linkChild(parentId, id) {
92
108
  if (!parentId) return;
93
109
  let children = childrenById.get(parentId);
@@ -148,18 +164,50 @@ function assignSubtreeRoot(id, rootId) {
148
164
  }
149
165
  }
150
166
 
151
- function enforceLimits() {
152
- while (histories.size > maxEntries || totalBytes > maxBytes) {
153
- const oldestRootId = treeLru.keys().next().value;
154
- if (!oldestRootId) break;
155
- removeSubtree(oldestRootId);
167
+ function subtreeUsage(rootId) {
168
+ const pending = [rootId];
169
+ const seen = new Set();
170
+ let bytes = 0;
171
+ let entries = 0;
172
+ while (pending.length) {
173
+ const id = pending.pop();
174
+ if (seen.has(id)) continue;
175
+ seen.add(id);
176
+ const entry = histories.get(id);
177
+ if (entry) {
178
+ entries += 1;
179
+ bytes += entry.bytes;
180
+ }
181
+ const children = childrenById.get(id);
182
+ if (children) pending.push(...children);
183
+ }
184
+ return { bytes, entries };
185
+ }
186
+
187
+ function makeRoomFor({ additionalBytes, additionalEntries, protectedRoots }) {
188
+ let projectedBytes = totalBytes + additionalBytes;
189
+ let projectedEntries = histories.size + additionalEntries;
190
+ if (projectedEntries <= maxEntries && projectedBytes <= maxBytes) return true;
191
+
192
+ const candidates = [];
193
+ for (const rootId of treeLru.keys()) {
194
+ if (protectedRoots.has(rootId) || treeIsPinned(rootId)) continue;
195
+ const usage = subtreeUsage(rootId);
196
+ candidates.push(rootId);
197
+ projectedBytes -= usage.bytes;
198
+ projectedEntries -= usage.entries;
199
+ if (projectedEntries <= maxEntries && projectedBytes <= maxBytes) break;
156
200
  }
201
+ if (projectedEntries > maxEntries || projectedBytes > maxBytes) return false;
202
+ for (const rootId of candidates) removeSubtree(rootId);
203
+ return true;
157
204
  }
158
205
 
159
206
  export function clearResponseHistoryForTests() {
160
207
  histories.clear();
161
208
  childrenById.clear();
162
209
  treeLru.clear();
210
+ pinnedTrees.clear();
163
211
  evictedIds.clear();
164
212
  totalBytes = 0;
165
213
  maxBytes = DEFAULT_MAX_BYTES;
@@ -202,6 +250,50 @@ export function responseHistoryMaterializedBytes(responseId) {
202
250
  return chain.reduce((bytes, entry) => bytes + entry.bytes, 0);
203
251
  }
204
252
 
253
+ export function acquireResponseHistorySnapshot(responseId, { assertActive, signal } = {}) {
254
+ assertActive?.();
255
+ if (signal?.aborted) throw signal.reason || new DOMException("The operation was aborted", "AbortError");
256
+ const { chain, rootId } = responseHistoryChain(responseId);
257
+ const entries = chain.slice().reverse();
258
+ const bytes = chain.reduce((total, entry) => total + entry.bytes, 0);
259
+ pinTree(rootId);
260
+
261
+ let released = false;
262
+ const release = () => {
263
+ if (released) return;
264
+ released = true;
265
+ signal?.removeEventListener("abort", release);
266
+ unpinTree(rootId);
267
+ };
268
+ signal?.addEventListener("abort", release, { once: true });
269
+
270
+ return Object.freeze({
271
+ responseId,
272
+ rootId,
273
+ bytes,
274
+ materialize({ assertActive: assertSnapshotActive, routeMetadata } = {}) {
275
+ assertSnapshotActive?.();
276
+ if (released) throw new Error("Response history snapshot has been released");
277
+ const items = [];
278
+ for (let index = 0; index < entries.length; index += 1) {
279
+ if ((index & 63) === 0) assertSnapshotActive?.();
280
+ const entry = entries[index];
281
+ routeMetadata?.push({
282
+ affinity: entry.routeAffinity || null,
283
+ hasOpaque: entry.hasOpaque === true,
284
+ });
285
+ items.push(...entry.inputItems, ...entry.outputItems);
286
+ }
287
+ assertSnapshotActive?.();
288
+ const materialized = cloneJson(items);
289
+ assertSnapshotActive?.();
290
+ touchTree(rootId);
291
+ return materialized;
292
+ },
293
+ release,
294
+ });
295
+ }
296
+
205
297
  export function materializeResponseHistory(responseId, { assertActive, routeMetadata } = {}) {
206
298
  assertActive?.();
207
299
  const { chain, rootId } = responseHistoryChain(responseId);
@@ -231,7 +323,13 @@ export function rememberResponseHistoryNode({
231
323
  routeAffinity = null,
232
324
  takeOwnership = false,
233
325
  }) {
234
- if (!id || !Array.isArray(inputItems)) return;
326
+ if (!id || !Array.isArray(inputItems)) return false;
327
+ const existing = histories.get(id);
328
+ const parentEntry = parentId ? histories.get(parentId) : null;
329
+ if (parentId && !parentEntry) {
330
+ if (!existing) rememberEvictedId(id);
331
+ return false;
332
+ }
235
333
  clearResponsesToolOutputPartsCache(inputItems);
236
334
  clearResponsesToolOutputPartsCache(outputItems);
237
335
  const historyValue = routeAffinity
@@ -239,13 +337,11 @@ export function rememberResponseHistoryNode({
239
337
  : [inputItems, outputItems];
240
338
  const bytes = jsonByteLength(historyValue);
241
339
  if (bytes > maxBytes) {
242
- if (histories.has(id)) removeSubtree(id);
243
- rememberEvictedId(id);
244
- return;
340
+ if (!existing) rememberEvictedId(id);
341
+ else if (!treeIsPinned(existing.rootId)) removeSubtree(id);
342
+ return false;
245
343
  }
246
- const existing = histories.get(id);
247
- const parentEntry = parentId ? histories.get(parentId) : null;
248
- const rootId = parentEntry?.rootId || (parentId ? existing?.rootId : id) || id;
344
+ const rootId = parentEntry?.rootId || id;
249
345
  const entry = {
250
346
  parentId: parentId || null,
251
347
  rootId,
@@ -256,6 +352,19 @@ export function rememberResponseHistoryNode({
256
352
  bytes,
257
353
  };
258
354
  const oldRootId = existing?.rootId;
355
+ const protectedRoots = new Set([rootId]);
356
+ if (oldRootId) protectedRoots.add(oldRootId);
357
+ if (!makeRoomFor({
358
+ additionalBytes: bytes - (existing?.bytes || 0),
359
+ additionalEntries: existing ? 0 : 1,
360
+ protectedRoots,
361
+ })) {
362
+ if (!existing) {
363
+ if (parentEntry && !treeIsPinned(rootId)) removeSubtree(rootId);
364
+ rememberEvictedId(id);
365
+ }
366
+ return false;
367
+ }
259
368
  if (existing) {
260
369
  totalBytes -= existing.bytes;
261
370
  unlinkChild(existing.parentId, id);
@@ -270,5 +379,5 @@ export function rememberResponseHistoryNode({
270
379
  totalBytes += entry.bytes;
271
380
  evictedIds.delete(id);
272
381
  touchTree(rootId);
273
- enforceLimits();
382
+ return true;
274
383
  }