mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
feat(codex): name additional session-catalog homes (#124807)
This commit is contained in:
committed by
GitHub
parent
8b9650d42a
commit
2523943ef1
@@ -85,7 +85,10 @@ outside OpenClaw:
|
||||
enabled: true,
|
||||
config: {
|
||||
sessionCatalog: {
|
||||
homes: ["/path/to/additional-codex-home"],
|
||||
homes: [
|
||||
"/path/to/additional-codex-home",
|
||||
{ path: "/path/to/review-codex-home", label: "Review sessions" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -95,10 +98,12 @@ outside OpenClaw:
|
||||
```
|
||||
|
||||
Configured stores appear in the sidebar alongside automatically discovered
|
||||
ones, labeled `Local Codex · configured <n>` and grouped by each session's
|
||||
working directory. Sessions in these stores support the same view, continue,
|
||||
and archive actions, and the selected OpenClaw agent still owns the resulting
|
||||
connection; `homes` only adds catalog sources.
|
||||
ones, labeled `Local Codex · <label>` and grouped by each session's working
|
||||
directory. String entries and objects without `label` use the basename of the
|
||||
canonicalized home directory; an explicit `label` overrides that default.
|
||||
Sessions in these stores support the same view, continue, and archive actions,
|
||||
and the selected OpenClaw agent still owns the resulting connection; `homes`
|
||||
only adds catalog sources.
|
||||
|
||||
Only existing directories are included. Equivalent paths are canonicalized and
|
||||
deduplicated against the automatic homes, and automatic homes keep priority
|
||||
|
||||
@@ -84,7 +84,20 @@
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"homes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1, "pattern": "\\S" }
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "minLength": 1, "pattern": "\\S" },
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["path"],
|
||||
"properties": {
|
||||
"path": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
||||
"label": { "type": "string", "minLength": 1, "pattern": "\\S" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -406,7 +419,7 @@
|
||||
},
|
||||
"sessionCatalog.homes": {
|
||||
"label": "Additional Codex Homes",
|
||||
"help": "Existing local Codex home directories to include in native session discovery. Paths are canonicalized and deduplicated. Requires appServer.transport=stdio and a Gateway restart.",
|
||||
"help": "Existing local Codex home directories to include in native session discovery. String paths use the canonical directory basename as their label; object entries can override it with label. Paths are canonicalized and deduplicated. Requires appServer.transport=stdio and a Gateway restart.",
|
||||
"advanced": true
|
||||
},
|
||||
"codexDynamicToolsLoading": {
|
||||
|
||||
@@ -367,13 +367,29 @@ describe("Codex app-server config", () => {
|
||||
).toStrictEqual({});
|
||||
});
|
||||
|
||||
it("parses native session discovery options", () => {
|
||||
it("parses named native session discovery homes and rejects empty labels", () => {
|
||||
expect(
|
||||
readCodexPluginConfig({
|
||||
sessionCatalog: { enabled: false, homes: [" /srv/codex-extra "] },
|
||||
sessionCatalog: {
|
||||
enabled: false,
|
||||
homes: [
|
||||
" /srv/codex-string ",
|
||||
{ path: " /srv/codex-named ", label: " Named store " },
|
||||
{ path: " /srv/codex-default " },
|
||||
],
|
||||
},
|
||||
}).sessionCatalog,
|
||||
).toEqual({ enabled: false, homes: ["/srv/codex-extra"] });
|
||||
expect(readCodexPluginConfig({ sessionCatalog: { homes: [" "] } })).toStrictEqual({});
|
||||
).toEqual({
|
||||
enabled: false,
|
||||
homes: [
|
||||
"/srv/codex-string",
|
||||
{ path: "/srv/codex-named", label: "Named store" },
|
||||
{ path: "/srv/codex-default" },
|
||||
],
|
||||
});
|
||||
expect(
|
||||
readCodexPluginConfig({ sessionCatalog: { homes: [{ path: "/srv/codex", label: " " }] } }),
|
||||
).toStrictEqual({});
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const codexSessionCatalogHomeSchema = z.union([
|
||||
z.string().trim().min(1),
|
||||
z.object({ path: z.string().trim().min(1), label: z.string().trim().min(1).optional() }).strict(),
|
||||
]);
|
||||
|
||||
export const codexSessionCatalogConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
homes: z.array(z.string().trim().min(1)).optional(),
|
||||
homes: z.array(codexSessionCatalogHomeSchema).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export type { CodexCatalogHome } from "./session-catalog-types.js";
|
||||
type CatalogHomeCandidate = {
|
||||
codexHome: string;
|
||||
label: string;
|
||||
usesProcessHomeFallback: boolean;
|
||||
usesProcessHomeFallback?: boolean;
|
||||
};
|
||||
|
||||
function canonicalCatalogHome(value: string): string {
|
||||
@@ -36,13 +36,16 @@ function canonicalCatalogHome(value: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function existingCanonicalCatalogHome(value: string): string | undefined {
|
||||
function existingCatalogHomeCandidates(value: string, label?: string): CatalogHomeCandidate[] {
|
||||
const codexHome = canonicalCatalogHome(value);
|
||||
try {
|
||||
const codexHome = fs.realpathSync.native(path.resolve(value));
|
||||
return fs.statSync(codexHome).isDirectory() ? codexHome : undefined;
|
||||
if (!fs.statSync(codexHome).isDirectory()) {
|
||||
return [];
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
return [];
|
||||
}
|
||||
return [{ codexHome, label: `Local Codex · ${label ?? path.basename(codexHome)}` }];
|
||||
}
|
||||
|
||||
function catalogHomeId(codexHome: string): string {
|
||||
@@ -93,27 +96,15 @@ function resolveCodexCatalogHomes(params: {
|
||||
left === ownerAgentId ? -1 : right === ownerAgentId ? 1 : left.localeCompare(right),
|
||||
);
|
||||
candidates.push(
|
||||
...agentIds.flatMap((agentId) => {
|
||||
const codexHome = canonicalCatalogHome(
|
||||
...agentIds.flatMap((agentId) =>
|
||||
existingCatalogHomeCandidates(
|
||||
resolveCodexAppServerHomeDir(resolveAgentDir(config, agentId, env)),
|
||||
);
|
||||
return fs.existsSync(codexHome)
|
||||
? [{ codexHome, label: `Local Codex · ${agentId}`, usesProcessHomeFallback: false }]
|
||||
: [];
|
||||
}),
|
||||
);
|
||||
candidates.push(
|
||||
...configuredHomes.flatMap((value, index) => {
|
||||
const codexHome = existingCanonicalCatalogHome(value);
|
||||
return codexHome
|
||||
? [
|
||||
{
|
||||
codexHome,
|
||||
label: `Local Codex · configured ${index + 1}`,
|
||||
usesProcessHomeFallback: false,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
agentId,
|
||||
),
|
||||
),
|
||||
...configuredHomes.flatMap((entry) => {
|
||||
const { path: home, label } = typeof entry === "string" ? { path: entry } : entry;
|
||||
return existingCatalogHomeCandidates(home, label);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -144,7 +135,7 @@ function resolveCodexCatalogHomes(params: {
|
||||
env: { ...base.start.env, CODEX_HOME: candidate.codexHome },
|
||||
},
|
||||
},
|
||||
usesProcessHomeFallback: candidate.usesProcessHomeFallback,
|
||||
usesProcessHomeFallback: candidate.usesProcessHomeFallback ?? false,
|
||||
});
|
||||
if (homes.length >= MAX_HOST_COUNT) {
|
||||
break;
|
||||
|
||||
@@ -293,9 +293,11 @@ describe("Codex supervision catalog", () => {
|
||||
tempDirs.push(root);
|
||||
const alphaAgentDir = path.join(root, "agents", "alpha", "agent");
|
||||
const betaAgentDir = path.join(root, "agents", "beta", "agent");
|
||||
const fileAgentDir = path.join(root, "agents", "file", "agent");
|
||||
const processCodexHome = path.join(root, "process-codex-home");
|
||||
const alphaCodexHome = resolveCodexAppServerHomeDir(alphaAgentDir);
|
||||
const betaCodexHome = resolveCodexAppServerHomeDir(betaAgentDir);
|
||||
const fileAgentCodexHome = resolveCodexAppServerHomeDir(fileAgentDir);
|
||||
const configuredCodexHomes = Array.from({ length: MAX_HOST_COUNT }, (_, index) =>
|
||||
path.join(root, "configured-codex-home", String(index)),
|
||||
);
|
||||
@@ -310,6 +312,9 @@ describe("Codex supervision catalog", () => {
|
||||
await Promise.all([
|
||||
fs.symlink(configuredCodexHome, configuredCodexHomeAlias, "dir"),
|
||||
fs.writeFile(configuredFile, "not a directory"),
|
||||
fs
|
||||
.mkdir(fileAgentDir, { recursive: true })
|
||||
.then(() => fs.writeFile(fileAgentCodexHome, "not a directory")),
|
||||
]);
|
||||
const runtimeConfig = {
|
||||
agents: {
|
||||
@@ -317,6 +322,7 @@ describe("Codex supervision catalog", () => {
|
||||
list: [
|
||||
{ id: "alpha", agentDir: alphaAgentDir },
|
||||
{ id: "beta", agentDir: betaAgentDir },
|
||||
{ id: "file", agentDir: fileAgentDir },
|
||||
],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
@@ -331,11 +337,13 @@ describe("Codex supervision catalog", () => {
|
||||
sessionCatalog: {
|
||||
homes: [
|
||||
configuredCodexHome,
|
||||
configuredCodexHomeAlias,
|
||||
{ path: configuredCodexHomeAlias, label: "Duplicate alias" },
|
||||
{ path: configuredCodexHomes[1]!, label: "Named store" },
|
||||
{ path: configuredCodexHomes[2]! },
|
||||
alphaCodexHome,
|
||||
path.join(root, "missing-codex-home"),
|
||||
configuredFile,
|
||||
...configuredCodexHomes.slice(1),
|
||||
...configuredCodexHomes.slice(3),
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -350,6 +358,11 @@ describe("Codex supervision catalog", () => {
|
||||
expect(resolvedHomes.filter((home) => configuredCodexHomes.includes(home))).toHaveLength(
|
||||
MAX_HOST_COUNT - 3,
|
||||
);
|
||||
expect(homes.slice(3, 6).map((home) => home.label)).toEqual([
|
||||
"Local Codex · 0",
|
||||
"Local Codex · Named store",
|
||||
"Local Codex · 2",
|
||||
]);
|
||||
expect(homes.map((home) => home.agentDir)).toEqual(Array(MAX_HOST_COUNT).fill(betaAgentDir));
|
||||
expect(homes[0]?.hostId).toBe(CODEX_LOCAL_SESSION_HOST_ID);
|
||||
expect(homes.slice(1).every((home) => home.hostId.startsWith("gateway:local:"))).toBe(true);
|
||||
@@ -423,7 +436,7 @@ describe("Codex supervision catalog", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
let runtimeConfig = configA;
|
||||
const existsSync = vi.spyOn(fsSync, "existsSync");
|
||||
const statSync = vi.spyOn(fsSync, "statSync");
|
||||
try {
|
||||
const resolver = createCodexCatalogHomeResolver({
|
||||
config: configA,
|
||||
@@ -431,11 +444,11 @@ describe("Codex supervision catalog", () => {
|
||||
getPluginConfig: () => ({ supervision: { enabled: true } }),
|
||||
env: { ...process.env, CODEX_HOME: processCodexHome },
|
||||
});
|
||||
const seedDiscoveryCount = existsSync.mock.calls.length;
|
||||
const seedDiscoveryCount = statSync.mock.calls.length;
|
||||
|
||||
expect(resolver.forAgent("alpha")).not.toHaveLength(0);
|
||||
expect(resolver.forAgent("alpha")).not.toHaveLength(0);
|
||||
expect(existsSync).toHaveBeenCalledTimes(seedDiscoveryCount);
|
||||
expect(statSync).toHaveBeenCalledTimes(seedDiscoveryCount);
|
||||
|
||||
runtimeConfig = configB;
|
||||
const betaHomes = resolver.forAgent("beta");
|
||||
@@ -446,12 +459,12 @@ describe("Codex supervision catalog", () => {
|
||||
betaCodexHome,
|
||||
),
|
||||
).toBe(true);
|
||||
const reloadedDiscoveryCount = existsSync.mock.calls.length;
|
||||
const reloadedDiscoveryCount = statSync.mock.calls.length;
|
||||
|
||||
expect(resolver.forAgent("beta")).toEqual(betaHomes);
|
||||
expect(existsSync).toHaveBeenCalledTimes(reloadedDiscoveryCount);
|
||||
expect(statSync).toHaveBeenCalledTimes(reloadedDiscoveryCount);
|
||||
} finally {
|
||||
existsSync.mockRestore();
|
||||
statSync.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user