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

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-dOafa0IY.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-0noHpRXD.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,19 +575,23 @@ 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}`);
513
582
  }
514
583
  throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`);
515
584
  }
516
- if (outcome !== "injected") {
585
+ if (outcome !== "injected" && outcome !== "steered") {
586
+ abandonResponseHandoff?.();
517
587
  await clearStartup("idle");
518
588
  if (request.clientMessageId) {
519
589
  await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, `live injection ${outcome}`);
520
590
  }
521
591
  throw new MachineSessionServiceFailure("failed_precondition", `live injection ${outcome}`);
522
592
  }
593
+ if (outcome === "steered")
594
+ abandonResponseHandoff?.();
523
595
  await clearStartup("running");
524
596
  if (request.clientMessageId) {
525
597
  try {
@@ -564,6 +636,9 @@ export class MachineSessionService {
564
636
  * delivery worker starts the pane now, waits for a real native thread, then
565
637
  * injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
566
638
  async enqueueMessage(sessionIdInput, messageInput) {
639
+ return this.withAdmission(() => this.enqueueMessageAdmitted(sessionIdInput, messageInput));
640
+ }
641
+ async enqueueMessageAdmitted(sessionIdInput, messageInput) {
567
642
  const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
568
643
  await this.assertNotForkReserved(sessionId);
569
644
  if (typeof messageInput !== "string" || messageInput.length === 0) {
@@ -595,6 +670,9 @@ export class MachineSessionService {
595
670
  }
596
671
  /** Explicitly restore the target daemon's live runner without starting a turn. */
597
672
  async startTerminal(sessionIdInput) {
673
+ return this.withAdmission(() => this.startTerminalAdmitted(sessionIdInput));
674
+ }
675
+ async startTerminalAdmitted(sessionIdInput) {
598
676
  const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
599
677
  const request = await this.liveSessionRequest(sessionId);
600
678
  const start = request.execution.runner.startLiveSession?.bind(request.execution.runner) ??
@@ -662,6 +740,24 @@ export class MachineSessionService {
662
740
  }
663
741
  return this.ports.agents;
664
742
  }
743
+ reserveAdmission() {
744
+ const reservation = this.admissionReserve?.();
745
+ if (reservation)
746
+ return reservation;
747
+ if (!this.admissionReserve && this.admissionOpen()) {
748
+ return { release() { } };
749
+ }
750
+ throw new MachineSessionServiceFailure("failed_precondition", "daemon maintenance is in progress");
751
+ }
752
+ async withAdmission(operation) {
753
+ const reservation = this.reserveAdmission();
754
+ try {
755
+ return await operation();
756
+ }
757
+ finally {
758
+ reservation.release();
759
+ }
760
+ }
665
761
  requireResources() {
666
762
  if (!this.ports.resources) {
667
763
  throw new MachineSessionServiceFailure("failed_precondition", "Session resources are unavailable");
@@ -713,7 +809,9 @@ export class MachineSessionService {
713
809
  if (!pending || pending.state !== "queued")
714
810
  return;
715
811
  let injectionAttempted = false;
812
+ let admission;
716
813
  try {
814
+ admission = this.reserveAdmission();
717
815
  const execution = await this.ensureLiveSession(sessionId);
718
816
  if (this.cancelledPendingDeliveries.has(sessionId))
719
817
  return;
@@ -725,7 +823,7 @@ export class MachineSessionService {
725
823
  return;
726
824
  injectionAttempted = true;
727
825
  const outcome = await execution.runner.injectMessage(sessionId, pending.message);
728
- if (outcome === "injected") {
826
+ if (outcome === "injected" || outcome === "steered") {
729
827
  await Promise.resolve(pendingStore.delete(sessionId)).catch(async () => {
730
828
  await Promise.resolve(pendingStore.markOutcomeUnknown(sessionId)).catch(() => undefined);
731
829
  });
@@ -746,6 +844,9 @@ export class MachineSessionService {
746
844
  return;
747
845
  }
748
846
  }
847
+ finally {
848
+ admission?.release();
849
+ }
749
850
  await retryDelay(this.pendingMessageRetryMs);
750
851
  }
751
852
  }
@@ -948,6 +1049,13 @@ function sessionSummary(id, meta, log, runtimeState, pendingState) {
948
1049
  updatedAt: validTimestamp(updatedAt, "updatedAt"),
949
1050
  };
950
1051
  }
1052
+ function executionSettingsResult(execution) {
1053
+ return {
1054
+ provider: execution.provider,
1055
+ model: execution.model,
1056
+ reasoningEffort: execution.reasoningEffort,
1057
+ };
1058
+ }
951
1059
  function compareMetaRecency(left, right) {
952
1060
  const leftTime = validTimestamp(left.updatedAt ?? left.createdAt, "updatedAt");
953
1061
  const rightTime = validTimestamp(right.updatedAt ?? right.createdAt, "updatedAt");
@@ -1042,6 +1150,15 @@ function validOpaqueToken(value, field, maximum) {
1042
1150
  }
1043
1151
  return value;
1044
1152
  }
1153
+ function validRequiredText(value, field) {
1154
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 512) {
1155
+ throw new MachineSessionServiceInputError(`${field} must be a non-empty string of at most 512 characters`);
1156
+ }
1157
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
1158
+ throw new MachineSessionServiceInputError(`${field} contains invalid control characters`);
1159
+ }
1160
+ return value.trim();
1161
+ }
1045
1162
  function validTimestamp(value, field) {
1046
1163
  if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
1047
1164
  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>;