@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.
- package/dist/control-api.d.ts +10 -3
- package/dist/control-api.js +168 -7
- package/dist/control-web/assets/{highlighted-body-OFNGDK62-DWWsUtHF.js → highlighted-body-OFNGDK62-Ctle6Bpj.js} +1 -1
- package/dist/control-web/assets/index-0noHpRXD.css +32 -0
- package/dist/control-web/assets/index-dOafa0IY.js +709 -0
- package/dist/control-web/assets/{mermaid-GHXKKRXX-C1fxAgiw.js → mermaid-GHXKKRXX-DvISAPzD.js} +3 -3
- package/dist/control-web/index.html +2 -2
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +4 -2
- package/dist/machine-session-service.d.ts +31 -6
- package/dist/machine-session-service.js +124 -7
- package/dist/remote-runtime-dispatcher.d.ts +1 -1
- package/dist/remote-runtime-dispatcher.js +4 -0
- package/dist/server.d.ts +25 -3
- package/dist/server.js +117 -43
- package/dist/session-browser-service.d.ts +8 -0
- package/dist/session-browser-service.js +37 -20
- package/dist/session-portal.d.ts +42 -0
- package/dist/session-portal.js +746 -0
- 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/dist/terminal-ws.js +8 -6
- package/package.json +7 -7
- package/dist/control-web/assets/index-BnX7NWJX.js +0 -689
- package/dist/control-web/assets/index-CUoWRX2i.css +0 -32
|
@@ -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/dist/terminal-ws.js
CHANGED
|
@@ -298,13 +298,15 @@ function parseBrowserInput(data, isBinary) {
|
|
|
298
298
|
throw new Error("Unknown or invalid terminal control frame");
|
|
299
299
|
}
|
|
300
300
|
async function applyBrowserInput(terminal, input) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
301
|
+
// Defense in depth: tmux -r also blocks writes, but neither keystrokes nor
|
|
302
|
+
// resize/SIGWINCH may reach a read-only Session. Resizing changes live TUI
|
|
303
|
+
// layout and is therefore a mutation, not a harmless viewer preference.
|
|
304
|
+
if (terminal.role !== "owner")
|
|
305
305
|
return;
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
if (input.type === "input")
|
|
307
|
+
await terminal.write(input.data);
|
|
308
|
+
else
|
|
309
|
+
await terminal.resize(input.cols, input.rows);
|
|
308
310
|
}
|
|
309
311
|
async function sendBinary(ws, data) {
|
|
310
312
|
if (ws.readyState !== WebSocket.OPEN)
|
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.21",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"@koa/router": "^15.4.0",
|
|
28
28
|
"koa": "^3.2.0",
|
|
29
29
|
"ws": "^8.21.0",
|
|
30
|
-
"@rynx-ai/core": "0.1.11-beta.
|
|
31
|
-
"@rynx-ai/plugin-sdk": "0.1.11-beta.
|
|
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/core": "0.1.11-beta.21",
|
|
31
|
+
"@rynx-ai/plugin-sdk": "0.1.11-beta.21",
|
|
32
|
+
"@rynx-ai/protocol": "0.1.11-beta.21",
|
|
33
|
+
"@rynx-ai/remote-runtime-e2ee": "0.1.11-beta.21",
|
|
34
|
+
"@rynx-ai/runtime": "0.1.11-beta.21"
|
|
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.21"
|
|
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/",
|