@rynx-ai/runtime 0.1.11-beta.28 → 0.1.11-beta.29

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.
@@ -1,5 +1,5 @@
1
1
  import type { RuntimeUserInput, SessionInteractionResolution } from "@rynx-ai/core";
2
- import type { AskForApproval, ClientInfo, CollaborationModeListResponse, GetAuthStatusParams, GetAuthStatusResponse, InitializeResponse, ModelListParams, ModelListResponse, ReviewStartParams, ReviewStartResponse, SandboxMode, ThreadForkParams, ThreadGoalClearParams, ThreadGoalGetParams, ThreadGoalGetResponse, ThreadGoalSetParams, ThreadListParams, ThreadListResponse, ResumedThread, ThreadResumeParams, ThreadSettingsUpdateParams, ThreadStartParams, TurnInterruptParams, TurnStartParams, TurnSteerParams, UserInput } from "./protocol.js";
2
+ import type { AskForApproval, ClientInfo, CollaborationModeListResponse, ConfigReadParams, ConfigReadResponse, GetAuthStatusParams, GetAuthStatusResponse, InitializeResponse, ModelListParams, ModelListResponse, ReviewStartParams, ReviewStartResponse, SandboxMode, ThreadForkParams, ThreadGoalClearParams, ThreadGoalGetParams, ThreadGoalGetResponse, ThreadGoalSetParams, ThreadListParams, ThreadListResponse, ResumedThread, ThreadResumeParams, ThreadRuntimeSettings, ThreadSettingsUpdateParams, ThreadStartParams, TurnInterruptParams, TurnStartParams, TurnSteerParams, UserInput } from "./protocol.js";
3
3
  import { CodexAppServerTransport, type CodexAppServerProcessSpawner, type RpcChannel, type TransportLogger } from "./transport.js";
4
4
  import type { ResolveInteractionResult, RuntimeInteractionListener } from "../interactions.js";
5
5
  export type ApprovalDecisionPolicy = "auto-approve-session" | "auto-decline" | "auto-cancel";
@@ -44,13 +44,16 @@ export declare class CodexAppServerClient {
44
44
  terminalRemoteUrl(): string | undefined;
45
45
  ensureInitialized(): Promise<InitializeResponse>;
46
46
  getAuthStatus(params?: GetAuthStatusParams): Promise<GetAuthStatusResponse>;
47
+ /** Read the runtime's merged effective config. This is the stable seam for
48
+ * discovering a fresh thread's model across Traex YAML/TOML generations. */
49
+ configRead(params: ConfigReadParams): Promise<ConfigReadResponse>;
47
50
  threadStart(params: ThreadStartParams): Promise<{
48
51
  threadId: string;
49
- }>;
52
+ } & ThreadRuntimeSettings>;
50
53
  threadResume(params: ThreadResumeParams): Promise<{
51
54
  threadId: string;
52
55
  thread: ResumedThread;
53
- }>;
56
+ } & ThreadRuntimeSettings>;
54
57
  turnStart(params: TurnStartParams): Promise<{
55
58
  turnId: string;
56
59
  }>;
@@ -1020,17 +1020,36 @@ export class CodexAppServerClient {
1020
1020
  await this.ensureInitialized();
1021
1021
  return this.transport.sendRequest("getAuthStatus", params);
1022
1022
  }
1023
+ /** Read the runtime's merged effective config. This is the stable seam for
1024
+ * discovering a fresh thread's model across Traex YAML/TOML generations. */
1025
+ async configRead(params) {
1026
+ await this.ensureInitialized();
1027
+ return this.transport.sendRequest("config/read", params);
1028
+ }
1023
1029
  async threadStart(params) {
1024
1030
  await this.ensureInitialized();
1025
1031
  const response = await this.transport.sendRequest("thread/start", params);
1026
- return { threadId: response.thread.id };
1032
+ return {
1033
+ threadId: response.thread.id,
1034
+ ...(response.model ? { model: response.model } : {}),
1035
+ ...(response.reasoningEffort === undefined
1036
+ ? {}
1037
+ : { reasoningEffort: response.reasoningEffort }),
1038
+ };
1027
1039
  }
