feat(codex): share sessions across terminal and web

Use one native app-server for terminal, Web and phone clients while retaining the existing CLI and Runner lifecycle.

Synchronize native queues, permissions, question history and steering state; preserve explicit permission precedence and per-turn usage models. Resume inactive clear commands through Runner and reject independent child cold resumes.

Add shared-runtime regression tests, generated protocol fixtures and lifecycle documentation.
This commit is contained in:
weishu
2026-09-12 10:59:17 +08:00
parent 25af3f8e13
commit 0c4abcb3d1
111 changed files with 4684 additions and 960 deletions
@@ -22,6 +22,7 @@ type ElicitationSchemaProperty = {
};
type UserInputAnswer = Record<string, string[]> | Record<string, { answers: string[] }>;
export const IGNORE_SHARED_REQUEST = Symbol('ignore-shared-request');
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== 'object') {
@@ -376,8 +377,9 @@ function isHapiBridgeElicitation(params: unknown): boolean {
}
export function registerAppServerPermissionHandlers(args: {
client: CodexAppServerClient;
permissionHandler: CodexPermissionHandler;
client: Pick<CodexAppServerClient, 'registerRequestHandler'>;
permissionHandler: Pick<CodexPermissionHandler, 'handleToolCall'>;
shared?: boolean;
getPermissionMode?: () => CodexPermissionMode | undefined;
onUserInputRequest?: (request: { id: string; input: unknown }) => Promise<
| { decision: 'accept'; answers: UserInputAnswer }
@@ -504,6 +506,7 @@ export function registerAppServerPermissionHandlers(args: {
const input = buildElicitationUserInput(params);
if (!onUserInputRequest || !input) {
if (args.shared) return IGNORE_SHARED_REQUEST;
logger.debug('[CodexAppServer] Cancelling unsupported MCP elicitation request', {
serverName: record.serverName,
mode: request.mode,
+2 -1
View File
@@ -45,6 +45,7 @@ export interface HapiMcpBridge {
}
export interface HapiMcpBridgeOptions {
exportSessionEnv?: boolean;
emitTitleSummary?: boolean;
enableChangeTitle?: boolean;
skillLookup?: {
@@ -76,7 +77,7 @@ export async function buildHapiMcpBridge(
}
}
// Belt-and-suspenders: onMaterialized already exports; keep env set for non-lazy too.
exportHapiSessionEnv(client.sessionId);
if (options.exportSessionEnv !== false) exportHapiSessionEnv(client.sessionId);
const happyServer = await startHappyServer(client, {
emitTitleSummary: options.emitTitleSummary,