@rynx-ai/server 0.1.11-beta.23 → 0.1.11-beta.25

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.
@@ -7,8 +7,8 @@
7
7
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
8
  <link rel="icon" type="image/png" href="/favicon.png" />
9
9
  <title>rynx · control</title>
10
- <script type="module" crossorigin src="/assets/index-D-5t2eCT.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-0noHpRXD.css">
10
+ <script type="module" crossorigin src="/assets/index-C1ODT189.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-DOUyXr2J.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="root"></div>
@@ -1,4 +1,4 @@
1
- import { type AdmissionReservation, type AgentRuntimeId, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionWorkspaceSnapshot, type SessionProviderId, type MachineSessionRecord, type LiveSessionFailure, type ReasoningEffort, type RuntimeUserInput, type SessionBus, type SessionLogStore, type SessionRegistry } from "@rynx-ai/core";
1
+ import { type AdmissionReservation, type AgentRuntimeId, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionWorkspaceSnapshot, type SessionProviderId, type MachineSessionRecord, type LiveSessionFailure, type ReasoningEffort, type RuntimeUserInput, type SessionCollaborationMode, type SessionBus, type SessionLogStore, type SessionRegistry } from "@rynx-ai/core";
2
2
  import type { SequencedSessionEvent, SessionEvent, SessionInteractionResolution, SessionItem, UserContentPart } from "@rynx-ai/protocol";
3
3
  import type { RemoteRuntimeSessionResourceDeleteParams, RemoteRuntimeSessionResourceDeleteResult, RemoteRuntimeSessionResourceGetParams, RemoteRuntimeSessionResourceGetResult, RemoteRuntimeSessionResourcePolicyResult, RemoteRuntimeSessionResourceReadParams, RemoteRuntimeSessionResourceReadResult, RemoteRuntimeSessionResourceUploadBeginParams, RemoteRuntimeSessionResourceUploadBeginResult, RemoteRuntimeSessionResourceUploadChunkParams, RemoteRuntimeSessionResourceUploadChunkResult, RemoteRuntimeSessionResourceUploadCommitParams, RemoteRuntimeSessionResourceUploadCommitResult } from "@rynx-ai/protocol/remote-runtime-rpc";
4
4
  import type { ControlAgentModelOption, ControlAgentSummary, ControlSessionInteractionResolveDisposition, ControlSessionRuntimeSnapshot, SessionPermissionPreset } from "@rynx-ai/protocol/control";
