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

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/host.d.ts CHANGED
@@ -196,6 +196,9 @@ export declare class LocalAgentHost implements CodexCapabilities {
196
196
  * remember only a successful RPC. A supplied client is already initialized
197
197
  * and remains owned by its caller (used by cold-resume preload). */
198
198
  private applyLiveCollaborationMode;
199
+ /** Apply pending explicit model/effort settings and, when requested, one
200
+ * collaboration mode in a single native update. */
201
+ private applyLiveThreadSettings;
199
202
  /**
200
203
  * The command to run in a session's live terminal so it co-drives the codex
201
204
  * app-server thread (Phase D). Returns `null` when live-terminal is off, the
package/dist/host.js CHANGED
@@ -199,6 +199,7 @@ const CODEX_PLAN_IMPLEMENTATION_NO = "No, stay in Plan mode";
199
199
  const CODEX_PLAN_IMPLEMENTATION_CODING_MESSAGE = "Implement the plan.";
200
200
  const CODEX_PLAN_PROMPT_DISMISS_TIMEOUT_MS = 1_500;
201
201
  const CODEX_PLAN_PROMPT_DISMISS_POLL_MS = 50;
202
+ const CODEX_SUBSCRIBE_FLUSH_RETRY_MS = 1_500;
202
203
  const CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX = "A previous agent produced the plan below to accomplish the user's task. " +
203
204
  "Implement the plan in a fresh context. Treat the plan as the source of " +
204
205
  "user intent, re-read files as needed, and carry the work through " +
@@ -525,21 +526,49 @@ export class LocalAgentHost {
525
526
  * remember only a successful RPC. A supplied client is already initialized
526
527
  * and remains owned by its caller (used by cold-resume preload). */
527
528
  async applyLiveCollaborationMode(live, threadId, mode, suppliedClient) {
528
- if (live.appliedCollaborationMode === mode)
529
- return;
529
+ await this.applyLiveThreadSettings(live, threadId, mode, suppliedClient);
530
+ }
531
+ /** Apply pending explicit model/effort settings and, when requested, one
532
+ * collaboration mode in a single native update. */
533
+ async applyLiveThreadSettings(live, threadId, mode, suppliedClient) {
530
534
  const client = suppliedClient ?? this.injectionClientFactory(live.appServerUrl);
531
535
  try {
532
536
  if (!suppliedClient)
533
537
  await client.ensureInitialized();
534
- const collaborationMode = buildCollaborationMode(mode, live.model, live.reasoningEffort);
535
- await client.threadSettingsUpdate({
538
+ const desiredModel = live.model;
539
+ const desiredReasoningEffort = live.reasoningEffort;
540
+ const modelChanged = desiredModel !== live.appliedModel;
541
+ const effortChanged = desiredReasoningEffort !== live.appliedReasoningEffort;
542
+ const collaborationMode = mode && live.appliedCollaborationMode !== mode
543
+ ? await resolveLiveCollaborationMode(client, live, mode)
544
+ : undefined;
545
+ const settings = {
536
546
  threadId,
537
- collaborationMode,
538
- });
539
- live.appliedModel = collaborationMode.settings.model;
540
- live.appliedReasoningEffort = collaborationMode.settings.reasoning_effort ?? undefined;
541
- live.appliedCollaborationMode = mode;
542
- live.execution = { ...live.execution, collaborationMode: mode };
547
+ ...(modelChanged ? { model: desiredModel || null } : {}),
548
+ ...(effortChanged
549
+ ? { effort: desiredReasoningEffort ?? null }
550
+ : {}),
551
+ ...(collaborationMode ? { collaborationMode } : {}),
552
+ };
553
+ if (Object.keys(settings).length === 1)
554
+ return;
555
+ await client.threadSettingsUpdate(settings);
556
+ if (collaborationMode) {
557
+ if (!live.model)
558
+ live.model = collaborationMode.settings.model;
559
+ live.mayUseProviderDefaultModel = false;
560
+ live.appliedModel = collaborationMode.settings.model;
561
+ live.appliedReasoningEffort =
562
+ collaborationMode.settings.reasoning_effort ?? undefined;
563
+ live.appliedCollaborationMode = mode;
564
+ live.execution = { ...live.execution, collaborationMode: mode };
565
+ }
566
+ else {
567
+ if (modelChanged)
568
+ live.appliedModel = desiredModel;
569
+ if (effortChanged)
570
+ live.appliedReasoningEffort = desiredReasoningEffort;
571
+ }
543
572
  }
544
573
  finally {
545
574
  if (!suppliedClient)
@@ -689,6 +718,7 @@ export class LocalAgentHost {
689
718
  this.stopLiveCodexSession(localThreadId);
690
719
  }
691
720
  const record = await this.sessionStore.get(localThreadId);
721
+ const coldResume = Boolean(record?.codexSessionId);
692
722
  const retargetMirror = opts.retargetMirror;
693
723
  const workspace = structuredClone(opts.workspace);
694
724
  const execution = structuredClone(opts.execution);
@@ -755,7 +785,7 @@ export class LocalAgentHost {
755
785
  // files or confusing `model/list.isDefault` with this Session's model.
756
786
  let configModel;
757
787
  let configReasoningEffort;
758
- if (!execution.model || !execution.reasoningEffort) {
788
+ if (!coldResume && (!execution.model || !execution.reasoningEffort)) {
759
789
  try {
760
790
  const effective = await appServerOwner.configRead({
761
791
  includeLayers: false,
@@ -792,7 +822,6 @@ export class LocalAgentHost {
792
822
  // its observer starts in the background only after the replacement TUI has
793
823
  // launched and is never part of the resume admission boundary.
794
824
  const forwarderClient = this.forwarderClientFactory(appServerUrl);
795
- const coldResume = Boolean(record?.codexSessionId);
796
825
  if (!coldResume) {
797
826
  try {
798
827
  await forwarderClient.ensureInitialized();
@@ -803,8 +832,9 @@ export class LocalAgentHost {
803
832
  return abandon();
804
833
  }
805
834
  }
806
- const model = execution.model?.trim() || configModel || "";
807
- const reasoningEffort = execution.reasoningEffort ?? configReasoningEffort;
835
+ const model = execution.model?.trim() || (!coldResume ? configModel : undefined) || "";
836
+ const reasoningEffort = execution.reasoningEffort ??
837
+ (!coldResume ? configReasoningEffort : undefined);
808
838
  let markReady;
809
839
  const ready = new Promise((resolve) => {
810
840
  markReady = resolve;
@@ -829,8 +859,9 @@ export class LocalAgentHost {
829
859
  approvalPolicy,
830
860
  model,
831
861
  reasoningEffort,
832
- appliedModel: model,
833
- appliedReasoningEffort: reasoningEffort,
862
+ mayUseProviderDefaultModel: !coldResume && !model,
863
+ appliedModel: coldResume ? "" : model,
864
+ appliedReasoningEffort: coldResume ? undefined : reasoningEffort,
834
865
  appliedCollaborationMode: undefined,
835
866
  ...(execution.instructions ? { instructions: execution.instructions } : {}),
836
867
  threadId: record?.codexSessionId ?? null,
@@ -851,6 +882,7 @@ export class LocalAgentHost {
851
882
  rotationPending: false,
852
883
  stopped: false,
853
884
  observerAvailable: !coldResume,
885
+ threadActive: false,
854
886
  skillsCleanup: snapshotSkills.skillsCleanup,
855
887
  canonicalInteractions: new Set(),
856
888
  settledInteractions: new Set(),
@@ -1055,7 +1087,7 @@ export class LocalAgentHost {
1055
1087
  const client = this.injectionClientFactory(live.appServerUrl);
1056
1088
  try {
1057
1089
  await client.ensureInitialized();
1058
- const collaborationMode = buildCollaborationMode("default", live.model, live.reasoningEffort);
1090
+ const collaborationMode = await resolveLiveCollaborationMode(client, live, "default");
1059
1091
  let threadId = prompt.threadId;
1060
1092
  let text = CODEX_PLAN_IMPLEMENTATION_CODING_MESSAGE;
1061
1093
  if (choice === CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT) {
@@ -1087,6 +1119,12 @@ export class LocalAgentHost {
1087
1119
  approvalPolicy: live.approvalPolicy,
1088
1120
  collaborationMode,
1089
1121
  });
1122
+ if (!live.model)
1123
+ live.model = collaborationMode.settings.model;
1124
+ live.mayUseProviderDefaultModel = false;
1125
+ live.appliedModel = collaborationMode.settings.model;
1126
+ live.appliedReasoningEffort =
1127
+ collaborationMode.settings.reasoning_effort ?? undefined;
1090
1128
  live.execution = { ...live.execution, collaborationMode: "default" };
1091
1129
  live.appliedCollaborationMode = "default";
1092
1130
  emitCurrent({
@@ -1313,7 +1351,10 @@ export class LocalAgentHost {
1313
1351
  // A resumed TUI may rebroadcast `thread/started`; binding is idempotent.
1314
1352
  shouldIgnoreThreadStarted: (threadId, forkedFromId) => this.shouldIgnoreManagedForkThreadStarted(live, threadId, forkedFromId),
1315
1353
  onThreadStarted: (threadId, forkedFromId) => this.onLiveThreadStarted(live, localThreadId, threadId, forkedFromId),
1316
- onThreadActive: () => live.releaseActive?.(),
1354
+ onThreadActive: () => {
1355
+ live.threadActive = true;
1356
+ live.releaseActive?.();
1357
+ },
1317
1358
  onCollaborationModeChanged: (mode) => {
1318
1359
  live.appliedCollaborationMode = mode;
1319
1360
  live.execution = { ...live.execution, collaborationMode: mode };
@@ -1327,6 +1368,7 @@ export class LocalAgentHost {
1327
1368
  if (reportedModel) {
1328
1369
  live.model = reportedModel;
1329
1370
  live.appliedModel = reportedModel;
1371
+ live.mayUseProviderDefaultModel = false;
1330
1372
  }
1331
1373
  if (reportedEffort !== undefined) {
1332
1374
  live.reasoningEffort = reportedEffort ?? undefined;
@@ -1442,9 +1484,7 @@ export class LocalAgentHost {
1442
1484
  excludeTurns: true,
1443
1485
  });
1444
1486
  adoptResumedThreadSettings(live, resumed);
1445
- if (execution.collaborationMode) {
1446
- await this.applyLiveCollaborationMode(live, resumed.threadId, execution.collaborationMode, preloadClient);
1447
- }
1487
+ await this.applyLiveThreadSettings(live, resumed.threadId, execution.collaborationMode, preloadClient);
1448
1488
  }
1449
1489
  finally {
1450
1490
  await preloadClient.stop().catch((error) => {
@@ -1466,6 +1506,7 @@ export class LocalAgentHost {
1466
1506
  // not classified here because it does not prove the history is empty.
1467
1507
  await this.sessionStore.delete(localThreadId);
1468
1508
  live.threadId = null;
1509
+ live.mayUseProviderDefaultModel = !live.model;
1469
1510
  try {
1470
1511
  await forwarderClient.ensureInitialized();
1471
1512
  }
@@ -1503,6 +1544,11 @@ export class LocalAgentHost {
1503
1544
  * it, unblock injection, and kick off the resume-subscribe loop (once). */
1504
1545
  onLiveThreadStarted(live, localThreadId, threadId, forkedFromId) {
1505
1546
  const previousThreadId = live.threadId;
1547
+ if (previousThreadId !== threadId) {
1548
+ live.threadActive = false;
1549
+ if (!live.model)
1550
+ live.mayUseProviderDefaultModel = true;
1551
+ }
1506
1552
  if (previousThreadId && previousThreadId !== threadId) {
1507
1553
  live.threadId = threadId;
1508
1554
  live.subscribing = true;
@@ -1654,13 +1700,20 @@ export class LocalAgentHost {
1654
1700
  return false;
1655
1701
  }
1656
1702
  sawNotReady = true;
1657
- // Park until the thread goes active (its first turn materializes the
1658
- // rollout); a short poll covers the flush race after "active".
1659
- await new Promise((resolve) => {
1660
- live.releaseActive = resolve;
1661
- setTimeout(resolve, 1500);
1662
- });
1663
- live.releaseActive = undefined;
1703
+ if (!live.threadActive) {
1704
+ // An idle fresh thread has no rollout until its first Turn. Wait only
1705
+ // for live activity instead of polling the app-server indefinitely.
1706
+ await new Promise((resolve) => {
1707
+ live.releaseActive = resolve;
1708
+ });
1709
+ live.releaseActive = undefined;
1710
+ }
1711
+ else {
1712
+ // Activity can precede rollout persistence by a short interval.
1713
+ await new Promise((resolve) => {
1714
+ setTimeout(resolve, CODEX_SUBSCRIBE_FLUSH_RETRY_MS);
1715
+ });
1716
+ }
1664
1717
  }
1665
1718
  }
1666
1719
  return false;
@@ -1891,7 +1944,7 @@ export class LocalAgentHost {
1891
1944
  const desiredCollaborationMode = options?.collaborationMode ?? live.execution.collaborationMode;
1892
1945
  const collaborationMode = desiredCollaborationMode &&
1893
1946
  desiredCollaborationMode !== live.appliedCollaborationMode
1894
- ? buildCollaborationMode(desiredCollaborationMode, desiredModel, desiredReasoningEffort)
1947
+ ? await resolveLiveCollaborationMode(injectionClient, live, desiredCollaborationMode)
1895
1948
  : undefined;
1896
1949
  const settings = {
1897
1950
  threadId,
@@ -1908,8 +1961,15 @@ export class LocalAgentHost {
1908
1961
  // Record exactly what this RPC carried. A concurrent settings save may
1909
1962
  // already have advanced the desired fields and must remain pending for
1910
1963
  // the following new Turn.
1911
- live.appliedModel = desiredModel;
1912
- live.appliedReasoningEffort = desiredReasoningEffort;
1964
+ if (collaborationMode && !live.model) {
1965
+ live.model = collaborationMode.settings.model;
1966
+ }
1967
+ if (collaborationMode)
1968
+ live.mayUseProviderDefaultModel = false;
1969
+ live.appliedModel = collaborationMode?.settings.model ?? desiredModel;
1970
+ live.appliedReasoningEffort = collaborationMode
1971
+ ? collaborationMode.settings.reasoning_effort ?? undefined
1972
+ : desiredReasoningEffort;
1913
1973
  if (desiredCollaborationMode) {
1914
1974
  live.appliedCollaborationMode = desiredCollaborationMode;
1915
1975
  live.execution = {
@@ -2944,6 +3004,7 @@ function raceReady(ready, timeoutMs, failed) {
2944
3004
  function adoptResumedThreadSettings(live, settings) {
2945
3005
  const model = settings.model?.trim();
2946
3006
  if (model) {
3007
+ live.mayUseProviderDefaultModel = false;
2947
3008
  live.appliedModel = model;
2948
3009
  if (!live.execution.model)
2949
3010
  live.model = model;
@@ -2955,9 +3016,32 @@ function adoptResumedThreadSettings(live, settings) {
2955
3016
  }
2956
3017
  }
2957
3018
  }
3019
+ /** Resolve the model required by a full collaboration-mode snapshot. Only a
3020
+ * fresh thread with no explicit/config model may use the catalog default; a
3021
+ * resumed thread must report its own authoritative model. */
3022
+ async function resolveLiveCollaborationMode(client, live, mode) {
3023
+ let model = live.model.trim();
3024
+ if (!model && live.mayUseProviderDefaultModel) {
3025
+ let catalog;
3026
+ try {
3027
+ catalog = await client.modelList();
3028
+ }
3029
+ catch (error) {
3030
+ throw new CodexRuntimeError(`collaboration mode requires the Provider default model: ${error instanceof Error ? error.message : String(error)}`, 503, "collaboration_mode_model_unknown");
3031
+ }
3032
+ const defaults = [...new Set(catalog.data
3033
+ .filter((candidate) => candidate.isDefault === true)
3034
+ .map((candidate) => (candidate.model || candidate.id).trim())
3035
+ .filter(Boolean))];
3036
+ if (defaults.length !== 1) {
3037
+ throw new CodexRuntimeError(`collaboration mode requires exactly one Provider default model; Provider advertised ${defaults.length}`, 503, "collaboration_mode_model_unknown");
3038
+ }
3039
+ model = defaults[0];
3040
+ }
3041
+ return buildCollaborationMode(mode, model, live.reasoningEffort);
3042
+ }
2958
3043
  /** Expand the public mode enum into the App Server's required settings
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. */
3044
+ * snapshot using the Session's tracked effective model. */
2961
3045
  function buildCollaborationMode(mode, desiredModel, desiredReasoningEffort) {
2962
3046
  const model = desiredModel.trim();
2963
3047
  if (!model) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/runtime",
3
- "version": "0.1.11-beta.29",
3
+ "version": "0.1.11-beta.30",
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.29"
30
+ "@rynx-ai/core": "0.1.11-beta.30"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/ws": "^8.18.1"