@rynx-ai/server 0.1.0 → 0.1.9
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/browser-surface-control-queue.d.ts +42 -0
- package/dist/browser-surface-control-queue.js +187 -0
- package/dist/browser-surface-ws.d.ts +14 -0
- package/dist/browser-surface-ws.js +355 -0
- package/dist/control-api.d.ts +59 -4
- package/dist/control-api.js +2213 -132
- package/dist/control-web/assets/highlighted-body-OFNGDK62-e-w61YLy.js +1 -0
- package/dist/control-web/assets/index-BQ7XVBKO.css +32 -0
- package/dist/control-web/assets/index-ClpzuBJx.js +606 -0
- package/dist/control-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/control-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/control-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/control-web/assets/mermaid-GHXKKRXX-DZn7Hbr2.js +131 -0
- package/dist/control-web/assets/rynx-wordmark-LL1QRYHD.png +0 -0
- package/dist/control-web/favicon.png +0 -0
- package/dist/control-web/favicon.svg +38 -0
- package/dist/control-web/index.html +16 -0
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +19 -14
- package/dist/desktop-browser-host.d.ts +153 -0
- package/dist/desktop-browser-host.js +936 -0
- package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
- package/dist/direct-runtime-browser-surface-server.js +821 -0
- package/dist/direct-runtime-server.d.ts +137 -0
- package/dist/direct-runtime-server.js +1931 -0
- package/dist/emulator-surface-ws.d.ts +6 -0
- package/dist/emulator-surface-ws.js +197 -0
- package/dist/machine-session-service.d.ts +182 -0
- package/dist/machine-session-service.js +559 -0
- package/dist/provider-cli-host.d.ts +10 -0
- package/dist/provider-cli-host.js +1 -0
- package/dist/remote-runtime-dispatcher.d.ts +94 -0
- package/dist/remote-runtime-dispatcher.js +380 -0
- package/dist/remote-runtime-session-projection.d.ts +19 -0
- package/dist/remote-runtime-session-projection.js +556 -0
- package/dist/remote-runtime.d.ts +22 -0
- package/dist/remote-runtime.js +32 -0
- package/dist/runtime-web-auth.d.ts +21 -0
- package/dist/runtime-web-auth.js +92 -0
- package/dist/server.d.ts +292 -10
- package/dist/server.js +314 -98
- package/dist/session-browser-service.d.ts +218 -0
- package/dist/session-browser-service.js +686 -0
- package/dist/session-browser-surface-coordinator.d.ts +23 -0
- package/dist/session-browser-surface-coordinator.js +563 -0
- package/dist/session-emulator-service.d.ts +167 -0
- package/dist/session-emulator-service.js +464 -0
- package/dist/session-runtime-index.d.ts +17 -0
- package/dist/session-runtime-index.js +86 -0
- package/dist/session-terminal-host.d.ts +46 -0
- package/dist/session-terminal-host.js +252 -0
- package/dist/terminal-ws.d.ts +9 -20
- package/dist/terminal-ws.js +420 -97
- package/package.json +9 -7
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-owned application service for the first Remote Runtime Session slice.
|
|
3
|
+
*
|
|
4
|
+
* This service deliberately has no HTTP, Koa, WebSocket, or Direct-binding
|
|
5
|
+
* knowledge. Durable records, live state, events, and interruption remain owned
|
|
6
|
+
* by the daemon process and arrive through narrow injected ports. Local and
|
|
7
|
+
* Direct adapters can therefore expose the same behavior without copying state
|
|
8
|
+
* into a Control Plane database.
|
|
9
|
+
*/
|
|
10
|
+
import { SESSION_PROVIDER_IDS, getRuntimeProfile, isSessionProviderId, newSessionId, normalizeSessionTitle, } from "@rynx-ai/core";
|
|
11
|
+
import { REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE, REMOTE_RUNTIME_SESSION_MAX_CURSOR_CHARS, REMOTE_RUNTIME_SESSION_MAX_ID_CHARS, REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
12
|
+
import { ensureCodexResumeRollout, } from "@rynx-ai/runtime";
|
|
13
|
+
const DEFAULT_LIST_LIMIT = REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE;
|
|
14
|
+
const MAX_LIST_LIMIT = REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE;
|
|
15
|
+
const DEFAULT_SNAPSHOT_ITEM_LIMIT = REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE;
|
|
16
|
+
const MAX_SNAPSHOT_ITEM_LIMIT = REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE;
|
|
17
|
+
const DEFAULT_LIST_PAGE_MAX_BYTES = 48 * 1024;
|
|
18
|
+
const DEFAULT_SNAPSHOT_PAGE_MAX_BYTES = 48 * 1024;
|
|
19
|
+
const MIN_PAGE_MAX_BYTES = 4 * 1024;
|
|
20
|
+
const MAX_PAGE_MAX_BYTES = 56 * 1024;
|
|
21
|
+
const DEFAULT_DIRECTORY_SCAN_LIMIT = 10_000;
|
|
22
|
+
const MAX_DIRECTORY_SCAN_LIMIT = 100_000;
|
|
23
|
+
const MAX_SESSION_ID_CHARS = REMOTE_RUNTIME_SESSION_MAX_ID_CHARS;
|
|
24
|
+
export class MachineSessionServiceInputError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "MachineSessionServiceInputError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Expected application failure; adapters decide how to represent it on the wire. */
|
|
31
|
+
export class MachineSessionServiceFailure extends Error {
|
|
32
|
+
code;
|
|
33
|
+
detail;
|
|
34
|
+
constructor(code, message, detail) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.detail = detail;
|
|
38
|
+
this.name = "MachineSessionServiceFailure";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Application boundary shared by an in-process Local adapter and Direct RPC. */
|
|
42
|
+
export class MachineSessionService {
|
|
43
|
+
ports;
|
|
44
|
+
maxListLimit;
|
|
45
|
+
listPageMaxBytes;
|
|
46
|
+
maxSnapshotItemLimit;
|
|
47
|
+
snapshotPageMaxBytes;
|
|
48
|
+
directoryScanLimit;
|
|
49
|
+
constructor(ports, options = {}) {
|
|
50
|
+
this.ports = ports;
|
|
51
|
+
this.maxListLimit = boundedInteger(options.maxListLimit ?? MAX_LIST_LIMIT, 1, MAX_LIST_LIMIT, "maxListLimit");
|
|
52
|
+
this.listPageMaxBytes = boundedInteger(options.listPageMaxBytes ?? DEFAULT_LIST_PAGE_MAX_BYTES, MIN_PAGE_MAX_BYTES, MAX_PAGE_MAX_BYTES, "listPageMaxBytes");
|
|
53
|
+
this.maxSnapshotItemLimit = boundedInteger(options.maxSnapshotItemLimit ?? MAX_SNAPSHOT_ITEM_LIMIT, 1, MAX_SNAPSHOT_ITEM_LIMIT, "maxSnapshotItemLimit");
|
|
54
|
+
this.snapshotPageMaxBytes = boundedInteger(options.snapshotPageMaxBytes ?? DEFAULT_SNAPSHOT_PAGE_MAX_BYTES, MIN_PAGE_MAX_BYTES, MAX_PAGE_MAX_BYTES, "snapshotPageMaxBytes");
|
|
55
|
+
this.directoryScanLimit = boundedInteger(options.directoryScanLimit ?? DEFAULT_DIRECTORY_SCAN_LIMIT, 1, MAX_DIRECTORY_SCAN_LIMIT, "directoryScanLimit");
|
|
56
|
+
}
|
|
57
|
+
async list(input = {}) {
|
|
58
|
+
const limit = requestLimit(input.limit, DEFAULT_LIST_LIMIT, this.maxListLimit, "limit");
|
|
59
|
+
const cursor = input.cursor === undefined
|
|
60
|
+
? undefined
|
|
61
|
+
: decodeListCursor(input.cursor);
|
|
62
|
+
const allMetas = this.ports.registry.list();
|
|
63
|
+
const metas = [...allMetas]
|
|
64
|
+
.sort(compareMetaRecency)
|
|
65
|
+
.slice(0, this.directoryScanLimit);
|
|
66
|
+
const logged = await this.ports.log.listSessions({ limit: this.directoryScanLimit + 1 });
|
|
67
|
+
const directoryTruncated = allMetas.length > this.directoryScanLimit || logged.length > this.directoryScanLimit;
|
|
68
|
+
const boundedLogs = logged.slice(0, this.directoryScanLimit);
|
|
69
|
+
const metaById = new Map(metas.map((meta) => [meta.id, meta]));
|
|
70
|
+
const logById = new Map(boundedLogs.map((entry) => [entry.sessionId, entry]));
|
|
71
|
+
const ids = new Set([...metaById.keys(), ...logById.keys()]);
|
|
72
|
+
const summaries = [...ids]
|
|
73
|
+
.map((id) => sessionSummary(id, metaById.get(id), logById.get(id), this.ports.runtimeState))
|
|
74
|
+
.sort(compareSummaryRecency);
|
|
75
|
+
const afterCursor = cursor === undefined
|
|
76
|
+
? summaries
|
|
77
|
+
: summaries.filter((summary) => isAfterListCursor(summary, cursor));
|
|
78
|
+
const page = afterCursor.slice(0, limit);
|
|
79
|
+
while (page.length > 0) {
|
|
80
|
+
const result = listPage(page, afterCursor.length > page.length, directoryTruncated);
|
|
81
|
+
if (page.length === 1 || jsonByteLength(result) <= this.listPageMaxBytes)
|
|
82
|
+
return result;
|
|
83
|
+
page.pop();
|
|
84
|
+
}
|
|
85
|
+
return listPage([], false, directoryTruncated);
|
|
86
|
+
}
|
|
87
|
+
async snapshot(input) {
|
|
88
|
+
const sessionId = validOpaqueToken(input.sessionId, "sessionId", MAX_SESSION_ID_CHARS);
|
|
89
|
+
const afterId = input.afterId === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: validOpaqueToken(input.afterId, "afterId", MAX_SESSION_ID_CHARS);
|
|
92
|
+
const limit = requestLimit(input.limit, DEFAULT_SNAPSHOT_ITEM_LIMIT, this.maxSnapshotItemLimit, "limit");
|
|
93
|
+
const candidates = (await this.ports.log.list(sessionId, {
|
|
94
|
+
...(afterId === undefined ? {} : { afterId }),
|
|
95
|
+
limit: limit + 1,
|
|
96
|
+
})).slice(0, limit + 1).map((item) => structuredClone(item));
|
|
97
|
+
const runtime = cloneRuntimeSnapshot(this.ports.runtimeState.snapshot(sessionId));
|
|
98
|
+
const empty = snapshotPage(sessionId, [], runtime, false);
|
|
99
|
+
const items = candidates.slice(0, limit);
|
|
100
|
+
while (items.length > 0) {
|
|
101
|
+
const result = snapshotPage(sessionId, items, runtime, candidates.length > items.length);
|
|
102
|
+
if (items.length === 1 || jsonByteLength(result) <= this.snapshotPageMaxBytes)
|
|
103
|
+
return result;
|
|
104
|
+
items.pop();
|
|
105
|
+
}
|
|
106
|
+
return empty;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Install a live subscriber synchronously, before returning control to an
|
|
110
|
+
* adapter. The adapter may now acknowledge readiness and request snapshot
|
|
111
|
+
* pages while buffering this tail, closing the subscribe/snapshot race.
|
|
112
|
+
*/
|
|
113
|
+
watch(sessionIdInput, options = {}) {
|
|
114
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
115
|
+
const iterator = this.ports.events.subscribe(sessionId)[Symbol.asyncIterator]();
|
|
116
|
+
return cancellableWatch(sessionId, iterator, options.signal);
|
|
117
|
+
}
|
|
118
|
+
async interrupt(sessionIdInput) {
|
|
119
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
120
|
+
return { interrupted: await this.ports.interruption.interrupt(sessionId) };
|
|
121
|
+
}
|
|
122
|
+
/** Session-scoped projection of the target daemon's own Agent catalog. */
|
|
123
|
+
async agentOptions() {
|
|
124
|
+
const catalog = this.requireAgents();
|
|
125
|
+
const agents = (await catalog.list()).map((agent) => ({
|
|
126
|
+
id: agent.id,
|
|
127
|
+
...(agent.name === undefined ? {} : { name: agent.name }),
|
|
128
|
+
...(agent.runtime === undefined ? {} : { runtime: agent.runtime }),
|
|
129
|
+
...(agent.model === undefined ? {} : { model: agent.model }),
|
|
130
|
+
}));
|
|
131
|
+
agents.sort((left, right) => (left.name ?? left.id).localeCompare(right.name ?? right.id) || left.id.localeCompare(right.id));
|
|
132
|
+
return { agents };
|
|
133
|
+
}
|
|
134
|
+
/** Target-owned Provider readiness plus the existing Agent preset catalog. */
|
|
135
|
+
async launchOptions() {
|
|
136
|
+
const statusCache = new Map();
|
|
137
|
+
const { agents } = await this.agentOptions();
|
|
138
|
+
const [providers, launchAgents] = await Promise.all([
|
|
139
|
+
Promise.all(SESSION_PROVIDER_IDS.map(async (provider) => ({
|
|
140
|
+
id: provider,
|
|
141
|
+
name: getRuntimeProfile(provider).displayName,
|
|
142
|
+
...await this.runtimeReadiness(provider, statusCache),
|
|
143
|
+
}))),
|
|
144
|
+
Promise.all(agents.map(async (agent) => {
|
|
145
|
+
const execution = this.ports.execution;
|
|
146
|
+
if (!execution) {
|
|
147
|
+
return {
|
|
148
|
+
...agent,
|
|
149
|
+
ready: false,
|
|
150
|
+
reason: "Session execution is unavailable on this Runtime.",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const runtime = await execution.runtime.resolveRuntime({ agentName: agent.id });
|
|
155
|
+
return {
|
|
156
|
+
...agent,
|
|
157
|
+
runtime,
|
|
158
|
+
...await this.runtimeReadiness(runtime, statusCache),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return {
|
|
163
|
+
...agent,
|
|
164
|
+
ready: false,
|
|
165
|
+
reason: "Agent execution settings could not be resolved.",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
})),
|
|
169
|
+
]);
|
|
170
|
+
return { providers, agents: launchAgents };
|
|
171
|
+
}
|
|
172
|
+
/** Create target-owned Session identity from one Agent preset or direct Provider. */
|
|
173
|
+
async create(input) {
|
|
174
|
+
const hasAgent = input.agent !== undefined;
|
|
175
|
+
const hasProvider = input.provider !== undefined;
|
|
176
|
+
if (hasAgent === hasProvider) {
|
|
177
|
+
throw new MachineSessionServiceInputError("exactly one of agent or provider is required");
|
|
178
|
+
}
|
|
179
|
+
let target;
|
|
180
|
+
if (hasAgent) {
|
|
181
|
+
const agent = validOpaqueToken(input.agent, "agent", MAX_SESSION_ID_CHARS);
|
|
182
|
+
const known = (await this.requireAgents().list()).some((candidate) => candidate.id === agent);
|
|
183
|
+
if (!known)
|
|
184
|
+
throw new MachineSessionServiceFailure("not_found", "agent not found");
|
|
185
|
+
target = { agent };
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
if (!isSessionProviderId(input.provider)) {
|
|
189
|
+
throw new MachineSessionServiceInputError("provider must be codex or claude");
|
|
190
|
+
}
|
|
191
|
+
const readiness = await this.runtimeReadiness(input.provider);
|
|
192
|
+
if (!readiness.ready) {
|
|
193
|
+
throw new MachineSessionServiceFailure("failed_precondition", readiness.reason ?? "provider is not ready");
|
|
194
|
+
}
|
|
195
|
+
target = { provider: input.provider };
|
|
196
|
+
}
|
|
197
|
+
const sessionId = newSessionId();
|
|
198
|
+
this.ports.registry.create({
|
|
199
|
+
id: sessionId,
|
|
200
|
+
source: "console",
|
|
201
|
+
...target,
|
|
202
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
203
|
+
...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
|
|
204
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
205
|
+
createdAt: new Date().toISOString(),
|
|
206
|
+
});
|
|
207
|
+
return { sessionId };
|
|
208
|
+
}
|
|
209
|
+
/** Inject one turn through the target daemon's native single-writer runner. */
|
|
210
|
+
async sendMessage(sessionIdInput, messageInput) {
|
|
211
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
212
|
+
if (typeof messageInput !== "string" || messageInput.length === 0) {
|
|
213
|
+
throw new MachineSessionServiceInputError("message must be a non-empty string");
|
|
214
|
+
}
|
|
215
|
+
const meta = this.ports.registry.get(sessionId);
|
|
216
|
+
const consoleMeta = meta?.source === "console" ? meta : undefined;
|
|
217
|
+
if (consoleMeta && !consoleMeta.title) {
|
|
218
|
+
this.ports.registry.setTitle(sessionId, synthesizeSessionTitle(messageInput));
|
|
219
|
+
}
|
|
220
|
+
const execution = await this.ensureLiveSession(sessionId, meta);
|
|
221
|
+
const outcome = await execution.runner.injectMessage(sessionId, messageInput);
|
|
222
|
+
if (outcome === "failed") {
|
|
223
|
+
throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`);
|
|
224
|
+
}
|
|
225
|
+
if (outcome !== "injected") {
|
|
226
|
+
throw new MachineSessionServiceFailure("failed_precondition", `live injection ${outcome}`);
|
|
227
|
+
}
|
|
228
|
+
return { ok: true, injected: true };
|
|
229
|
+
}
|
|
230
|
+
/** Explicitly restore the target daemon's live runner without starting a turn. */
|
|
231
|
+
async startTerminal(sessionIdInput) {
|
|
232
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
233
|
+
await this.ensureLiveSession(sessionId);
|
|
234
|
+
return { ok: true };
|
|
235
|
+
}
|
|
236
|
+
async ensureLiveSession(sessionId, meta = this.ports.registry.get(sessionId)) {
|
|
237
|
+
const execution = this.requireExecution();
|
|
238
|
+
const consoleMeta = meta?.source === "console" ? meta : undefined;
|
|
239
|
+
const bound = consoleMeta ? null : ((await execution.sessionStore?.get(sessionId)) ?? null);
|
|
240
|
+
if (!meta && !bound) {
|
|
241
|
+
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
242
|
+
}
|
|
243
|
+
const liveRuntime = await execution.runtime.resolveRuntime({
|
|
244
|
+
agentName: consoleMeta?.agent,
|
|
245
|
+
agentSpec: consoleMeta?.config,
|
|
246
|
+
provider: meta?.provider ?? bound?.runtime,
|
|
247
|
+
});
|
|
248
|
+
if (liveRuntime === "codex") {
|
|
249
|
+
const codexRecord = await execution.sessionStore?.get(sessionId);
|
|
250
|
+
if (codexRecord?.codexSessionId) {
|
|
251
|
+
ensureCodexResumeRollout({
|
|
252
|
+
sessionId,
|
|
253
|
+
threadId: codexRecord.codexSessionId,
|
|
254
|
+
cwd: codexRecord.cwd ?? process.cwd(),
|
|
255
|
+
items: await this.ports.log.snapshot(sessionId),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const live = await execution.runner.ensureLiveSession(sessionId, {
|
|
260
|
+
...(bound?.cwd
|
|
261
|
+
? { cwd: bound.cwd }
|
|
262
|
+
: consoleMeta?.config?.osEnv?.cwd
|
|
263
|
+
? { cwd: consoleMeta.config.osEnv.cwd }
|
|
264
|
+
: {}),
|
|
265
|
+
runtime: liveRuntime,
|
|
266
|
+
...(consoleMeta?.reasoningEffort
|
|
267
|
+
? { reasoningEffort: consoleMeta.reasoningEffort }
|
|
268
|
+
: {}),
|
|
269
|
+
...(consoleMeta?.agent ? { agentName: consoleMeta.agent } : {}),
|
|
270
|
+
...(consoleMeta?.config ? { agentSpec: consoleMeta.config } : {}),
|
|
271
|
+
});
|
|
272
|
+
if (!live) {
|
|
273
|
+
throw new MachineSessionServiceFailure("failed_precondition", `live session unavailable (${liveRuntime})`, execution.runner.lastLiveSessionError?.(sessionId));
|
|
274
|
+
}
|
|
275
|
+
return execution;
|
|
276
|
+
}
|
|
277
|
+
async resolveInteraction(sessionIdInput, interactionIdInput, resolution) {
|
|
278
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
279
|
+
const interactionId = validOpaqueToken(interactionIdInput, "interactionId", MAX_SESSION_ID_CHARS);
|
|
280
|
+
return this.requireExecution().runner.resolveInteraction(sessionId, interactionId, structuredClone(resolution));
|
|
281
|
+
}
|
|
282
|
+
async delete(sessionIdInput) {
|
|
283
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
284
|
+
try {
|
|
285
|
+
await this.ports.lifecycle?.beforeDelete?.(sessionId);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session-owned resources could not be fenced before deletion", error instanceof Error ? error.message : String(error));
|
|
289
|
+
}
|
|
290
|
+
this.ports.execution?.runner.stopRunner(sessionId);
|
|
291
|
+
this.ports.events.close?.(sessionId);
|
|
292
|
+
this.ports.registry.remove(sessionId);
|
|
293
|
+
await this.ports.log.deleteSession(sessionId);
|
|
294
|
+
this.ports.runtimeState.remove?.(sessionId);
|
|
295
|
+
try {
|
|
296
|
+
await this.ports.lifecycle?.afterDelete?.(sessionId);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
throw new MachineSessionServiceFailure("outcome_unknown", "Session was deleted but resource finalization did not complete", error instanceof Error ? error.message : String(error));
|
|
300
|
+
}
|
|
301
|
+
return { ok: true };
|
|
302
|
+
}
|
|
303
|
+
requireAgents() {
|
|
304
|
+
if (!this.ports.agents) {
|
|
305
|
+
throw new MachineSessionServiceFailure("failed_precondition", "session agent options are unavailable");
|
|
306
|
+
}
|
|
307
|
+
return this.ports.agents;
|
|
308
|
+
}
|
|
309
|
+
runtimeReadiness(runtime, cache) {
|
|
310
|
+
const cached = cache?.get(runtime);
|
|
311
|
+
if (cached)
|
|
312
|
+
return cached;
|
|
313
|
+
const pending = this.probeRuntimeReadiness(runtime);
|
|
314
|
+
cache?.set(runtime, pending);
|
|
315
|
+
return pending;
|
|
316
|
+
}
|
|
317
|
+
async probeRuntimeReadiness(runtime) {
|
|
318
|
+
const runner = this.ports.execution?.runner;
|
|
319
|
+
if (!runner?.getStatus) {
|
|
320
|
+
return { ready: false, reason: "Runtime readiness could not be checked." };
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const status = await runner.getStatus({ runtime });
|
|
324
|
+
const ready = status.codex_available &&
|
|
325
|
+
status.logged_in &&
|
|
326
|
+
status.session_store_writable &&
|
|
327
|
+
status.codex_sessions_accessible;
|
|
328
|
+
if (ready)
|
|
329
|
+
return { ready: true };
|
|
330
|
+
return {
|
|
331
|
+
ready: false,
|
|
332
|
+
reason: boundedLaunchReason(runtimeSetupReason(runtime, status)),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return { ready: false, reason: "Runtime readiness check failed." };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
requireExecution() {
|
|
340
|
+
if (!this.ports.execution) {
|
|
341
|
+
throw new MachineSessionServiceFailure("failed_precondition", "session execution is unavailable");
|
|
342
|
+
}
|
|
343
|
+
return this.ports.execution;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** One-line title derived from the first user message; no model call. */
|
|
347
|
+
export function synthesizeSessionTitle(message, limit = 60) {
|
|
348
|
+
const collapsed = message.replace(/\s+/g, " ").trim();
|
|
349
|
+
if (collapsed.length <= limit)
|
|
350
|
+
return collapsed;
|
|
351
|
+
return `${collapsed.slice(0, limit - 1).trimEnd()}…`;
|
|
352
|
+
}
|
|
353
|
+
/** Keep target-owned setup diagnostics comfortably inside the wire DTO bound. */
|
|
354
|
+
function boundedLaunchReason(reason) {
|
|
355
|
+
const normalized = reason.replace(/\s+/g, " ").trim();
|
|
356
|
+
return [...normalized].slice(0, 512).join("") || "Runtime needs setup.";
|
|
357
|
+
}
|
|
358
|
+
function runtimeSetupReason(runtime, status) {
|
|
359
|
+
const name = getRuntimeProfile(runtime).displayName;
|
|
360
|
+
if (!status.codex_available)
|
|
361
|
+
return `${name} CLI is unavailable.`;
|
|
362
|
+
if (!status.logged_in)
|
|
363
|
+
return `${name} is not authenticated.`;
|
|
364
|
+
if (!status.session_store_writable)
|
|
365
|
+
return "The Session store is not writable.";
|
|
366
|
+
if (!status.codex_sessions_accessible)
|
|
367
|
+
return `${name} Session storage is not accessible.`;
|
|
368
|
+
return status.issues.find((issue) => issue.trim().length > 0) ??
|
|
369
|
+
`${name} needs setup before it can start Sessions.`;
|
|
370
|
+
}
|
|
371
|
+
function listPage(sessions, hasMore, directoryTruncated) {
|
|
372
|
+
return {
|
|
373
|
+
sessions,
|
|
374
|
+
hasMore,
|
|
375
|
+
...(hasMore && sessions.length > 0 ? { nextCursor: encodeListCursor(sessions.at(-1)) } : {}),
|
|
376
|
+
directoryTruncated,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function isAfterListCursor(summary, cursor) {
|
|
380
|
+
const updatedAt = summaryUpdatedAt(summary);
|
|
381
|
+
return updatedAt < cursor.updatedAt || (updatedAt === cursor.updatedAt && summary.id > cursor.id);
|
|
382
|
+
}
|
|
383
|
+
function encodeListCursor(summary) {
|
|
384
|
+
const cursor = {
|
|
385
|
+
v: 1,
|
|
386
|
+
updatedAt: summaryUpdatedAt(summary),
|
|
387
|
+
id: summary.id,
|
|
388
|
+
};
|
|
389
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
390
|
+
}
|
|
391
|
+
function decodeListCursor(value) {
|
|
392
|
+
if (value.length === 0 ||
|
|
393
|
+
value.length > REMOTE_RUNTIME_SESSION_MAX_CURSOR_CHARS ||
|
|
394
|
+
!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
395
|
+
throw new MachineSessionServiceInputError("cursor is invalid");
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const bytes = Buffer.from(value, "base64url");
|
|
399
|
+
if (bytes.length === 0 || bytes.toString("base64url") !== value)
|
|
400
|
+
throw new Error();
|
|
401
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
402
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
403
|
+
throw new Error();
|
|
404
|
+
const record = parsed;
|
|
405
|
+
if (Object.keys(record).length !== 3 ||
|
|
406
|
+
!("v" in record) ||
|
|
407
|
+
!("updatedAt" in record) ||
|
|
408
|
+
!("id" in record)) {
|
|
409
|
+
throw new Error();
|
|
410
|
+
}
|
|
411
|
+
if (record.v !== 1) {
|
|
412
|
+
throw new MachineSessionServiceInputError("cursor version is not supported");
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
v: 1,
|
|
416
|
+
updatedAt: validTimestamp(record.updatedAt, "cursor.updatedAt"),
|
|
417
|
+
id: validOpaqueToken(record.id, "cursor.id", MAX_SESSION_ID_CHARS),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
if (error instanceof MachineSessionServiceInputError)
|
|
422
|
+
throw error;
|
|
423
|
+
throw new MachineSessionServiceInputError("cursor is invalid");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function snapshotPage(sessionId, items, runtime, hasMore) {
|
|
427
|
+
return {
|
|
428
|
+
sessionId,
|
|
429
|
+
items,
|
|
430
|
+
runtime,
|
|
431
|
+
hasMore,
|
|
432
|
+
...(hasMore && items.length > 0 ? { nextAfterId: items.at(-1).id } : {}),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function sessionSummary(id, meta, log, runtimeState) {
|
|
436
|
+
const createdAt = meta?.createdAt ?? timestampFromEpoch(log?.createdAt, "createdAt");
|
|
437
|
+
const updatedAt = log
|
|
438
|
+
? timestampFromEpoch(log.updatedAt, "updatedAt")
|
|
439
|
+
: (meta?.updatedAt ?? createdAt);
|
|
440
|
+
return {
|
|
441
|
+
id,
|
|
442
|
+
...(meta?.provider === undefined ? {} : { provider: meta.provider }),
|
|
443
|
+
...(meta?.agent === undefined ? {} : { agent: meta.agent }),
|
|
444
|
+
...(meta?.title === undefined ? {} : { title: normalizeSessionTitle(meta.title) }),
|
|
445
|
+
status: runtimeState.snapshot(id).status,
|
|
446
|
+
createdAt: validTimestamp(createdAt, "createdAt"),
|
|
447
|
+
source: meta?.source ?? "unknown",
|
|
448
|
+
updatedAt: validTimestamp(updatedAt, "updatedAt"),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function compareMetaRecency(left, right) {
|
|
452
|
+
const leftTime = validTimestamp(left.updatedAt ?? left.createdAt, "updatedAt");
|
|
453
|
+
const rightTime = validTimestamp(right.updatedAt ?? right.createdAt, "updatedAt");
|
|
454
|
+
return rightTime.localeCompare(leftTime) || left.id.localeCompare(right.id);
|
|
455
|
+
}
|
|
456
|
+
function compareSummaryRecency(left, right) {
|
|
457
|
+
return summaryUpdatedAt(right).localeCompare(summaryUpdatedAt(left)) || left.id.localeCompare(right.id);
|
|
458
|
+
}
|
|
459
|
+
function summaryUpdatedAt(summary) {
|
|
460
|
+
return summary.updatedAt ?? summary.createdAt;
|
|
461
|
+
}
|
|
462
|
+
function cancellableWatch(sessionId, iterator, signal) {
|
|
463
|
+
let cancelled = false;
|
|
464
|
+
let cancellationResolve;
|
|
465
|
+
const cancellation = new Promise((resolve) => {
|
|
466
|
+
cancellationResolve = resolve;
|
|
467
|
+
});
|
|
468
|
+
let cancelPromise;
|
|
469
|
+
const onAbort = () => {
|
|
470
|
+
void cancel().catch(() => undefined);
|
|
471
|
+
};
|
|
472
|
+
const cancel = () => {
|
|
473
|
+
if (cancelPromise)
|
|
474
|
+
return cancelPromise;
|
|
475
|
+
cancelled = true;
|
|
476
|
+
cancellationResolve();
|
|
477
|
+
signal?.removeEventListener("abort", onAbort);
|
|
478
|
+
cancelPromise = Promise.resolve(iterator.return?.()).then(() => undefined);
|
|
479
|
+
return cancelPromise;
|
|
480
|
+
};
|
|
481
|
+
if (signal?.aborted)
|
|
482
|
+
void cancel().catch(() => undefined);
|
|
483
|
+
else
|
|
484
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
485
|
+
const consumer = {
|
|
486
|
+
async next() {
|
|
487
|
+
if (cancelled)
|
|
488
|
+
return { value: undefined, done: true };
|
|
489
|
+
try {
|
|
490
|
+
const result = await Promise.race([
|
|
491
|
+
iterator.next(),
|
|
492
|
+
cancellation.then(() => ({ value: undefined, done: true })),
|
|
493
|
+
]);
|
|
494
|
+
if (result.done)
|
|
495
|
+
await cancel();
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
await cancel().catch(() => undefined);
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
async return() {
|
|
504
|
+
await cancel();
|
|
505
|
+
return { value: undefined, done: true };
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
const watch = {
|
|
509
|
+
sessionId,
|
|
510
|
+
cancel,
|
|
511
|
+
[Symbol.asyncIterator]() {
|
|
512
|
+
return consumer;
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
return watch;
|
|
516
|
+
}
|
|
517
|
+
function cloneRuntimeSnapshot(snapshot) {
|
|
518
|
+
return {
|
|
519
|
+
status: snapshot.status,
|
|
520
|
+
activeResponseIds: [...snapshot.activeResponseIds],
|
|
521
|
+
pendingInteractions: snapshot.pendingInteractions.map((pending) => ({
|
|
522
|
+
responseId: pending.responseId,
|
|
523
|
+
interaction: structuredClone(pending.interaction),
|
|
524
|
+
})),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function requestLimit(value, fallback, maximum, field) {
|
|
528
|
+
return value === undefined ? Math.min(fallback, maximum) : boundedInteger(value, 1, maximum, field);
|
|
529
|
+
}
|
|
530
|
+
function boundedInteger(value, minimum, maximum, field) {
|
|
531
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
532
|
+
throw new MachineSessionServiceInputError(`${field} must be an integer between ${minimum} and ${maximum}`);
|
|
533
|
+
}
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
function validOpaqueToken(value, field, maximum) {
|
|
537
|
+
if (typeof value !== "string" ||
|
|
538
|
+
value.length === 0 ||
|
|
539
|
+
value.length > maximum ||
|
|
540
|
+
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
541
|
+
throw new MachineSessionServiceInputError(`${field} must be a non-empty opaque string of at most ${maximum} characters`);
|
|
542
|
+
}
|
|
543
|
+
return value;
|
|
544
|
+
}
|
|
545
|
+
function validTimestamp(value, field) {
|
|
546
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
547
|
+
throw new MachineSessionServiceInputError(`${field} must be a valid timestamp`);
|
|
548
|
+
}
|
|
549
|
+
return new Date(value).toISOString();
|
|
550
|
+
}
|
|
551
|
+
function timestampFromEpoch(value, field) {
|
|
552
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
553
|
+
throw new MachineSessionServiceInputError(`${field} is unavailable`);
|
|
554
|
+
}
|
|
555
|
+
return new Date(value).toISOString();
|
|
556
|
+
}
|
|
557
|
+
function jsonByteLength(value) {
|
|
558
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
559
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProviderCliId, ProviderCliJob, ProviderCliOperation, ProviderCliStatusList } from "@rynx-ai/protocol/control";
|
|
2
|
+
/** Read-only Runtime-owned Provider CLI inventory, safe to expose over Direct Runtime. */
|
|
3
|
+
export interface ProviderCliStatusHost {
|
|
4
|
+
listStatuses(): Promise<ProviderCliStatusList>;
|
|
5
|
+
}
|
|
6
|
+
/** Local-machine mutation port. Control never forwards this interface remotely. */
|
|
7
|
+
export interface ProviderCliManagementHost extends ProviderCliStatusHost {
|
|
8
|
+
startOperation(provider: ProviderCliId, operation: ProviderCliOperation): Promise<ProviderCliJob>;
|
|
9
|
+
getJob(id: string): ProviderCliJob | undefined;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { RemoteRuntimeEmulatorGesturePoint, RemoteRuntimeRpcRequest, RemoteRuntimeRpcResponse, RemoteRuntimeSequencedSessionEvent } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
2
|
+
import { type DaemonRuntimeHost } from "./remote-runtime.js";
|
|
3
|
+
import { type MachineSessionService } from "./machine-session-service.js";
|
|
4
|
+
import { type SessionBrowserService } from "./session-browser-service.js";
|
|
5
|
+
import { type SessionEmulatorService } from "./session-emulator-service.js";
|
|
6
|
+
import { type SessionTerminalAttachment, type SessionTerminalHost, type SessionTerminalOpenOptions } from "./session-terminal-host.js";
|
|
7
|
+
import type { ProviderCliStatusHost } from "./provider-cli-host.js";
|
|
8
|
+
/** Binding-neutral application identity established before dispatch. */
|
|
9
|
+
export interface AuthenticatedRuntimeSubject {
|
|
10
|
+
subjectId: string;
|
|
11
|
+
effectiveAuthorities: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export interface RemoteRuntimeDispatchContext {
|
|
14
|
+
subject: AuthenticatedRuntimeSubject;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
export interface RemoteRuntimeDispatcherOptions {
|
|
18
|
+
runtime: DaemonRuntimeHost;
|
|
19
|
+
emulator?: RemoteRuntimeEmulatorHost;
|
|
20
|
+
sessionEmulators?: RemoteRuntimeSessionEmulatorHost;
|
|
21
|
+
sessions?: RemoteRuntimeSessionHost;
|
|
22
|
+
browsers?: RemoteRuntimeBrowserHost;
|
|
23
|
+
terminals?: SessionTerminalHost;
|
|
24
|
+
providerClis?: ProviderCliStatusHost;
|
|
25
|
+
/** Optional diagnostic sink. Wire errors deliberately omit internal details. */
|
|
26
|
+
onError?: (error: unknown, request: RemoteRuntimeRpcRequest) => void;
|
|
27
|
+
}
|
|
28
|
+
export type RemoteRuntimeSessionHost = Pick<MachineSessionService, "list" | "snapshot" | "watch" | "interrupt" | "agentOptions" | "launchOptions" | "create" | "sendMessage" | "startTerminal" | "delete" | "resolveInteraction">;
|
|
29
|
+
export type RemoteRuntimeBrowserHost = Pick<SessionBrowserService, "getState" | "open" | "close" | "createPage" | "closePage" | "activatePage" | "navigatePage" | "goBack" | "goForward" | "reload">;
|
|
30
|
+
export type RemoteRuntimeSessionEmulatorHost = Pick<SessionEmulatorService, "getState" | "listDevices" | "attach" | "detach" | "tap" | "type" | "button" | "rotate" | "launch" | "gesture" | "releaseDevice" | "stopDevice">;
|
|
31
|
+
export interface OpenRemoteRuntimeSessionEvents {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
daemonInstanceId: string;
|
|
34
|
+
events: AsyncIterable<RemoteRuntimeSequencedSessionEvent>;
|
|
35
|
+
cancel(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export type RemoteRuntimeSessionEventsOpenErrorCode = "unauthorized" | "invalid_request" | "method_not_found" | "internal";
|
|
38
|
+
export declare class RemoteRuntimeSessionEventsOpenError extends Error {
|
|
39
|
+
readonly code: RemoteRuntimeSessionEventsOpenErrorCode;
|
|
40
|
+
constructor(code: RemoteRuntimeSessionEventsOpenErrorCode, message: string, options?: ErrorOptions);
|
|
41
|
+
}
|
|
42
|
+
/** Transport-independent emulator application port used by Local and Direct bindings. */
|
|
43
|
+
export interface RemoteRuntimeEmulatorHost {
|
|
44
|
+
devices(): Promise<unknown[]>;
|
|
45
|
+
active(): Promise<unknown | null>;
|
|
46
|
+
deviceSession?(input: {
|
|
47
|
+
device: string;
|
|
48
|
+
}): Promise<unknown | null>;
|
|
49
|
+
attach(device?: string): Promise<unknown>;
|
|
50
|
+
tap(input: {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
device?: string;
|
|
54
|
+
}): Promise<unknown>;
|
|
55
|
+
type(input: {
|
|
56
|
+
text: string;
|
|
57
|
+
device?: string;
|
|
58
|
+
mode?: string;
|
|
59
|
+
}): Promise<unknown>;
|
|
60
|
+
button(input: {
|
|
61
|
+
name?: string;
|
|
62
|
+
device?: string;
|
|
63
|
+
}): Promise<unknown>;
|
|
64
|
+
rotate(input: {
|
|
65
|
+
orientation: string;
|
|
66
|
+
device?: string;
|
|
67
|
+
}): Promise<unknown>;
|
|
68
|
+
launch(input: {
|
|
69
|
+
appId: string;
|
|
70
|
+
device?: string;
|
|
71
|
+
}): Promise<unknown>;
|
|
72
|
+
gesture(input: {
|
|
73
|
+
points: RemoteRuntimeEmulatorGesturePoint[];
|
|
74
|
+
device?: string;
|
|
75
|
+
}): Promise<unknown>;
|
|
76
|
+
kill(input: {
|
|
77
|
+
device?: string;
|
|
78
|
+
}): Promise<unknown>;
|
|
79
|
+
shutdown(input: {
|
|
80
|
+
device?: string;
|
|
81
|
+
}): Promise<unknown>;
|
|
82
|
+
}
|
|
83
|
+
/** Transport-neutral V1 unary dispatcher shared by Local and Direct bindings. */
|
|
84
|
+
export declare class RemoteRuntimeDispatcher {
|
|
85
|
+
private readonly options;
|
|
86
|
+
constructor(options: RemoteRuntimeDispatcherOptions);
|
|
87
|
+
dispatch(request: RemoteRuntimeRpcRequest, { subject, signal }: RemoteRuntimeDispatchContext): Promise<RemoteRuntimeRpcResponse>;
|
|
88
|
+
openSessionEvents(sessionId: string, context: RemoteRuntimeDispatchContext): OpenRemoteRuntimeSessionEvents;
|
|
89
|
+
openSessionTerminal(sessionId: string, options: SessionTerminalOpenOptions, context: RemoteRuntimeDispatchContext): Promise<SessionTerminalAttachment>;
|
|
90
|
+
private withEmulator;
|
|
91
|
+
private withSessions;
|
|
92
|
+
private withSessionEmulators;
|
|
93
|
+
private withBrowsers;
|
|
94
|
+
}
|