@@ -69,6 +69,7 @@ export interface MachineSessionRunnerPort {
69
69
  lastLiveSessionError?(sessionId: string): string | undefined;
70
70
  lastLiveSessionFailure?(sessionId: string): LiveSessionFailure | undefined;
71
71
  injectMessage(sessionId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
72
+ updateCollaborationMode?(sessionId: string, mode: SessionCollaborationMode): Promise<void>;
72
73
  stopRunner(sessionId: string): void;
73
74
  resolveInteraction(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
74
75
  disposition: ControlSessionInteractionResolveDisposition;
@@ -312,11 +313,15 @@ export declare class MachineSessionService {
312
313
  launchOptions(): Promise<RemoteRuntimeSessionLaunchOptionsListResult>;
313
314
  /** Read the effective model defaults used for the next turn. */
314
315
  executionSettings(sessionIdInput: string): Promise<RemoteRuntimeSessionExecutionGetResult>;
315
- /** Replace the Turn defaults of an idle Session. Execution settings are
316
- * passed through to the Provider CLI, which owns availability validation.
317
- * The runner is closed so every Provider resumes its native context with
318
- * the new settings on the next turn. */
316
+ /** Replace the defaults for the next new Turn. Codex-lineage Providers keep
317
+ * their live runner: the host applies changed native thread settings under
318
+ * its injection lock immediately before the next `turn/start`. Claude still
319
+ * needs an idle cold resume because its live CLI has no confirmed settings
320
+ * update boundary. Provider CLIs own model availability validation. */
319
321
  updateExecutionSettings(input: RemoteRuntimeSessionExecutionUpdateParams): Promise<RemoteRuntimeSessionExecutionUpdateResult>;
322
+ /** Persist a mode changed from the native TUI before its reflected event is
323
+ * fanned out to Web clients. Unknown/deleted and Claude Sessions are ignored. */
324
+ recordNativeCollaborationMode(sessionIdInput: string, mode: SessionCollaborationMode): void;
320
325
  /** Create target-owned Session identity from one Agent preset or direct Provider. */
321
326
  create(input: MachineSessionCreateInput): Promise<MachineSessionCreateResult>;
322
327
  private createAdmitted;
@@ -210,10 +210,11 @@ export class MachineSessionService {
210
210
  }
211
211
  return executionSettingsResult(meta.execution);
212
212
  }
213
- /** Replace the Turn defaults of an idle Session. Execution settings are
214
- * passed through to the Provider CLI, which owns availability validation.
215
- * The runner is closed so every Provider resumes its native context with
216
- * the new settings on the next turn. */
213
+ /** Replace the defaults for the next new Turn. Codex-lineage Providers keep
214
+ * their live runner: the host applies changed native thread settings under
215
+ * its injection lock immediately before the next `turn/start`. Claude still
216
+ * needs an idle cold resume because its live CLI has no confirmed settings
217
+ * update boundary. Provider CLIs own model availability validation. */
217
218
  async updateExecutionSettings(input) {
218
219
  const sessionId = validOpaqueToken(input.sessionId, "sessionId", MAX_SESSION_ID_CHARS);
219
220
  const modelId = input.model === null ? null : validRequiredText(input.model, "model");
@@ -222,13 +223,15 @@ export class MachineSessionService {
222
223
  throw new MachineSessionServiceFailure("not_found", "session not found");
223
224
  }
224
225
  await this.assertNotForkReserved(sessionId);
225
- const runtime = this.ports.runtimeState.snapshot(sessionId);
226
- const pendingMessage = await this.ports.pendingMessages?.get(sessionId);
227
- if (runtime.status !== "idle" ||
228
- runtime.activeResponseIds.length > 0 ||
229
- runtime.pendingInteractions.length > 0 ||
230
- pendingMessage) {
231
- throw new MachineSessionServiceFailure("failed_precondition", "Session must be idle before changing model settings");
226
+ if (meta.execution.provider === "claude") {
227
+ const runtime = this.ports.runtimeState.snapshot(sessionId);
228
+ const pendingMessage = await this.ports.pendingMessages?.get(sessionId);
229
+ if (runtime.status !== "idle" ||
230
+ runtime.activeResponseIds.length > 0 ||
231
+ runtime.pendingInteractions.length > 0 ||
232
+ pendingMessage) {
233
+ throw new MachineSessionServiceFailure("failed_precondition", "Claude Session must be idle before changing model settings");
234
+ }
232
235
  }
233
236
  const executionPort = this.requireExecution();
234
237
  const reasoningEffort = input.reasoningEffort === null
@@ -238,11 +241,51 @@ export class MachineSessionService {
238
241
  ...structuredClone(meta.execution),
239
242
  model: modelId,
240
243
  reasoningEffort,
244
+ ...(meta.execution.provider !== "claude" && input.collaborationMode !== undefined
245
+ ? { collaborationMode: input.collaborationMode }
246
+ : {}),
241
247
  };
248
+ if (meta.execution.provider !== "claude" && input.collaborationMode !== undefined) {
249
+ const updateMode = executionPort.runner.updateCollaborationMode;
250
+ if (!updateMode) {
251
+ const message = "This Runtime does not support live collaboration-mode updates";
252
+ throw new MachineSessionServiceFailure("failed_precondition", message, message, {
253
+ message,
254
+ code: "collaboration_mode_unavailable",
255
+ statusCode: 409,
256
+ });
257
+ }
258
+ // Omnigent parity: native state is authoritative. Do not make a failed
259
+ // toggle survive reload when the loaded thread rejected it.
260
+ try {
261
+ await updateMode.call(executionPort.runner, sessionId, input.collaborationMode);
262
+ }
263
+ catch (error) {
264
+ const failure = liveSessionFailureFrom(error);
265
+ const message = collaborationModeFailureMessage(meta.execution.provider, failure, error instanceof Error ? error.message : "Native collaboration mode update failed");
266
+ throw new MachineSessionServiceFailure("failed_precondition", message, message, failure ? { ...failure, message } : undefined);
267
+ }
268
+ }
242
269
  this.ports.registry.setExecution(sessionId, nextExecution);
243
- executionPort.runner.stopRunner(sessionId);
270
+ if (nextExecution.provider === "claude") {
271
+ executionPort.runner.stopRunner(sessionId);
272
+ }
244
273
  return executionSettingsResult(nextExecution);
245
274
  }
275
+ /** Persist a mode changed from the native TUI before its reflected event is
276
+ * fanned out to Web clients. Unknown/deleted and Claude Sessions are ignored. */
277
+ recordNativeCollaborationMode(sessionIdInput, mode) {
278
+ const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
279
+ const meta = this.ports.registry.get(sessionId);
280
+ if (!meta || meta.execution.provider === "claude")
281
+ return;
282
+ if (meta.execution.collaborationMode === mode)
283
+ return;
284
+ this.ports.registry.setExecution(sessionId, {
285
+ ...structuredClone(meta.execution),
286
+ collaborationMode: mode,
287
+ });
288
+ }
246
289
  /** Create target-owned Session identity from one Agent preset or direct Provider. */
247
290
  async create(input) {
248
291
  return this.withAdmission(() => this.createAdmitted(input));
@@ -1016,6 +1059,7 @@ function executionSettingsResult(execution) {
1016
1059
  provider: execution.provider,
1017
1060
  model: execution.model,
1018
1061
  reasoningEffort: execution.reasoningEffort,
1062
+ collaborationMode: execution.collaborationMode ?? null,
1019
1063
  };
1020
1064
  }
1021
1065
  function compareMetaRecency(left, right) {
@@ -1108,6 +1152,20 @@ function liveSessionFailureFrom(error) {
1108
1152
  statusCode: candidate.statusCode,
1109
1153
  };
1110
1154
  }
1155
+ /** Turn structured native mode failures into actionable, Provider-specific
1156
+ * messages before the Direct Runtime adapter crosses the process boundary.
1157
+ * Unstructured failures keep their original text and remain subject to the
1158
+ * adapter's normal redaction policy. */
1159
+ function collaborationModeFailureMessage(provider, failure, fallback) {
1160
+ const providerLabel = provider === "traex" ? "TraeX" : "Codex";
1161
+ if (failure?.code === "native_session_not_live") {
1162
+ return `No live ${providerLabel} runner is available. Reconnect this Session and try again.`;
1163
+ }
1164
+ if (failure?.code === "native_thread_not_ready") {
1165
+ return `${providerLabel} is still loading its native thread. Reconnect this Session and try again.`;
1166
+ }
1167
+ return failure?.message ?? fallback;
1168
+ }
1111
1169
  function requestLimit(value, fallback, maximum, field) {
1112
1170
  return value === undefined ? Math.min(fallback, maximum) : boundedInteger(value, 1, maximum, field);
1113
1171
  }
@@ -530,6 +530,7 @@ function assertEventScope(event, expectedSessionId) {
530
530
  let actual;
531
531
  switch (event.type) {
532
532
  case "session.status":
533
+ case "session.collaboration_mode":
533
534
  case "response.created":
534
535
  case "session.interaction.requested":
535
536
  case "session.interaction.resolved":
package/dist/server.js CHANGED
@@ -257,6 +257,11 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
257
257
  kind: r.kind,
258
258
  });
259
259
  });
260
+ if (localMachineSessions) {
261
+ runnerManager.onCollaborationMode((sessionId, mode) => {
262
+ localMachineSessions.recordNativeCollaborationMode(sessionId, mode);
263
+ });
264
+ }
260
265
  const app = createApp({
261
266
  config,
262
267
  control: controlSurface,
@@ -21,4 +21,6 @@ export declare class SessionRuntimeIndex {
21
21
  private state;
22
22
  private removeResponseInteractions;
23
23
  private hasRunningTurn;
24
+ private statusAfterResponseTerminal;
25
+ private retireStatusResponses;
24
26
  }
@@ -17,29 +17,51 @@ export class SessionRuntimeIndex {
17
17
  case "response.completed":
18
18
  state.activeResponseIds.delete(event.responseId);
19
19
  this.removeResponseInteractions(state, event.responseId);
20
- state.status = this.hasRunningTurn(state) ? "running" : "idle";
20
+ state.status = this.statusAfterResponseTerminal(state);
21
21
  state.statusKind = undefined;
22
22
  return;
23
23
  case "response.failed":
24
24
  state.activeResponseIds.delete(event.responseId);
25
25
  this.removeResponseInteractions(state, event.responseId);
26
26
  // Response failure is Turn-local. It must not poison a reusable Session.
27
- state.status = this.hasRunningTurn(state) ? "running" : "idle";
27
+ state.status = this.statusAfterResponseTerminal(state);
28
28
  state.statusKind = undefined;
29
29
  return;
30
30
  case "session.interrupted":
31
31
  state.activeResponseIds.delete(event.responseId);
32
32
  this.removeResponseInteractions(state, event.responseId);
33
- state.status = this.hasRunningTurn(state) ? "running" : "idle";
33
+ state.status = this.statusAfterResponseTerminal(state);
34
34
  state.statusKind = undefined;
35
35
  return;
36
- case "session.status":
37
- state.status =
38
- state.pendingInteractions.size > 0 || this.hasRunningTurn(state)
39
- ? "running"
40
- : event.status;
41
- state.statusKind = event.statusKind;
36
+ case "session.status": {
37
+ if (event.status === "running") {
38
+ if (event.responseId)
39
+ state.activeResponseIds.add(event.responseId);
40
+ state.status = "running";
41
+ state.statusKind = event.statusKind;
42
+ return;
43
+ }
44
+ // A provider interaction is a stronger in-flight fact than a quiet-pane
45
+ // idle observation. Claude's status-file path reports waiting as running;
46
+ // this guard preserves the same truth on its legacy PTY fallback.
47
+ if (event.status === "idle" && state.pendingInteractions.size > 0) {
48
+ state.status = "running";
49
+ state.statusKind = undefined;
50
+ return;
51
+ }
52
+ this.retireStatusResponses(state);
53
+ if (event.status === "failed") {
54
+ state.status = "failed";
55
+ }
56
+ else if (state.status !== "failed") {
57
+ // Provider idle is authoritative even when final transcript records
58
+ // arrive later. Response identity remains on those events themselves;
59
+ // it must not keep Session activity/loading alive.
60
+ state.status = this.hasRunningTurn(state) ? "running" : "idle";
61
+ }
62
+ state.statusKind = undefined;
42
63
  return;
64
+ }
43
65
  case "session.interaction.requested":
44
66
  state.pendingInteractions.set(event.interaction.interactionId, {
45
67
  responseId: event.responseId,
@@ -52,7 +74,8 @@ export class SessionRuntimeIndex {
52
74
  case "session.interaction.resolved":
53
75
  case "session.interaction.cancelled":
54
76
  state.pendingInteractions.delete(event.interactionId);
55
- state.status = this.hasRunningTurn(state) ? "running" : state.status;
77
+ if (this.hasRunningTurn(state))
78
+ state.status = "running";
56
79
  return;
57
80
  case "session.rotated":
58
81
  state.activeResponseIds.clear();
@@ -69,9 +92,7 @@ export class SessionRuntimeIndex {
69
92
  if (!state)
70
93
  return { status: "idle", activeResponseIds: [], pendingInteractions: [] };
71
94
  return {
72
- status: state.pendingInteractions.size > 0 || this.hasRunningTurn(state)
73
- ? "running"
74
- : state.status,
95
+ status: state.pendingInteractions.size > 0 ? "running" : state.status,
75
96
  ...(state.statusKind === undefined ? {} : { statusKind: state.statusKind }),
76
97
  activeResponseIds: [...state.activeResponseIds],
77
98
  pendingInteractions: [...state.pendingInteractions.values()],
@@ -82,7 +103,9 @@ export class SessionRuntimeIndex {
82
103
  let runningTurns = 0;
83
104
  let pendingInteractions = 0;
84
105
  for (const state of this.sessions.values()) {
85
- runningTurns += state.activeResponseIds.size;
106
+ if (state.status === "running" || state.pendingInteractions.size > 0) {
107
+ runningTurns += Math.max(1, state.activeResponseIds.size);
108
+ }
86
109
  pendingInteractions += state.pendingInteractions.size;
87
110
  }
88
111
  return { runningTurns, pendingInteractions };
@@ -111,4 +134,18 @@ export class SessionRuntimeIndex {
111
134
  hasRunningTurn(state) {
112
135
  return state.activeResponseIds.size > 0;
113
136
  }
137
+ statusAfterResponseTerminal(state) {
138
+ if (state.pendingInteractions.size > 0 || this.hasRunningTurn(state))
139
+ return "running";
140
+ return state.status === "failed" ? "failed" : "idle";
141
+ }
142
+ retireStatusResponses(state) {
143
+ // session.status is Session-wide. Match reference implementation's
144
+ // _session_active_response_cache: every idle/failed edge closes the one
145
+ // in-flight native response projection, even when the edge happens to be
146
+ // stamped with that response id. A late transcript event may still carry
147
+ // the old id for grouping, but it cannot keep activity alive.
148
+ state.activeResponseIds.clear();
149
+ state.pendingInteractions.clear();
150
+ }
114
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/server",
3
- "version": "0.1.11-beta.23",
3
+ "version": "0.1.11-beta.25",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -27,15 +27,15 @@
27
27
  "@koa/router": "^15.4.0",
28
28
  "koa": "^3.2.0",
29
29
  "ws": "^8.21.0",
30
- "@rynx-ai/core": "0.1.11-beta.23",
31
- "@rynx-ai/plugin-sdk": "0.1.11-beta.23",
32
- "@rynx-ai/protocol": "0.1.11-beta.23",
33
- "@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.23",
34
- "@rynx-ai/runtime": "0.1.11-beta.23"
30
+ "@rynx-ai/plugin-sdk": "0.1.11-beta.25",
31
+ "@rynx-ai/protocol": "0.1.11-beta.25",
32
+ "@rynx-ai/core": "0.1.11-beta.25",
33
+ "@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.25",
34
+ "@rynx-ai/runtime": "0.1.11-beta.25"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/ws": "^8.18.1",
38
- "@rynx-ai/control-web": "0.1.11-beta.23"
38
+ "@rynx-ai/control-web": "0.1.11-beta.25"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "rm -rf dist && tsc -p tsconfig.json && mkdir -p dist/control-web && cp -R ../control-web/dist/. dist/control-web/",