@rynx-ai/server 0.1.10 → 0.1.11-beta.10
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 +17 -52
- package/dist/control-api.js +755 -341
- package/dist/control-web/assets/{highlighted-body-OFNGDK62-DgFZpTNw.js → highlighted-body-OFNGDK62-IdclXfSt.js} +1 -1
- package/dist/control-web/assets/index-5n6D-i-c.css +32 -0
- package/dist/control-web/assets/index-DhsO7WjD.js +709 -0
- package/dist/control-web/assets/{mermaid-GHXKKRXX-BDikE3W1.js → mermaid-GHXKKRXX-BqMPrbIZ.js} +3 -3
- package/dist/control-web/index.html +2 -2
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +4 -2
- package/dist/machine-session-service.d.ts +135 -27
- package/dist/machine-session-service.js +410 -58
- package/dist/remote-runtime-dispatcher.d.ts +12 -2
- package/dist/remote-runtime-dispatcher.js +56 -0
- package/dist/remote-runtime-session-projection.js +1 -0
- package/dist/server.d.ts +59 -29
- package/dist/server.js +158 -143
- package/dist/session-browser-service.d.ts +8 -0
- package/dist/session-browser-service.js +37 -20
- package/dist/session-portal.d.ts +42 -0
- package/dist/session-portal.js +746 -0
- package/dist/session-runtime-index.d.ts +7 -0
- package/dist/session-runtime-index.js +42 -6
- package/dist/session-terminal-host.d.ts +6 -0
- package/dist/session-terminal-host.js +56 -41
- package/dist/terminal-ws.js +8 -6
- package/package.json +7 -7
- package/dist/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
|
|
@@ -145,11 +159,18 @@ export class MachineSessionService {
|
|
|
145
159
|
const statusCache = new Map();
|
|
146
160
|
const { agents } = await this.agentOptions();
|
|
147
161
|
const [providers, launchAgents] = await Promise.all([
|
|
148
|
-
Promise.all(SESSION_PROVIDER_IDS.map(async (provider) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
Promise.all(SESSION_PROVIDER_IDS.map(async (provider) => {
|
|
163
|
+
const execution = this.ports.execution;
|
|
164
|
+
const models = execution?.listModels
|
|
165
|
+
? await execution.listModels(provider).catch(() => [])
|
|
166
|
+
: undefined;
|
|
167
|
+
return {
|
|
168
|
+
id: provider,
|
|
169
|
+
name: getRuntimeProfile(provider).displayName,
|
|
170
|
+
...await this.runtimeReadiness(provider, statusCache),
|
|
171
|
+
...(models === undefined ? {} : { models }),
|
|
172
|
+
};
|
|
173
|
+
})),
|
|
153
174
|
Promise.all(agents.map(async (agent) => {
|
|
154
175
|
const execution = this.ports.execution;
|
|
155
176
|
if (!execution) {
|
|
@@ -178,8 +199,53 @@ export class MachineSessionService {
|
|
|
178
199
|
]);
|
|
179
200
|
return { providers, agents: launchAgents };
|
|
180
201
|
}
|
|
202
|
+
/** Read the effective model defaults used for the next turn. */
|
|
203
|
+
async executionSettings(sessionIdInput) {
|
|
204
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
205
|
+
const meta = this.ports.registry.get(sessionId);
|
|
206
|
+
if (!meta) {
|
|
207
|
+
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
208
|
+
}
|
|
209
|
+
return executionSettingsResult(meta.execution);
|
|
210
|
+
}
|
|
211
|
+
/** Replace the Turn defaults of an idle Session. Execution settings are
|
|
212
|
+
* passed through to the Provider CLI, which owns availability validation.
|
|
213
|
+
* The runner is closed so every Provider resumes its native context with
|
|
214
|
+
* the new settings on the next turn. */
|
|
215
|
+
async updateExecutionSettings(input) {
|
|
216
|
+
const sessionId = validOpaqueToken(input.sessionId, "sessionId", MAX_SESSION_ID_CHARS);
|
|
217
|
+
const modelId = input.model === null ? null : validRequiredText(input.model, "model");
|
|
218
|
+
const meta = this.ports.registry.get(sessionId);
|
|
219
|
+
if (!meta) {
|
|
220
|
+
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
221
|
+
}
|
|
222
|
+
await this.assertNotForkReserved(sessionId);
|
|
223
|
+
const runtime = this.ports.runtimeState.snapshot(sessionId);
|
|
224
|
+
const pendingMessage = await this.ports.pendingMessages?.get(sessionId);
|
|
225
|
+
if (runtime.status !== "idle" ||
|
|
226
|
+
runtime.activeResponseIds.length > 0 ||
|
|
227
|
+
runtime.pendingInteractions.length > 0 ||
|
|
228
|
+
pendingMessage) {
|
|
229
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session must be idle before changing model settings");
|
|
230
|
+
}
|
|
231
|
+
const executionPort = this.requireExecution();
|
|
232
|
+
const reasoningEffort = input.reasoningEffort === null
|
|
233
|
+
? null
|
|
234
|
+
: validRequiredText(input.reasoningEffort, "reasoningEffort");
|
|
235
|
+
const nextExecution = {
|
|
236
|
+
...structuredClone(meta.execution),
|
|
237
|
+
model: modelId,
|
|
238
|
+
reasoningEffort,
|
|
239
|
+
};
|
|
240
|
+
this.ports.registry.setExecution(sessionId, nextExecution);
|
|
241
|
+
executionPort.runner.stopRunner(sessionId);
|
|
242
|
+
return executionSettingsResult(nextExecution);
|
|
243
|
+
}
|
|
181
244
|
/** Create target-owned Session identity from one Agent preset or direct Provider. */
|
|
182
245
|
async create(input) {
|
|
246
|
+
return this.withAdmission(() => this.createAdmitted(input));
|
|
247
|
+
}
|
|
248
|
+
async createAdmitted(input) {
|
|
183
249
|
const hasAgent = input.agent !== undefined;
|
|
184
250
|
const hasProvider = input.provider !== undefined;
|
|
185
251
|
if (hasAgent === hasProvider) {
|
|
@@ -199,21 +265,189 @@ export class MachineSessionService {
|
|
|
199
265
|
}
|
|
200
266
|
target = { provider: input.provider };
|
|
201
267
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
268
|
+
const workspacePort = this.requireWorkspace();
|
|
269
|
+
const workspace = await workspacePort.resolve(input.projectId);
|
|
270
|
+
try {
|
|
271
|
+
const execution = await this.requireExecution().resolve({
|
|
272
|
+
...target,
|
|
273
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
274
|
+
...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
|
|
275
|
+
...(input.permissionPreset === undefined ? {} : { permissionPreset: input.permissionPreset }),
|
|
276
|
+
cwd: workspace.cwd,
|
|
277
|
+
});
|
|
278
|
+
const sessionId = newSessionId();
|
|
279
|
+
this.ports.registry.create({
|
|
280
|
+
id: sessionId,
|
|
281
|
+
source: "console",
|
|
282
|
+
workspace,
|
|
283
|
+
execution,
|
|
284
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
285
|
+
createdAt: new Date().toISOString(),
|
|
286
|
+
});
|
|
287
|
+
return { sessionId };
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (input.projectId === undefined) {
|
|
291
|
+
await workspacePort.discardUnbound?.(workspace).catch(() => undefined);
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Create one independent Session at the source's stable Provider/canonical
|
|
297
|
+
* boundary. Project and Agent selectors are intentionally absent: the target
|
|
298
|
+
* receives exact copies of the source's already-frozen snapshots. */
|
|
299
|
+
async fork(input) {
|
|
300
|
+
return this.withAdmission(() => this.forkAdmitted(input));
|
|
301
|
+
}
|
|
302
|
+
async forkAdmitted(input) {
|
|
303
|
+
const sourceSessionId = validOpaqueToken(input.sourceSessionId, "sourceSessionId", MAX_SESSION_ID_CHARS);
|
|
304
|
+
const operationId = validOpaqueToken(input.operationId, "operationId", MAX_SESSION_ID_CHARS);
|
|
305
|
+
const title = input.title === undefined
|
|
306
|
+
? undefined
|
|
307
|
+
: normalizeSessionTitle(input.title.trim());
|
|
308
|
+
if (input.title !== undefined && !title) {
|
|
309
|
+
throw new MachineSessionServiceInputError("title must not be empty");
|
|
310
|
+
}
|
|
311
|
+
const requestHash = createHash("sha256")
|
|
312
|
+
.update(JSON.stringify({ sourceSessionId, title: title ?? null }))
|
|
313
|
+
.digest("hex");
|
|
314
|
+
const operation = await this.requireForks().claim({
|
|
315
|
+
operationId,
|
|
316
|
+
requestHash,
|
|
317
|
+
sourceSessionId,
|
|
318
|
+
...(title ? { title } : {}),
|
|
319
|
+
});
|
|
320
|
+
if (operation.state === "target_deleted") {
|
|
321
|
+
throw new MachineSessionServiceFailure("conflict", "fork target was deleted");
|
|
322
|
+
}
|
|
323
|
+
if (operation.state === "completed") {
|
|
324
|
+
return { sessionId: operation.targetSessionId, disposition: "replayed" };
|
|
325
|
+
}
|
|
326
|
+
const inflight = this.forkTasks.get(operationId);
|
|
327
|
+
if (inflight)
|
|
328
|
+
return inflight;
|
|
329
|
+
const task = this.performFork(operation).finally(() => {
|
|
330
|
+
if (this.forkTasks.get(operationId) === task) {
|
|
331
|
+
this.forkTasks.delete(operationId);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
this.forkTasks.set(operationId, task);
|
|
335
|
+
return task;
|
|
336
|
+
}
|
|
337
|
+
/** Publish a Provider TUI `/clear` or `/fork` after its native binding has
|
|
338
|
+
* already been persisted. The source snapshots remain the only workspace and
|
|
339
|
+
* execution authority; Provider events cannot alter them during rotation. */
|
|
340
|
+
async recordNativeRotation(input) {
|
|
341
|
+
const sourceSessionId = validOpaqueToken(input.sourceSessionId, "sourceSessionId", MAX_SESSION_ID_CHARS);
|
|
342
|
+
const targetSessionId = validOpaqueToken(input.targetSessionId, "targetSessionId", MAX_SESSION_ID_CHARS);
|
|
343
|
+
if (sourceSessionId === targetSessionId) {
|
|
344
|
+
throw new MachineSessionServiceInputError("source and target Session must differ");
|
|
345
|
+
}
|
|
346
|
+
const source = this.ports.registry.get(sourceSessionId);
|
|
347
|
+
if (!source) {
|
|
348
|
+
throw new MachineSessionServiceFailure("not_found", "source Session not found");
|
|
349
|
+
}
|
|
350
|
+
const existing = this.ports.registry.get(targetSessionId);
|
|
351
|
+
if (existing) {
|
|
352
|
+
if (input.kind === "fork" &&
|
|
353
|
+
existing.forkedFromSessionId === sourceSessionId)
|
|
354
|
+
return;
|
|
355
|
+
throw new MachineSessionServiceFailure("conflict", "target Session already exists");
|
|
356
|
+
}
|
|
357
|
+
const workspace = structuredClone(source.workspace);
|
|
358
|
+
const execution = structuredClone(source.execution);
|
|
359
|
+
const sourceTitle = source.title;
|
|
360
|
+
const now = new Date().toISOString();
|
|
361
|
+
const target = {
|
|
362
|
+
id: targetSessionId,
|
|
363
|
+
source: input.kind,
|
|
364
|
+
workspace,
|
|
365
|
+
execution,
|
|
366
|
+
...(input.kind === "fork" && sourceTitle ? { title: sourceTitle } : {}),
|
|
367
|
+
...(input.kind === "fork" ? { forkedFromSessionId: sourceSessionId } : {}),
|
|
368
|
+
createdAt: now,
|
|
369
|
+
updatedAt: now,
|
|
370
|
+
};
|
|
371
|
+
if (input.kind === "clear") {
|
|
372
|
+
this.ports.registry.create(target);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const items = (await this.ports.log.snapshot(sourceSessionId))
|
|
376
|
+
.map((item) => structuredClone(item));
|
|
377
|
+
await this.requireForks().publishNativeFork({
|
|
378
|
+
sourceSessionId,
|
|
379
|
+
target,
|
|
380
|
+
items,
|
|
211
381
|
});
|
|
212
|
-
|
|
382
|
+
}
|
|
383
|
+
async performFork(operation) {
|
|
384
|
+
const source = this.ports.registry.get(operation.sourceSessionId);
|
|
385
|
+
if (!source) {
|
|
386
|
+
throw new MachineSessionServiceFailure("not_found", "source Session not found");
|
|
387
|
+
}
|
|
388
|
+
const runner = this.requireExecution().runner;
|
|
389
|
+
if (!runner.forkSession) {
|
|
390
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Provider does not support Session fork");
|
|
391
|
+
}
|
|
392
|
+
const workspace = structuredClone(source.workspace);
|
|
393
|
+
const execution = structuredClone(source.execution);
|
|
394
|
+
const sourceTitle = source.title;
|
|
395
|
+
let items;
|
|
396
|
+
try {
|
|
397
|
+
this.assertForkableSource(operation.sourceSessionId);
|
|
398
|
+
const native = await runner.forkSession(operation.sourceSessionId, operation.targetSessionId, {
|
|
399
|
+
workspace: structuredClone(workspace),
|
|
400
|
+
execution: structuredClone(execution),
|
|
401
|
+
beforeProviderFork: async () => {
|
|
402
|
+
this.assertForkableSource(operation.sourceSessionId);
|
|
403
|
+
items = (await this.ports.log.snapshot(operation.sourceSessionId))
|
|
404
|
+
.map((item) => structuredClone(item));
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
if (!native.ok) {
|
|
408
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Provider could not fork the Session", native.message);
|
|
409
|
+
}
|
|
410
|
+
if (!items) {
|
|
411
|
+
throw new MachineSessionServiceFailure("outcome_unknown", "Provider fork completed without a canonical fork point");
|
|
412
|
+
}
|
|
413
|
+
const now = new Date().toISOString();
|
|
414
|
+
const targetTitle = operation.title ?? sourceTitle;
|
|
415
|
+
await this.requireForks().commit({
|
|
416
|
+
operationId: operation.operationId,
|
|
417
|
+
target: {
|
|
418
|
+
id: operation.targetSessionId,
|
|
419
|
+
source: "fork",
|
|
420
|
+
workspace: structuredClone(workspace),
|
|
421
|
+
execution: structuredClone(execution),
|
|
422
|
+
...(targetTitle ? { title: targetTitle } : {}),
|
|
423
|
+
forkedFromSessionId: operation.sourceSessionId,
|
|
424
|
+
createdAt: now,
|
|
425
|
+
updatedAt: now,
|
|
426
|
+
},
|
|
427
|
+
items,
|
|
428
|
+
});
|
|
429
|
+
return { sessionId: operation.targetSessionId, disposition: "created" };
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
await Promise.resolve(this.requireForks().markError(operation.operationId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
assertForkableSource(sessionId) {
|
|
437
|
+
const runtime = this.ports.runtimeState.snapshot(sessionId);
|
|
438
|
+
if (runtime.status !== "idle" ||
|
|
439
|
+
runtime.activeResponseIds.length > 0 ||
|
|
440
|
+
runtime.pendingInteractions.length > 0) {
|
|
441
|
+
throw new MachineSessionServiceFailure("failed_precondition", "source Session must be idle before it can be forked");
|
|
442
|
+
}
|
|
213
443
|
}
|
|
214
444
|
/** Inject one turn through the target daemon's native single-writer runner. */
|
|
215
445
|
async sendMessage(sessionIdInput, messageInput) {
|
|
446
|
+
return this.withAdmission(() => this.sendMessageAdmitted(sessionIdInput, messageInput));
|
|
447
|
+
}
|
|
448
|
+
async sendMessageAdmitted(sessionIdInput, messageInput) {
|
|
216
449
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
450
|
+
await this.assertNotForkReserved(sessionId);
|
|
217
451
|
const request = typeof messageInput === "string"
|
|
218
452
|
? { message: messageInput }
|
|
219
453
|
: messageInput;
|
|
@@ -272,11 +506,54 @@ export class MachineSessionService {
|
|
|
272
506
|
if (request.clientMessageId) {
|
|
273
507
|
await resources?.markMessageInjecting(sessionId, request.clientMessageId);
|
|
274
508
|
}
|
|
509
|
+
const responseId = this.ports.publishEvent ? `resp_${randomUUID()}` : undefined;
|
|
510
|
+
if (responseId) {
|
|
511
|
+
await this.ports.publishEvent(sessionId, {
|
|
512
|
+
type: "session.input.consumed",
|
|
513
|
+
item: {
|
|
514
|
+
id: newSessionItemId("message"),
|
|
515
|
+
sessionId,
|
|
516
|
+
position: 0,
|
|
517
|
+
responseId,
|
|
518
|
+
status: "completed",
|
|
519
|
+
createdAt: Date.now(),
|
|
520
|
+
type: "message",
|
|
521
|
+
data: {
|
|
522
|
+
role: "user",
|
|
523
|
+
content: prepared.input.content.map((part) => part.type === "text"
|
|
524
|
+
? { type: "input_text", text: part.text }
|
|
525
|
+
: { ...part.resource }),
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
await this.ports.publishEvent(sessionId, {
|
|
530
|
+
type: "session.status",
|
|
531
|
+
sessionId,
|
|
532
|
+
responseId,
|
|
533
|
+
status: "running",
|
|
534
|
+
statusKind: "startup",
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const abandonResponseHandoff = responseId
|
|
538
|
+
? this.ports.runtimeState.beginResponseHandoff?.(sessionId, responseId)
|
|
539
|
+
: undefined;
|
|
540
|
+
const clearStartup = async (status) => {
|
|
541
|
+
if (!responseId)
|
|
542
|
+
return;
|
|
543
|
+
await this.ports.publishEvent(sessionId, {
|
|
544
|
+
type: "session.status",
|
|
545
|
+
sessionId,
|
|
546
|
+
responseId,
|
|
547
|
+
status,
|
|
548
|
+
});
|
|
549
|
+
};
|
|
275
550
|
let execution;
|
|
276
551
|
try {
|
|
277
552
|
execution = await this.ensureLiveSession(sessionId, meta);
|
|
278
553
|
}
|
|
279
554
|
catch (error) {
|
|
555
|
+
abandonResponseHandoff?.();
|
|
556
|
+
await clearStartup("idle");
|
|
280
557
|
if (request.clientMessageId) {
|
|
281
558
|
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error));
|
|
282
559
|
}
|
|
@@ -284,26 +561,36 @@ export class MachineSessionService {
|
|
|
284
561
|
}
|
|
285
562
|
let outcome;
|
|
286
563
|
try {
|
|
287
|
-
|
|
564
|
+
const runtimeInput = responseId
|
|
565
|
+
? { ...prepared.input, responseId }
|
|
566
|
+
: prepared.input;
|
|
567
|
+
outcome = await execution.runner.injectMessage(sessionId, responseId || needsPreparedOperation ? runtimeInput : request.message);
|
|
288
568
|
}
|
|
289
569
|
catch (error) {
|
|
570
|
+
abandonResponseHandoff?.();
|
|
571
|
+
await clearStartup("idle");
|
|
290
572
|
if (request.clientMessageId) {
|
|
291
573
|
await Promise.resolve(resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
|
|
292
574
|
}
|
|
293
575
|
throw new MachineSessionServiceFailure("outcome_unknown", "live injection outcome is unknown");
|
|
294
576
|
}
|
|
295
577
|
if (outcome === "failed") {
|
|
578
|
+
abandonResponseHandoff?.();
|
|
579
|
+
await clearStartup("idle");
|
|
296
580
|
if (request.clientMessageId) {
|
|
297
581
|
await resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
298
582
|
}
|
|
299
583
|
throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`);
|
|
300
584
|
}
|
|
301
585
|
if (outcome !== "injected") {
|
|
586
|
+
abandonResponseHandoff?.();
|
|
587
|
+
await clearStartup("idle");
|
|
302
588
|
if (request.clientMessageId) {
|
|
303
589
|
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
304
590
|
}
|
|
305
591
|
throw new MachineSessionServiceFailure("failed_precondition", `live injection ${outcome}`);
|
|
306
592
|
}
|
|
593
|
+
await clearStartup("running");
|
|
307
594
|
if (request.clientMessageId) {
|
|
308
595
|
try {
|
|
309
596
|
await resources?.markMessageInjected(sessionId, request.clientMessageId);
|
|
@@ -347,7 +634,11 @@ export class MachineSessionService {
|
|
|
347
634
|
* delivery worker starts the pane now, waits for a real native thread, then
|
|
348
635
|
* injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
|
|
349
636
|
async enqueueMessage(sessionIdInput, messageInput) {
|
|
637
|
+
return this.withAdmission(() => this.enqueueMessageAdmitted(sessionIdInput, messageInput));
|
|
638
|
+
}
|
|
639
|
+
async enqueueMessageAdmitted(sessionIdInput, messageInput) {
|
|
350
640
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
641
|
+
await this.assertNotForkReserved(sessionId);
|
|
351
642
|
if (typeof messageInput !== "string" || messageInput.length === 0) {
|
|
352
643
|
throw new MachineSessionServiceInputError("message must be a non-empty string");
|
|
353
644
|
}
|
|
@@ -377,6 +668,9 @@ export class MachineSessionService {
|
|
|
377
668
|
}
|
|
378
669
|
/** Explicitly restore the target daemon's live runner without starting a turn. */
|
|
379
670
|
async startTerminal(sessionIdInput) {
|
|
671
|
+
return this.withAdmission(() => this.startTerminalAdmitted(sessionIdInput));
|
|
672
|
+
}
|
|
673
|
+
async startTerminalAdmitted(sessionIdInput) {
|
|
380
674
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
381
675
|
const request = await this.liveSessionRequest(sessionId);
|
|
382
676
|
const start = request.execution.runner.startLiveSession?.bind(request.execution.runner) ??
|
|
@@ -397,40 +691,13 @@ export class MachineSessionService {
|
|
|
397
691
|
}
|
|
398
692
|
async liveSessionRequest(sessionId, meta = this.ports.registry.get(sessionId)) {
|
|
399
693
|
const execution = this.requireExecution();
|
|
400
|
-
|
|
401
|
-
const bound = consoleMeta ? null : ((await execution.sessionStore?.get(sessionId)) ?? null);
|
|
402
|
-
if (!meta && !bound) {
|
|
694
|
+
if (!meta) {
|
|
403
695
|
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
404
696
|
}
|
|
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
|
-
}
|
|
697
|
+
const liveRuntime = meta.execution.provider;
|
|
422
698
|
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 } : {}),
|
|
699
|
+
workspace: structuredClone(meta.workspace),
|
|
700
|
+
execution: structuredClone(meta.execution),
|
|
434
701
|
};
|
|
435
702
|
return { execution, liveRuntime, options };
|
|
436
703
|
}
|
|
@@ -441,6 +708,7 @@ export class MachineSessionService {
|
|
|
441
708
|
}
|
|
442
709
|
async delete(sessionIdInput) {
|
|
443
710
|
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
711
|
+
await this.assertNotForkReserved(sessionId);
|
|
444
712
|
try {
|
|
445
713
|
await this.ports.lifecycle?.beforeDelete?.(sessionId);
|
|
446
714
|
}
|
|
@@ -455,6 +723,7 @@ export class MachineSessionService {
|
|
|
455
723
|
this.ports.registry.remove(sessionId);
|
|
456
724
|
await this.ports.log.deleteSession(sessionId);
|
|
457
725
|
this.ports.runtimeState.remove?.(sessionId);
|
|
726
|
+
await this.ports.forks?.markTargetDeleted(sessionId);
|
|
458
727
|
try {
|
|
459
728
|
await this.ports.lifecycle?.afterDelete?.(sessionId);
|
|
460
729
|
}
|
|
@@ -469,6 +738,24 @@ export class MachineSessionService {
|
|
|
469
738
|
}
|
|
470
739
|
return this.ports.agents;
|
|
471
740
|
}
|
|
741
|
+
reserveAdmission() {
|
|
742
|
+
const reservation = this.admissionReserve?.();
|
|
743
|
+
if (reservation)
|
|
744
|
+
return reservation;
|
|
745
|
+
if (!this.admissionReserve && this.admissionOpen()) {
|
|
746
|
+
return { release() { } };
|
|
747
|
+
}
|
|
748
|
+
throw new MachineSessionServiceFailure("failed_precondition", "daemon maintenance is in progress");
|
|
749
|
+
}
|
|
750
|
+
async withAdmission(operation) {
|
|
751
|
+
const reservation = this.reserveAdmission();
|
|
752
|
+
try {
|
|
753
|
+
return await operation();
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
reservation.release();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
472
759
|
requireResources() {
|
|
473
760
|
if (!this.ports.resources) {
|
|
474
761
|
throw new MachineSessionServiceFailure("failed_precondition", "Session resources are unavailable");
|
|
@@ -520,7 +807,9 @@ export class MachineSessionService {
|
|
|
520
807
|
if (!pending || pending.state !== "queued")
|
|
521
808
|
return;
|
|
522
809
|
let injectionAttempted = false;
|
|
810
|
+
let admission;
|
|
523
811
|
try {
|
|
812
|
+
admission = this.reserveAdmission();
|
|
524
813
|
const execution = await this.ensureLiveSession(sessionId);
|
|
525
814
|
if (this.cancelledPendingDeliveries.has(sessionId))
|
|
526
815
|
return;
|
|
@@ -553,6 +842,9 @@ export class MachineSessionService {
|
|
|
553
842
|
return;
|
|
554
843
|
}
|
|
555
844
|
}
|
|
845
|
+
finally {
|
|
846
|
+
admission?.release();
|
|
847
|
+
}
|
|
556
848
|
await retryDelay(this.pendingMessageRetryMs);
|
|
557
849
|
}
|
|
558
850
|
}
|
|
@@ -592,6 +884,44 @@ export class MachineSessionService {
|
|
|
592
884
|
}
|
|
593
885
|
return this.ports.execution;
|
|
594
886
|
}
|
|
887
|
+
requireWorkspace() {
|
|
888
|
+
const workspace = this.ports.workspace;
|
|
889
|
+
if (!workspace) {
|
|
890
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session workspace resolution is unavailable");
|
|
891
|
+
}
|
|
892
|
+
return workspace;
|
|
893
|
+
}
|
|
894
|
+
requireForks() {
|
|
895
|
+
if (!this.ports.forks) {
|
|
896
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session fork storage is unavailable");
|
|
897
|
+
}
|
|
898
|
+
return this.ports.forks;
|
|
899
|
+
}
|
|
900
|
+
async assertNotForkReserved(sessionId) {
|
|
901
|
+
if (await this.ports.forks?.hasReservedSource(sessionId)) {
|
|
902
|
+
throw new MachineSessionServiceFailure("conflict", "Session fork is in progress");
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async resumeReservedForks() {
|
|
906
|
+
if (!this.ports.forks)
|
|
907
|
+
return;
|
|
908
|
+
try {
|
|
909
|
+
for (const operation of await this.ports.forks.listReserved()) {
|
|
910
|
+
if (this.forkTasks.has(operation.operationId))
|
|
911
|
+
continue;
|
|
912
|
+
const task = this.performFork(operation).finally(() => {
|
|
913
|
+
if (this.forkTasks.get(operation.operationId) === task) {
|
|
914
|
+
this.forkTasks.delete(operation.operationId);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
this.forkTasks.set(operation.operationId, task);
|
|
918
|
+
void task.catch(() => undefined);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
// A direct replay or the next daemon restart retries durable operations.
|
|
923
|
+
}
|
|
924
|
+
}
|
|
595
925
|
}
|
|
596
926
|
/** One-line title derived from the first user message; no model call. */
|
|
597
927
|
export function synthesizeSessionTitle(message, limit = 60) {
|
|
@@ -688,6 +1018,13 @@ function snapshotPage(sessionId, items, runtime, hasMore) {
|
|
|
688
1018
|
...(hasMore && items.length > 0 ? { nextAfterId: items.at(-1).id } : {}),
|
|
689
1019
|
};
|
|
690
1020
|
}
|
|
1021
|
+
/** Exact UTF-8 size of {@link snapshotPage} without repeatedly serializing the
|
|
1022
|
+
* complete item array while searching for a page boundary. */
|
|
1023
|
+
function snapshotPageJsonByteLength(sessionId, itemsJsonBytes, finalItemId, runtime, hasMore) {
|
|
1024
|
+
return Buffer.byteLength(`{"sessionId":${JSON.stringify(sessionId)},"items":[`, "utf8")
|
|
1025
|
+
+ itemsJsonBytes
|
|
1026
|
+
+ Buffer.byteLength(`],"runtime":${JSON.stringify(runtime)},"hasMore":${hasMore}${hasMore ? `,"nextAfterId":${JSON.stringify(finalItemId)}` : ""}}`, "utf8");
|
|
1027
|
+
}
|
|
691
1028
|
function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
692
1029
|
const createdAt = meta?.createdAt ?? timestampFromEpoch(log?.createdAt, "createdAt");
|
|
693
1030
|
const updatedAt = log
|
|
@@ -695,8 +1032,7 @@ function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
|
695
1032
|
: (meta?.updatedAt ?? createdAt);
|
|
696
1033
|
return {
|
|
697
1034
|
id,
|
|
698
|
-
...(meta?.provider === undefined ? {} : { provider: meta.provider }),
|
|
699
|
-
...(meta?.agent === undefined ? {} : { agent: meta.agent }),
|
|
1035
|
+
...(meta?.execution?.provider === undefined ? {} : { provider: meta.execution.provider }),
|
|
700
1036
|
...(meta?.title === undefined ? {} : { title: normalizeSessionTitle(meta.title) }),
|
|
701
1037
|
status: runtimeState.snapshot(id).status,
|
|
702
1038
|
...(pendingState === undefined
|
|
@@ -711,6 +1047,13 @@ function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
|
711
1047
|
updatedAt: validTimestamp(updatedAt, "updatedAt"),
|
|
712
1048
|
};
|
|
713
1049
|
}
|
|
1050
|
+
function executionSettingsResult(execution) {
|
|
1051
|
+
return {
|
|
1052
|
+
provider: execution.provider,
|
|
1053
|
+
model: execution.model,
|
|
1054
|
+
reasoningEffort: execution.reasoningEffort,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
714
1057
|
function compareMetaRecency(left, right) {
|
|
715
1058
|
const leftTime = validTimestamp(left.updatedAt ?? left.createdAt, "updatedAt");
|
|
716
1059
|
const rightTime = validTimestamp(right.updatedAt ?? right.createdAt, "updatedAt");
|
|
@@ -805,6 +1148,15 @@ function validOpaqueToken(value, field, maximum) {
|
|
|
805
1148
|
}
|
|
806
1149
|
return value;
|
|
807
1150
|
}
|
|
1151
|
+
function validRequiredText(value, field) {
|
|
1152
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > 512) {
|
|
1153
|
+
throw new MachineSessionServiceInputError(`${field} must be a non-empty string of at most 512 characters`);
|
|
1154
|
+
}
|
|
1155
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
1156
|
+
throw new MachineSessionServiceInputError(`${field} contains invalid control characters`);
|
|
1157
|
+
}
|
|
1158
|
+
return value.trim();
|
|
1159
|
+
}
|
|
808
1160
|
function validTimestamp(value, field) {
|
|
809
1161
|
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
810
1162
|
throw new MachineSessionServiceInputError(`${field} must be a valid timestamp`);
|