@yansigit/opencodex 2.31.2 → 2.32.0
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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +377 -0
- package/src/adapters/command-code.ts +7 -4
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +21 -0
- package/src/adapters/cursor/native-exec-bridge.ts +141 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google-http.ts +12 -2
- package/src/adapters/google-wire-compiler.ts +83 -2
- package/src/adapters/google.ts +62 -21
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +77 -812
- package/src/generated/compatibility-version.json +162 -94
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +3 -0
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +242 -101
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +12 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +35 -13
- package/src/web-search/index.ts +85 -1
- package/gui/dist/assets/index-BAMgarF9.js +0 -102
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
|
@@ -75,6 +75,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
|
|
|
75
75
|
|
|
76
76
|
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
|
|
77
77
|
import type { CatalogModel } from "./parsing";
|
|
78
|
+
import { capsFromProvider, enrichCatalogModelMetadata, persistLiveModelMetadata, type LiveSnapshotRow } from "./model-metadata";
|
|
78
79
|
import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
79
80
|
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
|
|
80
81
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
@@ -662,9 +663,16 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
662
663
|
const hintedWindow = discoveredWindow !== undefined
|
|
663
664
|
? (configuredCap !== undefined ? Math.min(discoveredWindow, configuredCap) : discoveredWindow)
|
|
664
665
|
: (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined));
|
|
666
|
+
const capFilledMissingWindow = discoveredWindow === undefined && hintedWindow !== undefined;
|
|
665
667
|
const hinted = {
|
|
666
668
|
...modelWithoutServiceTier,
|
|
667
669
|
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
|
|
670
|
+
...(discoveredWindow !== undefined
|
|
671
|
+
? { detectedContextWindow: model.detectedContextWindow ?? discoveredWindow }
|
|
672
|
+
: {}),
|
|
673
|
+
...(capFilledMissingWindow && !model.metadataSource
|
|
674
|
+
? { metadataSource: "config_fallback" as const }
|
|
675
|
+
: {}),
|
|
668
676
|
...(inputModalities ? { inputModalities } : {}),
|
|
669
677
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
670
678
|
...(configuredMaxInput !== undefined
|
|
@@ -689,10 +697,26 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
689
697
|
...(prov.codexToolMode !== undefined ? { codexToolMode: prov.codexToolMode } : {}),
|
|
690
698
|
};
|
|
691
699
|
const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
|
|
700
|
+
const detectedContextWindow = hinted.detectedContextWindow
|
|
701
|
+
?? discoveredWindow;
|
|
702
|
+
const contextCapped = providerCap !== undefined
|
|
703
|
+
&& typeof capped === "number"
|
|
704
|
+
&& (
|
|
705
|
+
capped !== hinted.contextWindow
|
|
706
|
+
|| (typeof detectedContextWindow === "number" && detectedContextWindow > capped)
|
|
707
|
+
|| hinted.contextCapped === true
|
|
708
|
+
);
|
|
692
709
|
if (providerCap !== undefined && capped !== hinted.contextWindow) {
|
|
693
|
-
return {
|
|
710
|
+
return {
|
|
711
|
+
...hinted,
|
|
712
|
+
contextWindow: capped,
|
|
713
|
+
contextCap: providerCap,
|
|
714
|
+
contextCapped,
|
|
715
|
+
};
|
|
694
716
|
}
|
|
695
|
-
return providerCap !== undefined
|
|
717
|
+
return providerCap !== undefined
|
|
718
|
+
? { ...hinted, contextCap: providerCap, contextCapped }
|
|
719
|
+
: hinted;
|
|
696
720
|
}
|
|
697
721
|
|
|
698
722
|
export function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string, contextCap?: number): Partial<CatalogModel> {
|
|
@@ -1043,15 +1067,35 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
1043
1067
|
item.context_size,
|
|
1044
1068
|
item.max_model_len,
|
|
1045
1069
|
item.max_context_length,
|
|
1070
|
+
item.context_window,
|
|
1071
|
+
item.max_context_window,
|
|
1072
|
+
item.max_context_size,
|
|
1073
|
+
item.n_ctx,
|
|
1074
|
+
plainRecord(item.top_provider)?.max_context_length,
|
|
1075
|
+
plainRecord(metadata?.top_provider)?.max_context_length,
|
|
1046
1076
|
// llama.cpp reports the served context under `meta`: `n_ctx` is what the
|
|
1047
1077
|
// server was actually started with, `n_ctx_train` the model's trained
|
|
1048
1078
|
// maximum. Prefer the served value — routing must not promise a window the
|
|
1049
1079
|
// running server will refuse. Both come LAST so no provider already
|
|
1050
1080
|
// supplying a recognized field changes behavior (#1797).
|
|
1051
1081
|
plainRecord(item.meta)?.n_ctx,
|
|
1082
|
+
item.default_context_size,
|
|
1052
1083
|
plainRecord(item.meta)?.n_ctx_train,
|
|
1053
1084
|
);
|
|
1054
|
-
const maxInputTokens = positiveSafeInteger(
|
|
1085
|
+
const maxInputTokens = positiveSafeInteger(
|
|
1086
|
+
limits?.max_input_tokens,
|
|
1087
|
+
item.max_input_tokens,
|
|
1088
|
+
item.max_input_length,
|
|
1089
|
+
item.max_prompt_tokens,
|
|
1090
|
+
);
|
|
1091
|
+
// Output ceilings only. A top-level `max_tokens` is ambiguous (some catalogs
|
|
1092
|
+
// use it as context) and generated jawcode `maxTokens` is output-only — never
|
|
1093
|
+
// treat either as a context window.
|
|
1094
|
+
const maxOutputTokens = positiveSafeInteger(
|
|
1095
|
+
limits?.max_output_tokens,
|
|
1096
|
+
item.max_output_tokens,
|
|
1097
|
+
limits?.max_tokens,
|
|
1098
|
+
);
|
|
1055
1099
|
// Some OpenAI-compatible catalogs expose the selectable ladder under
|
|
1056
1100
|
// `reasoning_parameters.efforts` instead of the older `reasoning_efforts` key.
|
|
1057
1101
|
// Treat both as model metadata: otherwise a valid upstream capability disappears
|
|
@@ -1079,6 +1123,7 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
1079
1123
|
return {
|
|
1080
1124
|
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
1081
1125
|
...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}),
|
|
1126
|
+
...(maxOutputTokens && maxOutputTokens > 0 ? { maxOutputTokens } : {}),
|
|
1082
1127
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
1083
1128
|
...(inputModalities ? { inputModalities } : {}),
|
|
1084
1129
|
...(capabilities ? { capabilities } : {}),
|
|
@@ -1283,9 +1328,13 @@ async function fetchProviderModelsWithAuth(
|
|
|
1283
1328
|
const stale = getStaleCached(name);
|
|
1284
1329
|
return observed(
|
|
1285
1330
|
withConfiguredRetention(
|
|
1286
|
-
stale
|
|
1331
|
+
(stale
|
|
1287
1332
|
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
|
|
1288
|
-
: failedDiscoveryConfigured
|
|
1333
|
+
: failedDiscoveryConfigured
|
|
1334
|
+
).map(model => enrichCatalogModelMetadata(model, {
|
|
1335
|
+
liveFresh: false,
|
|
1336
|
+
caps: capsFromProvider(prov, model.id, contextCap),
|
|
1337
|
+
})),
|
|
1289
1338
|
),
|
|
1290
1339
|
"degraded",
|
|
1291
1340
|
);
|
|
@@ -1312,12 +1361,14 @@ async function fetchProviderModelsWithAuth(
|
|
|
1312
1361
|
markModelsFetchFailure(name);
|
|
1313
1362
|
markProviderDiscoveryFailed(name, failure);
|
|
1314
1363
|
const stale = getStaleCached(name);
|
|
1364
|
+
const fallbackModels = stale
|
|
1365
|
+
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
|
|
1366
|
+
: failedDiscoveryConfigured;
|
|
1315
1367
|
return {
|
|
1316
|
-
models: withConfiguredRetention(
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
),
|
|
1368
|
+
models: withConfiguredRetention(fallbackModels.map(model => enrichCatalogModelMetadata(model, {
|
|
1369
|
+
liveFresh: false,
|
|
1370
|
+
caps: capsFromProvider(prov, model.id, contextCap),
|
|
1371
|
+
}))),
|
|
1321
1372
|
fallback: stale ? "stale" : "configured",
|
|
1322
1373
|
shouldLog,
|
|
1323
1374
|
};
|
|
@@ -1384,19 +1435,40 @@ async function fetchProviderModelsWithAuth(
|
|
|
1384
1435
|
return observed(models, "degraded");
|
|
1385
1436
|
}
|
|
1386
1437
|
if (antigravity) {
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1438
|
+
const observedAt = new Date().toISOString();
|
|
1439
|
+
const snapshotRows: LiveSnapshotRow[] = [];
|
|
1440
|
+
const live = antigravity.map(model => {
|
|
1441
|
+
const hints = {
|
|
1442
|
+
// CCA only exposes a numeric thinking budget. Until the adapter owns an exact Codex
|
|
1443
|
+
// effort-to-wire mapping for a newly discovered model, do not advertise a false ladder.
|
|
1444
|
+
reasoningEfforts: [] as string[],
|
|
1445
|
+
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
|
|
1446
|
+
...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
|
|
1447
|
+
};
|
|
1448
|
+
if (model.contextWindow || model.inputModalities) {
|
|
1449
|
+
snapshotRows.push({
|
|
1450
|
+
id: model.id,
|
|
1451
|
+
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
|
|
1452
|
+
...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
|
|
1453
|
+
observedAt,
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
const enriched = enrichCatalogModelMetadata({
|
|
1457
|
+
id: model.id,
|
|
1458
|
+
provider: name,
|
|
1459
|
+
...hints,
|
|
1460
|
+
...(model.contextWindow ? { metadataSource: "live" as const, metadataObservedAt: observedAt } : {}),
|
|
1461
|
+
}, {
|
|
1462
|
+
liveFresh: true,
|
|
1463
|
+
caps: capsFromProvider(prov, model.id, contextCap),
|
|
1464
|
+
});
|
|
1465
|
+
return applyProviderConfigHints(name, prov, enriched, contextCap);
|
|
1466
|
+
});
|
|
1396
1467
|
const forCache = withConfiguredRetention(live, { retainComboTargets: false });
|
|
1397
1468
|
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
1398
1469
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
1399
1470
|
}
|
|
1471
|
+
persistLiveModelMetadata(name, snapshotRows, { writerGeneration: cacheGeneration, observedAt });
|
|
1400
1472
|
registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, {
|
|
1401
1473
|
provider: name,
|
|
1402
1474
|
cacheGeneration,
|
|
@@ -1421,22 +1493,30 @@ async function fetchProviderModelsWithAuth(
|
|
|
1421
1493
|
return observed(models, "degraded");
|
|
1422
1494
|
}
|
|
1423
1495
|
const items = extracted.items;
|
|
1496
|
+
const observedAt = new Date().toISOString();
|
|
1497
|
+
const snapshotRows: LiveSnapshotRow[] = [];
|
|
1424
1498
|
const live = items.map(m => {
|
|
1425
1499
|
const ownedBy = boundedOwnedBy(m.owned_by);
|
|
1426
|
-
|
|
1500
|
+
const hints = catalogHintsFromModelsApiItem(name, m);
|
|
1501
|
+
const discovered = Boolean(hints.contextWindow || hints.maxInputTokens || hints.maxOutputTokens
|
|
1502
|
+
|| hints.inputModalities || hints.capabilities || hints.reasoningEfforts);
|
|
1503
|
+
if (discovered) {
|
|
1504
|
+
snapshotRows.push({ id: m.id, ...hints, observedAt });
|
|
1505
|
+
}
|
|
1506
|
+
const enriched = enrichCatalogModelMetadata({
|
|
1427
1507
|
id: m.id,
|
|
1428
1508
|
provider: name,
|
|
1429
1509
|
...(ownedBy ? { owned_by: ownedBy } : {}),
|
|
1430
|
-
...
|
|
1431
|
-
|
|
1510
|
+
...hints,
|
|
1511
|
+
...(discovered ? { metadataSource: "live" as const, metadataObservedAt: observedAt } : {}),
|
|
1512
|
+
}, {
|
|
1513
|
+
liveFresh: true,
|
|
1514
|
+
caps: capsFromProvider(prov, m.id, contextCap),
|
|
1515
|
+
});
|
|
1516
|
+
return applyProviderConfigHints(name, prov, enriched, contextCap);
|
|
1432
1517
|
})
|
|
1433
1518
|
.filter(m => shouldExposeProviderModel(name, m.id));
|
|
1434
|
-
// Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
|
|
1435
|
-
// `live`; otherwise configured entries would be reported as discovered ones.
|
|
1436
1519
|
const liveModelCount = live.length;
|
|
1437
|
-
// Dated-release aliases + configured retention (compat allow-list, combo targets,
|
|
1438
|
-
// Vertex default). Cache without combo retention so a later gather re-applies the
|
|
1439
|
-
// current capture's retain set on read (warm-cache OCX-111 / #1308).
|
|
1440
1520
|
const forCache = withConfiguredRetention(live, { retainComboTargets: false });
|
|
1441
1521
|
const returned = withConfiguredRetention(forCache, { warnDrops: true });
|
|
1442
1522
|
const droppedConfiguredIds = configured
|
|
@@ -1450,6 +1530,7 @@ async function fetchProviderModelsWithAuth(
|
|
|
1450
1530
|
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
1451
1531
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
1452
1532
|
}
|
|
1533
|
+
persistLiveModelMetadata(name, snapshotRows, { writerGeneration: cacheGeneration, observedAt });
|
|
1453
1534
|
markProviderDiscoveryOk(name, liveModelCount);
|
|
1454
1535
|
return observed(returned, "authoritative");
|
|
1455
1536
|
} catch (error) {
|
|
@@ -2035,7 +2116,14 @@ export function augmentRoutedModelsWithMetadata(
|
|
|
2035
2116
|
providers?: Record<string, OcxProviderConfig>,
|
|
2036
2117
|
caps?: Pick<OcxConfig, "providerContextCaps">,
|
|
2037
2118
|
): CatalogModel[] {
|
|
2038
|
-
const out =
|
|
2119
|
+
const out = models.map((model) => {
|
|
2120
|
+
const provider = providers?.[model.provider];
|
|
2121
|
+
const contextCap = caps ? providerContextCap(caps, model.provider) : undefined;
|
|
2122
|
+
return enrichCatalogModelMetadata(model, {
|
|
2123
|
+
liveFresh: model.metadataSource === "live" && model.metadataStale !== true,
|
|
2124
|
+
caps: capsFromProvider(provider, model.id, contextCap),
|
|
2125
|
+
});
|
|
2126
|
+
});
|
|
2039
2127
|
const seen = new Set(out.map(m => `${m.provider}/${m.id}`));
|
|
2040
2128
|
for (const provider of providerNames) {
|
|
2041
2129
|
if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
|
|
@@ -2054,10 +2142,13 @@ export function augmentRoutedModelsWithMetadata(
|
|
|
2054
2142
|
...(typeof meta.contextWindow === "number" && meta.contextWindow > 0 ? { contextWindow: meta.contextWindow } : {}),
|
|
2055
2143
|
...(Array.isArray(meta.input) && meta.input.length > 0 ? { inputModalities: [...meta.input] } : {}),
|
|
2056
2144
|
};
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2145
|
+
const hinted = providers?.[provider]
|
|
2146
|
+
? applyProviderConfigHints(provider, providers[provider], model, contextCap)
|
|
2147
|
+
: model;
|
|
2148
|
+
out.push(enrichCatalogModelMetadata(hinted, {
|
|
2149
|
+
liveFresh: false,
|
|
2150
|
+
caps: capsFromProvider(providers?.[provider], meta.id, contextCap),
|
|
2151
|
+
}));
|
|
2061
2152
|
}
|
|
2062
2153
|
}
|
|
2063
2154
|
return out;
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "../model-entitlements";
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing";
|
|
44
|
+
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readNativeBaseline } from "./parsing";
|
|
45
45
|
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
46
46
|
import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
|
|
47
47
|
import {
|
|
@@ -1832,11 +1832,12 @@ export function invalidateCodexModelsCacheWithPermit(
|
|
|
1832
1832
|
// The catalog-only sync override applies here too so an explicit refresh
|
|
1833
1833
|
// keeps the cache consistent with the catalog it just wrote.
|
|
1834
1834
|
if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) return false;
|
|
1835
|
-
const catalogPath =
|
|
1835
|
+
const catalogPath = readCodexCatalogPathForHome(owningCodexHome);
|
|
1836
|
+
const cachePath = join(owningCodexHome, "models_cache.json");
|
|
1836
1837
|
if (!existsSync(catalogPath)) return false;
|
|
1837
1838
|
const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
|
|
1838
1839
|
const models = catalog.models ?? catalog;
|
|
1839
|
-
const currentCache = readCatalog(
|
|
1840
|
+
const currentCache = readCatalog(cachePath);
|
|
1840
1841
|
const existingSlugs = new Set(models.flatMap((entry: RawEntry) =>
|
|
1841
1842
|
typeof entry.slug === "string" ? [entry.slug] : []));
|
|
1842
1843
|
const currentConfig = loadConfig();
|
|
@@ -1864,7 +1865,7 @@ export function invalidateCodexModelsCacheWithPermit(
|
|
|
1864
1865
|
models: [...models, ...observedAccountModels],
|
|
1865
1866
|
};
|
|
1866
1867
|
replaceCodexModelsCache(permit, owningCodexHome, {
|
|
1867
|
-
path:
|
|
1868
|
+
path: cachePath,
|
|
1868
1869
|
content: `${JSON.stringify(wrapper, null, 2)}\n`,
|
|
1869
1870
|
});
|
|
1870
1871
|
return true;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full restart of the Codex desktop app (the Electron shell), Windows only.
|
|
3
|
+
*
|
|
4
|
+
* `--restart-codex` deliberately signals only `codex app-server` /
|
|
5
|
+
* `codex-code-mode-host` processes: `isCodexAppServerCommandLine` requires a
|
|
6
|
+
* `codex` executable token, so the shell that owns the model picker is never a
|
|
7
|
+
* match. On macOS that is enough, because the respawned app-server re-emits
|
|
8
|
+
* `codex-app-server-initialized` and the renderer drops its cached
|
|
9
|
+
* `model/list`. On Windows MSIX it is not: externally terminating the child
|
|
10
|
+
* does not reliably re-emit that event in the surviving shell, so the picker
|
|
11
|
+
* keeps showing the old catalog until the app itself is restarted (#2292).
|
|
12
|
+
*
|
|
13
|
+
* This is therefore a SEPARATE opt-in flag rather than a widening of
|
|
14
|
+
* `--restart-codex`. Quitting the desktop app ends live conversations, which is
|
|
15
|
+
* a different consent from restarting a background helper, and the CLI contract
|
|
16
|
+
* for `--restart-codex` promises the narrow behavior.
|
|
17
|
+
*
|
|
18
|
+
* Everything here fails CLOSED: if the package cannot be identified, if a
|
|
19
|
+
* target is part of our own ancestry, or if any target survives termination,
|
|
20
|
+
* nothing is relaunched and the caller is told to restart manually. A stale
|
|
21
|
+
* picker is a much smaller problem than a wrongly killed process.
|
|
22
|
+
*/
|
|
23
|
+
import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsTaskkillExe } from "../lib/windows-elevation";
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
|
|
26
|
+
/** Bounded subprocess options. A hung Appx/CIM probe must never wedge `ocx sync`. */
|
|
27
|
+
export interface DesktopAppExecOptions {
|
|
28
|
+
timeout?: number;
|
|
29
|
+
windowsHide?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DesktopAppRestartIo {
|
|
33
|
+
platform?: NodeJS.Platform;
|
|
34
|
+
/** Returns stdout. Options are part of the seam so the timeout is testable. */
|
|
35
|
+
execFile?: (file: string, args: readonly string[], options?: DesktopAppExecOptions) => string;
|
|
36
|
+
/** Process ancestry of the current process, innermost first. Used for the self-kill guard. */
|
|
37
|
+
ancestryPids?: () => number[];
|
|
38
|
+
isAlive?: (pid: number) => boolean;
|
|
39
|
+
sleep?: (ms: number) => void;
|
|
40
|
+
now?: () => number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type DesktopAppRestartReason =
|
|
44
|
+
| "windows_only"
|
|
45
|
+
| "package_discovery_failed"
|
|
46
|
+
| "no_targets"
|
|
47
|
+
| "self_ancestry"
|
|
48
|
+
| "targets_survived";
|
|
49
|
+
|
|
50
|
+
export interface DesktopAppRestartResult {
|
|
51
|
+
attempted: boolean;
|
|
52
|
+
stopped: number[];
|
|
53
|
+
surviving: number[];
|
|
54
|
+
relaunch: "started" | "skipped";
|
|
55
|
+
reason?: DesktopAppRestartReason;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** How long a graceful close is given before the forced pass. */
|
|
59
|
+
const GRACEFUL_EXIT_TIMEOUT_MS = 15_000;
|
|
60
|
+
/** How long a forced kill is given before the target counts as surviving. */
|
|
61
|
+
const FORCED_EXIT_TIMEOUT_MS = 5_000;
|
|
62
|
+
/** Every probe is bounded; PowerShell module loading is the slow part. */
|
|
63
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
64
|
+
|
|
65
|
+
interface DesktopPackage {
|
|
66
|
+
family: string;
|
|
67
|
+
installLocation: string;
|
|
68
|
+
aumid: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Runtime discovery, never a hardcoded identifier. The beta MSIX package family
|
|
73
|
+
* changes between builds, so a literal AUMID would silently stop matching and
|
|
74
|
+
* then either do nothing or — worse — match a package we did not mean.
|
|
75
|
+
*/
|
|
76
|
+
function discoverPackage(exec: NonNullable<DesktopAppRestartIo["execFile"]>): DesktopPackage | null {
|
|
77
|
+
const script = [
|
|
78
|
+
"$ErrorActionPreference='SilentlyContinue'",
|
|
79
|
+
"Import-Module Appx -ErrorAction SilentlyContinue",
|
|
80
|
+
"$p = Get-AppxPackage -Name OpenAI.Codex",
|
|
81
|
+
"if (-not $p) { $p = Get-AppxPackage -Name OpenAI.CodexBeta }",
|
|
82
|
+
"if (-not $p -or -not $p.InstallLocation) { 'MISS' } else {",
|
|
83
|
+
" $p.PackageFamilyName; $p.InstallLocation; \"$($p.PackageFamilyName)!App\"",
|
|
84
|
+
"}",
|
|
85
|
+
].join("; ");
|
|
86
|
+
let stdout: string;
|
|
87
|
+
try {
|
|
88
|
+
stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
89
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
|
|
96
|
+
if (lines.length < 3 || lines[0] === "MISS") return null;
|
|
97
|
+
const [family, installLocation, aumid] = lines;
|
|
98
|
+
if (!family || !installLocation || !aumid) return null;
|
|
99
|
+
return { family, installLocation, aumid };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface DesktopProcess {
|
|
103
|
+
pid: number;
|
|
104
|
+
parentPid: number;
|
|
105
|
+
/** Win32_Process CreationDate. Guards against PID reuse across the wait window. */
|
|
106
|
+
createdAt: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Only `ChatGPT.exe` processes whose image lives under the discovered install
|
|
111
|
+
* location AND owned by the current user. The install location alone is not
|
|
112
|
+
* enough: an MSIX package under `WindowsApps` is shared, so on a multi-user
|
|
113
|
+
* machine another account's Codex desktop matches the same path. The app-server
|
|
114
|
+
* collector already pays for `GetOwner` for exactly this reason.
|
|
115
|
+
*
|
|
116
|
+
* `CreationDate` is captured so a PID can be re-verified before it is signalled;
|
|
117
|
+
* a graceful-close window is long enough for Windows to recycle a PID.
|
|
118
|
+
*/
|
|
119
|
+
function listPackageProcesses(
|
|
120
|
+
exec: NonNullable<DesktopAppRestartIo["execFile"]>,
|
|
121
|
+
installLocation: string,
|
|
122
|
+
): DesktopProcess[] {
|
|
123
|
+
const literal = installLocation.replace(/'/g, "''");
|
|
124
|
+
const script = [
|
|
125
|
+
"$ErrorActionPreference='SilentlyContinue'",
|
|
126
|
+
`$root = '${literal}'`,
|
|
127
|
+
"$me = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name",
|
|
128
|
+
"Get-CimInstance Win32_Process -Filter \"Name='ChatGPT.exe'\" |",
|
|
129
|
+
" Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($root, 'OrdinalIgnoreCase') } |",
|
|
130
|
+
" ForEach-Object {",
|
|
131
|
+
" $o = Invoke-CimMethod -InputObject $_ -MethodName GetOwner",
|
|
132
|
+
" if ($o -and $o.ReturnValue -eq 0 -and $o.User) {",
|
|
133
|
+
" $owner = if ($o.Domain) { \"$($o.Domain)\\$($o.User)\" } else { $o.User }",
|
|
134
|
+
" if ($owner -ieq $me) {",
|
|
135
|
+
" \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o'))\"",
|
|
136
|
+
" }",
|
|
137
|
+
" }",
|
|
138
|
+
" }",
|
|
139
|
+
].join(" ");
|
|
140
|
+
let stdout: string;
|
|
141
|
+
try {
|
|
142
|
+
stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
143
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const processes: DesktopProcess[] = [];
|
|
150
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
151
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
|
|
152
|
+
if (!match) continue;
|
|
153
|
+
const pid = Number(match[1]);
|
|
154
|
+
const parentPid = Number(match[2]);
|
|
155
|
+
const createdAt = match[3]!;
|
|
156
|
+
if (Number.isSafeInteger(pid) && Number.isSafeInteger(parentPid)) {
|
|
157
|
+
processes.push({ pid, parentPid, createdAt });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return processes;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* True when the PID still names the same process we verified. Between listing
|
|
165
|
+
* and signalling there is a graceful-close window, and a `taskkill /T /F` on a
|
|
166
|
+
* recycled PID would tear down an unrelated process tree.
|
|
167
|
+
*/
|
|
168
|
+
function stillSameProcess(
|
|
169
|
+
exec: NonNullable<DesktopAppRestartIo["execFile"]>,
|
|
170
|
+
installLocation: string,
|
|
171
|
+
target: DesktopProcess,
|
|
172
|
+
): boolean {
|
|
173
|
+
const current = listPackageProcesses(exec, installLocation)
|
|
174
|
+
.find(p => p.pid === target.pid);
|
|
175
|
+
return current !== undefined && current.createdAt === target.createdAt;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Roots are the package processes whose parent is not itself in the package tree. */
|
|
179
|
+
function rootProcesses(processes: readonly DesktopProcess[]): DesktopProcess[] {
|
|
180
|
+
const inTree = new Set(processes.map(p => p.pid));
|
|
181
|
+
return processes.filter(p => !inTree.has(p.parentPid));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Full Windows parent chain for this process, innermost first.
|
|
186
|
+
*
|
|
187
|
+
* `process.ppid` is one level, which is not enough: a terminal hosted inside the
|
|
188
|
+
* desktop app sits several hops below `ChatGPT.exe`, so a one-level check would
|
|
189
|
+
* miss the exact case the guard exists for and we would terminate our own host.
|
|
190
|
+
* The chain therefore comes from CIM, with a bound so a corrupted parent cycle
|
|
191
|
+
* cannot spin.
|
|
192
|
+
*/
|
|
193
|
+
function windowsAncestryPids(exec: NonNullable<DesktopAppRestartIo["execFile"]>): number[] {
|
|
194
|
+
const chain: number[] = [process.pid];
|
|
195
|
+
let current = process.pid;
|
|
196
|
+
for (let hop = 0; hop < 16; hop++) {
|
|
197
|
+
let stdout: string;
|
|
198
|
+
try {
|
|
199
|
+
stdout = exec(resolveTrustedWindowsPowerShellExe(), [
|
|
200
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
201
|
+
`$ErrorActionPreference='SilentlyContinue'; (Get-CimInstance Win32_Process -Filter "ProcessId=${current}").ParentProcessId`,
|
|
202
|
+
], { timeout: PROBE_TIMEOUT_MS, windowsHide: true });
|
|
203
|
+
} catch {
|
|
204
|
+
// An unreadable chain must not be read as "not our ancestor".
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const parent = Number(stdout.trim());
|
|
208
|
+
if (!Number.isSafeInteger(parent) || parent <= 0 || chain.includes(parent)) break;
|
|
209
|
+
chain.push(parent);
|
|
210
|
+
current = parent;
|
|
211
|
+
}
|
|
212
|
+
return chain;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function defaultExecFile(file: string, args: readonly string[], options?: DesktopAppExecOptions): string {
|
|
216
|
+
return execFileSync(file, [...args], {
|
|
217
|
+
encoding: "utf-8",
|
|
218
|
+
timeout: options?.timeout ?? PROBE_TIMEOUT_MS,
|
|
219
|
+
windowsHide: options?.windowsHide ?? true,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function defaultIsAlive(pid: number): boolean {
|
|
224
|
+
try {
|
|
225
|
+
process.kill(pid, 0);
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function defaultSleep(ms: number): void {
|
|
233
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function waitForExit(
|
|
237
|
+
pid: number,
|
|
238
|
+
timeoutMs: number,
|
|
239
|
+
isAlive: (pid: number) => boolean,
|
|
240
|
+
sleep: (ms: number) => void,
|
|
241
|
+
now: () => number,
|
|
242
|
+
): boolean {
|
|
243
|
+
const deadline = now() + timeoutMs;
|
|
244
|
+
while (now() < deadline) {
|
|
245
|
+
if (!isAlive(pid)) return true;
|
|
246
|
+
sleep(250);
|
|
247
|
+
}
|
|
248
|
+
return !isAlive(pid);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Stop every package-tree root gracefully, force the stragglers, then relaunch
|
|
253
|
+
* through the discovered AUMID. Returns without relaunching if anything
|
|
254
|
+
* survived, because launching a second shell beside a stuck one is worse than
|
|
255
|
+
* leaving the user to restart it.
|
|
256
|
+
*/
|
|
257
|
+
export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopAppRestartResult {
|
|
258
|
+
const platform = io.platform ?? process.platform;
|
|
259
|
+
const skipped = (reason: DesktopAppRestartReason): DesktopAppRestartResult => ({
|
|
260
|
+
attempted: false, stopped: [], surviving: [], relaunch: "skipped", reason,
|
|
261
|
+
});
|
|
262
|
+
if (platform !== "win32") return skipped("windows_only");
|
|
263
|
+
|
|
264
|
+
const exec = io.execFile ?? defaultExecFile;
|
|
265
|
+
const pkg = discoverPackage(exec);
|
|
266
|
+
if (!pkg) return skipped("package_discovery_failed");
|
|
267
|
+
|
|
268
|
+
const processes = listPackageProcesses(exec, pkg.installLocation);
|
|
269
|
+
const roots = rootProcesses(processes);
|
|
270
|
+
if (roots.length === 0) return skipped("no_targets");
|
|
271
|
+
|
|
272
|
+
const ancestryPids = io.ancestryPids ? io.ancestryPids() : windowsAncestryPids(exec);
|
|
273
|
+
if (ancestryPids.length === 0) {
|
|
274
|
+
// Fail closed: an unreadable ancestry chain cannot prove we are outside the
|
|
275
|
+
// tree we are about to terminate.
|
|
276
|
+
return skipped("self_ancestry");
|
|
277
|
+
}
|
|
278
|
+
const ancestry = new Set(ancestryPids);
|
|
279
|
+
if (processes.some(p => ancestry.has(p.pid))) {
|
|
280
|
+
// Terminating our own tree would kill this command mid-flight and leave the
|
|
281
|
+
// user with neither a restarted app nor an explanation.
|
|
282
|
+
return skipped("self_ancestry");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const isAlive = io.isAlive ?? defaultIsAlive;
|
|
286
|
+
const sleep = io.sleep ?? defaultSleep;
|
|
287
|
+
const now = io.now ?? (() => Date.now());
|
|
288
|
+
const stopped: number[] = [];
|
|
289
|
+
const surviving: number[] = [];
|
|
290
|
+
|
|
291
|
+
for (const root of roots) {
|
|
292
|
+
const pid = root.pid;
|
|
293
|
+
// Re-verify immediately before the graceful close: the listing is already
|
|
294
|
+
// one probe old.
|
|
295
|
+
if (!stillSameProcess(exec, pkg.installLocation, root)) {
|
|
296
|
+
stopped.push(pid);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
exec(resolveTrustedWindowsPowerShellExe(), [
|
|
301
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
302
|
+
`$p = Get-Process -Id ${pid} -ErrorAction SilentlyContinue; if ($p) { [void]$p.CloseMainWindow() }`,
|
|
303
|
+
], { timeout: PROBE_TIMEOUT_MS, windowsHide: true });
|
|
304
|
+
} catch {
|
|
305
|
+
/* a refused graceful close still gets the forced pass below */
|
|
306
|
+
}
|
|
307
|
+
if (waitForExit(pid, GRACEFUL_EXIT_TIMEOUT_MS, isAlive, sleep, now)) {
|
|
308
|
+
stopped.push(pid);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
// The wait window is long enough for Windows to recycle a PID, and the next
|
|
312
|
+
// step is `/T /F` against a whole tree. Confirm the PID is still the process
|
|
313
|
+
// we verified, or leave it alone.
|
|
314
|
+
if (!stillSameProcess(exec, pkg.installLocation, root)) {
|
|
315
|
+
stopped.push(pid);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
exec(resolveTrustedWindowsTaskkillExe(), ["/PID", String(pid), "/T", "/F"], {
|
|
320
|
+
timeout: PROBE_TIMEOUT_MS, windowsHide: true,
|
|
321
|
+
});
|
|
322
|
+
} catch {
|
|
323
|
+
/* fall through to the liveness check: the process state decides, not the exit code */
|
|
324
|
+
}
|
|
325
|
+
if (waitForExit(pid, FORCED_EXIT_TIMEOUT_MS, isAlive, sleep, now)) stopped.push(pid);
|
|
326
|
+
else surviving.push(pid);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (surviving.length > 0) {
|
|
330
|
+
return { attempted: true, stopped, surviving, relaunch: "skipped", reason: "targets_survived" };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
exec(resolveTrustedWindowsPowerShellExe(), [
|
|
335
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
336
|
+
`Start-Process 'shell:AppsFolder\\${pkg.aumid}'`,
|
|
337
|
+
], { timeout: PROBE_TIMEOUT_MS, windowsHide: true });
|
|
338
|
+
} catch {
|
|
339
|
+
return { attempted: true, stopped, surviving, relaunch: "skipped", reason: "targets_survived" };
|
|
340
|
+
}
|
|
341
|
+
return { attempted: true, stopped, surviving, relaunch: "started" };
|
|
342
|
+
}
|