@rynx-ai/server 0.1.11-beta.2 → 0.1.11-beta.4
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/control-api.js +27 -3
- package/dist/machine-session-service.d.ts +16 -1
- package/dist/machine-session-service.js +49 -0
- package/dist/server.d.ts +13 -2
- package/dist/server.js +5 -2
- package/dist/session-browser-service.d.ts +8 -0
- package/dist/session-browser-service.js +37 -20
- package/dist/session-runtime-index.d.ts +7 -0
- package/dist/session-runtime-index.js +35 -6
- package/dist/session-terminal-host.d.ts +6 -0
- package/dist/session-terminal-host.js +56 -41
- package/package.json +7 -7
package/dist/control-api.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { timingSafeEqual } from "node:crypto";
|
|
10
10
|
import Router from "@koa/router";
|
|
11
11
|
import { skillInstallRecipeSchema, } from "@rynx-ai/core";
|
|
12
|
-
import { DAEMON_BROWSER_ARTIFACT_CLEAN_PATH, DAEMON_BROWSER_ARTIFACT_INSTALL_PATH, DAEMON_BROWSER_ARTIFACT_UPDATE_PATH, DAEMON_BROWSER_ARTIFACT_VERSION_PATH, DAEMON_CHROME_INSPECTION_PATH, DAEMON_CLEANUP_SESSIONS_PATH, DAEMON_SHUTDOWN_IF_IDLE_PATH, parseDaemonBrowserArtifactInstallInput, parseDaemonBrowserArtifactUpdateInput, parseDaemonChromeInspectionConfigureInput, parseDaemonCleanupSessionsInput, } from "@rynx-ai/protocol/control";
|
|
12
|
+
import { DAEMON_BROWSER_ARTIFACT_CLEAN_PATH, DAEMON_BROWSER_ARTIFACT_INSTALL_PATH, DAEMON_BROWSER_ARTIFACT_UPDATE_PATH, DAEMON_BROWSER_ARTIFACT_VERSION_PATH, DAEMON_CHROME_INSPECTION_PATH, DAEMON_CLEANUP_SESSIONS_PATH, DAEMON_MAINTENANCE_LEASE_HEADER, DAEMON_MAINTENANCE_LEASE_RELEASE_PATH, DAEMON_SHUTDOWN_IF_IDLE_PATH, parseDaemonBrowserArtifactInstallInput, parseDaemonBrowserArtifactUpdateInput, parseDaemonChromeInspectionConfigureInput, parseDaemonCleanupSessionsInput, } from "@rynx-ai/protocol/control";
|
|
13
13
|
import { DIRECT_RUNTIME_CONTROL_PATH, encodePairingCode, } from "@rynx-ai/protocol/direct-runtime";
|
|
14
14
|
import { PLUGIN_CONSOLE_INSTALL_PREPARATIONS_PATH, PLUGIN_CONSOLE_MARKETPLACES_PATH, PLUGIN_INSTALL_PREPARATIONS_PATH, PLUGIN_MARKETPLACES_PATH, parsePluginInstallCommitInput, parsePluginInstallPrepareInput, parsePluginMarketplaceAddInput, } from "@rynx-ai/protocol/plugin-management";
|
|
15
15
|
import { parseDaemonStatus } from "@rynx-ai/protocol/remote-runtime";
|
|
@@ -1918,18 +1918,42 @@ export function createControlRouter(opts) {
|
|
|
1918
1918
|
return;
|
|
1919
1919
|
ctx.body = encodeDaemonStatus(remoteRuntime.status());
|
|
1920
1920
|
});
|
|
1921
|
-
router.post(DAEMON_SHUTDOWN_IF_IDLE_PATH, (ctx) => {
|
|
1921
|
+
router.post(DAEMON_SHUTDOWN_IF_IDLE_PATH, async (ctx) => {
|
|
1922
1922
|
if (!isConfiguredDaemonManagement(daemonManagement) ||
|
|
1923
1923
|
!daemonManagement.shutdownIfIdle) {
|
|
1924
1924
|
return notFound(ctx, "daemon shutdown management is not configured");
|
|
1925
1925
|
}
|
|
1926
1926
|
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
1927
1927
|
return;
|
|
1928
|
-
const
|
|
1928
|
+
const maintenanceLeaseToken = ctx.get(DAEMON_MAINTENANCE_LEASE_HEADER).trim() || undefined;
|
|
1929
|
+
let result;
|
|
1930
|
+
try {
|
|
1931
|
+
result = await daemonManagement.shutdownIfIdle(maintenanceLeaseToken);
|
|
1932
|
+
}
|
|
1933
|
+
catch (error) {
|
|
1934
|
+
ctx.set("Cache-Control", "no-store");
|
|
1935
|
+
ctx.status = 409;
|
|
1936
|
+
ctx.body = {
|
|
1937
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1938
|
+
};
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1929
1941
|
ctx.set("Cache-Control", "no-store");
|
|
1930
1942
|
ctx.status = result.outcome === "accepted" ? 202 : 409;
|
|
1931
1943
|
ctx.body = result;
|
|
1932
1944
|
});
|
|
1945
|
+
router.post(DAEMON_MAINTENANCE_LEASE_RELEASE_PATH, (ctx) => {
|
|
1946
|
+
if (!isConfiguredDaemonManagement(daemonManagement)
|
|
1947
|
+
|| !daemonManagement.releaseMaintenanceLease) {
|
|
1948
|
+
return notFound(ctx, "daemon maintenance lease management is not configured");
|
|
1949
|
+
}
|
|
1950
|
+
if (!authorizeLoopbackManagementRequest(ctx, daemonManagement.managementToken))
|
|
1951
|
+
return;
|
|
1952
|
+
const result = daemonManagement.releaseMaintenanceLease(ctx.get(DAEMON_MAINTENANCE_LEASE_HEADER).trim());
|
|
1953
|
+
ctx.set("Cache-Control", "no-store");
|
|
1954
|
+
ctx.status = result.outcome === "denied" ? 403 : 200;
|
|
1955
|
+
ctx.body = result;
|
|
1956
|
+
});
|
|
1933
1957
|
router.post(DAEMON_BROWSER_ARTIFACT_INSTALL_PATH, async (ctx) => {
|
|
1934
1958
|
await mutateBrowserArtifact(ctx, "install");
|
|
1935
1959
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
import type { ControlAgentSummary, ControlSessionInteractionResolveDisposition, ControlSessionRuntimeSnapshot } from "@rynx-ai/protocol/control";
|
|
@@ -6,6 +6,7 @@ import { type RemoteRuntimeSessionInterruptResult, type RemoteRuntimeSessionLaun
|
|
|
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 {
|
|
@@ -181,6 +182,11 @@ export interface MachineSessionServiceOptions {
|
|
|
181
182
|
directoryScanLimit?: number;
|
|
182
183
|
/** Delay between setup-readiness retries for durable pending messages. */
|
|
183
184
|
pendingMessageRetryMs?: number;
|
|
185
|
+
/** Dynamic daemon-wide admission fence used during process replacement. */
|
|
186
|
+
admissionOpen?: () => boolean;
|
|
187
|
+
/** Atomically reserves one work-producing admission until it is rejected or
|
|
188
|
+
* has become visible in the daemon activity projection. */
|
|
189
|
+
admissionReserve?: () => AdmissionReservation | undefined;
|
|
184
190
|
}
|
|
185
191
|
export type MachineSessionListInput = RemoteRuntimeSessionListParams;
|
|
186
192
|
export interface MachineSessionListPage extends RemoteRuntimeSessionListResult {
|
|
@@ -277,6 +283,8 @@ export declare class MachineSessionService {
|
|
|
277
283
|
private readonly snapshotPageMaxBytes;
|
|
278
284
|
private readonly directoryScanLimit;
|
|
279
285
|
private readonly pendingMessageRetryMs;
|
|
286
|
+
private readonly admissionOpen;
|
|
287
|
+
private readonly admissionReserve;
|
|
280
288
|
private readonly pendingDeliveries;
|
|
281
289
|
private readonly cancelledPendingDeliveries;
|
|
282
290
|
private readonly forkTasks;
|
|
@@ -300,10 +308,12 @@ export declare class MachineSessionService {
|
|
|
300
308
|
launchOptions(): Promise<RemoteRuntimeSessionLaunchOptionsListResult>;
|
|
301
309
|
/** Create target-owned Session identity from one Agent preset or direct Provider. */
|
|
302
310
|
create(input: MachineSessionCreateInput): Promise<MachineSessionCreateResult>;
|
|
311
|
+
private createAdmitted;
|
|
303
312
|
/** Create one independent Session at the source's stable Provider/canonical
|
|
304
313
|
* boundary. Project and Agent selectors are intentionally absent: the target
|
|
305
314
|
* receives exact copies of the source's already-frozen snapshots. */
|
|
306
315
|
fork(input: MachineSessionForkInput): Promise<MachineSessionForkResult>;
|
|
316
|
+
private forkAdmitted;
|
|
307
317
|
/** Publish a Provider TUI `/clear` or `/fork` after its native binding has
|
|
308
318
|
* already been persisted. The source snapshots remain the only workspace and
|
|
309
319
|
* execution authority; Provider events cannot alter them during rotation. */
|
|
@@ -312,6 +322,7 @@ export declare class MachineSessionService {
|
|
|
312
322
|
private assertForkableSource;
|
|
313
323
|
/** Inject one turn through the target daemon's native single-writer runner. */
|
|
314
324
|
sendMessage(sessionIdInput: string, messageInput: string | MachineSessionMessageInput): Promise<MachineSessionMessageResult>;
|
|
325
|
+
private sendMessageAdmitted;
|
|
315
326
|
resourcePolicy(sessionIdInput: string): RemoteRuntimeSessionResourcePolicyResult;
|
|
316
327
|
beginResourceUpload(input: RemoteRuntimeSessionResourceUploadBeginParams): Promise<RemoteRuntimeSessionResourceUploadBeginResult>;
|
|
317
328
|
writeResourceUploadChunk(input: RemoteRuntimeSessionResourceUploadChunkParams): Promise<RemoteRuntimeSessionResourceUploadChunkResult>;
|
|
@@ -323,13 +334,17 @@ export declare class MachineSessionService {
|
|
|
323
334
|
* delivery worker starts the pane now, waits for a real native thread, then
|
|
324
335
|
* injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
|
|
325
336
|
enqueueMessage(sessionIdInput: string, messageInput: string): Promise<MachineSessionMessageEnqueueResult>;
|
|
337
|
+
private enqueueMessageAdmitted;
|
|
326
338
|
/** Explicitly restore the target daemon's live runner without starting a turn. */
|
|
327
339
|
startTerminal(sessionIdInput: string): Promise<MachineSessionTerminalStartResult>;
|
|
340
|
+
private startTerminalAdmitted;
|
|
328
341
|
private ensureLiveSession;
|
|
329
342
|
private liveSessionRequest;
|
|
330
343
|
resolveInteraction(sessionIdInput: string, interactionIdInput: string, resolution: SessionInteractionResolution): Promise<MachineSessionInteractionResult>;
|
|
331
344
|
delete(sessionIdInput: string): Promise<MachineSessionDeleteResult>;
|
|
332
345
|
private requireAgents;
|
|
346
|
+
private reserveAdmission;
|
|
347
|
+
private withAdmission;
|
|
333
348
|
private requireResources;
|
|
334
349
|
private requireResourceSession;
|
|
335
350
|
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
|
}
|
|
@@ -190,6 +194,9 @@ export class MachineSessionService {
|
|
|
190
194
|
}
|
|
191
195
|
/** Create target-owned Session identity from one Agent preset or direct Provider. */
|
|
192
196
|
async create(input) {
|
|
197
|
+
return this.withAdmission(() => this.createAdmitted(input));
|
|
198
|
+
}
|
|
199
|
+
async createAdmitted(input) {
|
|
193
200
|
const hasAgent = input.agent !== undefined;
|
|
194
201
|
const hasProvider = input.provider !== undefined;
|
|
195
202
|
if (hasAgent === hasProvider) {
|
|
@@ -240,6 +247,9 @@ export class MachineSessionService {
|
|
|
240
247
|
* boundary. Project and Agent selectors are intentionally absent: the target
|
|
241
248
|
* receives exact copies of the source's already-frozen snapshots. */
|
|
242
249
|
async fork(input) {
|
|
250
|
+
return this.withAdmission(() => this.forkAdmitted(input));
|
|
251
|
+
}
|
|
252
|
+
async forkAdmitted(input) {
|
|
243
253
|
const sourceSessionId = validOpaqueToken(input.sourceSessionId, "sourceSessionId", MAX_SESSION_ID_CHARS);
|
|
244
254
|
const operationId = validOpaqueToken(input.operationId, "operationId", MAX_SESSION_ID_CHARS);
|
|
245
255
|
const title = input.title === undefined
|
|
@@ -383,6 +393,9 @@ export class MachineSessionService {
|
|
|
383
393
|
}
|
|
384
394
|
/** Inject one turn through the target daemon's native single-writer runner. */
|
|
385
395
|
async sendMessage(sessionIdInput, messageInput) {
|
|
396
|
+
return this.withAdmission(() => this.sendMessageAdmitted(sessionIdInput, messageInput));
|
|
397
|
+
}
|
|
398
|
+
async sendMessageAdmitted(sessionIdInput, messageInput) {
|
|
386
399
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
387
400
|
await this.assertNotForkReserved(sessionId);
|
|
388
401
|
const request = typeof messageInput === "string"
|
|
@@ -471,6 +484,9 @@ export class MachineSessionService {
|
|
|
471
484
|
statusKind: "startup",
|
|
472
485
|
});
|
|
473
486
|
}
|
|
487
|
+
const abandonResponseHandoff = responseId
|
|
488
|
+
? this.ports.runtimeState.beginResponseHandoff?.(sessionId, responseId)
|
|
489
|
+
: undefined;
|
|
474
490
|
const clearStartup = async (status) => {
|
|
475
491
|
if (!responseId)
|
|
476
492
|
return;
|
|
@@ -486,6 +502,7 @@ export class MachineSessionService {
|
|
|
486
502
|
execution = await this.ensureLiveSession(sessionId, meta);
|
|
487
503
|
}
|
|
488
504
|
catch (error) {
|
|
505
|
+
abandonResponseHandoff?.();
|
|
489
506
|
await clearStartup("idle");
|
|
490
507
|
if (request.clientMessageId) {
|
|
491
508
|
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error));
|
|
@@ -500,6 +517,7 @@ export class MachineSessionService {
|
|
|
500
517
|
outcome = await execution.runner.injectMessage(sessionId, responseId || needsPreparedOperation ? runtimeInput : request.message);
|
|
501
518
|
}
|
|
502
519
|
catch (error) {
|
|
520
|
+
abandonResponseHandoff?.();
|
|
503
521
|
await clearStartup("idle");
|
|
504
522
|
if (request.clientMessageId) {
|
|
505
523
|
await Promise.resolve(resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
|
|
@@ -507,6 +525,7 @@ export class MachineSessionService {
|
|
|
507
525
|
throw new MachineSessionServiceFailure("outcome_unknown", "live injection outcome is unknown");
|
|
508
526
|
}
|
|
509
527
|
if (outcome === "failed") {
|
|
528
|
+
abandonResponseHandoff?.();
|
|
510
529
|
await clearStartup("idle");
|
|
511
530
|
if (request.clientMessageId) {
|
|
512
531
|
await resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
@@ -514,6 +533,7 @@ export class MachineSessionService {
|
|
|
514
533
|
throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`);
|
|
515
534
|
}
|
|
516
535
|
if (outcome !== "injected") {
|
|
536
|
+
abandonResponseHandoff?.();
|
|
517
537
|
await clearStartup("idle");
|
|
518
538
|
if (request.clientMessageId) {
|
|
519
539
|
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
@@ -564,6 +584,9 @@ export class MachineSessionService {
|
|
|
564
584
|
* delivery worker starts the pane now, waits for a real native thread, then
|
|
565
585
|
* injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
|
|
566
586
|
async enqueueMessage(sessionIdInput, messageInput) {
|
|
587
|
+
return this.withAdmission(() => this.enqueueMessageAdmitted(sessionIdInput, messageInput));
|
|
588
|
+
}
|
|
589
|
+
async enqueueMessageAdmitted(sessionIdInput, messageInput) {
|
|
567
590
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
568
591
|
await this.assertNotForkReserved(sessionId);
|
|
569
592
|
if (typeof messageInput !== "string" || messageInput.length === 0) {
|
|
@@ -595,6 +618,9 @@ export class MachineSessionService {
|
|
|
595
618
|
}
|
|
596
619
|
/** Explicitly restore the target daemon's live runner without starting a turn. */
|
|
597
620
|
async startTerminal(sessionIdInput) {
|
|
621
|
+
return this.withAdmission(() => this.startTerminalAdmitted(sessionIdInput));
|
|
622
|
+
}
|
|
623
|
+
async startTerminalAdmitted(sessionIdInput) {
|
|
598
624
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
599
625
|
const request = await this.liveSessionRequest(sessionId);
|
|
600
626
|
const start = request.execution.runner.startLiveSession?.bind(request.execution.runner) ??
|
|
@@ -662,6 +688,24 @@ export class MachineSessionService {
|
|
|
662
688
|
}
|
|
663
689
|
return this.ports.agents;
|
|
664
690
|
}
|
|
691
|
+
reserveAdmission() {
|
|
692
|
+
const reservation = this.admissionReserve?.();
|
|
693
|
+
if (reservation)
|
|
694
|
+
return reservation;
|
|
695
|
+
if (!this.admissionReserve && this.admissionOpen()) {
|
|
696
|
+
return { release() { } };
|
|
697
|
+
}
|
|
698
|
+
throw new MachineSessionServiceFailure("failed_precondition", "daemon maintenance is in progress");
|
|
699
|
+
}
|
|
700
|
+
async withAdmission(operation) {
|
|
701
|
+
const reservation = this.reserveAdmission();
|
|
702
|
+
try {
|
|
703
|
+
return await operation();
|
|
704
|
+
}
|
|
705
|
+
finally {
|
|
706
|
+
reservation.release();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
665
709
|
requireResources() {
|
|
666
710
|
if (!this.ports.resources) {
|
|
667
711
|
throw new MachineSessionServiceFailure("failed_precondition", "Session resources are unavailable");
|
|
@@ -713,7 +757,9 @@ export class MachineSessionService {
|
|
|
713
757
|
if (!pending || pending.state !== "queued")
|
|
714
758
|
return;
|
|
715
759
|
let injectionAttempted = false;
|
|
760
|
+
let admission;
|
|
716
761
|
try {
|
|
762
|
+
admission = this.reserveAdmission();
|
|
717
763
|
const execution = await this.ensureLiveSession(sessionId);
|
|
718
764
|
if (this.cancelledPendingDeliveries.has(sessionId))
|
|
719
765
|
return;
|
|
@@ -746,6 +792,9 @@ export class MachineSessionService {
|
|
|
746
792
|
return;
|
|
747
793
|
}
|
|
748
794
|
}
|
|
795
|
+
finally {
|
|
796
|
+
admission?.release();
|
|
797
|
+
}
|
|
749
798
|
await retryDelay(this.pendingMessageRetryMs);
|
|
750
799
|
}
|
|
751
800
|
}
|
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";
|
|
@@ -155,9 +155,14 @@ export interface PluginRuntimeHostServices {
|
|
|
155
155
|
}): Promise<PluginResolvedSessionExecution>;
|
|
156
156
|
};
|
|
157
157
|
interruptSession(sessionId: string): Promise<boolean>;
|
|
158
|
+
terminateSession?(sessionId: string): Promise<boolean>;
|
|
158
159
|
resolveSessionInteraction(sessionId: string, interactionId: string, resolution: SessionInteractionResolution): Promise<{
|
|
159
160
|
disposition: "applied" | "already_resolved" | "not_found" | "invalid";
|
|
160
161
|
}>;
|
|
162
|
+
/** Dynamic daemon-wide admission state. Plugin runtimes may start while this is
|
|
163
|
+
* closed, but cannot provision Sessions or begin Turns. */
|
|
164
|
+
admissionOpen?: () => boolean;
|
|
165
|
+
admissionReserve?: () => AdmissionReservation | undefined;
|
|
161
166
|
}
|
|
162
167
|
export interface PluginRuntimeLifecycle {
|
|
163
168
|
bindHostServices(services: PluginRuntimeHostServices): void;
|
|
@@ -236,7 +241,11 @@ export interface DaemonManagementHost {
|
|
|
236
241
|
restartRequired: boolean;
|
|
237
242
|
};
|
|
238
243
|
/** Perform one final activity check and schedule shutdown only when idle. */
|
|
239
|
-
shutdownIfIdle?(): DaemonShutdownIfIdleResult
|
|
244
|
+
shutdownIfIdle?(maintenanceLeaseToken?: string): DaemonShutdownIfIdleResult | Promise<DaemonShutdownIfIdleResult>;
|
|
245
|
+
/** Release a cross-process maintenance fence after a verified cutover. */
|
|
246
|
+
releaseMaintenanceLease?(maintenanceLeaseToken: string): {
|
|
247
|
+
outcome: "released" | "not_active" | "denied";
|
|
248
|
+
};
|
|
240
249
|
/** Stateful plugin registry operations owned by the resident daemon. */
|
|
241
250
|
pluginManagement?: PluginManagementHost;
|
|
242
251
|
/** Registered Git/local plugin catalogs. */
|
|
@@ -415,6 +424,8 @@ export declare function createSessionRuntimeServices(input: {
|
|
|
415
424
|
sessionLog?: SessionLogStore;
|
|
416
425
|
onMirrorError?: (error: unknown, sessionId: string) => void;
|
|
417
426
|
sessionContextProvider?: RunnerSessionContextProvider;
|
|
427
|
+
admissionOpen?: () => boolean;
|
|
428
|
+
admissionReserve?: () => AdmissionReservation | undefined;
|
|
418
429
|
}): SessionRuntimeServices;
|
|
419
430
|
/**
|
|
420
431
|
* The HTTP surface is just a liveness probe for container orchestration — all
|
package/dist/server.js
CHANGED
|
@@ -59,6 +59,8 @@ export function createSessionRuntimeServices(input) {
|
|
|
59
59
|
...(input.sessionContextProvider
|
|
60
60
|
? { sessionContextProvider: input.sessionContextProvider }
|
|
61
61
|
: {}),
|
|
62
|
+
...(input.admissionOpen ? { admissionOpen: input.admissionOpen } : {}),
|
|
63
|
+
...(input.admissionReserve ? { admissionReserve: input.admissionReserve } : {}),
|
|
62
64
|
});
|
|
63
65
|
const sessionBus = new InMemorySessionBus();
|
|
64
66
|
const sessionRuntimeIndex = new SessionRuntimeIndex();
|
|
@@ -179,6 +181,7 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
|
|
|
179
181
|
}),
|
|
180
182
|
},
|
|
181
183
|
interruptSession: (sessionId) => runnerManager.interruptLiveSession(sessionId),
|
|
184
|
+
terminateSession: (sessionId) => runnerManager.terminateLiveSession(sessionId),
|
|
182
185
|
resolveSessionInteraction: (sessionId, interactionId, resolution) => runnerManager.resolveInteraction(sessionId, interactionId, resolution),
|
|
183
186
|
});
|
|
184
187
|
}
|
|
@@ -232,8 +235,8 @@ export async function startServer({ config = loadConfig(), control, sessionLog,
|
|
|
232
235
|
msg: "Harness agent server listening",
|
|
233
236
|
host: config.HOST,
|
|
234
237
|
port: config.PORT,
|
|
235
|
-
runtime: config.
|
|
236
|
-
model: resolveRuntimeModel(config, config.
|
|
238
|
+
runtime: config.DEFAULT_RUNTIME,
|
|
239
|
+
model: resolveRuntimeModel(config, config.DEFAULT_RUNTIME),
|
|
237
240
|
}));
|
|
238
241
|
const startup = pluginRuntime?.startPluginRuntimes() ?? Promise.resolve();
|
|
239
242
|
void startup.catch((error) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AdmissionReservation } from "@rynx-ai/core";
|
|
1
2
|
import { type RuntimeBrowserCloseParams, type RuntimeBrowserCloseResult, type RuntimeBrowserExecutionBackend, type RuntimeBrowserOpenParams, type RuntimeBrowserPageCreateParams, type RuntimeBrowserPageMutationParams, type RuntimeBrowserPageNavigateParams, type RuntimeBrowserState, type RuntimeBrowserStateGetParams } from "@rynx-ai/protocol/runtime-browser";
|
|
2
3
|
import type { RuntimeBrowserSurfaceFormat, RuntimeBrowserSurfaceInputEvent } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
3
4
|
import { type RuntimeBrowserEndpointDescriptor } from "@rynx-ai/protocol/runtime-browser-bootstrap";
|
|
@@ -155,6 +156,11 @@ export interface SessionBrowserServiceOptions {
|
|
|
155
156
|
pageIdFactory?: () => string;
|
|
156
157
|
/** Injectable boot nonce; production randomization fences stale pre-restart mutations. */
|
|
157
158
|
bootGenerationSeed?: number;
|
|
159
|
+
/**
|
|
160
|
+
* Synchronous daemon admission fence. A successful reservation is held until
|
|
161
|
+
* Browser open has either failed or registered the physical Host handle.
|
|
162
|
+
*/
|
|
163
|
+
admissionReserve?: () => AdmissionReservation | undefined;
|
|
158
164
|
}
|
|
159
165
|
export type SessionBrowserHostErrorCode = "unavailable" | "page_not_found" | "capacity" | "outcome_unknown";
|
|
160
166
|
/** Expected failure reported by a Browser Host implementation. */
|
|
@@ -181,6 +187,7 @@ export declare class SessionBrowserService {
|
|
|
181
187
|
private readonly observers;
|
|
182
188
|
private readonly pageIdFactory;
|
|
183
189
|
private readonly bootGenerationSeed;
|
|
190
|
+
private readonly admissionReserve?;
|
|
184
191
|
private shuttingDown;
|
|
185
192
|
private closeAllPromise?;
|
|
186
193
|
constructor(ports: SessionBrowserServicePorts, options?: SessionBrowserServiceOptions);
|
|
@@ -225,6 +232,7 @@ export declare class SessionBrowserService {
|
|
|
225
232
|
closeAll(): Promise<void>;
|
|
226
233
|
private closeEveryRecord;
|
|
227
234
|
private createBrowser;
|
|
235
|
+
private reserveOpenAdmission;
|
|
228
236
|
private closeUnavailableForRecreate;
|
|
229
237
|
private pageMutation;
|
|
230
238
|
private pageMutationParsed;
|
|
@@ -42,12 +42,14 @@ export class SessionBrowserService {
|
|
|
42
42
|
observers = new Set();
|
|
43
43
|
pageIdFactory;
|
|
44
44
|
bootGenerationSeed;
|
|
45
|
+
admissionReserve;
|
|
45
46
|
shuttingDown = false;
|
|
46
47
|
closeAllPromise;
|
|
47
48
|
constructor(ports, options = {}) {
|
|
48
49
|
this.ports = ports;
|
|
49
50
|
this.pageIdFactory = options.pageIdFactory ?? (() => `page_${randomUUID()}`);
|
|
50
51
|
this.bootGenerationSeed = options.bootGenerationSeed ?? randomBootGenerationSeed();
|
|
52
|
+
this.admissionReserve = options.admissionReserve;
|
|
51
53
|
if (!Number.isSafeInteger(this.bootGenerationSeed) ||
|
|
52
54
|
this.bootGenerationSeed < 0 ||
|
|
53
55
|
this.bootGenerationSeed >= Number.MAX_SAFE_INTEGER) {
|
|
@@ -224,29 +226,35 @@ export class SessionBrowserService {
|
|
|
224
226
|
});
|
|
225
227
|
}
|
|
226
228
|
async open(input) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
await this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (existing?.status === "ready") {
|
|
236
|
-
try {
|
|
237
|
-
await this.reconcileForRead(existing);
|
|
229
|
+
const admission = this.reserveOpenAdmission();
|
|
230
|
+
try {
|
|
231
|
+
const params = parseInput(() => parseRuntimeBrowserOpenParams(input), "Browser open request is invalid");
|
|
232
|
+
return await this.exclusive(params.sessionId, async () => {
|
|
233
|
+
this.ensureRunning();
|
|
234
|
+
await this.ensureSessionExists(params.sessionId);
|
|
235
|
+
const existing = this.records.get(params.sessionId);
|
|
236
|
+
if (existing?.status === "opening") {
|
|
238
237
|
return this.project(existing);
|
|
239
238
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
if (existing?.status === "ready") {
|
|
240
|
+
try {
|
|
241
|
+
await this.reconcileForRead(existing);
|
|
242
|
+
return this.project(existing);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// An App Host may have gone away immediately before this open. The
|
|
246
|
+
// failed reconciliation marks the old generation unavailable, so an
|
|
247
|
+
// idempotent open can recreate it on the current preferred Host.
|
|
248
|
+
}
|
|
244
249
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
+
if (existing)
|
|
251
|
+
await this.closeUnavailableForRecreate(existing);
|
|
252
|
+
return this.createBrowser(params);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
admission?.release();
|
|
257
|
+
}
|
|
250
258
|
}
|
|
251
259
|
async close(input) {
|
|
252
260
|
const params = parseInput(() => parseRuntimeBrowserCloseParams(input), "Browser close request is invalid");
|
|
@@ -438,6 +446,15 @@ export class SessionBrowserService {
|
|
|
438
446
|
throw new SessionBrowserServiceError("host_failure", "Browser Host state could not be initialized", { cause: error });
|
|
439
447
|
}
|
|
440
448
|
}
|
|
449
|
+
reserveOpenAdmission() {
|
|
450
|
+
if (!this.admissionReserve)
|
|
451
|
+
return undefined;
|
|
452
|
+
const admission = this.admissionReserve();
|
|
453
|
+
if (!admission) {
|
|
454
|
+
throw new SessionBrowserServiceError("shutting_down", "Browser service is shutting down");
|
|
455
|
+
}
|
|
456
|
+
return admission;
|
|
457
|
+
}
|
|
441
458
|
async closeUnavailableForRecreate(record) {
|
|
442
459
|
record.status = "closing";
|
|
443
460
|
try {
|
|
@@ -13,6 +13,12 @@ export interface SessionRuntimeActivitySnapshot {
|
|
|
13
13
|
*/
|
|
14
14
|
export declare class SessionRuntimeIndex {
|
|
15
15
|
private readonly sessions;
|
|
16
|
+
/**
|
|
17
|
+
* Make an accepted Turn visible before the request admission reservation is
|
|
18
|
+
* released. The returned callback only abandons the pre-created handoff; once
|
|
19
|
+
* response.created arrives, the native response remains authoritative.
|
|
20
|
+
*/
|
|
21
|
+
beginResponseHandoff(sessionId: string, responseId: string): () => void;
|
|
16
22
|
observe(sessionId: string, event: SessionEvent): void;
|
|
17
23
|
snapshot(sessionId: string): SessionRuntimeSnapshot;
|
|
18
24
|
/** Aggregate only process-live work; durable Session history is not activity. */
|
|
@@ -20,4 +26,5 @@ export declare class SessionRuntimeIndex {
|
|
|
20
26
|
remove(sessionId: string): void;
|
|
21
27
|
private state;
|
|
22
28
|
private removeResponseInteractions;
|
|
29
|
+
private hasRunningTurn;
|
|
23
30
|
}
|
|
@@ -6,35 +6,57 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export class SessionRuntimeIndex {
|
|
8
8
|
sessions = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Make an accepted Turn visible before the request admission reservation is
|
|
11
|
+
* released. The returned callback only abandons the pre-created handoff; once
|
|
12
|
+
* response.created arrives, the native response remains authoritative.
|
|
13
|
+
*/
|
|
14
|
+
beginResponseHandoff(sessionId, responseId) {
|
|
15
|
+
const state = this.state(sessionId);
|
|
16
|
+
state.pendingResponseIds.add(responseId);
|
|
17
|
+
state.status = "running";
|
|
18
|
+
state.statusKind = "startup";
|
|
19
|
+
let pending = true;
|
|
20
|
+
return () => {
|
|
21
|
+
if (!pending)
|
|
22
|
+
return;
|
|
23
|
+
pending = false;
|
|
24
|
+
state.pendingResponseIds.delete(responseId);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
9
27
|
observe(sessionId, event) {
|
|
10
28
|
const state = this.state(sessionId);
|
|
11
29
|
switch (event.type) {
|
|
12
30
|
case "response.created":
|
|
31
|
+
state.pendingResponseIds.delete(event.responseId);
|
|
13
32
|
state.activeResponseIds.add(event.responseId);
|
|
14
33
|
state.status = "running";
|
|
15
34
|
state.statusKind = undefined;
|
|
16
35
|
return;
|
|
17
36
|
case "response.completed":
|
|
37
|
+
state.pendingResponseIds.delete(event.responseId);
|
|
18
38
|
state.activeResponseIds.delete(event.responseId);
|
|
19
39
|
this.removeResponseInteractions(state, event.responseId);
|
|
20
|
-
state.status = state
|
|
40
|
+
state.status = this.hasRunningTurn(state) ? "running" : "idle";
|
|
21
41
|
state.statusKind = undefined;
|
|
22
42
|
return;
|
|
23
43
|
case "response.failed":
|
|
44
|
+
state.pendingResponseIds.delete(event.responseId);
|
|
24
45
|
state.activeResponseIds.delete(event.responseId);
|
|
25
46
|
this.removeResponseInteractions(state, event.responseId);
|
|
26
47
|
// Response failure is Turn-local. It must not poison a reusable Session.
|
|
27
|
-
state.status = state
|
|
48
|
+
state.status = this.hasRunningTurn(state) ? "running" : "idle";
|
|
28
49
|
state.statusKind = undefined;
|
|
29
50
|
return;
|
|
30
51
|
case "session.status":
|
|
31
52
|
state.status =
|
|
32
|
-
state.pendingInteractions.size > 0 || state
|
|
53
|
+
state.pendingInteractions.size > 0 || this.hasRunningTurn(state)
|
|
33
54
|
? "running"
|
|
34
55
|
: event.status;
|
|
35
56
|
state.statusKind = event.statusKind;
|
|
36
57
|
return;
|
|
37
58
|
case "session.interaction.requested":
|
|
59
|
+
state.pendingResponseIds.delete(event.responseId);
|
|
38
60
|
state.pendingInteractions.set(event.interaction.interactionId, {
|
|
39
61
|
responseId: event.responseId,
|
|
40
62
|
interaction: event.interaction,
|
|
@@ -46,9 +68,10 @@ export class SessionRuntimeIndex {
|
|
|
46
68
|
case "session.interaction.resolved":
|
|
47
69
|
case "session.interaction.cancelled":
|
|
48
70
|
state.pendingInteractions.delete(event.interactionId);
|
|
49
|
-
state.status = state
|
|
71
|
+
state.status = this.hasRunningTurn(state) ? "running" : state.status;
|
|
50
72
|
return;
|
|
51
73
|
case "session.rotated":
|
|
74
|
+
state.pendingResponseIds.clear();
|
|
52
75
|
state.activeResponseIds.clear();
|
|
53
76
|
state.pendingInteractions.clear();
|
|
54
77
|
state.status = "idle";
|
|
@@ -63,7 +86,9 @@ export class SessionRuntimeIndex {
|
|
|
63
86
|
if (!state)
|
|
64
87
|
return { status: "idle", activeResponseIds: [], pendingInteractions: [] };
|
|
65
88
|
return {
|
|
66
|
-
status: state.pendingInteractions.size > 0
|
|
89
|
+
status: state.pendingInteractions.size > 0 || this.hasRunningTurn(state)
|
|
90
|
+
? "running"
|
|
91
|
+
: state.status,
|
|
67
92
|
...(state.statusKind === undefined ? {} : { statusKind: state.statusKind }),
|
|
68
93
|
activeResponseIds: [...state.activeResponseIds],
|
|
69
94
|
pendingInteractions: [...state.pendingInteractions.values()],
|
|
@@ -74,7 +99,7 @@ export class SessionRuntimeIndex {
|
|
|
74
99
|
let runningTurns = 0;
|
|
75
100
|
let pendingInteractions = 0;
|
|
76
101
|
for (const state of this.sessions.values()) {
|
|
77
|
-
runningTurns += state.activeResponseIds.size;
|
|
102
|
+
runningTurns += state.pendingResponseIds.size + state.activeResponseIds.size;
|
|
78
103
|
pendingInteractions += state.pendingInteractions.size;
|
|
79
104
|
}
|
|
80
105
|
return { runningTurns, pendingInteractions };
|
|
@@ -87,6 +112,7 @@ export class SessionRuntimeIndex {
|
|
|
87
112
|
if (!state) {
|
|
88
113
|
state = {
|
|
89
114
|
status: "idle",
|
|
115
|
+
pendingResponseIds: new Set(),
|
|
90
116
|
activeResponseIds: new Set(),
|
|
91
117
|
pendingInteractions: new Map(),
|
|
92
118
|
};
|
|
@@ -100,4 +126,7 @@ export class SessionRuntimeIndex {
|
|
|
100
126
|
state.pendingInteractions.delete(interactionId);
|
|
101
127
|
}
|
|
102
128
|
}
|
|
129
|
+
hasRunningTurn(state) {
|
|
130
|
+
return state.pendingResponseIds.size > 0 || state.activeResponseIds.size > 0;
|
|
131
|
+
}
|
|
103
132
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AdmissionReservation } from "@rynx-ai/core";
|
|
1
2
|
import { type RunnerManager } from "@rynx-ai/runtime";
|
|
2
3
|
export type SessionTerminalRole = "owner" | "read-only";
|
|
3
4
|
export interface SessionTerminalOpenOptions {
|
|
@@ -46,6 +47,11 @@ export interface RunnerSessionTerminalHostOptions {
|
|
|
46
47
|
} | undefined | Promise<{
|
|
47
48
|
cwd: string;
|
|
48
49
|
} | undefined>;
|
|
50
|
+
/**
|
|
51
|
+
* Synchronous daemon admission fence. A successful reservation is held until
|
|
52
|
+
* the attachment is registered and the runner confirms that it is ready.
|
|
53
|
+
*/
|
|
54
|
+
admissionReserve?: () => AdmissionReservation | undefined;
|
|
49
55
|
}
|
|
50
56
|
/** Adapt the runner-child terminal protocol without exposing cwd or commands to clients. */
|
|
51
57
|
export declare function createRunnerSessionTerminalHost(options: RunnerSessionTerminalHostOptions): RunnerSessionTerminalHost;
|
|
@@ -24,56 +24,71 @@ export function createRunnerSessionTerminalHost(options) {
|
|
|
24
24
|
return {
|
|
25
25
|
activeTerminalCount: () => attachmentCounts.size,
|
|
26
26
|
async open(sessionId, openOptions) {
|
|
27
|
-
|
|
28
|
-
throwIfAborted(openOptions.signal);
|
|
29
|
-
const session = await options.resolveLiveSession(sessionId);
|
|
30
|
-
if (!session || typeof session.cwd !== "string" || session.cwd.trim().length === 0) {
|
|
31
|
-
throw new SessionTerminalOpenError("not_live", "Session terminal is not live");
|
|
32
|
-
}
|
|
33
|
-
throwIfAborted(openOptions.signal);
|
|
34
|
-
let parent;
|
|
27
|
+
const admission = reserveTerminalAdmission(options.admissionReserve);
|
|
35
28
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
validateOpen(sessionId, openOptions);
|
|
30
|
+
throwIfAborted(openOptions.signal);
|
|
31
|
+
const session = await options.resolveLiveSession(sessionId);
|
|
32
|
+
if (!session || typeof session.cwd !== "string" || session.cwd.trim().length === 0) {
|
|
33
|
+
throw new SessionTerminalOpenError("not_live", "Session terminal is not live");
|
|
34
|
+
}
|
|
35
|
+
throwIfAborted(openOptions.signal);
|
|
36
|
+
let parent;
|
|
37
|
+
try {
|
|
38
|
+
parent = options.runnerManager.openLiveTerminal(sessionId, {
|
|
39
|
+
role: openOptions.role,
|
|
40
|
+
cwd: session.cwd,
|
|
41
|
+
cols: openOptions.cols,
|
|
42
|
+
rows: openOptions.rows,
|
|
47
43
|
});
|
|
48
44
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
attachment.opened(opened.role);
|
|
57
|
-
attachment.throwIfOpeningFailed();
|
|
58
|
-
return attachment;
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
await attachment.close().catch(() => undefined);
|
|
62
|
-
if (isAbort(error)) {
|
|
63
|
-
throw new SessionTerminalOpenError("invalid_request", "Session terminal open was cancelled", { cause: error });
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof TerminalOpenError && error.code === "terminal_not_live") {
|
|
47
|
+
throw new SessionTerminalOpenError("not_live", "Session terminal is not live", {
|
|
48
|
+
cause: error,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
throw new SessionTerminalOpenError("internal", "Session terminal could not be opened", { cause: error });
|
|
64
52
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
const attachment = new RunnerSessionTerminalAttachment(sessionId, parent, openOptions.signal);
|
|
54
|
+
const releaseActivity = trackTerminalAttachment(attachmentCounts, sessionId);
|
|
55
|
+
void attachment.closed.then(releaseActivity, releaseActivity);
|
|
56
|
+
try {
|
|
57
|
+
const opened = await waitFor(parent.ready, openOptions.signal);
|
|
58
|
+
attachment.opened(opened.role);
|
|
59
|
+
attachment.throwIfOpeningFailed();
|
|
60
|
+
return attachment;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
await attachment.close().catch(() => undefined);
|
|
64
|
+
if (isAbort(error)) {
|
|
65
|
+
throw new SessionTerminalOpenError("invalid_request", "Session terminal open was cancelled", { cause: error });
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof TerminalOpenError && error.code === "terminal_not_live") {
|
|
68
|
+
throw new SessionTerminalOpenError("not_live", "Session terminal is not live", {
|
|
69
|
+
cause: error,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (error instanceof SessionTerminalOpenError)
|
|
73
|
+
throw error;
|
|
74
|
+
throw new SessionTerminalOpenError("internal", "Session terminal could not be opened", { cause: error });
|
|
69
75
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
admission?.release();
|
|
73
79
|
}
|
|
74
80
|
},
|
|
75
81
|
};
|
|
76
82
|
}
|
|
83
|
+
function reserveTerminalAdmission(reserve) {
|
|
84
|
+
if (!reserve)
|
|
85
|
+
return undefined;
|
|
86
|
+
const admission = reserve();
|
|
87
|
+
if (!admission) {
|
|
88
|
+
throw new SessionTerminalOpenError("not_live", "Session terminal is unavailable during daemon maintenance");
|
|
89
|
+
}
|
|
90
|
+
return admission;
|
|
91
|
+
}
|
|
77
92
|
function trackTerminalAttachment(attachmentCounts, sessionId) {
|
|
78
93
|
attachmentCounts.set(sessionId, (attachmentCounts.get(sessionId) ?? 0) + 1);
|
|
79
94
|
let released = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/server",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.4",
|
|
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/
|
|
31
|
-
"@rynx-ai/
|
|
32
|
-
"@rynx-ai/protocol": "0.1.11-beta.
|
|
33
|
-
"@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.
|
|
34
|
-
"@rynx-ai/runtime": "0.1.11-beta.
|
|
30
|
+
"@rynx-ai/plugin-sdk": "0.1.11-beta.4",
|
|
31
|
+
"@rynx-ai/core": "0.1.11-beta.4",
|
|
32
|
+
"@rynx-ai/protocol": "0.1.11-beta.4",
|
|
33
|
+
"@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.4",
|
|
34
|
+
"@rynx-ai/runtime": "0.1.11-beta.4"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/ws": "^8.18.1",
|
|
38
|
-
"@rynx-ai/control-web": "0.1.11-beta.
|
|
38
|
+
"@rynx-ai/control-web": "0.1.11-beta.4"
|
|
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/",
|