@canonmsg/codex-plugin 0.22.2 → 0.22.3

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/dist/adapter.d.ts CHANGED
@@ -32,6 +32,10 @@ export type CodexEvent = {
32
32
  cached_input_tokens?: number;
33
33
  output_tokens?: number;
34
34
  };
35
+ } | {
36
+ type: 'settings.updated';
37
+ model: string | null;
38
+ effort: string | null;
35
39
  } | {
36
40
  type: 'skills.changed';
37
41
  };
@@ -8,6 +8,19 @@ export interface CodexSkillMetadata {
8
8
  shortDescription?: string;
9
9
  displayName?: string;
10
10
  }
11
+ export interface CodexReasoningEffortMetadata {
12
+ value: string;
13
+ description?: string;
14
+ }
15
+ export interface CodexModelMetadata {
16
+ id: string;
17
+ model: string;
18
+ displayName: string;
19
+ description?: string;
20
+ supportedReasoningEfforts: CodexReasoningEffortMetadata[];
21
+ defaultReasoningEffort?: string;
22
+ isDefault: boolean;
23
+ }
11
24
  export declare class CodexAppServerAdapter {
12
25
  private readonly cwd;
13
26
  private readonly codexBin;
@@ -24,6 +37,7 @@ export declare class CodexAppServerAdapter {
24
37
  private threadId;
25
38
  private loadedThreadId;
26
39
  private resolvedModel;
40
+ private resolvedReasoningEffort;
27
41
  private currentTurnId;
28
42
  private requestSeq;
29
43
  private pending;
@@ -54,6 +68,8 @@ export declare class CodexAppServerAdapter {
54
68
  dynamicTools?: readonly JsonRecord[];
55
69
  });
56
70
  getThreadId(): string | null;
71
+ getResolvedModel(): string | null;
72
+ getResolvedReasoningEffort(): string | null;
57
73
  clearThreadId(): void;
58
74
  setModel(model: string | null): void;
59
75
  /**
@@ -78,7 +94,11 @@ export declare class CodexAppServerAdapter {
78
94
  private buildWritableRoots;
79
95
  listSkills(options?: {
80
96
  forceReload?: boolean;
97
+ requestTimeoutMs?: number;
81
98
  }): Promise<CodexSkillMetadata[]>;
99
+ listModels(options?: {
100
+ requestTimeoutMs?: number;
101
+ }): Promise<CodexModelMetadata[]>;
82
102
  private buildTurnInput;
83
103
  private ensureStarted;
84
104
  private handleLine;
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
+ const DEFAULT_DISCOVERY_REQUEST_TIMEOUT_MS = 5_000;
3
4
  export class CodexAppServerAdapter {
4
5
  cwd;
5
6
  codexBin;
@@ -16,6 +17,7 @@ export class CodexAppServerAdapter {
16
17
  threadId;
17
18
  loadedThreadId = null;
18
19
  resolvedModel = null;
20
+ resolvedReasoningEffort;
19
21
  currentTurnId = null;
20
22
  requestSeq = 1;
21
23
  pending = new Map();
@@ -37,6 +39,7 @@ export class CodexAppServerAdapter {
37
39
  this.codexBin = opts.codexBin ?? 'codex';
38
40
  this.model = opts.model ?? null;
39
41
  this.reasoningEffort = opts.reasoningEffort ?? null;
42
+ this.resolvedReasoningEffort = this.reasoningEffort;
40
43
  this.sandbox = opts.sandbox ?? null;
41
44
  this.legacyApprovalPolicy = opts.approvalPolicy ?? null;
42
45
  this.addDirs = opts.addDirs ?? [];
@@ -48,13 +51,21 @@ export class CodexAppServerAdapter {
48
51
  getThreadId() {
49
52
  return this.threadId;
50
53
  }
54
+ getResolvedModel() {
55
+ return this.resolvedModel ?? this.model;
56
+ }
57
+ getResolvedReasoningEffort() {
58
+ return this.resolvedReasoningEffort ?? this.reasoningEffort;
59
+ }
51
60
  clearThreadId() {
52
61
  this.threadId = null;
53
62
  this.loadedThreadId = null;
54
63
  this.resolvedModel = null;
64
+ this.resolvedReasoningEffort = this.reasoningEffort;
55
65
  }
56
66
  setModel(model) {
57
67
  this.model = model;
68
+ this.resolvedModel = model;
58
69
  }
59
70
  /**
60
71
  * Sets GPT reasoning effort, applied via model_reasoning_effort. The config
@@ -66,9 +77,10 @@ export class CodexAppServerAdapter {
66
77
  */
67
78
  setReasoningEffort(effort) {
68
79
  const next = effort && effort.trim() ? effort.trim() : null;
69
- if (next === this.reasoningEffort)
80
+ if (next === this.reasoningEffort && next === this.resolvedReasoningEffort)
70
81
  return;
71
82
  this.reasoningEffort = next;
83
+ this.resolvedReasoningEffort = next;
72
84
  if (this.threadId && this.loadedThreadId === this.threadId) {
73
85
  this.loadedThreadId = null;
74
86
  }
@@ -200,7 +212,7 @@ export class CodexAppServerAdapter {
200
212
  continue;
201
213
  config[key] = parseConfigValue(raw.slice(separator + 1));
202
214
  }
203
- if (this.reasoningEffort && config.model_reasoning_effort === undefined) {
215
+ if (this.reasoningEffort) {
204
216
  config.model_reasoning_effort = this.reasoningEffort;
205
217
  }
206
218
  return Object.keys(config).length > 0 ? { config } : {};
@@ -249,11 +261,38 @@ export class CodexAppServerAdapter {
249
261
  const result = await this.sendRequest('skills/list', {
250
262
  cwds: [this.cwd],
251
263
  ...(options.forceReload ? { forceReload: true } : {}),
252
- });
264
+ }, options.requestTimeoutMs ?? DEFAULT_DISCOVERY_REQUEST_TIMEOUT_MS);
253
265
  const parsed = parseSkillsListResponse(result);
254
266
  this.skillsCache = parsed;
255
267
  return parsed;
256
268
  }
269
+ async listModels(options = {}) {
270
+ await this.ensureStarted();
271
+ const models = [];
272
+ const seen = new Set();
273
+ const seenCursors = new Set();
274
+ let cursor = null;
275
+ do {
276
+ const result = await this.sendRequest('model/list', {
277
+ limit: 100,
278
+ includeHidden: false,
279
+ ...(cursor ? { cursor } : {}),
280
+ }, options.requestTimeoutMs ?? DEFAULT_DISCOVERY_REQUEST_TIMEOUT_MS);
281
+ for (const model of parseModelListResponse(result)) {
282
+ if (seen.has(model.model))
283
+ continue;
284
+ seen.add(model.model);
285
+ models.push(model);
286
+ }
287
+ const nextCursor = readString(result, 'nextCursor') ?? null;
288
+ if (nextCursor && seenCursors.has(nextCursor))
289
+ break;
290
+ if (nextCursor)
291
+ seenCursors.add(nextCursor);
292
+ cursor = nextCursor;
293
+ } while (cursor);
294
+ return models;
295
+ }
257
296
  async buildTurnInput(prompt, imagePaths) {
258
297
  const skillPrompt = parseSkillSlashPrompt(prompt);
259
298
  const input = [];
@@ -373,6 +412,25 @@ export class CodexAppServerAdapter {
373
412
  }
374
413
  if (!this.isCurrentThreadNotification(params))
375
414
  return;
415
+ if (method === 'thread/settings/updated') {
416
+ const settings = isRecord(params.threadSettings) ? params.threadSettings : {};
417
+ const model = readString(settings, 'model') ?? null;
418
+ const effort = readString(settings, 'effort') ?? null;
419
+ this.resolvedModel = model ?? this.resolvedModel;
420
+ this.resolvedReasoningEffort = effort;
421
+ this.currentOnEvent?.({ type: 'settings.updated', model, effort });
422
+ return;
423
+ }
424
+ if (method === 'model/rerouted') {
425
+ const model = readString(params, 'toModel') ?? null;
426
+ this.resolvedModel = model ?? this.resolvedModel;
427
+ this.currentOnEvent?.({
428
+ type: 'settings.updated',
429
+ model,
430
+ effort: this.getResolvedReasoningEffort(),
431
+ });
432
+ return;
433
+ }
376
434
  if (method === 'turn/started') {
377
435
  this.currentTurnId = readString(params.turn, 'id') ?? this.currentTurnId;
378
436
  this.currentOnEvent?.({ type: 'turn.started' });
@@ -518,15 +576,39 @@ export class CodexAppServerAdapter {
518
576
  this.messageTextByItem.clear();
519
577
  this.planText = '';
520
578
  }
521
- sendRequest(method, params) {
579
+ sendRequest(method, params, timeoutMs) {
522
580
  const id = this.requestSeq++;
523
581
  return new Promise((resolve, reject) => {
524
- this.pending.set(id, { resolve, reject });
582
+ let timer = null;
583
+ const clearTimer = () => {
584
+ if (!timer)
585
+ return;
586
+ clearTimeout(timer);
587
+ timer = null;
588
+ };
589
+ this.pending.set(id, {
590
+ resolve: (value) => {
591
+ clearTimer();
592
+ resolve(value);
593
+ },
594
+ reject: (error) => {
595
+ clearTimer();
596
+ reject(error);
597
+ },
598
+ });
599
+ if (timeoutMs !== undefined && timeoutMs > 0) {
600
+ timer = setTimeout(() => {
601
+ if (!this.pending.delete(id))
602
+ return;
603
+ reject(new Error(`Codex app-server ${method} timed out after ${timeoutMs}ms`));
604
+ }, timeoutMs);
605
+ }
525
606
  try {
526
607
  this.write({ id, method, params });
527
608
  }
528
609
  catch (error) {
529
610
  this.pending.delete(id);
611
+ clearTimer();
530
612
  reject(error);
531
613
  }
532
614
  });
@@ -619,6 +701,45 @@ function parseSkillsListResponse(result) {
619
701
  }
620
702
  return skills;
621
703
  }
704
+ function parseModelListResponse(result) {
705
+ const entries = Array.isArray(result.data) ? result.data : [];
706
+ const models = [];
707
+ for (const entry of entries) {
708
+ if (!isRecord(entry) || entry.hidden === true)
709
+ continue;
710
+ const id = readString(entry, 'id');
711
+ const model = readString(entry, 'model') ?? id;
712
+ if (!id || !model)
713
+ continue;
714
+ const displayName = readString(entry, 'displayName') ?? model;
715
+ const description = readString(entry, 'description');
716
+ const efforts = Array.isArray(entry.supportedReasoningEfforts)
717
+ ? entry.supportedReasoningEfforts.flatMap((rawEffort) => {
718
+ if (!isRecord(rawEffort))
719
+ return [];
720
+ const value = readString(rawEffort, 'reasoningEffort');
721
+ if (!value)
722
+ return [];
723
+ const effortDescription = readString(rawEffort, 'description');
724
+ return [{
725
+ value,
726
+ ...(effortDescription ? { description: effortDescription } : {}),
727
+ }];
728
+ })
729
+ : [];
730
+ const defaultReasoningEffort = readString(entry, 'defaultReasoningEffort');
731
+ models.push({
732
+ id,
733
+ model,
734
+ displayName,
735
+ ...(description ? { description } : {}),
736
+ supportedReasoningEfforts: efforts,
737
+ ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
738
+ isDefault: entry.isDefault === true,
739
+ });
740
+ }
741
+ return models;
742
+ }
622
743
  function stringifyPreview(value) {
623
744
  if (typeof value === 'string')
624
745
  return value;
package/dist/host.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type WorkspaceOption, type CanonWorkspaceRootMetadata } from '@canonmsg/core';
3
3
  import { type CodexSkillMetadata } from './app-server-adapter.js';
4
+ import { type CodexControlOption } from './model-catalog.js';
4
5
  interface HostSessionState {
5
6
  lastError?: string;
6
7
  model?: string;
@@ -32,30 +33,14 @@ export declare function buildCodexLiveSessionConfig(input: {
32
33
  permissionMode?: string | undefined;
33
34
  model?: string | undefined;
34
35
  };
35
- /** GPT reasoning-effort levels, applied next turn via model_reasoning_effort. */
36
- export declare const CODEX_EFFORT_OPTIONS: readonly [{
37
- readonly value: "minimal";
38
- readonly label: "Minimal";
39
- }, {
40
- readonly value: "low";
41
- readonly label: "Low";
42
- }, {
43
- readonly value: "medium";
44
- readonly label: "Medium";
45
- }, {
46
- readonly value: "high";
47
- readonly label: "High";
48
- }, {
49
- readonly value: "xhigh";
50
- readonly label: "Extra high";
51
- }];
36
+ /** Conservative fallback used only when native app-server discovery is unavailable. */
37
+ export declare const CODEX_EFFORT_OPTIONS: readonly CodexControlOption[];
52
38
  export declare const CODEX_SESSION_CONFIG_FIELDS: readonly ["permissionMode", "effort"];
53
39
  export declare function buildCodexSkillCommands(skills: ReadonlyArray<CodexSkillMetadata>): CanonRuntimeCommandDescriptor[];
54
40
  export declare function buildCodexRuntimeDescriptor(input: {
55
- models: Array<{
56
- value: string;
57
- label: string;
58
- }>;
41
+ models: CodexControlOption[];
42
+ effortOptions?: CodexControlOption[];
43
+ defaultEffort?: string | null;
59
44
  workspaces: WorkspaceOption[];
60
45
  workspaceRoots?: CanonWorkspaceRootMetadata[];
61
46
  executionModes: ExecutionEnvironmentMode[];
package/dist/host.js CHANGED
@@ -9,7 +9,7 @@ import { captureTurnArtifactSnapshot, collectTurnArtifacts, } from '@canonmsg/co
9
9
  import { RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildRuntimeCardOutcome, resolveQuestionAllowOther, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, buildCanonInboundFrameV1, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, markLocalRuntimeStopped, normalizeTurnMetadata, parseRuntimeCardV1, RuntimeRequestManager, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, CanonApiError, loadRuntimeSessionState, sendMessageWithRetry, sendMessageWithRetryChunked, saveRuntimeSessionState, buildBoundedTurnTrail, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
10
10
  import { decideAutoReply, } from './inbound-policy.js';
11
11
  import { CodexConversationAdapter, } from './adapter.js';
12
- import { CodexAppServerAdapter } from './app-server-adapter.js';
12
+ import { CodexAppServerAdapter, } from './app-server-adapter.js';
13
13
  import { CODEX_APP_DYNAMIC_TOOLS, deniedCodexAppToolResult, handleCodexAppToolCall, isCodexAppToolCall, } from './codex-app-tools.js';
14
14
  import { mapCanonApprovalResultToCodexDecision, mapCodexAppServerApprovalRequest, } from './app-server-approval.js';
15
15
  import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
@@ -21,6 +21,7 @@ import { createCodexControlPoller } from './control-channel.js';
21
21
  import { runCli } from './cli-entry.js';
22
22
  import { collectMissedInboundMessages, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
23
23
  import { applyTextSegmentBlock, beginCommandBlock, claimCommandBlock, createCommandBlockTracker, } from './turn-activity.js';
24
+ import { FALLBACK_CODEX_EFFORT_OPTIONS, buildCodexEffortOptions, buildCodexModelOptions, readCodexConfiguredEffort, resolveCodexDefaultModel, resolveCodexEffortForModel, } from './model-catalog.js';
24
25
  const HELP = `canon-codex — run a local Codex agent host for Canon
25
26
 
26
27
  USAGE
@@ -88,15 +89,8 @@ let workingDir = process.cwd();
88
89
  let workspaceOptions = [];
89
90
  let workspaceRoots = [];
90
91
  let workspaceRootMetadata = [];
91
- /** GPT reasoning-effort levels, applied next turn via model_reasoning_effort. */
92
- export const CODEX_EFFORT_OPTIONS = [
93
- { value: 'minimal', label: 'Minimal' },
94
- { value: 'low', label: 'Low' },
95
- { value: 'medium', label: 'Medium' },
96
- { value: 'high', label: 'High' },
97
- { value: 'xhigh', label: 'Extra high' },
98
- ];
99
- const CODEX_EFFORT_VALUES = new Set(CODEX_EFFORT_OPTIONS.map((option) => option.value));
92
+ /** Conservative fallback used only when native app-server discovery is unavailable. */
93
+ export const CODEX_EFFORT_OPTIONS = FALLBACK_CODEX_EFFORT_OPTIONS;
100
94
  export const CODEX_SESSION_CONFIG_FIELDS = ['permissionMode', 'effort'];
101
95
  const MAX_CODEX_SKILL_COMMAND_CHOICES = 50;
102
96
  export function buildCodexSkillCommands(skills) {
@@ -176,8 +170,8 @@ export function buildCodexRuntimeDescriptor(input) {
176
170
  defaultPermissionMode: input.defaultPermissionMode,
177
171
  permissionModeLabel: 'Execution policy',
178
172
  modelLiveBehavior: 'next_turn',
179
- effortOptions: [...CODEX_EFFORT_OPTIONS],
180
- defaultEffort: 'medium',
173
+ effortOptions: input.effortOptions ?? [...CODEX_EFFORT_OPTIONS],
174
+ defaultEffort: input.defaultEffort ?? 'medium',
181
175
  effortLiveBehavior: 'next_turn',
182
176
  presentation: input.presentation,
183
177
  streamingTextMode: 'snapshot',
@@ -242,20 +236,6 @@ export function buildCodexTurnResponseRouting(input) {
242
236
  export function getCodexRequestingUserId(message) {
243
237
  return message.senderType === 'human' ? message.senderId : null;
244
238
  }
245
- function modelOptionLabel(model) {
246
- if (/^gpt-5\.5(?:$|[-_:])/i.test(model))
247
- return 'GPT-5.5';
248
- if (/^gpt-5\.4-mini(?:$|[-_:])/i.test(model))
249
- return 'GPT-5.4 Mini';
250
- if (/^gpt-5\.4(?:$|[-_:])/i.test(model))
251
- return 'GPT-5.4';
252
- return model;
253
- }
254
- function buildCodexModelOptions(model) {
255
- return typeof model === 'string' && model.trim()
256
- ? [{ value: model.trim(), label: modelOptionLabel(model.trim()) }]
257
- : [];
258
- }
259
239
  async function publishAgentRuntime(agentId, runtime) {
260
240
  await publishHostAgentRuntime(agentId, 'codex', runtime);
261
241
  }
@@ -1019,9 +999,13 @@ export async function main() {
1019
999
  throw new ExecutionEnvironmentError(modelGuard, modelGuard);
1020
1000
  }
1021
1001
  const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode, policy.fingerprint);
1022
- const initialEffort = config?.effort && CODEX_EFFORT_VALUES.has(config.effort)
1023
- ? config.effort
1024
- : null;
1002
+ const effectiveModel = policy.model ?? codexDefaultModel;
1003
+ const initialEffortResolution = resolveCodexEffortForModel({
1004
+ models: codexModels,
1005
+ model: effectiveModel,
1006
+ requestedEffort: config?.effort ?? configuredCodexEffort,
1007
+ });
1008
+ const initialEffort = initialEffortResolution.value;
1025
1009
  const adapter = useAppServer
1026
1010
  ? new CodexAppServerAdapter({
1027
1011
  cwd: sessionCwd,
@@ -1059,7 +1043,7 @@ export async function main() {
1059
1043
  queue: [],
1060
1044
  running: false,
1061
1045
  state: buildCodexInitialSessionState({
1062
- model: policy.model,
1046
+ model: effectiveModel ?? undefined,
1063
1047
  permissionMode: policy.permissionMode,
1064
1048
  effort: initialEffort,
1065
1049
  }),
@@ -1577,6 +1561,13 @@ export async function main() {
1577
1561
  void refreshCodexSkillInventory(true).then(() => publishRuntimeHeartbeat());
1578
1562
  return;
1579
1563
  }
1564
+ if (event.type === 'settings.updated') {
1565
+ if (event.model)
1566
+ session.state.model = event.model;
1567
+ session.state.effort = event.effort ?? undefined;
1568
+ writeState(session);
1569
+ return;
1570
+ }
1580
1571
  if (event.type === 'message') {
1581
1572
  session.turnState = 'streaming';
1582
1573
  markTurnProgress(session);
@@ -1679,6 +1670,17 @@ export async function main() {
1679
1670
  clearStoredThread();
1680
1671
  result = await runTurnOnce();
1681
1672
  }
1673
+ if (session.adapter instanceof CodexAppServerAdapter) {
1674
+ const resolvedModel = session.adapter.getResolvedModel();
1675
+ const resolvedEffort = session.adapter.getResolvedReasoningEffort();
1676
+ if ((resolvedModel && resolvedModel !== session.state.model)
1677
+ || resolvedEffort !== (session.state.effort ?? null)) {
1678
+ if (resolvedModel)
1679
+ session.state.model = resolvedModel;
1680
+ session.state.effort = resolvedEffort ?? undefined;
1681
+ writeState(session);
1682
+ }
1683
+ }
1682
1684
  if (result.threadId && !session.resetRequested) {
1683
1685
  saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
1684
1686
  }
@@ -1867,7 +1869,16 @@ export async function main() {
1867
1869
  ...EXECUTION_ENVIRONMENT_MODES,
1868
1870
  ];
1869
1871
  const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
1870
- const codexModelOptions = buildCodexModelOptions(args.model);
1872
+ const configuredCodexEffort = readCodexConfiguredEffort(args.config ?? []);
1873
+ let codexModels = [];
1874
+ let codexModelOptions = buildCodexModelOptions(codexModels, args.model);
1875
+ let codexEffortOptions = buildCodexEffortOptions(codexModels);
1876
+ let codexDefaultModel = resolveCodexDefaultModel(codexModels, args.model);
1877
+ let codexDefaultEffort = resolveCodexEffortForModel({
1878
+ models: codexModels,
1879
+ model: codexDefaultModel,
1880
+ requestedEffort: configuredCodexEffort,
1881
+ }).value;
1871
1882
  const runtimePresentation = buildRuntimePresentationPolicy({
1872
1883
  base: DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION,
1873
1884
  preset: parseRuntimeVisibilityPreset(args['runtime-visibility']),
@@ -1877,7 +1888,7 @@ export async function main() {
1877
1888
  let codexSkills = [];
1878
1889
  const buildCurrentRuntimeDescriptor = () => ({
1879
1890
  defaultWorkspaceId: workspaceOptions[0]?.id,
1880
- ...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
1891
+ ...(codexDefaultModel ? { defaultModel: codexDefaultModel } : {}),
1881
1892
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
1882
1893
  availableExecutionModes: hostAvailableExecutionModes,
1883
1894
  availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
@@ -1886,6 +1897,8 @@ export async function main() {
1886
1897
  : {}),
1887
1898
  runtimeDescriptor: buildCodexRuntimeDescriptor({
1888
1899
  models: codexModelOptions,
1900
+ effortOptions: codexEffortOptions,
1901
+ defaultEffort: codexDefaultEffort,
1889
1902
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1890
1903
  workspaceRoots: workspaceRootMetadata,
1891
1904
  executionModes: hostAvailableExecutionModes,
@@ -1908,16 +1921,32 @@ export async function main() {
1908
1921
  model: typeof args.model === 'string' ? args.model : null,
1909
1922
  configOverrides: args.config ?? [],
1910
1923
  });
1924
+ try {
1925
+ const discoveredModels = await probe.listModels();
1926
+ if (discoveredModels.length > 0) {
1927
+ codexModels = discoveredModels;
1928
+ codexModelOptions = buildCodexModelOptions(codexModels, args.model);
1929
+ codexEffortOptions = buildCodexEffortOptions(codexModels);
1930
+ codexDefaultModel = resolveCodexDefaultModel(codexModels, args.model);
1931
+ codexDefaultEffort = resolveCodexEffortForModel({
1932
+ models: codexModels,
1933
+ model: codexDefaultModel,
1934
+ requestedEffort: configuredCodexEffort,
1935
+ }).value;
1936
+ }
1937
+ }
1938
+ catch (error) {
1939
+ console.error('[canon-codex] Failed to load Codex models:', error instanceof Error ? error.message : error);
1940
+ }
1911
1941
  try {
1912
1942
  codexSkills = await probe.listSkills({ forceReload });
1913
- runtimeDescriptor = buildCurrentRuntimeDescriptor();
1914
1943
  }
1915
1944
  catch (error) {
1916
1945
  codexSkills = [];
1917
- runtimeDescriptor = buildCurrentRuntimeDescriptor();
1918
1946
  console.error('[canon-codex] Failed to load Codex skills:', error instanceof Error ? error.message : error);
1919
1947
  }
1920
1948
  finally {
1949
+ runtimeDescriptor = buildCurrentRuntimeDescriptor();
1921
1950
  probe.close();
1922
1951
  }
1923
1952
  }
@@ -1925,7 +1954,13 @@ export async function main() {
1925
1954
  const session = sessions.get(conversationId);
1926
1955
  if (!session || session.closed)
1927
1956
  return;
1957
+ let modelChanged = false;
1928
1958
  if (control.model && control.model !== session.state.model) {
1959
+ if (codexModelOptions.length > 0 && !codexModelOptions.some((option) => option.value === control.model)) {
1960
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring model outside the discovered Codex catalog (${control.model})`);
1961
+ writeState(session);
1962
+ return;
1963
+ }
1929
1964
  const modelGuard = buildCodexModelGuardMessage(control.model, codexCliStatus);
1930
1965
  if (modelGuard) {
1931
1966
  session.state.lastError = modelGuard;
@@ -1937,29 +1972,36 @@ export async function main() {
1937
1972
  }
1938
1973
  session.adapter.setModel(control.model);
1939
1974
  session.state.model = control.model;
1975
+ modelChanged = true;
1940
1976
  console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Model set for next turn -> ${control.model}`);
1941
- writeState(session);
1942
1977
  }
1943
1978
  if (control.permissionMode) {
1944
1979
  console.error(`[canon-codex] [${conversationId.slice(0, 8)}] approval mode is session-creation-only; ignoring mid-session change request (${control.permissionMode})`);
1945
1980
  // Convergence contract: a consumed session control must always be
1946
1981
  // answered. Re-publish the currently applied state so clients settle on
1947
1982
  // the authoritative value instead of holding the composer until timeout.
1948
- writeState(session);
1949
1983
  }
1950
- if (control.effort) {
1951
- if (CODEX_EFFORT_VALUES.has(control.effort)) {
1952
- session.adapter.setReasoningEffort(control.effort);
1953
- session.state.effort = control.effort;
1984
+ if (control.effort || modelChanged) {
1985
+ const effortResolution = resolveCodexEffortForModel({
1986
+ models: codexModels,
1987
+ model: session.state.model,
1988
+ requestedEffort: control.effort ?? session.state.effort,
1989
+ });
1990
+ if (effortResolution.value !== session.state.effort) {
1991
+ session.adapter.setReasoningEffort(effortResolution.value);
1992
+ session.state.effort = effortResolution.value ?? undefined;
1993
+ }
1994
+ if (control.effort && !effortResolution.accepted) {
1995
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${control.effort} is unsupported by ${session.state.model ?? 'the active model'}; reset to ${effortResolution.value ?? 'the model default'}`);
1996
+ }
1997
+ else if (control.effort) {
1954
1998
  console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Reasoning effort set for next turn -> ${control.effort}`);
1955
- writeState(session);
1956
1999
  }
1957
- else {
1958
- console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Ignoring unknown effort level (${control.effort})`);
1959
- // Same contract: ignored values still get an authoritative re-publish.
1960
- writeState(session);
2000
+ else if (modelChanged && effortResolution.value) {
2001
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Reasoning effort for ${session.state.model} -> ${effortResolution.value}`);
1961
2002
  }
1962
2003
  }
2004
+ writeState(session);
1963
2005
  }
1964
2006
  async function handleControlSignal(event) {
1965
2007
  const { conversationId, type } = event;
@@ -0,0 +1,19 @@
1
+ import type { CodexModelMetadata } from './app-server-adapter.js';
2
+ export interface CodexControlOption {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ export declare const FALLBACK_CODEX_EFFORT_OPTIONS: ReadonlyArray<CodexControlOption>;
8
+ export declare function buildCodexModelOptions(models: ReadonlyArray<CodexModelMetadata>, configuredModel?: unknown): CodexControlOption[];
9
+ export declare function buildCodexEffortOptions(models: ReadonlyArray<CodexModelMetadata>): CodexControlOption[];
10
+ export declare function resolveCodexDefaultModel(models: ReadonlyArray<CodexModelMetadata>, configuredModel?: unknown): string | null;
11
+ export declare function resolveCodexEffortForModel(input: {
12
+ models: ReadonlyArray<CodexModelMetadata>;
13
+ model: string | null | undefined;
14
+ requestedEffort: string | null | undefined;
15
+ }): {
16
+ value: string | null;
17
+ accepted: boolean;
18
+ };
19
+ export declare function readCodexConfiguredEffort(configOverrides: ReadonlyArray<string>): string | null;
@@ -0,0 +1,137 @@
1
+ const EFFORT_LABELS = {
2
+ minimal: 'Minimal',
3
+ low: 'Low',
4
+ medium: 'Medium',
5
+ high: 'High',
6
+ xhigh: 'Extra high',
7
+ max: 'Max',
8
+ ultra: 'Ultra',
9
+ };
10
+ const EFFORT_ORDER = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
11
+ export const FALLBACK_CODEX_EFFORT_OPTIONS = [
12
+ { value: 'minimal', label: 'Minimal' },
13
+ { value: 'low', label: 'Low' },
14
+ { value: 'medium', label: 'Medium' },
15
+ { value: 'high', label: 'High' },
16
+ { value: 'xhigh', label: 'Extra high' },
17
+ ];
18
+ export function buildCodexModelOptions(models, configuredModel) {
19
+ const configured = typeof configuredModel === 'string' ? configuredModel.trim() : '';
20
+ const ordered = [...models].sort((left, right) => {
21
+ if (configured) {
22
+ if (left.model === configured)
23
+ return -1;
24
+ if (right.model === configured)
25
+ return 1;
26
+ }
27
+ return Number(right.isDefault) - Number(left.isDefault);
28
+ });
29
+ const options = ordered.map((model) => ({
30
+ value: model.model,
31
+ label: model.displayName,
32
+ ...(model.description ? { description: model.description } : {}),
33
+ }));
34
+ if (configured && !options.some((option) => option.value === configured)) {
35
+ options.push({ value: configured, label: configured });
36
+ }
37
+ return options;
38
+ }
39
+ export function buildCodexEffortOptions(models) {
40
+ if (models.length === 0)
41
+ return [...FALLBACK_CODEX_EFFORT_OPTIONS];
42
+ const values = new Set();
43
+ const descriptions = new Map();
44
+ for (const model of models) {
45
+ for (const effort of model.supportedReasoningEfforts) {
46
+ values.add(effort.value);
47
+ if (effort.description && !descriptions.has(effort.value)) {
48
+ descriptions.set(effort.value, effort.description);
49
+ }
50
+ }
51
+ }
52
+ return [...values]
53
+ .sort((left, right) => {
54
+ const leftIndex = EFFORT_ORDER.indexOf(left);
55
+ const rightIndex = EFFORT_ORDER.indexOf(right);
56
+ if (leftIndex === -1 && rightIndex === -1)
57
+ return left.localeCompare(right);
58
+ if (leftIndex === -1)
59
+ return 1;
60
+ if (rightIndex === -1)
61
+ return -1;
62
+ return leftIndex - rightIndex;
63
+ })
64
+ .map((value) => {
65
+ const description = buildEffortDescription(value, descriptions.get(value), models);
66
+ return {
67
+ value,
68
+ label: EFFORT_LABELS[value] ?? humanizeEffort(value),
69
+ ...(description ? { description } : {}),
70
+ };
71
+ });
72
+ }
73
+ export function resolveCodexDefaultModel(models, configuredModel) {
74
+ const configured = typeof configuredModel === 'string' ? configuredModel.trim() : '';
75
+ if (configured)
76
+ return configured;
77
+ return models.find((model) => model.isDefault)?.model ?? models[0]?.model ?? null;
78
+ }
79
+ export function resolveCodexEffortForModel(input) {
80
+ const requested = input.requestedEffort?.trim() || null;
81
+ const model = input.models.find((candidate) => candidate.model === input.model);
82
+ if (!model) {
83
+ const fallbackValues = new Set([
84
+ ...FALLBACK_CODEX_EFFORT_OPTIONS.map((option) => option.value),
85
+ ...input.models.flatMap((candidate) => (candidate.supportedReasoningEfforts.map((effort) => effort.value))),
86
+ ]);
87
+ if (!requested)
88
+ return { value: 'medium', accepted: true };
89
+ return fallbackValues.has(requested)
90
+ ? { value: requested, accepted: true }
91
+ : { value: 'medium', accepted: false };
92
+ }
93
+ const supported = new Set(model.supportedReasoningEfforts.map((effort) => effort.value));
94
+ const fallback = supported.has(model.defaultReasoningEffort ?? '')
95
+ ? model.defaultReasoningEffort ?? null
96
+ : model.supportedReasoningEfforts[0]?.value ?? null;
97
+ if (!requested)
98
+ return { value: fallback, accepted: true };
99
+ return supported.has(requested)
100
+ ? { value: requested, accepted: true }
101
+ : { value: fallback, accepted: false };
102
+ }
103
+ export function readCodexConfiguredEffort(configOverrides) {
104
+ let configured = null;
105
+ for (const raw of configOverrides) {
106
+ const separator = raw.indexOf('=');
107
+ if (separator <= 0 || raw.slice(0, separator).trim() !== 'model_reasoning_effort')
108
+ continue;
109
+ const value = raw.slice(separator + 1).trim();
110
+ if (!value)
111
+ continue;
112
+ configured = unquote(value);
113
+ }
114
+ return configured;
115
+ }
116
+ function humanizeEffort(value) {
117
+ return value
118
+ .replace(/[_-]+/g, ' ')
119
+ .replace(/\b\w/g, (character) => character.toUpperCase());
120
+ }
121
+ function buildEffortDescription(effort, nativeDescription, models) {
122
+ const supportedBy = models.filter((model) => (model.supportedReasoningEfforts.some((candidate) => candidate.value === effort)));
123
+ const compatibility = supportedBy.length > 0 && supportedBy.length < models.length
124
+ ? `Supported by ${supportedBy.map((model) => model.displayName).join(', ')}.`
125
+ : '';
126
+ return [nativeDescription, compatibility].filter(Boolean).join(' ') || undefined;
127
+ }
128
+ function unquote(value) {
129
+ if (value.length >= 2) {
130
+ const first = value[0];
131
+ const last = value[value.length - 1];
132
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
133
+ return value.slice(1, -1);
134
+ }
135
+ }
136
+ return value;
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.22.2",
3
+ "version": "0.22.3",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",