@rynx-ai/server 0.1.11-beta.1 → 0.1.11-beta.3
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 -52
- package/dist/control-api.js +615 -338
- package/dist/control-web/assets/{highlighted-body-OFNGDK62-DgFZpTNw.js → highlighted-body-OFNGDK62-DWWsUtHF.js} +1 -1
- package/dist/control-web/assets/index-BnX7NWJX.js +689 -0
- package/dist/control-web/assets/index-CUoWRX2i.css +32 -0
- package/dist/control-web/assets/{mermaid-GHXKKRXX-BDikE3W1.js → mermaid-GHXKKRXX-C1fxAgiw.js} +3 -3
- package/dist/control-web/index.html +2 -2
- package/dist/machine-session-service.d.ts +121 -23
- package/dist/machine-session-service.js +339 -53
- package/dist/remote-runtime-dispatcher.d.ts +12 -2
- package/dist/remote-runtime-dispatcher.js +52 -0
- package/dist/remote-runtime-session-projection.js +1 -0
- package/dist/server.d.ts +48 -29
- package/dist/server.js +86 -142
- 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 +42 -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/channel-manager.d.ts +0 -60
- package/dist/channel-manager.js +0 -106
- package/dist/control-web/assets/index-CnVtOmLv.css +0 -32
- package/dist/control-web/assets/index-Dlywgy58.js +0 -661
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* Direct adapters can therefore expose the same behavior without copying state
|
|
8
8
|
* into a Control Plane database.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { SESSION_PROVIDER_IDS, getRuntimeProfile, isSessionProviderId, newSessionItemId, newSessionId, normalizeSessionTitle, } from "@rynx-ai/core";
|
|
11
12
|
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
13
|
const DEFAULT_LIST_LIMIT = REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE;
|
|
14
14
|
const MAX_LIST_LIMIT = REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE;
|
|
15
15
|
const DEFAULT_SNAPSHOT_ITEM_LIMIT = REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE;
|
|
@@ -47,8 +47,11 @@ 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();
|
|
54
|
+
forkTasks = new Map();
|
|
52
55
|
constructor(ports, options = {}) {
|
|
53
56
|
this.ports = ports;
|
|
54
57
|
this.maxListLimit = boundedInteger(options.maxListLimit ?? MAX_LIST_LIMIT, 1, MAX_LIST_LIMIT, "maxListLimit");
|
|
@@ -57,7 +60,10 @@ export class MachineSessionService {
|
|
|
57
60
|
this.snapshotPageMaxBytes = boundedInteger(options.snapshotPageMaxBytes ?? DEFAULT_SNAPSHOT_PAGE_MAX_BYTES, MIN_PAGE_MAX_BYTES, MAX_PAGE_MAX_BYTES, "snapshotPageMaxBytes");
|
|
58
61
|
this.directoryScanLimit = boundedInteger(options.directoryScanLimit ?? DEFAULT_DIRECTORY_SCAN_LIMIT, 1, MAX_DIRECTORY_SCAN_LIMIT, "directoryScanLimit");
|
|
59
62
|
this.pendingMessageRetryMs = boundedInteger(options.pendingMessageRetryMs ?? 1_000, 10, 60_000, "pendingMessageRetryMs");
|
|
63
|
+
this.admissionOpen = options.admissionOpen ?? (() => true);
|
|
64
|
+
this.admissionReserve = options.admissionReserve;
|
|
60
65
|
void this.resumePendingDeliveries();
|
|
66
|
+
void this.resumeReservedForks();
|
|
61
67
|
}
|
|
62
68
|
async list(input = {}) {
|
|
63
69
|
const limit = requestLimit(input.limit, DEFAULT_LIST_LIMIT, this.maxListLimit, "limit");
|
|
@@ -105,14 +111,22 @@ export class MachineSessionService {
|
|
|
105
111
|
})).slice(0, limit + 1).map((item) => structuredClone(item));
|
|
106
112
|
const runtime = cloneRuntimeSnapshot(this.ports.runtimeState.snapshot(sessionId));
|
|
107
113
|
const empty = snapshotPage(sessionId, [], runtime, false);
|
|
108
|
-
const items =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
const items = [];
|
|
115
|
+
let itemsJsonBytes = 0;
|
|
116
|
+
for (const item of candidates.slice(0, limit)) {
|
|
117
|
+
const nextItemsJsonBytes = itemsJsonBytes + (items.length > 0 ? 1 : 0) + jsonByteLength(item);
|
|
118
|
+
const nextCount = items.length + 1;
|
|
119
|
+
const hasMore = candidates.length > nextCount;
|
|
120
|
+
if (items.length > 0 &&
|
|
121
|
+
snapshotPageJsonByteLength(sessionId, nextItemsJsonBytes, item.id, runtime, hasMore) > this.snapshotPageMaxBytes) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
items.push(item);
|
|
125
|
+
itemsJsonBytes = nextItemsJsonBytes;
|
|
114
126
|
}
|
|
115
|
-
return
|
|
127
|
+
return items.length > 0
|
|
128
|
+
? snapshotPage(sessionId, items, runtime, candidates.length > items.length)
|
|
129
|
+
: empty;
|
|
116
130
|
}
|
|
117
131
|
/**
|
|
118
132
|
* Install a live subscriber synchronously, before returning control to an
|
|
@@ -180,6 +194,9 @@ export class MachineSessionService {
|
|
|
180
194
|
}
|
|
181
195
|
/** Create target-owned Session identity from one Agent preset or direct Provider. */
|
|
182
196
|
async create(input) {
|
|
197
|
+
return this.withAdmission(() => this.createAdmitted(input));
|
|
198
|
+
}
|
|
199
|
+
async createAdmitted(input) {
|
|
183
200
|
const hasAgent = input.agent !== undefined;
|
|
184
201
|
const hasProvider = input.provider !== undefined;
|
|
185
202
|
if (hasAgent === hasProvider) {
|
|
@@ -199,21 +216,188 @@ export class MachineSessionService {
|
|
|
199
216
|
}
|
|
200
217
|
target = { provider: input.provider };
|
|
201
218
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
const workspacePort = this.requireWorkspace();
|
|
220
|
+
const workspace = await workspacePort.resolve(input.projectId);
|
|
221
|
+
try {
|
|
222
|
+
const execution = await this.requireExecution().resolve({
|
|
223
|
+
...target,
|
|
224
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
225
|
+
...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
|
|
226
|
+
cwd: workspace.cwd,
|
|
227
|
+
});
|
|
228
|
+
const sessionId = newSessionId();
|
|
229
|
+
this.ports.registry.create({
|
|
230
|
+
id: sessionId,
|
|
231
|
+
source: "console",
|
|
232
|
+
workspace,
|
|
233
|
+
execution,
|
|
234
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
235
|
+
createdAt: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
return { sessionId };
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (input.projectId === undefined) {
|
|
241
|
+
await workspacePort.discardUnbound?.(workspace).catch(() => undefined);
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Create one independent Session at the source's stable Provider/canonical
|
|
247
|
+
* boundary. Project and Agent selectors are intentionally absent: the target
|
|
248
|
+
* receives exact copies of the source's already-frozen snapshots. */
|
|
249
|
+
async fork(input) {
|
|
250
|
+
return this.withAdmission(() => this.forkAdmitted(input));
|
|
251
|
+
}
|
|
252
|
+
async forkAdmitted(input) {
|
|
253
|
+
const sourceSessionId = validOpaqueToken(input.sourceSessionId, "sourceSessionId", MAX_SESSION_ID_CHARS);
|
|
254
|
+
const operationId = validOpaqueToken(input.operationId, "operationId", MAX_SESSION_ID_CHARS);
|
|
255
|
+
const title = input.title === undefined
|
|
256
|
+
? undefined
|
|
257
|
+
: normalizeSessionTitle(input.title.trim());
|
|
258
|
+
if (input.title !== undefined && !title) {
|
|
259
|
+
throw new MachineSessionServiceInputError("title must not be empty");
|
|
260
|
+
}
|
|
261
|
+
const requestHash = createHash("sha256")
|
|
262
|
+
.update(JSON.stringify({ sourceSessionId, title: title ?? null }))
|
|
263
|
+
.digest("hex");
|
|
264
|
+
const operation = await this.requireForks().claim({
|
|
265
|
+
operationId,
|
|
266
|
+
requestHash,
|
|
267
|
+
sourceSessionId,
|
|
268
|
+
...(title ? { title } : {}),
|
|
269
|
+
});
|
|
270
|
+
if (operation.state === "target_deleted") {
|
|
271
|
+
throw new MachineSessionServiceFailure("conflict", "fork target was deleted");
|
|
272
|
+
}
|
|
273
|
+
if (operation.state === "completed") {
|
|
274
|
+
return { sessionId: operation.targetSessionId, disposition: "replayed" };
|
|
275
|
+
}
|
|
276
|
+
const inflight = this.forkTasks.get(operationId);
|
|
277
|
+
if (inflight)
|
|
278
|
+
return inflight;
|
|
279
|
+
const task = this.performFork(operation).finally(() => {
|
|
280
|
+
if (this.forkTasks.get(operationId) === task) {
|
|
281
|
+
this.forkTasks.delete(operationId);
|
|
282
|
+
}
|
|
211
283
|
});
|
|
212
|
-
|
|
284
|
+
this.forkTasks.set(operationId, task);
|
|
285
|
+
return task;
|
|
286
|
+
}
|
|
287
|
+
/** Publish a Provider TUI `/clear` or `/fork` after its native binding has
|
|
288
|
+
* already been persisted. The source snapshots remain the only workspace and
|
|
289
|
+
* execution authority; Provider events cannot alter them during rotation. */
|
|
290
|
+
async recordNativeRotation(input) {
|
|
291
|
+
const sourceSessionId = validOpaqueToken(input.sourceSessionId, "sourceSessionId", MAX_SESSION_ID_CHARS);
|
|
292
|
+
const targetSessionId = validOpaqueToken(input.targetSessionId, "targetSessionId", MAX_SESSION_ID_CHARS);
|
|
293
|
+
if (sourceSessionId === targetSessionId) {
|
|
294
|
+
throw new MachineSessionServiceInputError("source and target Session must differ");
|
|
295
|
+
}
|
|
296
|
+
const source = this.ports.registry.get(sourceSessionId);
|
|
297
|
+
if (!source) {
|
|
298
|
+
throw new MachineSessionServiceFailure("not_found", "source Session not found");
|
|
299
|
+
}
|
|
300
|
+
const existing = this.ports.registry.get(targetSessionId);
|
|
301
|
+
if (existing) {
|
|
302
|
+
if (input.kind === "fork" &&
|
|
303
|
+
existing.forkedFromSessionId === sourceSessionId)
|
|
304
|
+
return;
|
|
305
|
+
throw new MachineSessionServiceFailure("conflict", "target Session already exists");
|
|
306
|
+
}
|
|
307
|
+
const workspace = structuredClone(source.workspace);
|
|
308
|
+
const execution = structuredClone(source.execution);
|
|
309
|
+
const sourceTitle = source.title;
|
|
310
|
+
const now = new Date().toISOString();
|
|
311
|
+
const target = {
|
|
312
|
+
id: targetSessionId,
|
|
313
|
+
source: input.kind,
|
|
314
|
+
workspace,
|
|
315
|
+
execution,
|
|
316
|
+
...(input.kind === "fork" && sourceTitle ? { title: sourceTitle } : {}),
|
|
317
|
+
...(input.kind === "fork" ? { forkedFromSessionId: sourceSessionId } : {}),
|
|
318
|
+
createdAt: now,
|
|
319
|
+
updatedAt: now,
|
|
320
|
+
};
|
|
321
|
+
if (input.kind === "clear") {
|
|
322
|
+
this.ports.registry.create(target);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const items = (await this.ports.log.snapshot(sourceSessionId))
|
|
326
|
+
.map((item) => structuredClone(item));
|
|
327
|
+
await this.requireForks().publishNativeFork({
|
|
328
|
+
sourceSessionId,
|
|
329
|
+
target,
|
|
330
|
+
items,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async performFork(operation) {
|
|
334
|
+
const source = this.ports.registry.get(operation.sourceSessionId);
|
|
335
|
+
if (!source) {
|
|
336
|
+
throw new MachineSessionServiceFailure("not_found", "source Session not found");
|
|
337
|
+
}
|
|
338
|
+
const runner = this.requireExecution().runner;
|
|
339
|
+
if (!runner.forkSession) {
|
|
340
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Provider does not support Session fork");
|
|
341
|
+
}
|
|
342
|
+
const workspace = structuredClone(source.workspace);
|
|
343
|
+
const execution = structuredClone(source.execution);
|
|
344
|
+
const sourceTitle = source.title;
|
|
345
|
+
let items;
|
|
346
|
+
try {
|
|
347
|
+
this.assertForkableSource(operation.sourceSessionId);
|
|
348
|
+
const native = await runner.forkSession(operation.sourceSessionId, operation.targetSessionId, {
|
|
349
|
+
workspace: structuredClone(workspace),
|
|
350
|
+
execution: structuredClone(execution),
|
|
351
|
+
beforeProviderFork: async () => {
|
|
352
|
+
this.assertForkableSource(operation.sourceSessionId);
|
|
353
|
+
items = (await this.ports.log.snapshot(operation.sourceSessionId))
|
|
354
|
+
.map((item) => structuredClone(item));
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
if (!native.ok) {
|
|
358
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Provider could not fork the Session", native.message);
|
|
359
|
+
}
|
|
360
|
+
if (!items) {
|
|
361
|
+
throw new MachineSessionServiceFailure("outcome_unknown", "Provider fork completed without a canonical fork point");
|
|
362
|
+
}
|
|
363
|
+
const now = new Date().toISOString();
|
|
364
|
+
const targetTitle = operation.title ?? sourceTitle;
|
|
365
|
+
await this.requireForks().commit({
|
|
366
|
+
operationId: operation.operationId,
|
|
367
|
+
target: {
|
|
368
|
+
id: operation.targetSessionId,
|
|
369
|
+
source: "fork",
|
|
370
|
+
workspace: structuredClone(workspace),
|
|
371
|
+
execution: structuredClone(execution),
|
|
372
|
+
...(targetTitle ? { title: targetTitle } : {}),
|
|
373
|
+
forkedFromSessionId: operation.sourceSessionId,
|
|
374
|
+
createdAt: now,
|
|
375
|
+
updatedAt: now,
|
|
376
|
+
},
|
|
377
|
+
items,
|
|
378
|
+
});
|
|
379
|
+
return { sessionId: operation.targetSessionId, disposition: "created" };
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
await Promise.resolve(this.requireForks().markError(operation.operationId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
assertForkableSource(sessionId) {
|
|
387
|
+
const runtime = this.ports.runtimeState.snapshot(sessionId);
|
|
388
|
+
if (runtime.status !== "idle" ||
|
|
389
|
+
runtime.activeResponseIds.length > 0 ||
|
|
390
|
+
runtime.pendingInteractions.length > 0) {
|
|
391
|
+
throw new MachineSessionServiceFailure("failed_precondition", "source Session must be idle before it can be forked");
|
|
392
|
+
}
|
|
213
393
|
}
|
|
214
394
|
/** Inject one turn through the target daemon's native single-writer runner. */
|
|
215
395
|
async sendMessage(sessionIdInput, messageInput) {
|
|
396
|
+
return this.withAdmission(() => this.sendMessageAdmitted(sessionIdInput, messageInput));
|
|
397
|
+
}
|
|
398
|
+
async sendMessageAdmitted(sessionIdInput, messageInput) {
|
|
216
399
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
400
|
+
await this.assertNotForkReserved(sessionId);
|
|
217
401
|
const request = typeof messageInput === "string"
|
|
218
402
|
? { message: messageInput }
|
|
219
403
|
: messageInput;
|
|
@@ -272,11 +456,54 @@ export class MachineSessionService {
|
|
|
272
456
|
if (request.clientMessageId) {
|
|
273
457
|
await resources?.markMessageInjecting(sessionId, request.clientMessageId);
|
|
274
458
|
}
|
|
459
|
+
const responseId = this.ports.publishEvent ? `resp_${randomUUID()}` : undefined;
|
|
460
|
+
if (responseId) {
|
|
461
|
+
await this.ports.publishEvent(sessionId, {
|
|
462
|
+
type: "session.input.consumed",
|
|
463
|
+
item: {
|
|
464
|
+
id: newSessionItemId("message"),
|
|
465
|
+
sessionId,
|
|
466
|
+
position: 0,
|
|
467
|
+
responseId,
|
|
468
|
+
status: "completed",
|
|
469
|
+
createdAt: Date.now(),
|
|
470
|
+
type: "message",
|
|
471
|
+
data: {
|
|
472
|
+
role: "user",
|
|
473
|
+
content: prepared.input.content.map((part) => part.type === "text"
|
|
474
|
+
? { type: "input_text", text: part.text }
|
|
475
|
+
: { ...part.resource }),
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
await this.ports.publishEvent(sessionId, {
|
|
480
|
+
type: "session.status",
|
|
481
|
+
sessionId,
|
|
482
|
+
responseId,
|
|
483
|
+
status: "running",
|
|
484
|
+
statusKind: "startup",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const abandonResponseHandoff = responseId
|
|
488
|
+
? this.ports.runtimeState.beginResponseHandoff?.(sessionId, responseId)
|
|
489
|
+
: undefined;
|
|
490
|
+
const clearStartup = async (status) => {
|
|
491
|
+
if (!responseId)
|
|
492
|
+
return;
|
|
493
|
+
await this.ports.publishEvent(sessionId, {
|
|
494
|
+
type: "session.status",
|
|
495
|
+
sessionId,
|
|
496
|
+
responseId,
|
|
497
|
+
status,
|
|
498
|
+
});
|
|
499
|
+
};
|
|
275
500
|
let execution;
|
|
276
501
|
try {
|
|
277
502
|
execution = await this.ensureLiveSession(sessionId, meta);
|
|
278
503
|
}
|
|
279
504
|
catch (error) {
|
|
505
|
+
abandonResponseHandoff?.();
|
|
506
|
+
await clearStartup("idle");
|
|
280
507
|
if (request.clientMessageId) {
|
|
281
508
|
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error));
|
|
282
509
|
}
|
|
@@ -284,26 +511,36 @@ export class MachineSessionService {
|
|
|
284
511
|
}
|
|
285
512
|
let outcome;
|
|
286
513
|
try {
|
|
287
|
-
|
|
514
|
+
const runtimeInput = responseId
|
|
515
|
+
? { ...prepared.input, responseId }
|
|
516
|
+
: prepared.input;
|
|
517
|
+
outcome = await execution.runner.injectMessage(sessionId, responseId || needsPreparedOperation ? runtimeInput : request.message);
|
|
288
518
|
}
|
|
289
519
|
catch (error) {
|
|
520
|
+
abandonResponseHandoff?.();
|
|
521
|
+
await clearStartup("idle");
|
|
290
522
|
if (request.clientMessageId) {
|
|
291
523
|
await Promise.resolve(resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
|
|
292
524
|
}
|
|
293
525
|
throw new MachineSessionServiceFailure("outcome_unknown", "live injection outcome is unknown");
|
|
294
526
|
}
|
|
295
527
|
if (outcome === "failed") {
|
|
528
|
+
abandonResponseHandoff?.();
|
|
529
|
+
await clearStartup("idle");
|
|
296
530
|
if (request.clientMessageId) {
|
|
297
531
|
await resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
298
532
|
}
|
|
299
533
|
throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`);
|
|
300
534
|
}
|
|
301
535
|
if (outcome !== "injected") {
|
|
536
|
+
abandonResponseHandoff?.();
|
|
537
|
+
await clearStartup("idle");
|
|
302
538
|
if (request.clientMessageId) {
|
|
303
539
|
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
304
540
|
}
|
|
305
541
|
throw new MachineSessionServiceFailure("failed_precondition", `live injection ${outcome}`);
|
|
306
542
|
}
|
|
543
|
+
await clearStartup("running");
|
|
307
544
|
if (request.clientMessageId) {
|
|
308
545
|
try {
|
|
309
546
|
await resources?.markMessageInjected(sessionId, request.clientMessageId);
|
|
@@ -347,7 +584,11 @@ export class MachineSessionService {
|
|
|
347
584
|
* delivery worker starts the pane now, waits for a real native thread, then
|
|
348
585
|
* injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
|
|
349
586
|
async enqueueMessage(sessionIdInput, messageInput) {
|
|
587
|
+
return this.withAdmission(() => this.enqueueMessageAdmitted(sessionIdInput, messageInput));
|
|
588
|
+
}
|
|
589
|
+
async enqueueMessageAdmitted(sessionIdInput, messageInput) {
|
|
350
590
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
591
|
+
await this.assertNotForkReserved(sessionId);
|
|
351
592
|
if (typeof messageInput !== "string" || messageInput.length === 0) {
|
|
352
593
|
throw new MachineSessionServiceInputError("message must be a non-empty string");
|
|
353
594
|
}
|
|
@@ -377,6 +618,9 @@ export class MachineSessionService {
|
|
|
377
618
|
}
|
|
378
619
|
/** Explicitly restore the target daemon's live runner without starting a turn. */
|
|
379
620
|
async startTerminal(sessionIdInput) {
|
|
621
|
+
return this.withAdmission(() => this.startTerminalAdmitted(sessionIdInput));
|
|
622
|
+
}
|
|
623
|
+
async startTerminalAdmitted(sessionIdInput) {
|
|
380
624
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
381
625
|
const request = await this.liveSessionRequest(sessionId);
|
|
382
626
|
const start = request.execution.runner.startLiveSession?.bind(request.execution.runner) ??
|
|
@@ -397,40 +641,13 @@ export class MachineSessionService {
|
|
|
397
641
|
}
|
|
398
642
|
async liveSessionRequest(sessionId, meta = this.ports.registry.get(sessionId)) {
|
|
399
643
|
const execution = this.requireExecution();
|
|
400
|
-
|
|
401
|
-
const bound = consoleMeta ? null : ((await execution.sessionStore?.get(sessionId)) ?? null);
|
|
402
|
-
if (!meta && !bound) {
|
|
644
|
+
if (!meta) {
|
|
403
645
|
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
404
646
|
}
|
|
405
|
-
const liveRuntime =
|
|
406
|
-
agentName: consoleMeta?.agent,
|
|
407
|
-
agentSpec: consoleMeta?.config,
|
|
408
|
-
provider: meta?.provider ?? bound?.runtime,
|
|
409
|
-
});
|
|
410
|
-
if (liveRuntime === "codex" || liveRuntime === "traex") {
|
|
411
|
-
const codexRecord = await execution.sessionStore?.get(sessionId);
|
|
412
|
-
if (codexRecord?.codexSessionId) {
|
|
413
|
-
ensureCodexResumeRollout({
|
|
414
|
-
sessionId,
|
|
415
|
-
runtime: liveRuntime,
|
|
416
|
-
threadId: codexRecord.codexSessionId,
|
|
417
|
-
cwd: codexRecord.cwd ?? process.cwd(),
|
|
418
|
-
items: await this.ports.log.snapshot(sessionId),
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
}
|
|
647
|
+
const liveRuntime = meta.execution.provider;
|
|
422
648
|
const options = {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
: consoleMeta?.config?.osEnv?.cwd
|
|
426
|
-
? { cwd: consoleMeta.config.osEnv.cwd }
|
|
427
|
-
: {}),
|
|
428
|
-
runtime: liveRuntime,
|
|
429
|
-
...(consoleMeta?.reasoningEffort
|
|
430
|
-
? { reasoningEffort: consoleMeta.reasoningEffort }
|
|
431
|
-
: {}),
|
|
432
|
-
...(consoleMeta?.agent ? { agentName: consoleMeta.agent } : {}),
|
|
433
|
-
...(consoleMeta?.config ? { agentSpec: consoleMeta.config } : {}),
|
|
649
|
+
workspace: structuredClone(meta.workspace),
|
|
650
|
+
execution: structuredClone(meta.execution),
|
|
434
651
|
};
|
|
435
652
|
return { execution, liveRuntime, options };
|
|
436
653
|
}
|
|
@@ -441,6 +658,7 @@ export class MachineSessionService {
|
|
|
441
658
|
}
|
|
442
659
|
async delete(sessionIdInput) {
|
|
443
660
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
661
|
+
await this.assertNotForkReserved(sessionId);
|
|
444
662
|
try {
|
|
445
663
|
await this.ports.lifecycle?.beforeDelete?.(sessionId);
|
|
446
664
|
}
|
|
@@ -455,6 +673,7 @@ export class MachineSessionService {
|
|
|
455
673
|
this.ports.registry.remove(sessionId);
|
|
456
674
|
await this.ports.log.deleteSession(sessionId);
|
|
457
675
|
this.ports.runtimeState.remove?.(sessionId);
|
|
676
|
+
await this.ports.forks?.markTargetDeleted(sessionId);
|
|
458
677
|
try {
|
|
459
678
|
await this.ports.lifecycle?.afterDelete?.(sessionId);
|
|
460
679
|
}
|
|
@@ -469,6 +688,24 @@ export class MachineSessionService {
|
|
|
469
688
|
}
|
|
470
689
|
return this.ports.agents;
|
|
471
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
|
+
}
|
|
472
709
|
requireResources() {
|
|
473
710
|
if (!this.ports.resources) {
|
|
474
711
|
throw new MachineSessionServiceFailure("failed_precondition", "Session resources are unavailable");
|
|
@@ -520,7 +757,9 @@ export class MachineSessionService {
|
|
|
520
757
|
if (!pending || pending.state !== "queued")
|
|
521
758
|
return;
|
|
522
759
|
let injectionAttempted = false;
|
|
760
|
+
let admission;
|
|
523
761
|
try {
|
|
762
|
+
admission = this.reserveAdmission();
|
|
524
763
|
const execution = await this.ensureLiveSession(sessionId);
|
|
525
764
|
if (this.cancelledPendingDeliveries.has(sessionId))
|
|
526
765
|
return;
|
|
@@ -553,6 +792,9 @@ export class MachineSessionService {
|
|
|
553
792
|
return;
|
|
554
793
|
}
|
|
555
794
|
}
|
|
795
|
+
finally {
|
|
796
|
+
admission?.release();
|
|
797
|
+
}
|
|
556
798
|
await retryDelay(this.pendingMessageRetryMs);
|
|
557
799
|
}
|
|
558
800
|
}
|
|
@@ -592,6 +834,44 @@ export class MachineSessionService {
|
|
|
592
834
|
}
|
|
593
835
|
return this.ports.execution;
|
|
594
836
|
}
|
|
837
|
+
requireWorkspace() {
|
|
838
|
+
const workspace = this.ports.workspace;
|
|
839
|
+
if (!workspace) {
|
|
840
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session workspace resolution is unavailable");
|
|
841
|
+
}
|
|
842
|
+
return workspace;
|
|
843
|
+
}
|
|
844
|
+
requireForks() {
|
|
845
|
+
if (!this.ports.forks) {
|
|
846
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session fork storage is unavailable");
|
|
847
|
+
}
|
|
848
|
+
return this.ports.forks;
|
|
849
|
+
}
|
|
850
|
+
async assertNotForkReserved(sessionId) {
|
|
851
|
+
if (await this.ports.forks?.hasReservedSource(sessionId)) {
|
|
852
|
+
throw new MachineSessionServiceFailure("conflict", "Session fork is in progress");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async resumeReservedForks() {
|
|
856
|
+
if (!this.ports.forks)
|
|
857
|
+
return;
|
|
858
|
+
try {
|
|
859
|
+
for (const operation of await this.ports.forks.listReserved()) {
|
|
860
|
+
if (this.forkTasks.has(operation.operationId))
|
|
861
|
+
continue;
|
|
862
|
+
const task = this.performFork(operation).finally(() => {
|
|
863
|
+
if (this.forkTasks.get(operation.operationId) === task) {
|
|
864
|
+
this.forkTasks.delete(operation.operationId);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
this.forkTasks.set(operation.operationId, task);
|
|
868
|
+
void task.catch(() => undefined);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
// A direct replay or the next daemon restart retries durable operations.
|
|
873
|
+
}
|
|
874
|
+
}
|
|
595
875
|
}
|
|
596
876
|
/** One-line title derived from the first user message; no model call. */
|
|
597
877
|
export function synthesizeSessionTitle(message, limit = 60) {
|
|
@@ -688,6 +968,13 @@ function snapshotPage(sessionId, items, runtime, hasMore) {
|
|
|
688
968
|
...(hasMore && items.length > 0 ? { nextAfterId: items.at(-1).id } : {}),
|
|
689
969
|
};
|
|
690
970
|
}
|
|
971
|
+
/** Exact UTF-8 size of {@link snapshotPage} without repeatedly serializing the
|
|
972
|
+
* complete item array while searching for a page boundary. */
|
|
973
|
+
function snapshotPageJsonByteLength(sessionId, itemsJsonBytes, finalItemId, runtime, hasMore) {
|
|
974
|
+
return Buffer.byteLength(`{"sessionId":${JSON.stringify(sessionId)},"items":[`, "utf8")
|
|
975
|
+
+ itemsJsonBytes
|
|
976
|
+
+ Buffer.byteLength(`],"runtime":${JSON.stringify(runtime)},"hasMore":${hasMore}${hasMore ? `,"nextAfterId":${JSON.stringify(finalItemId)}` : ""}}`, "utf8");
|
|
977
|
+
}
|
|
691
978
|
function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
692
979
|
const createdAt = meta?.createdAt ?? timestampFromEpoch(log?.createdAt, "createdAt");
|
|
693
980
|
const updatedAt = log
|
|
@@ -695,8 +982,7 @@ function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
|
695
982
|
: (meta?.updatedAt ?? createdAt);
|
|
696
983
|
return {
|
|
697
984
|
id,
|
|
698
|
-
...(meta?.provider === undefined ? {} : { provider: meta.provider }),
|
|
699
|
-
...(meta?.agent === undefined ? {} : { agent: meta.agent }),
|
|
985
|
+
...(meta?.execution?.provider === undefined ? {} : { provider: meta.execution.provider }),
|
|
700
986
|
...(meta?.title === undefined ? {} : { title: normalizeSessionTitle(meta.title) }),
|
|
701
987
|
status: runtimeState.snapshot(id).status,
|
|
702
988
|
...(pendingState === undefined
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RemoteRuntimeEmulatorGesturePoint, RemoteRuntimeRpcRequest, RemoteRuntimeRpcResponse, RemoteRuntimeSequencedSessionEvent } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
1
|
+
import type { RemoteRuntimeEmulatorGesturePoint, RemoteRuntimeProject, RemoteRuntimeProjectCreateParams, RemoteRuntimeRpcRequest, RemoteRuntimeRpcResponse, RemoteRuntimeSequencedSessionEvent } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
2
2
|
import { type DaemonRuntimeHost } from "./remote-runtime.js";
|
|
3
3
|
import { type MachineSessionService } from "./machine-session-service.js";
|
|
4
4
|
import { type SessionBrowserService } from "./session-browser-service.js";
|
|
@@ -22,10 +22,19 @@ export interface RemoteRuntimeDispatcherOptions {
|
|
|
22
22
|
browsers?: RemoteRuntimeBrowserHost;
|
|
23
23
|
terminals?: SessionTerminalHost;
|
|
24
24
|
providerClis?: ProviderCliStatusHost;
|
|
25
|
+
projects?: RemoteRuntimeProjectHost;
|
|
25
26
|
/** Optional diagnostic sink. Wire errors deliberately omit internal details. */
|
|
26
27
|
onError?: (error: unknown, request: RemoteRuntimeRpcRequest) => void;
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
/** Runtime-local Project persistence. Project ids never cross into Session records. */
|
|
30
|
+
export interface RemoteRuntimeProjectHost {
|
|
31
|
+
list(): RemoteRuntimeProject[] | Promise<RemoteRuntimeProject[]>;
|
|
32
|
+
get(projectId: string): RemoteRuntimeProject | undefined | Promise<RemoteRuntimeProject | undefined>;
|
|
33
|
+
create(input: RemoteRuntimeProjectCreateParams): RemoteRuntimeProject | Promise<RemoteRuntimeProject>;
|
|
34
|
+
update(projectId: string, input: RemoteRuntimeProjectCreateParams): RemoteRuntimeProject | undefined | Promise<RemoteRuntimeProject | undefined>;
|
|
35
|
+
remove(projectId: string): boolean | Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
export type RemoteRuntimeSessionHost = Pick<MachineSessionService, "list" | "snapshot" | "watch" | "interrupt" | "agentOptions" | "launchOptions" | "create" | "fork" | "sendMessage" | "resourcePolicy" | "beginResourceUpload" | "writeResourceUploadChunk" | "commitResourceUpload" | "getResource" | "readResource" | "deleteResource" | "enqueueMessage" | "startTerminal" | "delete" | "resolveInteraction">;
|
|
29
38
|
export type RemoteRuntimeBrowserHost = Pick<SessionBrowserService, "getState" | "open" | "close" | "createPage" | "closePage" | "activatePage" | "navigatePage" | "goBack" | "goForward" | "reload">;
|
|
30
39
|
export type RemoteRuntimeSessionEmulatorHost = Pick<SessionEmulatorService, "getState" | "listDevices" | "attach" | "detach" | "tap" | "type" | "button" | "rotate" | "launch" | "gesture" | "releaseDevice" | "stopDevice">;
|
|
31
40
|
export interface OpenRemoteRuntimeSessionEvents {
|
|
@@ -88,6 +97,7 @@ export declare class RemoteRuntimeDispatcher {
|
|
|
88
97
|
openSessionEvents(sessionId: string, context: RemoteRuntimeDispatchContext): OpenRemoteRuntimeSessionEvents;
|
|
89
98
|
openSessionTerminal(sessionId: string, options: SessionTerminalOpenOptions, context: RemoteRuntimeDispatchContext): Promise<SessionTerminalAttachment>;
|
|
90
99
|
private withEmulator;
|
|
100
|
+
private withProjects;
|
|
91
101
|
private withSessions;
|
|
92
102
|
private withSessionEmulators;
|
|
93
103
|
private withBrowsers;
|