Files
openclaw/extensions/codex/src/app-server/request.ts
Sarah Fortune 48529f1a96 feat(onboard): offer codex migration after harness install (#81192)
Add a post-install seam so the wizard can prompt the user to import their
existing Codex CLI state (skills, archived config/hooks, advisory cached
plugins) through the existing `openclaw migrate codex` flow once the
harness plugin is in place. Fires on both fresh installs and repair runs;
the user can decline at any time.

Trigger sites, both routing through one helper:

- src/plugins/provider-auth-choice.ts: after
  `ensureCodexRuntimePluginForModelSelection` reports `installed: true`,
  dynamically import `offerPostInstallMigrations` and call it before the
  wizard moves on.
- src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts:
  same call shape with `nonInteractive: true`, so the helper emits a hint
  line only and never mutates state.

Helper (src/wizard/setup.post-install-migration.ts) is generic, not
Codex-hardcoded — it resolves migration providers via the manifest
`migrationProviders` contract, filters to providers owned by plugins the
caller flags as installed in this onboarding step, runs `provider.detect`,
and on TTY hands accepted runs to `migrateDefaultCommand`. All detect,
prompt, and migrate failures are swallowed so onboarding never aborts on
this optional offer.

Also harden the Codex app-server subprocess lifecycle now that `detect()`
runs from a hotter onboarding path: isolate the plugin-install
`plugin/read` call (extensions/codex/src/migration/apply.ts) and have the
isolated request wait for child exit with a SIGKILL fallback
(extensions/codex/src/app-server/request.ts) so parents are not held open
by an orphaned codex binary.

Tests:

- src/wizard/setup.post-install-migration.test.ts (new, 10 cases)
- src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts
  extended with hint-call assertions and a not-required-no-offer case.
2026-05-12 16:51:27 -07:00

74 lines
2.7 KiB
TypeScript

import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
import type { CodexAppServerStartOptions } from "./config.js";
import type {
CodexAppServerRequestMethod,
CodexAppServerRequestParams,
CodexAppServerRequestResult,
JsonValue,
} from "./protocol.js";
import {
createIsolatedCodexAppServerClient,
getSharedCodexAppServerClient,
} from "./shared-client.js";
import { withTimeout } from "./timeout.js";
export async function requestCodexAppServerJson<M extends CodexAppServerRequestMethod>(params: {
method: M;
requestParams: CodexAppServerRequestParams<M>;
timeoutMs?: number;
startOptions?: CodexAppServerStartOptions;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
isolated?: boolean;
}): Promise<CodexAppServerRequestResult<M>>;
export async function requestCodexAppServerJson<T = JsonValue | undefined>(params: {
method: string;
requestParams?: unknown;
timeoutMs?: number;
startOptions?: CodexAppServerStartOptions;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
isolated?: boolean;
}): Promise<T>;
export async function requestCodexAppServerJson<T = JsonValue | undefined>(params: {
method: string;
requestParams?: unknown;
timeoutMs?: number;
startOptions?: CodexAppServerStartOptions;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
isolated?: boolean;
}): Promise<T> {
const timeoutMs = params.timeoutMs ?? 60_000;
return await withTimeout(
(async () => {
const client = await (
params.isolated ? createIsolatedCodexAppServerClient : getSharedCodexAppServerClient
)({
startOptions: params.startOptions,
timeoutMs,
authProfileId: params.authProfileId,
agentDir: params.agentDir,
config: params.config,
});
try {
return await client.request<T>(params.method, params.requestParams, { timeoutMs });
} finally {
if (params.isolated) {
// Wait for the child to actually exit (with a SIGKILL fallback) so
// the parent process doesn't hang on an orphaned codex app-server.
// The stdio bin shim does not always propagate stdin EOF to the
// underlying codex binary, so the unref'd close() path can leave
// the child running and keep the parent's event loop alive.
await client.closeAndWait({ exitTimeoutMs: 2_000, forceKillDelayMs: 250 });
}
}
})(),
timeoutMs,
`codex app-server ${params.method} timed out`,
);
}