@yansigit/opencodex 2.36.1-dev.20260829.35 → 2.36.1-dev.20260829.39
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/gui/dist/assets/{ApiKeys-Dmc3vk4f.js → ApiKeys-BDLkAqHs.js} +1 -1
- package/gui/dist/assets/{Claude-B8nBn_TE.js → Claude-CbTHl2B0.js} +1 -1
- package/gui/dist/assets/{CodexSet-RKnAl3Rr.js → CodexSet-BWMY8up4.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-LBYKB5gf.js → FileIntegrationPage-BFywFnu_.js} +1 -1
- package/gui/dist/assets/{Grok-B6nDl-pq.js → Grok-D7_rD_UK.js} +1 -1
- package/gui/dist/assets/{Integrations-DPcI3mwy.js → Integrations-BM5cNRt8.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-BLgOYwUO.js → IntegrationsOverview-DUL8qhww.js} +1 -1
- package/gui/dist/assets/{Logs-BkM5ic5m.js → Logs-BG9kIs3A.js} +1 -1
- package/gui/dist/assets/Models-BJWzO0Kh.js +1 -0
- package/gui/dist/assets/{NumberStepper-Dsiahf_V.js → NumberStepper-K9tocDBx.js} +1 -1
- package/gui/dist/assets/{Providers-DFBGM2BG.js → Providers-BLweq_Hw.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-0YZ3cmhw.js → RestoreDialog-D9aPE4O0.js} +1 -1
- package/gui/dist/assets/{Startup-BRXwp250.js → Startup-CkcpO3Jv.js} +1 -1
- package/gui/dist/assets/{Storage-D0Ve2fj0.js → Storage-YIqUviJJ.js} +1 -1
- package/gui/dist/assets/Subagents-C1AfLEl9.js +1 -0
- package/gui/dist/assets/{Usage-C0T5SUrf.js → Usage-BerHlEN0.js} +1 -1
- package/gui/dist/assets/codex-stale-banner-D4gvSWnO.js +1 -0
- package/gui/dist/assets/{data-surface-D8RXShV2.js → data-surface-BtO3lwam.js} +1 -1
- package/gui/dist/assets/{data-surface-Bfu-W0pf.js → data-surface-DCct07RT.js} +1 -1
- package/gui/dist/assets/{index-WNYAjf83.js → index-Bs27ITj_.js} +3 -3
- package/gui/dist/assets/{model-display-hWp9VTdv.js → model-display-KCeq505s.js} +1 -1
- package/gui/dist/assets/{provider-payload-F7zzYMeI.js → provider-payload-DHs_ctdK.js} +1 -1
- package/gui/dist/assets/{section-tabs-DEASco68.js → section-tabs-BP7gEPK6.js} +1 -1
- package/gui/dist/assets/shared-BdYGV-yJ.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/codex/app-server-processes.ts +160 -54
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/subagent-defaults.ts +114 -0
- package/src/codex/sync.ts +5 -0
- package/src/generated/compatibility-version.json +13 -13
- package/src/server/effort-policy.ts +43 -12
- package/src/server/index.ts +6 -0
- package/src/server/management/agent-settings-routes.ts +2 -0
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/collaboration.ts +20 -4
- package/src/server/responses/core.ts +6 -4
- package/src/server/ws-bridge.ts +5 -0
- package/src/usage/log.ts +9 -0
- package/gui/dist/assets/Models-uh6UTNK5.js +0 -1
- package/gui/dist/assets/Subagents-TaTl5eEL.js +0 -1
- package/gui/dist/assets/shared-p0R307IX.js +0 -69
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/shared-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-Bs27ITj_.js"></script>
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/shared-BdYGV-yJ.js">
|
|
21
21
|
<link rel="stylesheet" crossorigin href="/assets/index-pLho_LBF.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yansigit/opencodex",
|
|
3
|
-
"version": "2.36.1-dev.20260829.
|
|
3
|
+
"version": "2.36.1-dev.20260829.39",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resolveTrustedWindowsTaskkillExe,
|
|
16
16
|
} from "../lib/windows-elevation";
|
|
17
17
|
import { readCodexCatalogPath } from "./catalog/parsing";
|
|
18
|
+
import { CODEX_CONFIG_PATH } from "./paths";
|
|
18
19
|
|
|
19
20
|
export const STALE_CODEX_APP_SERVER_HINT =
|
|
20
21
|
"If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex). "
|
|
@@ -104,6 +105,10 @@ export interface CodexAppServerProcessIo {
|
|
|
104
105
|
/** Async batch start-time seam used by the request-path Windows collector. */
|
|
105
106
|
readStartMsBatchAsync?: (pids: readonly number[]) => Promise<Map<number, number | null>>;
|
|
106
107
|
catalogMtimeMs?: () => number | null;
|
|
108
|
+
/** Which on-disk write should be compared with app-server start times. */
|
|
109
|
+
freshnessTarget?: "catalog" | "config";
|
|
110
|
+
/** Custom path for the default freshness stat; process-observation identity excludes it. */
|
|
111
|
+
freshnessPath?: string;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
function execFileTextAsync(
|
|
@@ -680,9 +685,12 @@ export interface CodexAppServerCatalogStatus {
|
|
|
680
685
|
}
|
|
681
686
|
|
|
682
687
|
/** Resolve the catalog file Codex app-servers loaded at startup, for staleness checks. */
|
|
683
|
-
function defaultCatalogMtimeMs(
|
|
688
|
+
function defaultCatalogMtimeMs(
|
|
689
|
+
target: "catalog" | "config" = "catalog",
|
|
690
|
+
freshnessPath?: string,
|
|
691
|
+
): number | null {
|
|
684
692
|
try {
|
|
685
|
-
return statSync(readCodexCatalogPath()).mtimeMs;
|
|
693
|
+
return statSync(freshnessPath ?? (target === "config" ? CODEX_CONFIG_PATH : readCodexCatalogPath())).mtimeMs;
|
|
686
694
|
} catch {
|
|
687
695
|
return null;
|
|
688
696
|
}
|
|
@@ -720,23 +728,58 @@ function catalogStatusFromProcesses(
|
|
|
720
728
|
return { state: stale ? "stale" : "fresh", processes: withStarts, catalogMtimeMs };
|
|
721
729
|
}
|
|
722
730
|
|
|
731
|
+
function startsHasUnreadable(starts: ReadonlyMap<number, number | null>): boolean {
|
|
732
|
+
for (const startedAtMs of starts.values()) {
|
|
733
|
+
if (startedAtMs === null) return true;
|
|
734
|
+
}
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
|
|
723
738
|
// Short TTL: process listing + stat run once per window even under per-turn
|
|
724
|
-
// guidance calls (#857).
|
|
725
|
-
|
|
739
|
+
// guidance calls (#857). The process evidence is independent of which on-disk
|
|
740
|
+
// write callers compare it with; only the mtime lookup and final status are
|
|
741
|
+
// target-specific.
|
|
742
|
+
interface ProcessEvidence {
|
|
743
|
+
processes: CodexAppServerProcess[];
|
|
744
|
+
starts: Map<number, number | null>;
|
|
745
|
+
enumerationFailed: boolean;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
interface ProcessEvidenceIdentity {
|
|
749
|
+
platform: NodeJS.Platform;
|
|
750
|
+
getuid?: CodexAppServerProcessIo["getuid"];
|
|
751
|
+
listSnapshots?: CodexAppServerProcessIo["listSnapshots"];
|
|
752
|
+
readStartMs?: CodexAppServerProcessIo["readStartMs"];
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
let processEvidenceCache: {
|
|
756
|
+
atMs: number;
|
|
757
|
+
identity: ProcessEvidenceIdentity;
|
|
758
|
+
evidence: ProcessEvidence;
|
|
759
|
+
ttlState: CodexAppServerCatalogState;
|
|
760
|
+
} | null = null;
|
|
761
|
+
const catalogStatusCache = new Map<"catalog" | "config", {
|
|
762
|
+
evidence: ProcessEvidence;
|
|
763
|
+
status: CodexAppServerCatalogStatus;
|
|
764
|
+
}>();
|
|
726
765
|
interface RequestCatalogStateIdentity {
|
|
727
766
|
platform: NodeJS.Platform;
|
|
728
767
|
listSnapshots?: CodexAppServerProcessIo["listSnapshots"];
|
|
729
768
|
listSnapshotsAsync?: CodexAppServerProcessIo["listSnapshotsAsync"];
|
|
730
769
|
readStartMs?: CodexAppServerProcessIo["readStartMs"];
|
|
731
770
|
readStartMsBatchAsync?: CodexAppServerProcessIo["readStartMsBatchAsync"];
|
|
732
|
-
catalogMtimeMs?: CodexAppServerProcessIo["catalogMtimeMs"];
|
|
733
771
|
now?: CodexAppServerProcessIo["now"];
|
|
734
772
|
}
|
|
735
773
|
|
|
736
774
|
interface RequestCatalogStateFlight {
|
|
737
775
|
generation: number;
|
|
738
776
|
identity: RequestCatalogStateIdentity;
|
|
739
|
-
promise: Promise<
|
|
777
|
+
promise: Promise<RequestCatalogStateReading>;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
interface RequestCatalogStateReading {
|
|
781
|
+
status: CodexAppServerCatalogStatus;
|
|
782
|
+
observationState: CodexAppServerCatalogState;
|
|
740
783
|
}
|
|
741
784
|
|
|
742
785
|
let requestCatalogStateGeneration = 0;
|
|
@@ -745,6 +788,7 @@ let requestCatalogStateCache: {
|
|
|
745
788
|
identity: RequestCatalogStateIdentity;
|
|
746
789
|
atMs: number;
|
|
747
790
|
status: CodexAppServerCatalogStatus;
|
|
791
|
+
observationState: CodexAppServerCatalogState;
|
|
748
792
|
} | null = null;
|
|
749
793
|
let requestCatalogStateFlight: RequestCatalogStateFlight | null = null;
|
|
750
794
|
const CATALOG_STATE_TTL_MS = 5_000;
|
|
@@ -792,10 +836,34 @@ function sameRequestCatalogStateIdentity(
|
|
|
792
836
|
&& left.listSnapshotsAsync === right.listSnapshotsAsync
|
|
793
837
|
&& left.readStartMs === right.readStartMs
|
|
794
838
|
&& left.readStartMsBatchAsync === right.readStartMsBatchAsync
|
|
795
|
-
&& left.catalogMtimeMs === right.catalogMtimeMs
|
|
796
839
|
&& left.now === right.now;
|
|
797
840
|
}
|
|
798
841
|
|
|
842
|
+
function statusForFreshnessTarget(
|
|
843
|
+
status: CodexAppServerCatalogStatus,
|
|
844
|
+
io: CodexAppServerProcessIo,
|
|
845
|
+
): CodexAppServerCatalogStatus {
|
|
846
|
+
if (status.processes.length === 0) return status;
|
|
847
|
+
let catalogMtimeMs: number | null;
|
|
848
|
+
try {
|
|
849
|
+
const target = io.freshnessTarget ?? "catalog";
|
|
850
|
+
catalogMtimeMs = (io.catalogMtimeMs ?? (() => defaultCatalogMtimeMs(target, io.freshnessPath)))();
|
|
851
|
+
} catch {
|
|
852
|
+
catalogMtimeMs = null;
|
|
853
|
+
}
|
|
854
|
+
if (status.catalogMtimeMs === catalogMtimeMs) return status;
|
|
855
|
+
return catalogStatusFromProcesses(
|
|
856
|
+
status.processes.map(proc => ({ pid: proc.pid, commandLine: "" })),
|
|
857
|
+
catalogMtimeMs,
|
|
858
|
+
new Map(status.processes.map(proc => [proc.pid, proc.startedAtMs] as const)),
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function processObservationState(status: CodexAppServerCatalogStatus): CodexAppServerCatalogState {
|
|
863
|
+
if (status.processes.length === 0) return status.state === "unknown" ? "unknown" : "not_running";
|
|
864
|
+
return status.processes.some(process => process.startedAtMs === null) ? "unknown" : "fresh";
|
|
865
|
+
}
|
|
866
|
+
|
|
799
867
|
/**
|
|
800
868
|
* Compare the on-disk catalog mtime against the start time of running Codex
|
|
801
869
|
* app-servers (#857): a server that started before the catalog changed keeps
|
|
@@ -816,15 +884,26 @@ export function collectCodexAppServerCatalogState(
|
|
|
816
884
|
io: CodexAppServerProcessIo = {},
|
|
817
885
|
): CodexAppServerCatalogStatus {
|
|
818
886
|
const now = (io.now ?? Date.now)();
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
887
|
+
const target = io.freshnessTarget ?? "catalog";
|
|
888
|
+
const platform = io.platform ?? process.platform;
|
|
889
|
+
const identity: ProcessEvidenceIdentity = {
|
|
890
|
+
platform,
|
|
891
|
+
getuid: io.getuid,
|
|
892
|
+
listSnapshots: io.listSnapshots,
|
|
893
|
+
readStartMs: io.readStartMs,
|
|
894
|
+
};
|
|
895
|
+
const cachedEvidence = processEvidenceCache
|
|
896
|
+
&& processEvidenceCache.identity.platform === identity.platform
|
|
897
|
+
&& processEvidenceCache.identity.getuid === identity.getuid
|
|
898
|
+
&& processEvidenceCache.identity.listSnapshots === identity.listSnapshots
|
|
899
|
+
&& processEvidenceCache.identity.readStartMs === identity.readStartMs
|
|
900
|
+
&& now - processEvidenceCache.atMs < catalogStateTtlMs(processEvidenceCache.ttlState)
|
|
901
|
+
? processEvidenceCache.evidence
|
|
902
|
+
: null;
|
|
903
|
+
let evidence: ProcessEvidence;
|
|
904
|
+
if (cachedEvidence) {
|
|
905
|
+
evidence = cachedEvidence;
|
|
906
|
+
} else {
|
|
828
907
|
const getuid = io.getuid ?? (() => {
|
|
829
908
|
try {
|
|
830
909
|
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
@@ -841,26 +920,38 @@ export function collectCodexAppServerCatalogState(
|
|
|
841
920
|
// Enumeration failure must never read as "nothing running" — that would let
|
|
842
921
|
// positive model guidance through on guesswork (#857). The injected seam gets
|
|
843
922
|
// the same contract as the default path: whoever enumerates, a failure to read
|
|
844
|
-
// the process list
|
|
923
|
+
// the process list, unknown, not an empty machine.
|
|
845
924
|
snapshots = [];
|
|
846
925
|
enumerationFailed = true;
|
|
847
926
|
}
|
|
848
927
|
const processes = codexAppServerProcessesFromSnapshots(snapshots);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
928
|
+
const starts = processes.length === 0
|
|
929
|
+
? new Map<number, number | null>()
|
|
930
|
+
: io.readStartMs
|
|
931
|
+
? new Map(processes.map(proc => [proc.pid, io.readStartMs!(proc.pid)] as const))
|
|
932
|
+
: readProcessStartMsBatch(processes.map(proc => proc.pid), platform);
|
|
933
|
+
const ttlState: CodexAppServerCatalogState = enumerationFailed || startsHasUnreadable(starts)
|
|
934
|
+
? "unknown"
|
|
935
|
+
: processes.length === 0 ? "not_running" : "fresh";
|
|
936
|
+
evidence = { processes, starts, enumerationFailed };
|
|
937
|
+
processEvidenceCache = { atMs: now, identity, evidence, ttlState };
|
|
938
|
+
}
|
|
939
|
+
if (evidence.processes.length === 0) {
|
|
940
|
+
const status: CodexAppServerCatalogStatus = evidence.enumerationFailed
|
|
941
|
+
? { state: "unknown", processes: [], catalogMtimeMs: null }
|
|
942
|
+
: { state: "not_running", processes: [], catalogMtimeMs: null };
|
|
943
|
+
const prior = catalogStatusCache.get(target);
|
|
944
|
+
if (prior?.evidence === evidence) return prior.status;
|
|
945
|
+
catalogStatusCache.set(target, { evidence, status });
|
|
946
|
+
return status;
|
|
863
947
|
}
|
|
948
|
+
const catalogMtimeMs = (io.catalogMtimeMs ?? (() => defaultCatalogMtimeMs(target, io.freshnessPath)))();
|
|
949
|
+
const status = catalogStatusFromProcesses(evidence.processes, catalogMtimeMs, evidence.starts);
|
|
950
|
+
const prior = catalogStatusCache.get(target);
|
|
951
|
+
if (prior?.evidence === evidence && prior.status.catalogMtimeMs === status.catalogMtimeMs) {
|
|
952
|
+
return prior.status;
|
|
953
|
+
}
|
|
954
|
+
catalogStatusCache.set(target, { evidence, status });
|
|
864
955
|
return status;
|
|
865
956
|
}
|
|
866
957
|
|
|
@@ -897,7 +988,6 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
897
988
|
listSnapshotsAsync: io.listSnapshotsAsync,
|
|
898
989
|
readStartMs: io.readStartMs,
|
|
899
990
|
readStartMsBatchAsync: io.readStartMsBatchAsync,
|
|
900
|
-
catalogMtimeMs: io.catalogMtimeMs,
|
|
901
991
|
now: io.now,
|
|
902
992
|
};
|
|
903
993
|
const cached = requestCatalogStateCache
|
|
@@ -905,8 +995,8 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
905
995
|
&& sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity)
|
|
906
996
|
? requestCatalogStateCache
|
|
907
997
|
: null;
|
|
908
|
-
if (cached && now - cached.atMs < catalogStateTtlMs(cached.
|
|
909
|
-
return cached.status;
|
|
998
|
+
if (cached && now - cached.atMs < catalogStateTtlMs(cached.observationState)) {
|
|
999
|
+
return statusForFreshnessTarget(cached.status, io);
|
|
910
1000
|
}
|
|
911
1001
|
// An expired real reading is still worth handing back while the refresh runs. It
|
|
912
1002
|
// cannot have been invalidated by an ocx catalog write: every such write calls
|
|
@@ -915,18 +1005,20 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
915
1005
|
// What it can miss is an app-server that started or stopped meanwhile -- and a
|
|
916
1006
|
// server started after the reading is newer than the catalog, which is the `fresh`
|
|
917
1007
|
// this entry already says.
|
|
1008
|
+
const targetStatus = cached ? statusForFreshnessTarget(cached.status, io) : null;
|
|
918
1009
|
const servableStale = cached
|
|
919
|
-
&& cached.
|
|
920
|
-
&&
|
|
921
|
-
|
|
1010
|
+
&& cached.observationState !== "unknown"
|
|
1011
|
+
&& targetStatus?.state !== "unknown"
|
|
1012
|
+
&& now - cached.atMs < catalogStateTtlMs(cached.observationState) + CATALOG_STATE_MAX_STALE_MS
|
|
1013
|
+
? targetStatus
|
|
922
1014
|
: null;
|
|
923
1015
|
if (requestCatalogStateFlight
|
|
924
1016
|
&& requestCatalogStateFlight.generation === generation
|
|
925
1017
|
&& sameRequestCatalogStateIdentity(requestCatalogStateFlight.identity, identity)) {
|
|
926
|
-
return servableStale ?? requestCatalogStateFlight.promise;
|
|
1018
|
+
return servableStale ?? requestCatalogStateFlight.promise.then(reading => statusForFreshnessTarget(reading.status, io));
|
|
927
1019
|
}
|
|
928
1020
|
|
|
929
|
-
const refresh = async (): Promise<
|
|
1021
|
+
const refresh = async (): Promise<RequestCatalogStateReading> => {
|
|
930
1022
|
let snapshots: ProcessSnapshot[];
|
|
931
1023
|
try {
|
|
932
1024
|
snapshots = io.listSnapshotsAsync
|
|
@@ -935,15 +1027,21 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
935
1027
|
? io.listSnapshots()
|
|
936
1028
|
: await listWindowsSnapshotsAsync();
|
|
937
1029
|
} catch {
|
|
938
|
-
return {
|
|
1030
|
+
return {
|
|
1031
|
+
status: { state: "unknown", processes: [], catalogMtimeMs: null },
|
|
1032
|
+
observationState: "unknown",
|
|
1033
|
+
};
|
|
939
1034
|
}
|
|
940
1035
|
const processes = codexAppServerProcessesFromSnapshots(snapshots);
|
|
941
1036
|
if (processes.length === 0) {
|
|
942
|
-
return {
|
|
1037
|
+
return {
|
|
1038
|
+
status: { state: "not_running", processes: [], catalogMtimeMs: null },
|
|
1039
|
+
observationState: "not_running",
|
|
1040
|
+
};
|
|
943
1041
|
}
|
|
944
1042
|
let catalogMtimeMs: number | null;
|
|
945
1043
|
try {
|
|
946
|
-
catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
|
|
1044
|
+
catalogMtimeMs = (io.catalogMtimeMs ?? (() => defaultCatalogMtimeMs(io.freshnessTarget, io.freshnessPath)))();
|
|
947
1045
|
} catch {
|
|
948
1046
|
catalogMtimeMs = null;
|
|
949
1047
|
}
|
|
@@ -953,16 +1051,19 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
953
1051
|
: io.readStartMs
|
|
954
1052
|
? new Map(pids.map(pid => [pid, io.readStartMs!(pid)] as const))
|
|
955
1053
|
: await readWindowsProcessStartMsBatchAsync(pids);
|
|
956
|
-
|
|
1054
|
+
const status = catalogStatusFromProcesses(processes, catalogMtimeMs, starts);
|
|
1055
|
+
return {
|
|
1056
|
+
status,
|
|
1057
|
+
observationState: processObservationState(status),
|
|
1058
|
+
};
|
|
957
1059
|
};
|
|
958
1060
|
|
|
959
1061
|
const pending = refresh().catch(() => ({
|
|
960
|
-
state: "unknown" as const,
|
|
961
|
-
|
|
962
|
-
catalogMtimeMs: null,
|
|
1062
|
+
status: { state: "unknown" as const, processes: [], catalogMtimeMs: null },
|
|
1063
|
+
observationState: "unknown" as const,
|
|
963
1064
|
}));
|
|
964
1065
|
let flight: RequestCatalogStateFlight;
|
|
965
|
-
const promise = pending.then(
|
|
1066
|
+
const promise = pending.then(reading => {
|
|
966
1067
|
// A catalog write can invalidate while slow CIM is still running. The result
|
|
967
1068
|
// describes the PRE-write world, so it must neither repopulate the post-write
|
|
968
1069
|
// cache nor reach the caller.
|
|
@@ -979,7 +1080,10 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
979
1080
|
// invalidated observation knows, and the guidance path already treats it as
|
|
980
1081
|
// "say nothing positive".
|
|
981
1082
|
if (requestCatalogStateGeneration !== generation) {
|
|
982
|
-
return {
|
|
1083
|
+
return {
|
|
1084
|
+
status: { state: "unknown" as const, processes: [], catalogMtimeMs: null },
|
|
1085
|
+
observationState: "unknown" as const,
|
|
1086
|
+
};
|
|
983
1087
|
}
|
|
984
1088
|
// A refresh that failed must not evict a real reading. Before this function
|
|
985
1089
|
// served stale entries, caching `unknown` cost at most the 250ms that state
|
|
@@ -988,19 +1092,20 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
988
1092
|
// them on a transient failure -- `unknown` is not servable, so the next
|
|
989
1093
|
// caller waits for a probe instead of getting the reading it would have had.
|
|
990
1094
|
// Keep the observation and let its own age retire it.
|
|
991
|
-
const wouldEvictAnObservation =
|
|
1095
|
+
const wouldEvictAnObservation = reading.observationState === "unknown"
|
|
992
1096
|
&& requestCatalogStateCache?.generation === generation
|
|
993
|
-
&& requestCatalogStateCache.
|
|
1097
|
+
&& requestCatalogStateCache.observationState !== "unknown"
|
|
994
1098
|
&& sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity);
|
|
995
1099
|
if (requestCatalogStateFlight === flight && !wouldEvictAnObservation) {
|
|
996
1100
|
requestCatalogStateCache = {
|
|
997
1101
|
generation,
|
|
998
1102
|
identity,
|
|
999
1103
|
atMs: (io.now ?? Date.now)(),
|
|
1000
|
-
status,
|
|
1104
|
+
status: reading.status,
|
|
1105
|
+
observationState: reading.observationState,
|
|
1001
1106
|
};
|
|
1002
1107
|
}
|
|
1003
|
-
return
|
|
1108
|
+
return reading;
|
|
1004
1109
|
}).finally(() => {
|
|
1005
1110
|
if (requestCatalogStateFlight === flight) requestCatalogStateFlight = null;
|
|
1006
1111
|
});
|
|
@@ -1008,7 +1113,7 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
1008
1113
|
requestCatalogStateFlight = flight;
|
|
1009
1114
|
// `promise` already absorbs its own failures, so leaving it unawaited here cannot
|
|
1010
1115
|
// surface as an unhandled rejection; the next caller picks up whatever it stored.
|
|
1011
|
-
return servableStale ?? flight.promise;
|
|
1116
|
+
return servableStale ?? flight.promise.then(reading => statusForFreshnessTarget(reading.status, io));
|
|
1012
1117
|
}
|
|
1013
1118
|
|
|
1014
1119
|
/**
|
|
@@ -1018,7 +1123,8 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
1018
1123
|
* write has completed.
|
|
1019
1124
|
*/
|
|
1020
1125
|
export function resetCodexAppServerCatalogStateCache(): void {
|
|
1021
|
-
|
|
1126
|
+
processEvidenceCache = null;
|
|
1127
|
+
catalogStatusCache.clear();
|
|
1022
1128
|
requestCatalogStateGeneration += 1;
|
|
1023
1129
|
requestCatalogStateCache = null;
|
|
1024
1130
|
requestCatalogStateFlight = null;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
forgetEphemeralSecretPath,
|
|
17
17
|
hardenSecretPath,
|
|
18
18
|
} from "../../lib/windows-secret-acl";
|
|
19
|
+
import { resetCodexAppServerCatalogStateCache } from "../app-server-processes";
|
|
19
20
|
|
|
20
21
|
export interface PreparedCatalogFileWrite {
|
|
21
22
|
readonly path: string;
|
|
@@ -167,6 +168,7 @@ export function replaceActiveCodexCatalog(
|
|
|
167
168
|
): void {
|
|
168
169
|
assertCatalogWritePermit(permit, owningCodexHome);
|
|
169
170
|
atomicWriteFile(prepared.path, prepared.content, io);
|
|
171
|
+
resetCodexAppServerCatalogStateCache();
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
/** Atomically publish the catalog-path-keyed immutable backup without clobbering. */
|
|
@@ -200,4 +202,5 @@ export function replaceCodexModelsCache(
|
|
|
200
202
|
): void {
|
|
201
203
|
assertCatalogWritePermit(permit, owningCodexHome);
|
|
202
204
|
atomicWriteFile(prepared.path, prepared.content, io);
|
|
205
|
+
resetCodexAppServerCatalogStateCache();
|
|
203
206
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { CODEX_CONFIG_PATH } from "./paths";
|
|
3
|
+
import { hasInjectedCodexRouting } from "./injected-marker";
|
|
4
|
+
import { resolveEffectiveProjectModelProvider } from "./project-config-warnings";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Pure, ownership-aware edits for Codex's native `[agents]` defaults.
|
|
3
8
|
*
|
|
@@ -19,6 +24,17 @@ export interface ManagedSubagentDefaults {
|
|
|
19
24
|
reasoningEffort?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
export type NativeDefaultState = "active" | "disabled" | "pending" | "blocked";
|
|
28
|
+
|
|
29
|
+
export interface NativeDefaultStateDeps {
|
|
30
|
+
configPath?: string;
|
|
31
|
+
readConfig?: () => string;
|
|
32
|
+
processIo?: import("./app-server-processes").CodexAppServerProcessIo;
|
|
33
|
+
collectCatalogState?: () => {
|
|
34
|
+
state: "fresh" | "stale" | "not_running" | "unknown";
|
|
35
|
+
} | Promise<{ state: "fresh" | "stale" | "not_running" | "unknown" }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
export interface ManagedSubagentDefaultsConflict {
|
|
23
39
|
key: ManagedSubagentDefaultKey;
|
|
24
40
|
line: number;
|
|
@@ -365,6 +381,104 @@ function analyzeToml(lines: readonly SourceLine[]): { shape: TomlShape } | { err
|
|
|
365
381
|
};
|
|
366
382
|
}
|
|
367
383
|
|
|
384
|
+
interface ManagedSubagentDefaultsInspection {
|
|
385
|
+
values: Partial<ManagedSubagentDefaults>;
|
|
386
|
+
owned: Record<ManagedSubagentDefaultKey, boolean>;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Read the effective native defaults without changing the TOML document. */
|
|
390
|
+
function inspectManagedSubagentDefaults(content: string):
|
|
391
|
+
| { ok: true; inspection: ManagedSubagentDefaultsInspection }
|
|
392
|
+
| { ok: false; error: string } {
|
|
393
|
+
const analysis = analyzeToml(splitSourceLines(content));
|
|
394
|
+
if ("error" in analysis) return { ok: false, error: analysis.error };
|
|
395
|
+
let parsed: unknown;
|
|
396
|
+
try {
|
|
397
|
+
parsed = Bun.TOML.parse(content);
|
|
398
|
+
} catch {
|
|
399
|
+
return { ok: false, error: "Codex config is not valid TOML" };
|
|
400
|
+
}
|
|
401
|
+
const root = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
402
|
+
? parsed as Record<string, unknown>
|
|
403
|
+
: null;
|
|
404
|
+
const agents = root?.agents && typeof root.agents === "object" && !Array.isArray(root.agents)
|
|
405
|
+
? root.agents as Record<string, unknown>
|
|
406
|
+
: {};
|
|
407
|
+
const values: Partial<ManagedSubagentDefaults> = {};
|
|
408
|
+
const owned = {} as Record<ManagedSubagentDefaultKey, boolean>;
|
|
409
|
+
for (const key of TARGET_KEYS) {
|
|
410
|
+
const definition = analysis.shape.definitions.get(key);
|
|
411
|
+
owned[key] = definition?.owned === true;
|
|
412
|
+
if (!definition) continue;
|
|
413
|
+
const value = agents[key];
|
|
414
|
+
if (typeof value !== "string") return { ok: false, error: `agents.${key} is not a string` };
|
|
415
|
+
if (key === "default_subagent_model") values.model = value;
|
|
416
|
+
else values.reasoningEffort = value;
|
|
417
|
+
}
|
|
418
|
+
return { ok: true, inspection: { values, owned } };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Resolve whether the configured preference is actually authoritative for omitted-model Codex
|
|
423
|
+
* subagents. This is intentionally read-only; sync/start remains the writer of native defaults.
|
|
424
|
+
*/
|
|
425
|
+
export async function resolveNativeDefaultState(
|
|
426
|
+
config: { syncCodexSubagentDefaults?: boolean; injectionModel?: string; injectionEffort?: string },
|
|
427
|
+
deps: NativeDefaultStateDeps = {},
|
|
428
|
+
): Promise<NativeDefaultState> {
|
|
429
|
+
const model = config.injectionModel?.trim();
|
|
430
|
+
const enabled = config.syncCodexSubagentDefaults === true && Boolean(model);
|
|
431
|
+
if (!enabled) return "disabled";
|
|
432
|
+
|
|
433
|
+
const configPath = deps.configPath ?? CODEX_CONFIG_PATH;
|
|
434
|
+
let content: string;
|
|
435
|
+
try {
|
|
436
|
+
content = (deps.readConfig ?? (() => readFileSync(configPath, "utf8")))();
|
|
437
|
+
} catch (error) {
|
|
438
|
+
// A config that has not been created yet is waiting for the next sync; other read failures
|
|
439
|
+
// cannot be safely diagnosed as a missing desired value.
|
|
440
|
+
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT" ? "pending" : "blocked";
|
|
441
|
+
}
|
|
442
|
+
const inspected = inspectManagedSubagentDefaults(content);
|
|
443
|
+
if (!inspected.ok) return "blocked";
|
|
444
|
+
|
|
445
|
+
let parsed: Record<string, unknown>;
|
|
446
|
+
try {
|
|
447
|
+
parsed = Bun.TOML.parse(content) as Record<string, unknown>;
|
|
448
|
+
} catch {
|
|
449
|
+
return "blocked";
|
|
450
|
+
}
|
|
451
|
+
const provider = resolveEffectiveProjectModelProvider(content).provider;
|
|
452
|
+
if (provider && provider !== "openai" && provider !== "opencodex") return "blocked";
|
|
453
|
+
const baseUrl = parsed.openai_base_url;
|
|
454
|
+
if (typeof baseUrl === "string" && baseUrl.trim() && !hasInjectedCodexRouting(content)) return "blocked";
|
|
455
|
+
|
|
456
|
+
const { values, owned } = inspected.inspection;
|
|
457
|
+
for (const key of TARGET_KEYS) {
|
|
458
|
+
if (owned[key] === false && Object.hasOwn(values, key === "default_subagent_model" ? "model" : "reasoningEffort")) {
|
|
459
|
+
return "blocked";
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (values.model !== model) return "pending";
|
|
463
|
+
const effort = config.injectionEffort?.trim() || undefined;
|
|
464
|
+
if (values.reasoningEffort !== effort) return "pending";
|
|
465
|
+
|
|
466
|
+
let catalogState: { state: "fresh" | "stale" | "not_running" | "unknown" };
|
|
467
|
+
try {
|
|
468
|
+
catalogState = await (deps.collectCatalogState ?? (async () => {
|
|
469
|
+
const { collectCodexAppServerCatalogStateForRequest } = await import("./app-server-processes");
|
|
470
|
+
return collectCodexAppServerCatalogStateForRequest({
|
|
471
|
+
...deps.processIo,
|
|
472
|
+
freshnessPath: configPath,
|
|
473
|
+
freshnessTarget: "config",
|
|
474
|
+
});
|
|
475
|
+
}))();
|
|
476
|
+
} catch {
|
|
477
|
+
return "pending";
|
|
478
|
+
}
|
|
479
|
+
return catalogState.state === "fresh" || catalogState.state === "not_running" ? "active" : "pending";
|
|
480
|
+
}
|
|
481
|
+
|
|
368
482
|
function quotedTomlString(value: string): string {
|
|
369
483
|
// JSON basic strings are valid TOML basic strings and cover quotes,
|
|
370
484
|
// backslashes, control characters, and newlines. JSON leaves DEL literal,
|
package/src/codex/sync.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { summarizeComboCatalogOmissions, type ComboCatalogOmission } from "./cat
|
|
|
8
8
|
import { shouldSyncCodexOnStart } from "./desired-state";
|
|
9
9
|
import { admitCodexWrite, type CodexAdmission } from "./admission";
|
|
10
10
|
import type { CodexCatalogSyncOptions } from "./catalog/sync";
|
|
11
|
+
import { resetCodexAppServerCatalogStateCache } from "./app-server-processes";
|
|
11
12
|
|
|
12
13
|
export interface CodexSyncResult {
|
|
13
14
|
/**
|
|
@@ -116,6 +117,10 @@ export async function syncModelsToCodex(
|
|
|
116
117
|
message: admission.message,
|
|
117
118
|
};
|
|
118
119
|
}
|
|
120
|
+
// Config injection is a relevant Codex write even when the catalog bytes are unchanged.
|
|
121
|
+
// Drop cached process evidence before async discovery so a process that appeared since the
|
|
122
|
+
// last read cannot make native-default guidance report active after this sync.
|
|
123
|
+
resetCodexAppServerCatalogStateCache();
|
|
119
124
|
const p = port ?? config.port ?? 10100;
|
|
120
125
|
const externalProvider = (deps.currentExternalCodexModelProvider ?? currentExternalCodexModelProvider)();
|
|
121
126
|
|