@rynx-ai/server 0.1.10 → 0.1.11-beta.2
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 +588 -335
- 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 +106 -23
- package/dist/machine-session-service.js +290 -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 +36 -28
- package/dist/server.js +81 -140
- package/dist/session-runtime-index.js +7 -0
- 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;
|
|
@@ -49,6 +49,7 @@ export class MachineSessionService {
|
|
|
49
49
|
pendingMessageRetryMs;
|
|
50
50
|
pendingDeliveries = new Map();
|
|
51
51
|
cancelledPendingDeliveries = new Set();
|
|
52
|
+
forkTasks = new Map();
|
|
52
53
|
constructor(ports, options = {}) {
|
|
53
54
|
this.ports = ports;
|
|
54
55
|
this.maxListLimit = boundedInteger(options.maxListLimit ?? MAX_LIST_LIMIT, 1, MAX_LIST_LIMIT, "maxListLimit");
|
|
@@ -58,6 +59,7 @@ export class MachineSessionService {
|
|
|
58
59
|
this.directoryScanLimit = boundedInteger(options.directoryScanLimit ?? DEFAULT_DIRECTORY_SCAN_LIMIT, 1, MAX_DIRECTORY_SCAN_LIMIT, "directoryScanLimit");
|
|
59
60
|
this.pendingMessageRetryMs = boundedInteger(options.pendingMessageRetryMs ?? 1_000, 10, 60_000, "pendingMessageRetryMs");
|
|
60
61
|
void this.resumePendingDeliveries();
|
|
62
|
+
void this.resumeReservedForks();
|
|
61
63
|
}
|
|
62
64
|
async list(input = {}) {
|
|
63
65
|
const limit = requestLimit(input.limit, DEFAULT_LIST_LIMIT, this.maxListLimit, "limit");
|
|
@@ -105,14 +107,22 @@ export class MachineSessionService {
|
|
|
105
107
|
})).slice(0, limit + 1).map((item) => structuredClone(item));
|
|
106
108
|
const runtime = cloneRuntimeSnapshot(this.ports.runtimeState.snapshot(sessionId));
|
|
107
109
|
const empty = snapshotPage(sessionId, [], runtime, false);
|
|
108
|
-
const items =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
const items = [];
|
|
111
|
+
let itemsJsonBytes = 0;
|
|
112
|
+
for (const item of candidates.slice(0, limit)) {
|
|
113
|
+
const nextItemsJsonBytes = itemsJsonBytes + (items.length > 0 ? 1 : 0) + jsonByteLength(item);
|
|
114
|
+
const nextCount = items.length + 1;
|
|
115
|
+
const hasMore = candidates.length > nextCount;
|
|
116
|
+
if (items.length > 0 &&
|
|
117
|
+
snapshotPageJsonByteLength(sessionId, nextItemsJsonBytes, item.id, runtime, hasMore) > this.snapshotPageMaxBytes) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
items.push(item);
|
|
121
|
+
itemsJsonBytes = nextItemsJsonBytes;
|
|
114
122
|
}
|
|
115
|
-
return
|
|
123
|
+
return items.length > 0
|
|
124
|
+
? snapshotPage(sessionId, items, runtime, candidates.length > items.length)
|
|
125
|
+
: empty;
|
|
116
126
|
}
|
|
117
127
|
/**
|
|
118
128
|
* Install a live subscriber synchronously, before returning control to an
|
|
@@ -199,21 +209,182 @@ export class MachineSessionService {
|
|
|
199
209
|
}
|
|
200
210
|
target = { provider: input.provider };
|
|
201
211
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
const workspacePort = this.requireWorkspace();
|
|
213
|
+
const workspace = await workspacePort.resolve(input.projectId);
|
|
214
|
+
try {
|
|
215
|
+
const execution = await this.requireExecution().resolve({
|
|
216
|
+
...target,
|
|
217
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
218
|
+
...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
|
|
219
|
+
cwd: workspace.cwd,
|
|
220
|
+
});
|
|
221
|
+
const sessionId = newSessionId();
|
|
222
|
+
this.ports.registry.create({
|
|
223
|
+
id: sessionId,
|
|
224
|
+
source: "console",
|
|
225
|
+
workspace,
|
|
226
|
+
execution,
|
|
227
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
228
|
+
createdAt: new Date().toISOString(),
|
|
229
|
+
});
|
|
230
|
+
return { sessionId };
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (input.projectId === undefined) {
|
|
234
|
+
await workspacePort.discardUnbound?.(workspace).catch(() => undefined);
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Create one independent Session at the source's stable Provider/canonical
|
|
240
|
+
* boundary. Project and Agent selectors are intentionally absent: the target
|
|
241
|
+
* receives exact copies of the source's already-frozen snapshots. */
|
|
242
|
+
async fork(input) {
|
|
243
|
+
const sourceSessionId = validOpaqueToken(input.sourceSessionId, "sourceSessionId", MAX_SESSION_ID_CHARS);
|
|
244
|
+
const operationId = validOpaqueToken(input.operationId, "operationId", MAX_SESSION_ID_CHARS);
|
|
245
|
+
const title = input.title === undefined
|
|
246
|
+
? undefined
|
|
247
|
+
: normalizeSessionTitle(input.title.trim());
|
|
248
|
+
if (input.title !== undefined && !title) {
|
|
249
|
+
throw new MachineSessionServiceInputError("title must not be empty");
|
|
250
|
+
}
|
|
251
|
+
const requestHash = createHash("sha256")
|
|
252
|
+
.update(JSON.stringify({ sourceSessionId, title: title ?? null }))
|
|
253
|
+
.digest("hex");
|
|
254
|
+
const operation = await this.requireForks().claim({
|
|
255
|
+
operationId,
|
|
256
|
+
requestHash,
|
|
257
|
+
sourceSessionId,
|
|
258
|
+
...(title ? { title } : {}),
|
|
259
|
+
});
|
|
260
|
+
if (operation.state === "target_deleted") {
|
|
261
|
+
throw new MachineSessionServiceFailure("conflict", "fork target was deleted");
|
|
262
|
+
}
|
|
263
|
+
if (operation.state === "completed") {
|
|
264
|
+
return { sessionId: operation.targetSessionId, disposition: "replayed" };
|
|
265
|
+
}
|
|
266
|
+
const inflight = this.forkTasks.get(operationId);
|
|
267
|
+
if (inflight)
|
|
268
|
+
return inflight;
|
|
269
|
+
const task = this.performFork(operation).finally(() => {
|
|
270
|
+
if (this.forkTasks.get(operationId) === task) {
|
|
271
|
+
this.forkTasks.delete(operationId);
|
|
272
|
+
}
|
|
211
273
|
});
|
|
212
|
-
|
|
274
|
+
this.forkTasks.set(operationId, task);
|
|
275
|
+
return task;
|
|
276
|
+
}
|
|
277
|
+
/** Publish a Provider TUI `/clear` or `/fork` after its native binding has
|
|
278
|
+
* already been persisted. The source snapshots remain the only workspace and
|
|
279
|
+
* execution authority; Provider events cannot alter them during rotation. */
|
|
280
|
+
async recordNativeRotation(input) {
|
|
281
|
+
const sourceSessionId = validOpaqueToken(input.sourceSessionId, "sourceSessionId", MAX_SESSION_ID_CHARS);
|
|
282
|
+
const targetSessionId = validOpaqueToken(input.targetSessionId, "targetSessionId", MAX_SESSION_ID_CHARS);
|
|
283
|
+
if (sourceSessionId === targetSessionId) {
|
|
284
|
+
throw new MachineSessionServiceInputError("source and target Session must differ");
|
|
285
|
+
}
|
|
286
|
+
const source = this.ports.registry.get(sourceSessionId);
|
|
287
|
+
if (!source) {
|
|
288
|
+
throw new MachineSessionServiceFailure("not_found", "source Session not found");
|
|
289
|
+
}
|
|
290
|
+
const existing = this.ports.registry.get(targetSessionId);
|
|
291
|
+
if (existing) {
|
|
292
|
+
if (input.kind === "fork" &&
|
|
293
|
+
existing.forkedFromSessionId === sourceSessionId)
|
|
294
|
+
return;
|
|
295
|
+
throw new MachineSessionServiceFailure("conflict", "target Session already exists");
|
|
296
|
+
}
|
|
297
|
+
const workspace = structuredClone(source.workspace);
|
|
298
|
+
const execution = structuredClone(source.execution);
|
|
299
|
+
const sourceTitle = source.title;
|
|
300
|
+
const now = new Date().toISOString();
|
|
301
|
+
const target = {
|
|
302
|
+
id: targetSessionId,
|
|
303
|
+
source: input.kind,
|
|
304
|
+
workspace,
|
|
305
|
+
execution,
|
|
306
|
+
...(input.kind === "fork" && sourceTitle ? { title: sourceTitle } : {}),
|
|
307
|
+
...(input.kind === "fork" ? { forkedFromSessionId: sourceSessionId } : {}),
|
|
308
|
+
createdAt: now,
|
|
309
|
+
updatedAt: now,
|
|
310
|
+
};
|
|
311
|
+
if (input.kind === "clear") {
|
|
312
|
+
this.ports.registry.create(target);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const items = (await this.ports.log.snapshot(sourceSessionId))
|
|
316
|
+
.map((item) => structuredClone(item));
|
|
317
|
+
await this.requireForks().publishNativeFork({
|
|
318
|
+
sourceSessionId,
|
|
319
|
+
target,
|
|
320
|
+
items,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async performFork(operation) {
|
|
324
|
+
const source = this.ports.registry.get(operation.sourceSessionId);
|
|
325
|
+
if (!source) {
|
|
326
|
+
throw new MachineSessionServiceFailure("not_found", "source Session not found");
|
|
327
|
+
}
|
|
328
|
+
const runner = this.requireExecution().runner;
|
|
329
|
+
if (!runner.forkSession) {
|
|
330
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Provider does not support Session fork");
|
|
331
|
+
}
|
|
332
|
+
const workspace = structuredClone(source.workspace);
|
|
333
|
+
const execution = structuredClone(source.execution);
|
|
334
|
+
const sourceTitle = source.title;
|
|
335
|
+
let items;
|
|
336
|
+
try {
|
|
337
|
+
this.assertForkableSource(operation.sourceSessionId);
|
|
338
|
+
const native = await runner.forkSession(operation.sourceSessionId, operation.targetSessionId, {
|
|
339
|
+
workspace: structuredClone(workspace),
|
|
340
|
+
execution: structuredClone(execution),
|
|
341
|
+
beforeProviderFork: async () => {
|
|
342
|
+
this.assertForkableSource(operation.sourceSessionId);
|
|
343
|
+
items = (await this.ports.log.snapshot(operation.sourceSessionId))
|
|
344
|
+
.map((item) => structuredClone(item));
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
if (!native.ok) {
|
|
348
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Provider could not fork the Session", native.message);
|
|
349
|
+
}
|
|
350
|
+
if (!items) {
|
|
351
|
+
throw new MachineSessionServiceFailure("outcome_unknown", "Provider fork completed without a canonical fork point");
|
|
352
|
+
}
|
|
353
|
+
const now = new Date().toISOString();
|
|
354
|
+
const targetTitle = operation.title ?? sourceTitle;
|
|
355
|
+
await this.requireForks().commit({
|
|
356
|
+
operationId: operation.operationId,
|
|
357
|
+
target: {
|
|
358
|
+
id: operation.targetSessionId,
|
|
359
|
+
source: "fork",
|
|
360
|
+
workspace: structuredClone(workspace),
|
|
361
|
+
execution: structuredClone(execution),
|
|
362
|
+
...(targetTitle ? { title: targetTitle } : {}),
|
|
363
|
+
forkedFromSessionId: operation.sourceSessionId,
|
|
364
|
+
createdAt: now,
|
|
365
|
+
updatedAt: now,
|
|
366
|
+
},
|
|
367
|
+
items,
|
|
368
|
+
});
|
|
369
|
+
return { sessionId: operation.targetSessionId, disposition: "created" };
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
await Promise.resolve(this.requireForks().markError(operation.operationId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
assertForkableSource(sessionId) {
|
|
377
|
+
const runtime = this.ports.runtimeState.snapshot(sessionId);
|
|
378
|
+
if (runtime.status !== "idle" ||
|
|
379
|
+
runtime.activeResponseIds.length > 0 ||
|
|
380
|
+
runtime.pendingInteractions.length > 0) {
|
|
381
|
+
throw new MachineSessionServiceFailure("failed_precondition", "source Session must be idle before it can be forked");
|
|
382
|
+
}
|
|
213
383
|
}
|
|
214
384
|
/** Inject one turn through the target daemon's native single-writer runner. */
|
|
215
385
|
async sendMessage(sessionIdInput, messageInput) {
|
|
216
386
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
387
|
+
await this.assertNotForkReserved(sessionId);
|
|
217
388
|
const request = typeof messageInput === "string"
|
|
218
389
|
? { message: messageInput }
|
|
219
390
|
: messageInput;
|
|
@@ -272,11 +443,50 @@ export class MachineSessionService {
|
|
|
272
443
|
if (request.clientMessageId) {
|
|
273
444
|
await resources?.markMessageInjecting(sessionId, request.clientMessageId);
|
|
274
445
|
}
|
|
446
|
+
const responseId = this.ports.publishEvent ? `resp_${randomUUID()}` : undefined;
|
|
447
|
+
if (responseId) {
|
|
448
|
+
await this.ports.publishEvent(sessionId, {
|
|
449
|
+
type: "session.input.consumed",
|
|
450
|
+
item: {
|
|
451
|
+
id: newSessionItemId("message"),
|
|
452
|
+
sessionId,
|
|
453
|
+
position: 0,
|
|
454
|
+
responseId,
|
|
455
|
+
status: "completed",
|
|
456
|
+
createdAt: Date.now(),
|
|
457
|
+
type: "message",
|
|
458
|
+
data: {
|
|
459
|
+
role: "user",
|
|
460
|
+
content: prepared.input.content.map((part) => part.type === "text"
|
|
461
|
+
? { type: "input_text", text: part.text }
|
|
462
|
+
: { ...part.resource }),
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
await this.ports.publishEvent(sessionId, {
|
|
467
|
+
type: "session.status",
|
|
468
|
+
sessionId,
|
|
469
|
+
responseId,
|
|
470
|
+
status: "running",
|
|
471
|
+
statusKind: "startup",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
const clearStartup = async (status) => {
|
|
475
|
+
if (!responseId)
|
|
476
|
+
return;
|
|
477
|
+
await this.ports.publishEvent(sessionId, {
|
|
478
|
+
type: "session.status",
|
|
479
|
+
sessionId,
|
|
480
|
+
responseId,
|
|
481
|
+
status,
|
|
482
|
+
});
|
|
483
|
+
};
|
|
275
484
|
let execution;
|
|
276
485
|
try {
|
|
277
486
|
execution = await this.ensureLiveSession(sessionId, meta);
|
|
278
487
|
}
|
|
279
488
|
catch (error) {
|
|
489
|
+
await clearStartup("idle");
|
|
280
490
|
if (request.clientMessageId) {
|
|
281
491
|
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error));
|
|
282
492
|
}
|
|
@@ -284,26 +494,33 @@ export class MachineSessionService {
|
|
|
284
494
|
}
|
|
285
495
|
let outcome;
|
|
286
496
|
try {
|
|
287
|
-
|
|
497
|
+
const runtimeInput = responseId
|
|
498
|
+
? { ...prepared.input, responseId }
|
|
499
|
+
: prepared.input;
|
|
500
|
+
outcome = await execution.runner.injectMessage(sessionId, responseId || needsPreparedOperation ? runtimeInput : request.message);
|
|
288
501
|
}
|
|
289
502
|
catch (error) {
|
|
503
|
+
await clearStartup("idle");
|
|
290
504
|
if (request.clientMessageId) {
|
|
291
505
|
await Promise.resolve(resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
|
|
292
506
|
}
|
|
293
507
|
throw new MachineSessionServiceFailure("outcome_unknown", "live injection outcome is unknown");
|
|
294
508
|
}
|
|
295
509
|
if (outcome === "failed") {
|
|
510
|
+
await clearStartup("idle");
|
|
296
511
|
if (request.clientMessageId) {
|
|
297
512
|
await resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
298
513
|
}
|
|
299
514
|
throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`);
|
|
300
515
|
}
|
|
301
516
|
if (outcome !== "injected") {
|
|
517
|
+
await clearStartup("idle");
|
|
302
518
|
if (request.clientMessageId) {
|
|
303
519
|
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
304
520
|
}
|
|
305
521
|
throw new MachineSessionServiceFailure("failed_precondition", `live injection ${outcome}`);
|
|
306
522
|
}
|
|
523
|
+
await clearStartup("running");
|
|
307
524
|
if (request.clientMessageId) {
|
|
308
525
|
try {
|
|
309
526
|
await resources?.markMessageInjected(sessionId, request.clientMessageId);
|
|
@@ -348,6 +565,7 @@ export class MachineSessionService {
|
|
|
348
565
|
* injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
|
|
349
566
|
async enqueueMessage(sessionIdInput, messageInput) {
|
|
350
567
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
568
|
+
await this.assertNotForkReserved(sessionId);
|
|
351
569
|
if (typeof messageInput !== "string" || messageInput.length === 0) {
|
|
352
570
|
throw new MachineSessionServiceInputError("message must be a non-empty string");
|
|
353
571
|
}
|
|
@@ -397,40 +615,13 @@ export class MachineSessionService {
|
|
|
397
615
|
}
|
|
398
616
|
async liveSessionRequest(sessionId, meta = this.ports.registry.get(sessionId)) {
|
|
399
617
|
const execution = this.requireExecution();
|
|
400
|
-
|
|
401
|
-
const bound = consoleMeta ? null : ((await execution.sessionStore?.get(sessionId)) ?? null);
|
|
402
|
-
if (!meta && !bound) {
|
|
618
|
+
if (!meta) {
|
|
403
619
|
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
404
620
|
}
|
|
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
|
-
}
|
|
621
|
+
const liveRuntime = meta.execution.provider;
|
|
422
622
|
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 } : {}),
|
|
623
|
+
workspace: structuredClone(meta.workspace),
|
|
624
|
+
execution: structuredClone(meta.execution),
|
|
434
625
|
};
|
|
435
626
|
return { execution, liveRuntime, options };
|
|
436
627
|
}
|
|
@@ -441,6 +632,7 @@ export class MachineSessionService {
|
|
|
441
632
|
}
|
|
442
633
|
async delete(sessionIdInput) {
|
|
443
634
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
635
|
+
await this.assertNotForkReserved(sessionId);
|
|
444
636
|
try {
|
|
445
637
|
await this.ports.lifecycle?.beforeDelete?.(sessionId);
|
|
446
638
|
}
|
|
@@ -455,6 +647,7 @@ export class MachineSessionService {
|
|
|
455
647
|
this.ports.registry.remove(sessionId);
|
|
456
648
|
await this.ports.log.deleteSession(sessionId);
|
|
457
649
|
this.ports.runtimeState.remove?.(sessionId);
|
|
650
|
+
await this.ports.forks?.markTargetDeleted(sessionId);
|
|
458
651
|
try {
|
|
459
652
|
await this.ports.lifecycle?.afterDelete?.(sessionId);
|
|
460
653
|
}
|
|
@@ -592,6 +785,44 @@ export class MachineSessionService {
|
|
|
592
785
|
}
|
|
593
786
|
return this.ports.execution;
|
|
594
787
|
}
|
|
788
|
+
requireWorkspace() {
|
|
789
|
+
const workspace = this.ports.workspace;
|
|
790
|
+
if (!workspace) {
|
|
791
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session workspace resolution is unavailable");
|
|
792
|
+
}
|
|
793
|
+
return workspace;
|
|
794
|
+
}
|
|
795
|
+
requireForks() {
|
|
796
|
+
if (!this.ports.forks) {
|
|
797
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session fork storage is unavailable");
|
|
798
|
+
}
|
|
799
|
+
return this.ports.forks;
|
|
800
|
+
}
|
|
801
|
+
async assertNotForkReserved(sessionId) {
|
|
802
|
+
if (await this.ports.forks?.hasReservedSource(sessionId)) {
|
|
803
|
+
throw new MachineSessionServiceFailure("conflict", "Session fork is in progress");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async resumeReservedForks() {
|
|
807
|
+
if (!this.ports.forks)
|
|
808
|
+
return;
|
|
809
|
+
try {
|
|
810
|
+
for (const operation of await this.ports.forks.listReserved()) {
|
|
811
|
+
if (this.forkTasks.has(operation.operationId))
|
|
812
|
+
continue;
|
|
813
|
+
const task = this.performFork(operation).finally(() => {
|
|
814
|
+
if (this.forkTasks.get(operation.operationId) === task) {
|
|
815
|
+
this.forkTasks.delete(operation.operationId);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
this.forkTasks.set(operation.operationId, task);
|
|
819
|
+
void task.catch(() => undefined);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
// A direct replay or the next daemon restart retries durable operations.
|
|
824
|
+
}
|
|
825
|
+
}
|
|
595
826
|
}
|
|
596
827
|
/** One-line title derived from the first user message; no model call. */
|
|
597
828
|
export function synthesizeSessionTitle(message, limit = 60) {
|
|
@@ -688,6 +919,13 @@ function snapshotPage(sessionId, items, runtime, hasMore) {
|
|
|
688
919
|
...(hasMore && items.length > 0 ? { nextAfterId: items.at(-1).id } : {}),
|
|
689
920
|
};
|
|
690
921
|
}
|
|
922
|
+
/** Exact UTF-8 size of {@link snapshotPage} without repeatedly serializing the
|
|
923
|
+
* complete item array while searching for a page boundary. */
|
|
924
|
+
function snapshotPageJsonByteLength(sessionId, itemsJsonBytes, finalItemId, runtime, hasMore) {
|
|
925
|
+
return Buffer.byteLength(`{"sessionId":${JSON.stringify(sessionId)},"items":[`, "utf8")
|
|
926
|
+
+ itemsJsonBytes
|
|
927
|
+
+ Buffer.byteLength(`],"runtime":${JSON.stringify(runtime)},"hasMore":${hasMore}${hasMore ? `,"nextAfterId":${JSON.stringify(finalItemId)}` : ""}}`, "utf8");
|
|
928
|
+
}
|
|
691
929
|
function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
692
930
|
const createdAt = meta?.createdAt ?? timestampFromEpoch(log?.createdAt, "createdAt");
|
|
693
931
|
const updatedAt = log
|
|
@@ -695,8 +933,7 @@ function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
|
695
933
|
: (meta?.updatedAt ?? createdAt);
|
|
696
934
|
return {
|
|
697
935
|
id,
|
|
698
|
-
...(meta?.provider === undefined ? {} : { provider: meta.provider }),
|
|
699
|
-
...(meta?.agent === undefined ? {} : { agent: meta.agent }),
|
|
936
|
+
...(meta?.execution?.provider === undefined ? {} : { provider: meta.execution.provider }),
|
|
700
937
|
...(meta?.title === undefined ? {} : { title: normalizeSessionTitle(meta.title) }),
|
|
701
938
|
status: runtimeState.snapshot(id).status,
|
|
702
939
|
...(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;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encodeRemoteRuntimeRpcResponse, parseRemoteRuntimeRpcResponseForMethod, REMOTE_RUNTIME_RPC_METHOD_METADATA, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
2
|
+
import { RuntimeProjectInputError } from "@rynx-ai/core";
|
|
2
3
|
import { encodeDaemonStatus } from "./remote-runtime.js";
|
|
3
4
|
import { MachineSessionServiceFailure, MachineSessionServiceInputError, } from "./machine-session-service.js";
|
|
4
5
|
import { SessionBrowserServiceError, } from "./session-browser-service.js";
|
|
@@ -35,6 +36,30 @@ export class RemoteRuntimeDispatcher {
|
|
|
35
36
|
return this.options.providerClis
|
|
36
37
|
? rpcResult(request, await this.options.providerClis.listStatuses())
|
|
37
38
|
: rpcError(request.id, "method_not_found", "Provider CLI status is unavailable");
|
|
39
|
+
case "project.list":
|
|
40
|
+
return await this.withProjects(request, async (projects) => ({
|
|
41
|
+
projects: await projects.list(),
|
|
42
|
+
}));
|
|
43
|
+
case "project.get":
|
|
44
|
+
return await this.withProjects(request, async (projects) => ({
|
|
45
|
+
project: requireProject(await projects.get(request.params.projectId)),
|
|
46
|
+
}));
|
|
47
|
+
case "project.create":
|
|
48
|
+
return await this.withProjects(request, async (projects) => ({
|
|
49
|
+
project: await projects.create(request.params),
|
|
50
|
+
}));
|
|
51
|
+
case "project.update": {
|
|
52
|
+
const { projectId, ...input } = request.params;
|
|
53
|
+
return await this.withProjects(request, async (projects) => ({
|
|
54
|
+
project: requireProject(await projects.update(projectId, input)),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
case "project.delete":
|
|
58
|
+
return await this.withProjects(request, async (projects) => {
|
|
59
|
+
if (!await projects.remove(request.params.projectId))
|
|
60
|
+
throw new ProjectNotFoundError();
|
|
61
|
+
return { deleted: true };
|
|
62
|
+
});
|
|
38
63
|
case "emulator.devices.list":
|
|
39
64
|
return await this.withEmulator(request, (emulator) => emulator.devices());
|
|
40
65
|
case "emulator.active.get":
|
|
@@ -102,6 +127,8 @@ export class RemoteRuntimeDispatcher {
|
|
|
102
127
|
return await this.withSessions(request, (sessions) => sessions.launchOptions());
|
|
103
128
|
case "session.create":
|
|
104
129
|
return await this.withSessions(request, (sessions) => sessions.create(request.params));
|
|
130
|
+
case "session.fork":
|
|
131
|
+
return await this.withSessions(request, (sessions) => sessions.fork(request.params));
|
|
105
132
|
case "session.message.send":
|
|
106
133
|
return await this.withSessions(request, (sessions) => {
|
|
107
134
|
const { sessionId, ...input } = request.params;
|
|
@@ -234,6 +261,13 @@ export class RemoteRuntimeDispatcher {
|
|
|
234
261
|
}
|
|
235
262
|
return rpcResult(request, await invoke(emulator));
|
|
236
263
|
}
|
|
264
|
+
async withProjects(request, invoke) {
|
|
265
|
+
const projects = this.options.projects;
|
|
266
|
+
if (!projects) {
|
|
267
|
+
return rpcError(request.id, "method_not_found", "Runtime Projects are unavailable");
|
|
268
|
+
}
|
|
269
|
+
return rpcResult(request, await invoke(projects));
|
|
270
|
+
}
|
|
237
271
|
async withSessions(request, invoke) {
|
|
238
272
|
const sessions = this.options.sessions;
|
|
239
273
|
if (!sessions) {
|
|
@@ -257,6 +291,17 @@ export class RemoteRuntimeDispatcher {
|
|
|
257
291
|
}
|
|
258
292
|
}
|
|
259
293
|
function mapHostError(request, error) {
|
|
294
|
+
if (error instanceof ProjectNotFoundError) {
|
|
295
|
+
return { code: "not_found", message: "Runtime Project was not found" };
|
|
296
|
+
}
|
|
297
|
+
if (error instanceof RuntimeProjectInputError) {
|
|
298
|
+
return {
|
|
299
|
+
code: error.code,
|
|
300
|
+
message: error.code === "invalid_request"
|
|
301
|
+
? "Runtime Project configuration is invalid"
|
|
302
|
+
: "Runtime Project directory is unavailable",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
260
305
|
if (error instanceof SessionBrowserServiceError) {
|
|
261
306
|
switch (error.code) {
|
|
262
307
|
case "invalid_request":
|
|
@@ -340,6 +385,13 @@ function mapHostError(request, error) {
|
|
|
340
385
|
}
|
|
341
386
|
return { code: "internal", message: "Remote Runtime request failed" };
|
|
342
387
|
}
|
|
388
|
+
class ProjectNotFoundError extends Error {
|
|
389
|
+
}
|
|
390
|
+
function requireProject(project) {
|
|
391
|
+
if (!project)
|
|
392
|
+
throw new ProjectNotFoundError();
|
|
393
|
+
return project;
|
|
394
|
+
}
|
|
343
395
|
function idempotentCancel(watch) {
|
|
344
396
|
let pending;
|
|
345
397
|
return () => {
|
|
@@ -64,6 +64,7 @@ function projectRuntimeSnapshot(value) {
|
|
|
64
64
|
const originalActive = runtime.activeResponseIds;
|
|
65
65
|
const projected = {
|
|
66
66
|
status: runtime.status,
|
|
67
|
+
...(runtime.statusKind === undefined ? {} : { statusKind: runtime.statusKind }),
|
|
67
68
|
activeResponseIds: [],
|
|
68
69
|
pendingInteractions: [],
|
|
69
70
|
};
|