@rynx-ai/server 0.1.11-beta.2 → 0.1.11-beta.20

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-BnX7NWJX.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-CUoWRX2i.css">
10
+ <script type="module" crossorigin src="/assets/index-BKgzgxRn.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-5n6D-i-c.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="root"></div>
@@ -3,4 +3,4 @@ import type { Context } from "koa";
3
3
  export declare function resolveControlWebDist(): string | null;
4
4
  /** Serve a static file from the SPA dir; missing non-asset paths fall back to
5
5
  * index.html (client-side routing), missing assets return 404. */
6
- export declare function sendSpaFile(ctx: Context, distDir: string, basePath?: string): Promise<void>;
6
+ export declare function sendSpaFile(ctx: Context, distDir: string, basePath?: string, transformIndex?: (html: string) => string): Promise<void>;
@@ -37,7 +37,7 @@ export function resolveControlWebDist() {
37
37
  }
38
38
  /** Serve a static file from the SPA dir; missing non-asset paths fall back to
39
39
  * index.html (client-side routing), missing assets return 404. */
40
- export async function sendSpaFile(ctx, distDir, basePath = "/") {
40
+ export async function sendSpaFile(ctx, distDir, basePath = "/", transformIndex) {
41
41
  const path = basePath !== "/" && ctx.path.startsWith(basePath)
42
42
  ? ctx.path.slice(basePath.length) || "/"
43
43
  : ctx.path;
@@ -64,5 +64,7 @@ export async function sendSpaFile(ctx, distDir, basePath = "/") {
64
64
  }
65
65
  }
66
66
  ctx.type = CONTENT_TYPES[ext] ?? "application/octet-stream";
67
- ctx.body = data;
67
+ ctx.body = ext === ".html" && transformIndex
68
+ ? transformIndex(data.toString("utf8"))
69
+ : data;
68
70
  }