1028
1040
  async threadResume(params) {
1029
1041
  await this.ensureInitialized();
1030
1042
  const response = await this.transport.sendRequest("thread/resume", params);
1031
1043
  // Expose the whole `thread` (not just its id): its `turns[].items[]` are the
1032
1044
  // backfill the forwarder replays for a fresh thread's first turn.
1033
- return { threadId: response.thread.id, thread: response.thread };
1045
+ return {
1046
+ threadId: response.thread.id,
1047
+ thread: response.thread,
1048
+ ...(response.model ? { model: response.model } : {}),
1049
+ ...(response.reasoningEffort === undefined
1050
+ ? {}
1051
+ : { reasoningEffort: response.reasoningEffort }),
1052
+ };
1034
1053
  }
1035
1054
  async turnStart(params) {
1036
1055
  await this.ensureInitialized();
@@ -24,7 +24,7 @@
24
24
  import type { AgentEvent, UserContentPart } from "@rynx-ai/core";
25
25
  import type { CodexAppServerClient } from "./client.js";
26
26
  import type { McpStartupPlan } from "./mcp-startup.js";
27
- import type { CollaborationModeKind, ResumedTurn } from "./protocol.js";
27
+ import type { CollaborationModeKind, ReasoningEffort, ResumedTurn } from "./protocol.js";
28
28
  export interface CodexForwarderSink {
29
29
  /** A turn began. `turnId` is codex's turn id, used to derive a stable
30
30
  * `responseId`. Start a fresh normalizer/response. */
@@ -65,6 +65,12 @@ export interface CodexForwarderSink {
65
65
  onThreadActive?(): void;
66
66
  /** The native TUI or another app-server client changed collaboration mode. */
67
67
  onCollaborationModeChanged?(mode: CollaborationModeKind): void;
68
+ /** Full mutable settings reported by the native thread. In particular, a
69
+ * TUI `/model` switch must become the model used by the next mode snapshot. */
70
+ onThreadSettingsChanged?(settings: {
71
+ model?: string;
72
+ reasoningEffort?: ReasoningEffort | null;
73
+ }): void;
68
74
  /** Codex's terminal-local Plan picker is not emitted by app-server today.
69
75
  * Synthesize it only after a live Plan item and its Turn both complete. */
70
76
  onPlanImplementationPrompt?(prompt: CodexPlanImplementationPrompt): void;
@@ -279,6 +279,18 @@ export class CodexSessionForwarder {
279
279
  }
280
280
  if (method === "thread/settings/updated") {
281
281
  const threadSettings = params?.threadSettings;
282
+ const model = typeof threadSettings?.model === "string" && threadSettings.model.trim()
283
+ ? threadSettings.model.trim()
284
+ : undefined;
285
+ const effort = threadSettings?.effort;
286
+ if (model !== undefined || effort === null || typeof effort === "string") {
287
+ this.sink.onThreadSettingsChanged?.({
288
+ ...(model === undefined ? {} : { model }),
289
+ ...(effort === null || typeof effort === "string"
290
+ ? { reasoningEffort: effort }
291
+ : {}),
292
+ });
293
+ }
282
294
  const mode = (threadSettings?.collaborationMode ?? threadSettings?.collaboration_mode)?.mode;
283
295
  if (mode === "plan" || mode === "default") {
284
296
  this.sink.onCollaborationModeChanged?.(mode);
@@ -56,6 +56,21 @@ export interface GetAuthStatusResponse {
56
56
  authToken: string | null;
57
57
  requiresOpenaiAuth: boolean;
58
58
  }
59
+ /** Read the Provider's effective configuration through the app-server rather
60
+ * than parsing a runtime-specific config file (Traex has used both YAML and
61
+ * TOML across releases). `cwd` includes project-scoped configuration layers. */
62
+ export interface ConfigReadParams {
63
+ includeLayers: boolean;
64
+ cwd?: string | null;
65
+ }
66
+ export interface ConfigReadResponse {
67
+ config: {
68
+ model: string | null;
69
+ model_reasoning_effort: ReasoningEffort | null;
70
+ [key: string]: unknown;
71
+ };
72
+ [key: string]: unknown;
73
+ }
59
74
  export type AskForApproval = "untrusted" | "on-failure" | "on-request" | "never";
60
75
  export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
61
76
  export type SandboxPolicy = {
@@ -139,6 +154,13 @@ export interface ResumedThread {
139
154
  turns?: ResumedTurn[];
140
155
  [key: string]: unknown;
141
156
  }
157
+ /** Settings selected by the Provider while starting or resuming a thread.
158
+ * Older app-server builds may omit them, so the bridge treats them as optional
159
+ * and falls back to `config/read` only for a fresh, not-yet-created thread. */
160
+ export interface ThreadRuntimeSettings {
161
+ model?: string;
162
+ reasoningEffort?: ReasoningEffort | null;
163
+ }
142
164
  export interface ThreadDescriptor {
143
165
  id: string;
144
166
  cwd: string;
package/dist/host.js CHANGED
@@ -531,10 +531,13 @@ export class LocalAgentHost {
531
531
  try {
532
532
  if (!suppliedClient)
533
533
  await client.ensureInitialized();
534
+ const collaborationMode = buildCollaborationMode(mode, live.model, live.reasoningEffort);
534
535
  await client.threadSettingsUpdate({
535
536
  threadId,
536
- collaborationMode: await buildCollaborationMode(client, mode, live.model, live.reasoningEffort),
537
+ collaborationMode,
537
538
  });
539
+ live.appliedModel = collaborationMode.settings.model;
540
+ live.appliedReasoningEffort = collaborationMode.settings.reasoning_effort ?? undefined;
538
541
  live.appliedCollaborationMode = mode;
539
542
  live.execution = { ...live.execution, collaborationMode: mode };
540
543
  }
@@ -746,6 +749,34 @@ export class LocalAgentHost {
746
749
  console.error(`[codex-live] session=${localThreadId} runtime=${runtime} app-server init failed: ${err instanceof Error ? err.message : String(err)}`);
747
750
  return abandon();
748
751
  }
752
+ // A fresh remote TUI creates the thread, so Rynx does not receive its
753
+ // `thread/start` response. Ask the already-initialized app-server for the
754
+ // effective model instead of parsing Traex's version-dependent YAML/TOML
755
+ // files or confusing `model/list.isDefault` with this Session's model.
756
+ let configModel;
757
+ let configReasoningEffort;
758
+ if (!execution.model || !execution.reasoningEffort) {
759
+ try {
760
+ const effective = await appServerOwner.configRead({
761
+ includeLayers: false,
762
+ cwd: workspace.cwd,
763
+ });
764
+ const reportedModel = effective.config.model;
765
+ if (typeof reportedModel === "string" && reportedModel.trim()) {
766
+ configModel = reportedModel.trim();
767
+ }
768
+ const reportedEffort = effective.config.model_reasoning_effort;
769
+ if (typeof reportedEffort === "string" && reportedEffort) {
770
+ configReasoningEffort = reportedEffort;
771
+ }
772
+ }
773
+ catch (error) {
774
+ // Explicit Session settings remain usable on an older Provider. If a
775
+ // requested collaboration mode later needs an unknown model, that
776
+ // update fails clearly without mutating the native thread.
777
+ console.warn(`[codex-live] session=${localThreadId} runtime=${runtime} effective config unavailable: ${error instanceof Error ? error.message : String(error)}`);
778
+ }
779
+ }
749
780
  const appServerUrl = appServerOwner.terminalRemoteUrl();
750
781
  if (!appServerUrl) {
751
782
  this.liveStartupErrors.set(localThreadId, nativeLiveFailure(runtime, "native_app_server_endpoint_missing", "app-server started without a Terminal remote endpoint"));
@@ -772,8 +803,8 @@ export class LocalAgentHost {
772
803
  return abandon();
773
804
  }
774
805
  }
775
- const model = execution.model ?? "";
776
- const reasoningEffort = execution.reasoningEffort ?? undefined;
806
+ const model = execution.model?.trim() || configModel || "";
807
+ const reasoningEffort = execution.reasoningEffort ?? configReasoningEffort;
777
808
  let markReady;
778
809
  const ready = new Promise((resolve) => {
779
810
  markReady = resolve;
@@ -1024,7 +1055,7 @@ export class LocalAgentHost {
1024
1055
  const client = this.injectionClientFactory(live.appServerUrl);
1025
1056
  try {
1026
1057
  await client.ensureInitialized();
1027
- const collaborationMode = await buildCollaborationMode(client, "default", live.model, live.reasoningEffort);
1058
+ const collaborationMode = buildCollaborationMode("default", live.model, live.reasoningEffort);
1028
1059
  let threadId = prompt.threadId;
1029
1060
  let text = CODEX_PLAN_IMPLEMENTATION_CODING_MESSAGE;
1030
1061
  if (choice === CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT) {
@@ -1292,6 +1323,16 @@ export class LocalAgentHost {
1292
1323
  mode,
1293
1324
  });
1294
1325
  },
1326
+ onThreadSettingsChanged: ({ model: reportedModel, reasoningEffort: reportedEffort }) => {
1327
+ if (reportedModel) {
1328
+ live.model = reportedModel;
1329
+ live.appliedModel = reportedModel;
1330
+ }
1331
+ if (reportedEffort !== undefined) {
1332
+ live.reasoningEffort = reportedEffort ?? undefined;
1333
+ live.appliedReasoningEffort = reportedEffort ?? undefined;
1334
+ }
1335
+ },
1295
1336
  onPlanImplementationPrompt: publishPlanImplementationPrompt,
1296
1337
  };
1297
1338
  const forwarder = new CodexSessionForwarder(forwarderClient, sink, {
@@ -1400,6 +1441,7 @@ export class LocalAgentHost {
1400
1441
  approvalPolicy,
1401
1442
  excludeTurns: true,
1402
1443
  });
1444
+ adoptResumedThreadSettings(live, resumed);
1403
1445
  if (execution.collaborationMode) {
1404
1446
  await this.applyLiveCollaborationMode(live, resumed.threadId, execution.collaborationMode, preloadClient);
1405
1447
  }
@@ -1597,6 +1639,7 @@ export class LocalAgentHost {
1597
1639
  approvalPolicy: live.approvalPolicy,
1598
1640
  ...(!sawNotReady ? { excludeTurns: true } : {}),
1599
1641
  });
1642
+ adoptResumedThreadSettings(live, resp);
1600
1643
  if (sawNotReady) {
1601
1644
  const turns = Array.isArray(resp.thread.turns)
1602
1645
  ? resp.thread.turns
@@ -1848,7 +1891,7 @@ export class LocalAgentHost {
1848
1891
  const desiredCollaborationMode = options?.collaborationMode ?? live.execution.collaborationMode;
1849
1892
  const collaborationMode = desiredCollaborationMode &&
1850
1893
  desiredCollaborationMode !== live.appliedCollaborationMode
1851
- ? await buildCollaborationMode(injectionClient, desiredCollaborationMode, desiredModel, desiredReasoningEffort)
1894
+ ? buildCollaborationMode(desiredCollaborationMode, desiredModel, desiredReasoningEffort)
1852
1895
  : undefined;
1853
1896
  const settings = {
1854
1897
  threadId,
@@ -2895,28 +2938,30 @@ function raceReady(ready, timeoutMs, failed) {
2895
2938
  clearTimeout(timer);
2896
2939
  });
2897
2940
  }
2941
+ /** Record the authoritative settings returned while binding an existing
2942
+ * native thread. A Session-level explicit selection remains a pending desired
2943
+ * override; otherwise the resumed thread itself is the source of truth. */
2944
+ function adoptResumedThreadSettings(live, settings) {
2945
+ const model = settings.model?.trim();
2946
+ if (model) {
2947
+ live.appliedModel = model;
2948
+ if (!live.execution.model)
2949
+ live.model = model;
2950
+ }
2951
+ if (settings.reasoningEffort !== undefined) {
2952
+ live.appliedReasoningEffort = settings.reasoningEffort ?? undefined;
2953
+ if (!live.execution.reasoningEffort) {
2954
+ live.reasoningEffort = settings.reasoningEffort ?? undefined;
2955
+ }
2956
+ }
2957
+ }
2898
2958
  /** Expand the public mode enum into the App Server's required settings
2899
- * snapshot. This mirrors Omnigent: an explicit Session model wins; otherwise
2900
- * the Provider's advertised native default is required, and an unknown model
2901
- * fails the update instead of silently dropping collaboration mode. */
2902
- async function buildCollaborationMode(client, mode, desiredModel, desiredReasoningEffort) {
2903
- let model = desiredModel.trim();
2959
+ * snapshot using the Session's tracked effective model. `model/list` is a
2960
+ * catalog and its `isDefault` marker is not the current thread setting. */
2961
+ function buildCollaborationMode(mode, desiredModel, desiredReasoningEffort) {
2962
+ const model = desiredModel.trim();
2904
2963
  if (!model) {
2905
- let catalog;
2906
- try {
2907
- catalog = await client.modelList();
2908
- }
2909
- catch (error) {
2910
- throw new CodexRuntimeError(`collaboration mode requires the current model: ${error instanceof Error ? error.message : String(error)}`, 503, "collaboration_mode_model_unknown");
2911
- }
2912
- const defaults = [...new Set(catalog.data
2913
- .filter((candidate) => candidate.isDefault === true)
2914
- .map((candidate) => (candidate.model || candidate.id).trim())
2915
- .filter(Boolean))];
2916
- if (defaults.length !== 1) {
2917
- throw new CodexRuntimeError(`collaboration mode requires exactly one current default model; Provider advertised ${defaults.length}`, 503, "collaboration_mode_model_unknown");
2918
- }
2919
- model = defaults[0];
2964
+ throw new CodexRuntimeError("collaboration mode requires the current model, but the Provider did not report one", 503, "collaboration_mode_model_unknown");
2920
2965
  }
2921
2966
  return {
2922
2967
  mode,
@@ -2958,9 +3003,18 @@ function updateLiveCodexTurnSettings(live, opts) {
2958
3003
  if (!sameImmutableSessionSnapshots(live.workspace, live.execution, opts)) {
2959
3004
  return false;
2960
3005
  }
3006
+ const previousModel = live.execution.model;
3007
+ const previousReasoningEffort = live.execution.reasoningEffort;
2961
3008
  live.execution = structuredClone(opts.execution);
2962
- live.model = opts.execution.model ?? "";
2963
- live.reasoningEffort = opts.execution.reasoningEffort ?? undefined;
3009
+ // Repeated calls normally carry the same immutable Session snapshot. Preserve
3010
+ // a newer native `/model` or effort change unless Core explicitly changed the
3011
+ // corresponding Session setting between calls.
3012
+ if (opts.execution.model !== previousModel) {
3013
+ live.model = opts.execution.model?.trim() ?? "";
3014
+ }
3015
+ if (opts.execution.reasoningEffort !== previousReasoningEffort) {
3016
+ live.reasoningEffort = opts.execution.reasoningEffort ?? undefined;
3017
+ }
2964
3018
  return true;
2965
3019
  }
2966
3020
  /** Freeze the Session's permission choice into the explicit managed settings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/runtime",
3
- "version": "0.1.11-beta.28",
3
+ "version": "0.1.11-beta.29",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -27,7 +27,7 @@
27
27
  "node-pty": "1.2.0-beta.15",
28
28
  "smol-toml": "1.7.1",
29
29
  "ws": "^8.21.0",
30
- "@rynx-ai/core": "0.1.11-beta.28"
30
+ "@rynx-ai/core": "0.1.11-beta.29"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/ws": "^8.18.1"