@@ -1,11 +1,12 @@
1
- import { type AgentRuntimeId, type ConversationRuntime, type ResolvedExecutionSnapshot, type SessionWorkspaceSnapshot, type SessionProviderId, type MachineSessionRecord, 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 ReasoningEffort, type RuntimeUserInput, 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
- import type { ControlAgentSummary, ControlSessionInteractionResolveDisposition, ControlSessionRuntimeSnapshot } from "@rynx-ai/protocol/control";
5
- import { type RemoteRuntimeSessionInterruptResult, type RemoteRuntimeSessionLaunchOptionsListResult, type RemoteRuntimeSessionListParams, type RemoteRuntimeSessionListResult, type RemoteRuntimeSessionSnapshotParams } from "@rynx-ai/protocol/remote-runtime-rpc";
4
+ import type { ControlAgentModelOption, ControlAgentSummary, ControlSessionInteractionResolveDisposition, ControlSessionRuntimeSnapshot, SessionPermissionPreset } from "@rynx-ai/protocol/control";
5
+ import { type RemoteRuntimeSessionInterruptResult, type RemoteRuntimeSessionExecutionGetResult, type RemoteRuntimeSessionExecutionUpdateParams, type RemoteRuntimeSessionExecutionUpdateResult, type RemoteRuntimeSessionLaunchOptionsListResult, type RemoteRuntimeSessionListParams, type RemoteRuntimeSessionListResult, type RemoteRuntimeSessionSnapshotParams } from "@rynx-ai/protocol/remote-runtime-rpc";
6
6
  import type { CodexRuntimeStatus, InjectOutcome } from "@rynx-ai/runtime";
7
7
  export interface MachineSessionRuntimeStatePort {
8
8
  snapshot(sessionId: string): ControlSessionRuntimeSnapshot;
9
+ beginResponseHandoff?(sessionId: string, responseId: string): () => void;
9
10
  remove?(sessionId: string): void;
10
11
  }
11
12
  export interface MachineSessionInterruptPort {
@@ -129,7 +130,7 @@ export interface MachineSessionForkStorePort {
129
130
  markTargetDeleted(sessionId: string): void | Promise<void>;
130
131
  }
131
132
  export interface MachineSessionServicePorts {
132
- registry: Pick<SessionRegistry, "create" | "get" | "list" | "setTitle" | "remove">;
133
+ registry: Pick<SessionRegistry, "create" | "get" | "list" | "setTitle" | "setExecution" | "remove">;
133
134
  log: Pick<SessionLogStore, "list" | "listSessions" | "snapshot" | "deleteSession">;
134
135
  events: Pick<SessionBus, "subscribe"> & Partial<Pick<SessionBus, "close">>;
135
136
  /** Canonical persistence + fan-out for user turns accepted before a native
@@ -153,10 +154,12 @@ export interface MachineSessionServicePorts {
153
154
  resolve(input: {
154
155
  agent?: string;
155
156
  provider?: SessionProviderId;
156
- model?: string;
157
+ model?: string | null;
157
158
  reasoningEffort?: ReasoningEffort;
159
+ permissionPreset?: SessionPermissionPreset;
158
160
  cwd: string;
159
161
  }): Promise<ResolvedExecutionSnapshot>;
162
+ listModels?(provider: SessionProviderId): Promise<ControlAgentModelOption[]>;
160
163
  runner: MachineSessionRunnerPort;
161
164
  };
162
165
  /** Resolve a temporary Project selection, or create a Session-only fallback directory. */
@@ -181,6 +184,11 @@ export interface MachineSessionServiceOptions {
181
184
  directoryScanLimit?: number;
182
185
  /** Delay between setup-readiness retries for durable pending messages. */
183
186
  pendingMessageRetryMs?: number;
187
+ /** Dynamic daemon-wide admission fence used during process replacement. */
188
+ admissionOpen?: () => boolean;
189
+ /** Atomically reserves one work-producing admission until it is rejected or
190
+ * has become visible in the daemon activity projection. */
191
+ admissionReserve?: () => AdmissionReservation | undefined;
184
192
  }
185
193
  export type MachineSessionListInput = RemoteRuntimeSessionListParams;
186
194
  export interface MachineSessionListPage extends RemoteRuntimeSessionListResult {
@@ -198,8 +206,9 @@ export interface MachineSessionSnapshotPage {
198
206
  }
199
207
  export type MachineSessionInterruptResult = RemoteRuntimeSessionInterruptResult;
200
208
  interface MachineSessionCreateOptions {
201
- model?: string;
209
+ model?: string | null;
202
210
  reasoningEffort?: ReasoningEffort;
211
+ permissionPreset?: SessionPermissionPreset;
203
212
  title?: string;
204
213
  projectId?: string;
205
214
  }
@@ -277,6 +286,8 @@ export declare class MachineSessionService {
277
286
  private readonly snapshotPageMaxBytes;
278
287
  private readonly directoryScanLimit;
279
288
  private readonly pendingMessageRetryMs;
289
+ private readonly admissionOpen;
290
+ private readonly admissionReserve;
280
291
  private readonly pendingDeliveries;
281
292
  private readonly cancelledPendingDeliveries;
282
293
  private readonly forkTasks;
@@ -298,12 +309,21 @@ export declare class MachineSessionService {
298
309
  }>;
299
310
  /** Target-owned Provider readiness plus the existing Agent preset catalog. */
300
311
  launchOptions(): Promise<RemoteRuntimeSessionLaunchOptionsListResult>;
312
+ /** Read the effective model defaults used for the next turn. */
313
+ executionSettings(sessionIdInput: string): Promise<RemoteRuntimeSessionExecutionGetResult>;
314
+ /** Replace the Turn defaults of an idle Session. Execution settings are
315
+ * passed through to the Provider CLI, which owns availability validation.
316
+ * The runner is closed so every Provider resumes its native context with
317
+ * the new settings on the next turn. */
318
+ updateExecutionSettings(input: RemoteRuntimeSessionExecutionUpdateParams): Promise<RemoteRuntimeSessionExecutionUpdateResult>;
301
319
  /** Create target-owned Session identity from one Agent preset or direct Provider. */
302
320
  create(input: MachineSessionCreateInput): Promise<MachineSessionCreateResult>;
321
+ private createAdmitted;
303
322
  /** Create one independent Session at the source's stable Provider/canonical
304
323
  * boundary. Project and Agent selectors are intentionally absent: the target
305
324
  * receives exact copies of the source's already-frozen snapshots. */
306
325
  fork(input: MachineSessionForkInput): Promise<MachineSessionForkResult>;
326
+ private forkAdmitted;
307
327
  /** Publish a Provider TUI `/clear` or `/fork` after its native binding has
308
328
  * already been persisted. The source snapshots remain the only workspace and
309
329
  * execution authority; Provider events cannot alter them during rotation. */
@@ -312,6 +332,7 @@ export declare class MachineSessionService {
312
332
  private assertForkableSource;
313
333
  /** Inject one turn through the target daemon's native single-writer runner. */
314
334
  sendMessage(sessionIdInput: string, messageInput: string | MachineSessionMessageInput): Promise<MachineSessionMessageResult>;
335
+ private sendMessageAdmitted;
315
336
  resourcePolicy(sessionIdInput: string): RemoteRuntimeSessionResourcePolicyResult;
316
337
  beginResourceUpload(input: RemoteRuntimeSessionResourceUploadBeginParams): Promise<RemoteRuntimeSessionResourceUploadBeginResult>;
317
338
  writeResourceUploadChunk(input: RemoteRuntimeSessionResourceUploadChunkParams): Promise<RemoteRuntimeSessionResourceUploadChunkResult>;
@@ -323,13 +344,17 @@ export declare class MachineSessionService {
323
344
  * delivery worker starts the pane now, waits for a real native thread, then
324
345
  * injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
325
346
  enqueueMessage(sessionIdInput: string, messageInput: string): Promise<MachineSessionMessageEnqueueResult>;
347
+ private enqueueMessageAdmitted;
326
348
  /** Explicitly restore the target daemon's live runner without starting a turn. */
327
349
  startTerminal(sessionIdInput: string): Promise<MachineSessionTerminalStartResult>;
350
+ private startTerminalAdmitted;
328
351
  private ensureLiveSession;
329
352
  private liveSessionRequest;
330
353
  resolveInteraction(sessionIdInput: string, interactionIdInput: string, resolution: SessionInteractionResolution): Promise<MachineSessionInteractionResult>;
331
354
  delete(sessionIdInput: string): Promise<MachineSessionDeleteResult>;
332
355
  private requireAgents;
356
+ private reserveAdmission;
357
+ private withAdmission;
333
358
  private requireResources;
334
359
  private requireResourceSession;
335
360
  private resumePendingDeliveries;
@@ -47,6 +47,8 @@ export class MachineSessionService {
47
47
  snapshotPageMaxBytes;
48
48
  directoryScanLimit;
49
49
  pendingMessageRetryMs;
50
+ admissionOpen;
51
+ admissionReserve;
50
52
  pendingDeliveries = new Map();
51
53
  cancelledPendingDeliveries = new Set();
52
54
  forkTasks = new Map();
@@ -58,6 +60,8 @@ export class MachineSessionService {
58
60
  this.snapshotPageMaxBytes = boundedInteger(options.snapshotPageMaxBytes ?? DEFAULT_SNAPSHOT_PAGE_MAX_BYTES, MIN_PAGE_MAX_BYTES, MAX_PAGE_MAX_BYTES, "snapshotPageMaxBytes");
59
61
  this.directoryScanLimit = boundedInteger(options.directoryScanLimit ?? DEFAULT_DIRECTORY_SCAN_LIMIT, 1, MAX_DIRECTORY_SCAN_LIMIT, "directoryScanLimit");
60
62
  this.pendingMessageRetryMs = boundedInteger(options.pendingMessageRetryMs ?? 1_000, 10, 60_000, "pendingMessageRetryMs");
63
+ this.admissionOpen = options.admissionOpen ?? (() => true);
64
+ this.admissionReserve = options.admissionReserve;
61
65
  void this.resumePendingDeliveries();
62
66
  void this.resumeReservedForks();
63
67
  }
@@ -155,11 +159,18 @@ export class MachineSessionService {
155
159
  const statusCache = new Map();
156
160
  const { agents } = await this.agentOptions();
157
161
  const [providers, launchAgents] = await Promise.all([
158
- Promise.all(SESSION_PROVIDER_IDS.map(async (provider) => ({
159
- id: provider,
160
- name: getRuntimeProfile(provider).displayName,
161
- ...await this.runtimeReadiness(provider, statusCache),
162
- }))),
162
+ Promise.all(SESSION_PROVIDER_IDS.map(async (provider) => {
163
+ const execution = this.ports.execution;
164
+ const models = execution?.listModels
165
+ ? await execution.listModels(provider).catch(() => [])
166
+ : undefined;
167
+ return {
168
+ id: provider,
169
+ name: getRuntimeProfile(provider).displayName,
170
+ ...await this.runtimeReadiness(provider, statusCache),
171
+ ...(models === undefined ? {} : { models }),
172
+ };
173
+ })),
163
174
  Promise.all(agents.map(async (agent) => {
164
175
  const execution = this.ports.execution;
165
176
  if (!execution) {
@@ -188,8 +199,53 @@ export class MachineSessionService {
188
199
  ]);
189
200
  return { providers, agents: launchAgents };
190
201
  }
202
+ /** Read the effective model defaults used for the next turn. */
203
+ async executionSettings(sessionIdInput) {
204
+ const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
205
+ const meta = this.ports.registry.get(sessionId);
206
+ if (!meta) {
207
+ throw new MachineSessionServiceFailure("not_found", "session not found");
208
+ }
209
+ return executionSettingsResult(meta.execution);
210
+ }
211
+ /** Replace the Turn defaults of an idle Session. Execution settings are
212
+ * passed through to the Provider CLI, which owns availability validation.
213
+ * The runner is closed so every Provider resumes its native context with
214
+ * the new settings on the next turn. */
215
+ async updateExecutionSettings(input) {
216
+ const sessionId = validOpaqueToken(input.sessionId, "sessionId", MAX_SESSION_ID_CHARS);
217
+ const modelId = input.model === null ? null : validRequiredText(input.model, "model");
218
+ const meta = this.ports.registry.get(sessionId);
219
+ if (!meta) {
220
+ throw new MachineSessionServiceFailure("not_found", "session not found");
221
+ }
222
+ await this.assertNotForkReserved(sessionId);
223
+ const runtime = this.ports.runtimeState.snapshot(sessionId);
224
+ const pendingMessage = await this.ports.pendingMessages?.get(sessionId);
225
+ if (runtime.status !== "idle" ||
226
+ runtime.activeResponseIds.length > 0 ||
227
+ runtime.pendingInteractions.length > 0 ||
228
+ pendingMessage) {
229
+ throw new MachineSessionServiceFailure("failed_precondition", "Session must be idle before changing model settings");
230
+ }
231
+ const executionPort = this.requireExecution();
232
+ const reasoningEffort = input.reasoningEffort === null
233
+ ? null
234
+ : validRequiredText(input.reasoningEffort, "reasoningEffort");
235
+ const nextExecution = {
236
+ ...structuredClone(meta.execution),
237
+ model: modelId,
238
+ reasoningEffort,
239
+ };
240
+ this.ports.registry.setExecution(sessionId, nextExecution);
241
+ executionPort.runner.stopRunner(sessionId);
242
+ return executionSettingsResult(nextExecution);
243
+ }
191
244
  /** Create target-owned Session identity from one Agent preset or direct Provider. */
192
245
  async create(input) {
246
+ return this.withAdmission(() => this.createAdmitted(input));
247
+ }
248
+ async createAdmitted(input) {
193
249
  const hasAgent = input.agent !== undefined;
194
250
  const hasProvider = input.provider !== undefined;
195
251
  if (hasAgent === hasProvider) {
@@ -216,6 +272,7 @@ export class MachineSessionService {
216
272
  ...target,
217
273
  ...(input.model === undefined ? {} : { model: input.model }),
218
274
  ...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
275
+ ...(input.permissionPreset === undefined ? {} : { permissionPreset: input.permissionPreset }),
219
276
  cwd: workspace.cwd,
220
277
  });
221
278
  const sessionId = newSessionId();
@@ -240,6 +297,9 @@ export class MachineSessionService {
240
297
  * boundary. Project and Agent selectors are intentionally absent: the target
241
298
  * receives exact copies of the source's already-frozen snapshots. */
242
299
  async fork(input) {
300
+ return this.withAdmission(() => this.forkAdmitted(input));
301
+ }
302
+ async forkAdmitted(input) {
243
303
  const sourceSessionId = validOpaqueToken(input.sourceSessionId, "sourceSessionId", MAX_SESSION_ID_CHARS);
244
304
  const operationId = validOpaqueToken(input.operationId, "operationId", MAX_SESSION_ID_CHARS);
245
305
  const title = input.title === undefined
@@ -383,6 +443,9 @@ export class MachineSessionService {
383
443
  }
384
444
  /** Inject one turn through the target daemon's native single-writer runner. */
385
445
  async sendMessage(sessionIdInput, messageInput) {
446
+ return this.withAdmission(() => this.sendMessageAdmitted(sessionIdInput, messageInput));
447
+ }
448
+ async sendMessageAdmitted(sessionIdInput, messageInput) {
386
449
  const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
387
450
  await this.assertNotForkReserved(sessionId);
388
451
  const request = typeof messageInput === "string"
@@ -471,6 +534,9 @@ export class MachineSessionService {
471
534
  statusKind: "startup",
472
535
  });
473
536
  }
537
+ const abandonResponseHandoff = responseId
538
+ ? this.ports.runtimeState.beginResponseHandoff?.(sessionId, responseId)
539
+ : undefined;
474
540
  const clearStartup = async (status) => {
475
541
  if (!responseId)
476
542
  return;
@@ -486,6 +552,7 @@ export class MachineSessionService {
486
552
  execution = await this.ensureLiveSession(sessionId, meta);
487
553
  }
488
554
  catch (error) {
555
+ abandonResponseHandoff?.();
489
556
  await clearStartup("idle");
490
557
  if (request.clientMessageId) {
491
558
  await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error));
@@ -500,6 +567,7 @@ export class MachineSessionService {
500
567
  outcome = await execution.runner.injectMessage(sessionId, responseId || needsPreparedOperation ? runtimeInput : request.message);
501
568
  }
502
569
  catch (error) {
570
+ abandonResponseHandoff?.();
503
571
  await clearStartup("idle");
504
572
  if (request.clientMessageId) {
505
573
  await Promise.resolve(resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
@@ -507,6 +575,7 @@ export class MachineSessionService {
507
575
  throw new MachineSessionServiceFailure("outcome_unknown", "live injection outcome is unknown");
508
576
  }
509
577
  if (outcome === "failed") {
578
+ abandonResponseHandoff?.();
510
579
  await clearStartup("idle");
511
580
  if (request.clientMessageId) {
512
581
  await resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, `live injection ${outcome}`);
@@ -514,6 +583,7 @@ export class MachineSessionService {
514
583
  throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`);
515
584
  }
516
585
  if (outcome !== "injected") {
586
+ abandonResponseHandoff?.();
517
587
  await clearStartup("idle");
518
588
  if (request.clientMessageId) {
519
589
  await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, `live injection ${outcome}`);
@@ -564,6 +634,9 @@ export class MachineSessionService {
564
634
  * delivery worker starts the pane now, waits for a real native thread, then
565
635
  * injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
566
636
  async enqueueMessage(sessionIdInput, messageInput) {
637
+ return this.withAdmission(() => this.enqueueMessageAdmitted(sessionIdInput, messageInput));
638
+ }
639
+ async enqueueMessageAdmitted(sessionIdInput, messageInput) {
567
640
  const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
568
641
  await this.assertNotForkReserved(sessionId);
569
642
  if (typeof messageInput !== "string" || messageInput.length === 0) {
@@ -595,6 +668,9 @@ export class MachineSessionService {
595
668
  }
596
669
  /** Explicitly restore the target daemon's live runner without starting a turn. */
597
670
  async startTerminal(sessionIdInput) {
671
+ return this.withAdmission(() => this.startTerminalAdmitted(sessionIdInput));
672
+ }
673
+ async startTerminalAdmitted(sessionIdInput) {
598
674
  const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
599
675
  const request = await this.liveSessionRequest(sessionId);
600
676
  const start = request.execution.runner.startLiveSession?.bind(request.execution.runner) ??
@@ -662,6 +738,24 @@ export class MachineSessionService {
662
738
  }
663
739
  return this.ports.agents;
664
740
  }
741
+ reserveAdmission() {
742
+ const reservation = this.admissionReserve?.();
743
+ if (reservation)
744
+ return reservation;
745
+ if (!this.admissionReserve && this.admissionOpen()) {
746
+ return { release() { } };
747
+ }
748
+ throw new MachineSessionServiceFailure("failed_precondition", "daemon maintenance is in progress");
749
+ }
750
+ async withAdmission(operation) {
751
+ const reservation = this.reserveAdmission();
752
+ try {
753
+ return await operation();
754
+ }
755
+ finally {
756
+ reservation.release();
757
+ }
758
+ }
665
759
  requireResources() {
666
760
  if (!this.ports.resources) {
667
761
  throw new MachineSessionServiceFailure("failed_precondition", "Session resources are unavailable");
@@ -713,7 +807,9 @@ export class MachineSessionService {
713
807
  if (!pending || pending.state !== "queued")
714
808
  return;
715
809
  let injectionAttempted = false;
810
+ let admission;
716
811
  try {
812
+ admission = this.reserveAdmission();
717
813
  const execution = await this.ensureLiveSession(sessionId);
718
814
  if (this.cancelledPendingDeliveries.has(sessionId))
719
815
  return;
@@ -746,6 +842,9 @@ export class MachineSessionService {
746
842
  return;
747
843
  }
748
844
  }
845
+ finally {
846
+ admission?.release();
847
+ }
749
848
  await retryDelay(this.pendingMessageRetryMs);
750
849
  }
751
850
  }
@@ -948,6 +1047,13 @@ function sessionSummary(id, meta, log, runtimeState, pendingState) {
948
1047
  updatedAt: validTimestamp(updatedAt, "updatedAt"),
949
1048
  };
950
1049
  }
1050
+ function executionSettingsResult(execution) {
1051
+ return {
1052
+ provider: execution.provider,
1053
+ model: execution.model,
1054
+ reasoningEffort: execution.reasoningEffort,
1055
+ };
1056
+ }
951
1057
  function compareMetaRecency(left, right) {
952
1058
  const leftTime = validTimestamp(left.updatedAt ?? left.createdAt, "updatedAt");
953
1059
  const rightTime = validTimestamp(right.updatedAt ?? right.createdAt, "updatedAt");
@@ -1042,6 +1148,15 @@ function validOpaqueToken(value, field, maximum) {
1042
1148
  }
1043
1149
  return value;
1044
1150
  }
1151
+ function validRequiredText(value, field) {
1152
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 512) {
1153
+ throw new MachineSessionServiceInputError(`${field} must be a non-empty string of at most 512 characters`);
1154
+ }
1155
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
1156
+ throw new MachineSessionServiceInputError(`${field} contains invalid control characters`);
1157
+ }
1158
+ return value.trim();
1159
+ }
1045
1160
  function validTimestamp(value, field) {
1046
1161
  if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
1047
1162
  throw new MachineSessionServiceInputError(`${field} must be a valid timestamp`);
@@ -34,7 +34,7 @@ export interface RemoteRuntimeProjectHost {
34
34
  update(projectId: string, input: RemoteRuntimeProjectCreateParams): RemoteRuntimeProject | undefined | Promise<RemoteRuntimeProject | undefined>;
35
35
  remove(projectId: string): boolean | Promise<boolean>;
36
36
  }
37
- export type RemoteRuntimeSessionHost = Pick<MachineSessionService, "list" | "snapshot" | "watch" | "interrupt" | "agentOptions" | "launchOptions" | "create" | "fork" | "sendMessage" | "resourcePolicy" | "beginResourceUpload" | "writeResourceUploadChunk" | "commitResourceUpload" | "getResource" | "readResource" | "deleteResource" | "enqueueMessage" | "startTerminal" | "delete" | "resolveInteraction">;
37
+ export type RemoteRuntimeSessionHost = Pick<MachineSessionService, "list" | "snapshot" | "watch" | "interrupt" | "agentOptions" | "launchOptions" | "executionSettings" | "updateExecutionSettings" | "create" | "fork" | "sendMessage" | "resourcePolicy" | "beginResourceUpload" | "writeResourceUploadChunk" | "commitResourceUpload" | "getResource" | "readResource" | "deleteResource" | "enqueueMessage" | "startTerminal" | "delete" | "resolveInteraction">;
38
38
  export type RemoteRuntimeBrowserHost = Pick<SessionBrowserService, "getState" | "open" | "close" | "createPage" | "closePage" | "activatePage" | "navigatePage" | "goBack" | "goForward" | "reload">;
39
39
  export type RemoteRuntimeSessionEmulatorHost = Pick<SessionEmulatorService, "getState" | "listDevices" | "attach" | "detach" | "tap" | "type" | "button" | "rotate" | "launch" | "gesture" | "releaseDevice" | "stopDevice">;
40
40
  export interface OpenRemoteRuntimeSessionEvents {
@@ -125,6 +125,10 @@ export class RemoteRuntimeDispatcher {
125
125
  return await this.withSessions(request, (sessions) => sessions.agentOptions());
126
126
  case "session.launchOptions.list":
127
127
  return await this.withSessions(request, (sessions) => sessions.launchOptions());
128
+ case "session.execution.get":
129
+ return await this.withSessions(request, (sessions) => sessions.executionSettings(request.params.sessionId));
130
+ case "session.execution.update":
131
+ return await this.withSessions(request, (sessions) => sessions.updateExecutionSettings(request.params));
128
132
  case "session.create":
129
133
  return await this.withSessions(request, (sessions) => sessions.create(request.params));
130
134
  case "session.fork":
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Server as HttpServer } from "node:http";
2
2
  import Koa from "koa";
3
- import { ConversationRuntime, type AgentCapabilities, type AgentSessionStore, type AppConfig, type SessionEvent, type SessionBus, type SessionLogStore, type SessionRegistry } from "@rynx-ai/core";
3
+ import { ConversationRuntime, type AdmissionReservation, type AgentCapabilities, type AgentSessionStore, type AppConfig, type SessionEvent, type SessionBus, type SessionLogStore, type SessionRegistry } from "@rynx-ai/core";
4
4
  import type { SessionInteractionResolution } from "@rynx-ai/protocol";
5
5
  import type { ControlRuntimeShareAddress, DaemonBrowserArtifactCleanResult, DaemonBrowserArtifactInstallInput, DaemonBrowserArtifactInstallResult, DaemonBrowserArtifactUpdateInput, DaemonBrowserArtifactVersionResult, DaemonCleanupSessionsInput, DaemonCleanupSessionsResult, DaemonChromeInspectionConfigureInput, DaemonChromeInspectionStatus, DaemonShutdownIfIdleResult } from "@rynx-ai/protocol/control";
6
6
  import type { PluginInstallCommitInput, PluginInstallCommitResult, PluginInstallPreparation, PluginInstallPrepareInput, PluginManagementItem, PluginMarketplaceAddInput, PluginMarketplaceItem } from "@rynx-ai/protocol/plugin-management";
@@ -18,6 +18,9 @@ import { type ControlPlaneDeps, type LocalSessionResourceHost, type SessionResou
18
18
  import type { MachineSessionService } from "./machine-session-service.js";
19
19
  import type { DaemonRuntimeHost } from "./remote-runtime.js";
20
20
  import { type AttachDesktopBrowserHostServerOptions } from "./desktop-browser-host.js";
21
+ import { type SessionPortalAuthorizationPorts } from "./session-portal.js";
22
+ export { createSessionPortalApp, startSessionPortal } from "./session-portal.js";
23
+ export type { SessionPortalAuthorization, SessionPortalAuthorizationPorts, SessionPortalOptions, SessionPortalServer, } from "./session-portal.js";
21
24
  import { SessionRuntimeIndex } from "./session-runtime-index.js";
22
25
  import type { SessionEmulatorService } from "./session-emulator-service.js";
23
26
  import { type SessionTerminalHost } from "./session-terminal-host.js";
@@ -132,6 +135,9 @@ export interface StartServerOptions {
132
135
  providerClis?: ProviderCliManagementHost;
133
136
  /** Private resident-app Browser Host socket, authenticated by daemonManagement. */
134
137
  desktopBrowserHost?: Omit<AttachDesktopBrowserHostServerOptions, "managementToken">;
138
+ /** Hashed ticket/grant authority for the native Session Portal. Required
139
+ * when RYNX_SESSION_PORTAL_PORT enables that listener. */
140
+ sessionPortalAuthorization?: SessionPortalAuthorizationPorts;
135
141
  }
136
142
  export interface SessionRuntimeServices {
137
143
  sessionStore: CodexSessionStore;
@@ -155,9 +161,19 @@ export interface PluginRuntimeHostServices {
155
161
  }): Promise<PluginResolvedSessionExecution>;
156
162
  };
157
163
  interruptSession(sessionId: string): Promise<boolean>;
164
+ terminateSession?(sessionId: string): Promise<boolean>;
158
165
  resolveSessionInteraction(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
159
166
  disposition: "applied" | "already_resolved" | "not_found" | "invalid";
160
167
  }>;
168
+ /** Dynamic daemon-wide admission state. Plugin runtimes may start while this is
169
+ * closed, but cannot provision Sessions or begin Turns. */
170
+ admissionOpen?: () => boolean;
171
+ admissionReserve?: () => AdmissionReservation | undefined;
172
+ sessionPortal?: {
173
+ scheme: "http" | "https";
174
+ bindHost: string;
175
+ port: number;
176
+ };
161
177
  }
162
178
  export interface PluginRuntimeLifecycle {
163
179
  bindHostServices(services: PluginRuntimeHostServices): void;
@@ -236,7 +252,11 @@ export interface DaemonManagementHost {
236
252
  restartRequired: boolean;
237
253
  };
238
254
  /** Perform one final activity check and schedule shutdown only when idle. */
239
- shutdownIfIdle?(): DaemonShutdownIfIdleResult;
255
+ shutdownIfIdle?(maintenanceLeaseToken?: string): DaemonShutdownIfIdleResult | Promise<DaemonShutdownIfIdleResult>;
256
+ /** Release a cross-process maintenance fence after a verified cutover. */
257
+ releaseMaintenanceLease?(maintenanceLeaseToken: string): {
258
+ outcome: "released" | "not_active" | "denied";
259
+ };
240
260
  /** Stateful plugin registry operations owned by the resident daemon. */
241
261
  pluginManagement?: PluginManagementHost;
242
262
  /** Registered Git/local plugin catalogs. */
@@ -415,6 +435,8 @@ export declare function createSessionRuntimeServices(input: {
415
435
  sessionLog?: SessionLogStore;
416
436
  onMirrorError?: (error: unknown, sessionId: string) => void;
417
437
  sessionContextProvider?: RunnerSessionContextProvider;
438
+ admissionOpen?: () => boolean;
439
+ admissionReserve?: () => AdmissionReservation | undefined;
418
440
  }): SessionRuntimeServices;
419
441
  /**
420
442
  * The HTTP surface is just a liveness probe for container orchestration — all
@@ -423,4 +445,4 @@ export declare function createSessionRuntimeServices(input: {
423
445
  * no agent request / response / SSE endpoints are exposed here.
424
446
  */
425
447
  export declare function createApp({ config, control }?: CreateAppOptions): Koa<Koa.DefaultState, Koa.DefaultContext>;
426
- export declare function startServer({ config, control, sessionLog, sessionRegistry, pluginRuntime, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeConnectionResolver, localSessionResources, localMachineSessions, sessionEmulators, sessionRuntimeServices, sessionLifecycle, sessionTerminals, runtimeLocalBrowser, providerClis, desktopBrowserHost, }?: StartServerOptions): Promise<RynxServer>;
448
+ export declare function startServer({ config, control, sessionLog, sessionRegistry, pluginRuntime, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeConnectionResolver, localSessionResources, localMachineSessions, sessionEmulators, sessionRuntimeServices, sessionLifecycle, sessionTerminals, runtimeLocalBrowser, providerClis, desktopBrowserHost, sessionPortalAuthorization, }?: StartServerOptions): Promise<RynxServer>;