@rynx-ai/server 0.1.0 → 0.1.10-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/browser-surface-control-queue.d.ts +42 -0
- package/dist/browser-surface-control-queue.js +187 -0
- package/dist/browser-surface-ws.d.ts +16 -0
- package/dist/browser-surface-ws.js +356 -0
- package/dist/control-api.d.ts +76 -4
- package/dist/control-api.js +3014 -133
- package/dist/control-web/assets/highlighted-body-OFNGDK62-DHu2g-rk.js +1 -0
- package/dist/control-web/assets/index-B6Pb5j9M.js +666 -0
- package/dist/control-web/assets/index-BU-_Qud_.css +32 -0
- package/dist/control-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/control-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/control-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/control-web/assets/mermaid-GHXKKRXX-CSYzOmBR.js +131 -0
- package/dist/control-web/assets/rynx-wordmark-LL1QRYHD.png +0 -0
- package/dist/control-web/favicon.png +0 -0
- package/dist/control-web/favicon.svg +38 -0
- package/dist/control-web/index.html +16 -0
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +19 -14
- package/dist/desktop-browser-host.d.ts +170 -0
- package/dist/desktop-browser-host.js +1018 -0
- package/dist/direct-runtime-browser-inspect-server.d.ts +72 -0
- package/dist/direct-runtime-browser-inspect-server.js +749 -0
- package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
- package/dist/direct-runtime-browser-surface-server.js +821 -0
- package/dist/direct-runtime-server.d.ts +143 -0
- package/dist/direct-runtime-server.js +1959 -0
- package/dist/emulator-surface-ws.d.ts +8 -0
- package/dist/emulator-surface-ws.js +198 -0
- package/dist/machine-session-service.d.ts +265 -0
- package/dist/machine-session-service.js +822 -0
- package/dist/provider-cli-host.d.ts +10 -0
- package/dist/provider-cli-host.js +1 -0
- package/dist/remote-runtime-dispatcher.d.ts +94 -0
- package/dist/remote-runtime-dispatcher.js +403 -0
- package/dist/remote-runtime-session-projection.d.ts +19 -0
- package/dist/remote-runtime-session-projection.js +566 -0
- package/dist/remote-runtime.d.ts +22 -0
- package/dist/remote-runtime.js +32 -0
- package/dist/runtime-web-auth.d.ts +26 -0
- package/dist/runtime-web-auth.js +132 -0
- package/dist/server.d.ts +364 -12
- package/dist/server.js +341 -100
- package/dist/session-browser-service.d.ts +248 -0
- package/dist/session-browser-service.js +772 -0
- package/dist/session-browser-surface-coordinator.d.ts +24 -0
- package/dist/session-browser-surface-coordinator.js +669 -0
- package/dist/session-emulator-service.d.ts +167 -0
- package/dist/session-emulator-service.js +464 -0
- package/dist/session-runtime-index.d.ts +23 -0
- package/dist/session-runtime-index.js +96 -0
- package/dist/session-terminal-host.d.ts +51 -0
- package/dist/session-terminal-host.js +270 -0
- package/dist/terminal-ws.d.ts +12 -20
- package/dist/terminal-ws.js +421 -97
- package/package.json +14 -7
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon-owned application service for the first Remote Runtime Session slice.
|
|
3
|
+
*
|
|
4
|
+
* This service deliberately has no HTTP, Koa, WebSocket, or Direct-binding
|
|
5
|
+
* knowledge. Durable records, live state, events, and interruption remain owned
|
|
6
|
+
* by the daemon process and arrive through narrow injected ports. Local and
|
|
7
|
+
* Direct adapters can therefore expose the same behavior without copying state
|
|
8
|
+
* into a Control Plane database.
|
|
9
|
+
*/
|
|
10
|
+
import { SESSION_PROVIDER_IDS, getRuntimeProfile, isSessionProviderId, newSessionId, normalizeSessionTitle, } from "@rynx-ai/core";
|
|
11
|
+
import { REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE, REMOTE_RUNTIME_SESSION_MAX_CURSOR_CHARS, REMOTE_RUNTIME_SESSION_MAX_ID_CHARS, REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE, } from "@rynx-ai/protocol/remote-runtime-rpc";
|
|
12
|
+
import { ensureCodexResumeRollout, } from "@rynx-ai/runtime";
|
|
13
|
+
const DEFAULT_LIST_LIMIT = REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE;
|
|
14
|
+
const MAX_LIST_LIMIT = REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE;
|
|
15
|
+
const DEFAULT_SNAPSHOT_ITEM_LIMIT = REMOTE_RUNTIME_SESSION_DEFAULT_PAGE_SIZE;
|
|
16
|
+
const MAX_SNAPSHOT_ITEM_LIMIT = REMOTE_RUNTIME_SESSION_MAX_PAGE_SIZE;
|
|
17
|
+
const DEFAULT_LIST_PAGE_MAX_BYTES = 48 * 1024;
|
|
18
|
+
const DEFAULT_SNAPSHOT_PAGE_MAX_BYTES = 48 * 1024;
|
|
19
|
+
const MIN_PAGE_MAX_BYTES = 4 * 1024;
|
|
20
|
+
const MAX_PAGE_MAX_BYTES = 56 * 1024;
|
|
21
|
+
const DEFAULT_DIRECTORY_SCAN_LIMIT = 10_000;
|
|
22
|
+
const MAX_DIRECTORY_SCAN_LIMIT = 100_000;
|
|
23
|
+
const MAX_SESSION_ID_CHARS = REMOTE_RUNTIME_SESSION_MAX_ID_CHARS;
|
|
24
|
+
export class MachineSessionServiceInputError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "MachineSessionServiceInputError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Expected application failure; adapters decide how to represent it on the wire. */
|
|
31
|
+
export class MachineSessionServiceFailure extends Error {
|
|
32
|
+
code;
|
|
33
|
+
detail;
|
|
34
|
+
constructor(code, message, detail) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.detail = detail;
|
|
38
|
+
this.name = "MachineSessionServiceFailure";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Application boundary shared by an in-process Local adapter and Direct RPC. */
|
|
42
|
+
export class MachineSessionService {
|
|
43
|
+
ports;
|
|
44
|
+
maxListLimit;
|
|
45
|
+
listPageMaxBytes;
|
|
46
|
+
maxSnapshotItemLimit;
|
|
47
|
+
snapshotPageMaxBytes;
|
|
48
|
+
directoryScanLimit;
|
|
49
|
+
pendingMessageRetryMs;
|
|
50
|
+
pendingDeliveries = new Map();
|
|
51
|
+
cancelledPendingDeliveries = new Set();
|
|
52
|
+
constructor(ports, options = {}) {
|
|
53
|
+
this.ports = ports;
|
|
54
|
+
this.maxListLimit = boundedInteger(options.maxListLimit ?? MAX_LIST_LIMIT, 1, MAX_LIST_LIMIT, "maxListLimit");
|
|
55
|
+
this.listPageMaxBytes = boundedInteger(options.listPageMaxBytes ?? DEFAULT_LIST_PAGE_MAX_BYTES, MIN_PAGE_MAX_BYTES, MAX_PAGE_MAX_BYTES, "listPageMaxBytes");
|
|
56
|
+
this.maxSnapshotItemLimit = boundedInteger(options.maxSnapshotItemLimit ?? MAX_SNAPSHOT_ITEM_LIMIT, 1, MAX_SNAPSHOT_ITEM_LIMIT, "maxSnapshotItemLimit");
|
|
57
|
+
this.snapshotPageMaxBytes = boundedInteger(options.snapshotPageMaxBytes ?? DEFAULT_SNAPSHOT_PAGE_MAX_BYTES, MIN_PAGE_MAX_BYTES, MAX_PAGE_MAX_BYTES, "snapshotPageMaxBytes");
|
|
58
|
+
this.directoryScanLimit = boundedInteger(options.directoryScanLimit ?? DEFAULT_DIRECTORY_SCAN_LIMIT, 1, MAX_DIRECTORY_SCAN_LIMIT, "directoryScanLimit");
|
|
59
|
+
this.pendingMessageRetryMs = boundedInteger(options.pendingMessageRetryMs ?? 1_000, 10, 60_000, "pendingMessageRetryMs");
|
|
60
|
+
void this.resumePendingDeliveries();
|
|
61
|
+
}
|
|
62
|
+
async list(input = {}) {
|
|
63
|
+
const limit = requestLimit(input.limit, DEFAULT_LIST_LIMIT, this.maxListLimit, "limit");
|
|
64
|
+
const cursor = input.cursor === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: decodeListCursor(input.cursor);
|
|
67
|
+
const allMetas = this.ports.registry.list();
|
|
68
|
+
const metas = [...allMetas]
|
|
69
|
+
.sort(compareMetaRecency)
|
|
70
|
+
.slice(0, this.directoryScanLimit);
|
|
71
|
+
const [logged, pendingMessages] = await Promise.all([
|
|
72
|
+
this.ports.log.listSessions({ limit: this.directoryScanLimit + 1 }),
|
|
73
|
+
this.ports.pendingMessages?.list() ?? [],
|
|
74
|
+
]);
|
|
75
|
+
const directoryTruncated = allMetas.length > this.directoryScanLimit || logged.length > this.directoryScanLimit;
|
|
76
|
+
const boundedLogs = logged.slice(0, this.directoryScanLimit);
|
|
77
|
+
const metaById = new Map(metas.map((meta) => [meta.id, meta]));
|
|
78
|
+
const logById = new Map(boundedLogs.map((entry) => [entry.sessionId, entry]));
|
|
79
|
+
const pendingById = new Map(pendingMessages.map((entry) => [entry.sessionId, entry.state]));
|
|
80
|
+
const ids = new Set([...metaById.keys(), ...logById.keys()]);
|
|
81
|
+
const summaries = [...ids]
|
|
82
|
+
.map((id) => sessionSummary(id, metaById.get(id), logById.get(id), this.ports.runtimeState, pendingById.get(id)))
|
|
83
|
+
.sort(compareSummaryRecency);
|
|
84
|
+
const afterCursor = cursor === undefined
|
|
85
|
+
? summaries
|
|
86
|
+
: summaries.filter((summary) => isAfterListCursor(summary, cursor));
|
|
87
|
+
const page = afterCursor.slice(0, limit);
|
|
88
|
+
while (page.length > 0) {
|
|
89
|
+
const result = listPage(page, afterCursor.length > page.length, directoryTruncated);
|
|
90
|
+
if (page.length === 1 || jsonByteLength(result) <= this.listPageMaxBytes)
|
|
91
|
+
return result;
|
|
92
|
+
page.pop();
|
|
93
|
+
}
|
|
94
|
+
return listPage([], false, directoryTruncated);
|
|
95
|
+
}
|
|
96
|
+
async snapshot(input) {
|
|
97
|
+
const sessionId = validOpaqueToken(input.sessionId, "sessionId", MAX_SESSION_ID_CHARS);
|
|
98
|
+
const afterId = input.afterId === undefined
|
|
99
|
+
? undefined
|
|
100
|
+
: validOpaqueToken(input.afterId, "afterId", MAX_SESSION_ID_CHARS);
|
|
101
|
+
const limit = requestLimit(input.limit, DEFAULT_SNAPSHOT_ITEM_LIMIT, this.maxSnapshotItemLimit, "limit");
|
|
102
|
+
const candidates = (await this.ports.log.list(sessionId, {
|
|
103
|
+
...(afterId === undefined ? {} : { afterId }),
|
|
104
|
+
limit: limit + 1,
|
|
105
|
+
})).slice(0, limit + 1).map((item) => structuredClone(item));
|
|
106
|
+
const runtime = cloneRuntimeSnapshot(this.ports.runtimeState.snapshot(sessionId));
|
|
107
|
+
const empty = snapshotPage(sessionId, [], runtime, false);
|
|
108
|
+
const items = candidates.slice(0, limit);
|
|
109
|
+
while (items.length > 0) {
|
|
110
|
+
const result = snapshotPage(sessionId, items, runtime, candidates.length > items.length);
|
|
111
|
+
if (items.length === 1 || jsonByteLength(result) <= this.snapshotPageMaxBytes)
|
|
112
|
+
return result;
|
|
113
|
+
items.pop();
|
|
114
|
+
}
|
|
115
|
+
return empty;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Install a live subscriber synchronously, before returning control to an
|
|
119
|
+
* adapter. The adapter may now acknowledge readiness and request snapshot
|
|
120
|
+
* pages while buffering this tail, closing the subscribe/snapshot race.
|
|
121
|
+
*/
|
|
122
|
+
watch(sessionIdInput, options = {}) {
|
|
123
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
124
|
+
const iterator = this.ports.events.subscribe(sessionId)[Symbol.asyncIterator]();
|
|
125
|
+
return cancellableWatch(sessionId, iterator, options.signal);
|
|
126
|
+
}
|
|
127
|
+
async interrupt(sessionIdInput) {
|
|
128
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
129
|
+
return { interrupted: await this.ports.interruption.interrupt(sessionId) };
|
|
130
|
+
}
|
|
131
|
+
/** Session-scoped projection of the target daemon's own Agent catalog. */
|
|
132
|
+
async agentOptions() {
|
|
133
|
+
const catalog = this.requireAgents();
|
|
134
|
+
const agents = (await catalog.list()).map((agent) => ({
|
|
135
|
+
id: agent.id,
|
|
136
|
+
...(agent.name === undefined ? {} : { name: agent.name }),
|
|
137
|
+
...(agent.runtime === undefined ? {} : { runtime: agent.runtime }),
|
|
138
|
+
...(agent.model === undefined ? {} : { model: agent.model }),
|
|
139
|
+
}));
|
|
140
|
+
agents.sort((left, right) => (left.name ?? left.id).localeCompare(right.name ?? right.id) || left.id.localeCompare(right.id));
|
|
141
|
+
return { agents };
|
|
142
|
+
}
|
|
143
|
+
/** Target-owned Provider readiness plus the existing Agent preset catalog. */
|
|
144
|
+
async launchOptions() {
|
|
145
|
+
const statusCache = new Map();
|
|
146
|
+
const { agents } = await this.agentOptions();
|
|
147
|
+
const [providers, launchAgents] = await Promise.all([
|
|
148
|
+
Promise.all(SESSION_PROVIDER_IDS.map(async (provider) => ({
|
|
149
|
+
id: provider,
|
|
150
|
+
name: getRuntimeProfile(provider).displayName,
|
|
151
|
+
...await this.runtimeReadiness(provider, statusCache),
|
|
152
|
+
}))),
|
|
153
|
+
Promise.all(agents.map(async (agent) => {
|
|
154
|
+
const execution = this.ports.execution;
|
|
155
|
+
if (!execution) {
|
|
156
|
+
return {
|
|
157
|
+
...agent,
|
|
158
|
+
ready: false,
|
|
159
|
+
reason: "Session execution is unavailable on this Runtime.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const runtime = await execution.runtime.resolveRuntime({ agentName: agent.id });
|
|
164
|
+
return {
|
|
165
|
+
...agent,
|
|
166
|
+
runtime,
|
|
167
|
+
...await this.runtimeReadiness(runtime, statusCache),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return {
|
|
172
|
+
...agent,
|
|
173
|
+
ready: false,
|
|
174
|
+
reason: "Agent execution settings could not be resolved.",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
})),
|
|
178
|
+
]);
|
|
179
|
+
return { providers, agents: launchAgents };
|
|
180
|
+
}
|
|
181
|
+
/** Create target-owned Session identity from one Agent preset or direct Provider. */
|
|
182
|
+
async create(input) {
|
|
183
|
+
const hasAgent = input.agent !== undefined;
|
|
184
|
+
const hasProvider = input.provider !== undefined;
|
|
185
|
+
if (hasAgent === hasProvider) {
|
|
186
|
+
throw new MachineSessionServiceInputError("exactly one of agent or provider is required");
|
|
187
|
+
}
|
|
188
|
+
let target;
|
|
189
|
+
if (hasAgent) {
|
|
190
|
+
const agent = validOpaqueToken(input.agent, "agent", MAX_SESSION_ID_CHARS);
|
|
191
|
+
const known = (await this.requireAgents().list()).some((candidate) => candidate.id === agent);
|
|
192
|
+
if (!known)
|
|
193
|
+
throw new MachineSessionServiceFailure("not_found", "agent not found");
|
|
194
|
+
target = { agent };
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
if (!isSessionProviderId(input.provider)) {
|
|
198
|
+
throw new MachineSessionServiceInputError("provider must be codex, traex, or claude");
|
|
199
|
+
}
|
|
200
|
+
target = { provider: input.provider };
|
|
201
|
+
}
|
|
202
|
+
const sessionId = newSessionId();
|
|
203
|
+
this.ports.registry.create({
|
|
204
|
+
id: sessionId,
|
|
205
|
+
source: "console",
|
|
206
|
+
...target,
|
|
207
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
208
|
+
...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
|
|
209
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
210
|
+
createdAt: new Date().toISOString(),
|
|
211
|
+
});
|
|
212
|
+
return { sessionId };
|
|
213
|
+
}
|
|
214
|
+
/** Inject one turn through the target daemon's native single-writer runner. */
|
|
215
|
+
async sendMessage(sessionIdInput, messageInput) {
|
|
216
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
217
|
+
const request = typeof messageInput === "string"
|
|
218
|
+
? { message: messageInput }
|
|
219
|
+
: messageInput;
|
|
220
|
+
const hasMessage = request.message !== undefined;
|
|
221
|
+
const hasContent = request.content !== undefined;
|
|
222
|
+
if (hasMessage === hasContent) {
|
|
223
|
+
throw new MachineSessionServiceInputError("provide exactly one of message or content");
|
|
224
|
+
}
|
|
225
|
+
if (hasMessage && (typeof request.message !== "string" || request.message.length === 0)) {
|
|
226
|
+
throw new MachineSessionServiceInputError("message must be a non-empty string");
|
|
227
|
+
}
|
|
228
|
+
if (hasContent && (!Array.isArray(request.content) || request.content.length === 0)) {
|
|
229
|
+
throw new MachineSessionServiceInputError("content must be a non-empty array");
|
|
230
|
+
}
|
|
231
|
+
const meta = this.ports.registry.get(sessionId);
|
|
232
|
+
const consoleMeta = meta?.source === "console" ? meta : undefined;
|
|
233
|
+
let prepared;
|
|
234
|
+
const needsPreparedOperation = hasContent || request.clientMessageId !== undefined;
|
|
235
|
+
if (needsPreparedOperation) {
|
|
236
|
+
const resources = this.ports.resources;
|
|
237
|
+
if (!resources) {
|
|
238
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session resources are unavailable");
|
|
239
|
+
}
|
|
240
|
+
if (hasContent &&
|
|
241
|
+
request.content.some((part) => part.type === "input_image") &&
|
|
242
|
+
!request.clientMessageId) {
|
|
243
|
+
throw new MachineSessionServiceInputError("clientMessageId is required for image input");
|
|
244
|
+
}
|
|
245
|
+
prepared = await resources.prepareInput(sessionId, hasContent
|
|
246
|
+
? request.content
|
|
247
|
+
: [{ type: "input_text", text: request.message }], request.clientMessageId);
|
|
248
|
+
if (prepared.operationState === "injecting" ||
|
|
249
|
+
prepared.operationState === "outcome_unknown") {
|
|
250
|
+
throw new MachineSessionServiceFailure("outcome_unknown", "message delivery outcome is unknown");
|
|
251
|
+
}
|
|
252
|
+
if (prepared.operationState === "injected" ||
|
|
253
|
+
prepared.operationState === "mirrored") {
|
|
254
|
+
return { ok: true, injected: true };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
prepared = {
|
|
259
|
+
input: {
|
|
260
|
+
content: [{ type: "text", text: request.message }],
|
|
261
|
+
...(request.clientMessageId
|
|
262
|
+
? { clientMessageId: request.clientMessageId }
|
|
263
|
+
: {}),
|
|
264
|
+
},
|
|
265
|
+
title: request.message,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (consoleMeta && !consoleMeta.title) {
|
|
269
|
+
this.ports.registry.setTitle(sessionId, synthesizeSessionTitle(prepared.title));
|
|
270
|
+
}
|
|
271
|
+
const resources = this.ports.resources;
|
|
272
|
+
if (request.clientMessageId) {
|
|
273
|
+
await resources?.markMessageInjecting(sessionId, request.clientMessageId);
|
|
274
|
+
}
|
|
275
|
+
let execution;
|
|
276
|
+
try {
|
|
277
|
+
execution = await this.ensureLiveSession(sessionId, meta);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (request.clientMessageId) {
|
|
281
|
+
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error));
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
let outcome;
|
|
286
|
+
try {
|
|
287
|
+
outcome = await execution.runner.injectMessage(sessionId, hasMessage && !needsPreparedOperation ? request.message : prepared.input);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (request.clientMessageId) {
|
|
291
|
+
await Promise.resolve(resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, error instanceof Error ? error.message : String(error))).catch(() => undefined);
|
|
292
|
+
}
|
|
293
|
+
throw new MachineSessionServiceFailure("outcome_unknown", "live injection outcome is unknown");
|
|
294
|
+
}
|
|
295
|
+
if (outcome === "failed") {
|
|
296
|
+
if (request.clientMessageId) {
|
|
297
|
+
await resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
298
|
+
}
|
|
299
|
+
throw new MachineSessionServiceFailure("outcome_unknown", `live injection ${outcome}`);
|
|
300
|
+
}
|
|
301
|
+
if (outcome !== "injected") {
|
|
302
|
+
if (request.clientMessageId) {
|
|
303
|
+
await resources?.markMessageFailedNotStarted(sessionId, request.clientMessageId, `live injection ${outcome}`);
|
|
304
|
+
}
|
|
305
|
+
throw new MachineSessionServiceFailure("failed_precondition", `live injection ${outcome}`);
|
|
306
|
+
}
|
|
307
|
+
if (request.clientMessageId) {
|
|
308
|
+
try {
|
|
309
|
+
await resources?.markMessageInjected(sessionId, request.clientMessageId);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
await Promise.resolve(resources?.markMessageOutcomeUnknown(sessionId, request.clientMessageId, "message state could not be persisted after injection")).catch(() => undefined);
|
|
313
|
+
throw new MachineSessionServiceFailure("outcome_unknown", "message was injected but its delivery state could not be persisted");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { ok: true, injected: true };
|
|
317
|
+
}
|
|
318
|
+
resourcePolicy(sessionIdInput) {
|
|
319
|
+
const sessionId = this.requireResourceSession(sessionIdInput);
|
|
320
|
+
return this.requireResources().policy(sessionId);
|
|
321
|
+
}
|
|
322
|
+
async beginResourceUpload(input) {
|
|
323
|
+
const sessionId = this.requireResourceSession(input.sessionId);
|
|
324
|
+
return this.requireResources().beginUpload({ ...input, sessionId });
|
|
325
|
+
}
|
|
326
|
+
async writeResourceUploadChunk(input) {
|
|
327
|
+
const sessionId = this.requireResourceSession(input.sessionId);
|
|
328
|
+
return this.requireResources().writeUploadChunk({ ...input, sessionId });
|
|
329
|
+
}
|
|
330
|
+
async commitResourceUpload(input) {
|
|
331
|
+
const sessionId = this.requireResourceSession(input.sessionId);
|
|
332
|
+
return this.requireResources().commitUpload({ ...input, sessionId });
|
|
333
|
+
}
|
|
334
|
+
async getResource(input) {
|
|
335
|
+
const sessionId = this.requireResourceSession(input.sessionId);
|
|
336
|
+
return this.requireResources().getResource({ ...input, sessionId });
|
|
337
|
+
}
|
|
338
|
+
async readResource(input) {
|
|
339
|
+
const sessionId = this.requireResourceSession(input.sessionId);
|
|
340
|
+
return this.requireResources().readResource({ ...input, sessionId });
|
|
341
|
+
}
|
|
342
|
+
async deleteResource(input) {
|
|
343
|
+
const sessionId = this.requireResourceSession(input.sessionId);
|
|
344
|
+
return this.requireResources().deleteResource({ ...input, sessionId });
|
|
345
|
+
}
|
|
346
|
+
/** Persist the first task while login/onboarding owns the Provider TUI. The
|
|
347
|
+
* delivery worker starts the pane now, waits for a real native thread, then
|
|
348
|
+
* injects exactly once. `failed` is fenced as outcome_unknown and never retried. */
|
|
349
|
+
async enqueueMessage(sessionIdInput, messageInput) {
|
|
350
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
351
|
+
if (typeof messageInput !== "string" || messageInput.length === 0) {
|
|
352
|
+
throw new MachineSessionServiceInputError("message must be a non-empty string");
|
|
353
|
+
}
|
|
354
|
+
const meta = this.ports.registry.get(sessionId);
|
|
355
|
+
if (!meta)
|
|
356
|
+
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
357
|
+
const pending = this.ports.pendingMessages;
|
|
358
|
+
if (!pending) {
|
|
359
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Provider setup task queue is unavailable");
|
|
360
|
+
}
|
|
361
|
+
const inserted = await pending.put({
|
|
362
|
+
sessionId,
|
|
363
|
+
message: messageInput,
|
|
364
|
+
createdAt: new Date().toISOString(),
|
|
365
|
+
});
|
|
366
|
+
if (!inserted) {
|
|
367
|
+
throw new MachineSessionServiceFailure("failed_precondition", "This Session already has a task waiting for Provider setup");
|
|
368
|
+
}
|
|
369
|
+
if (meta.source === "console" && !meta.title) {
|
|
370
|
+
this.ports.registry.setTitle(sessionId, synthesizeSessionTitle(messageInput));
|
|
371
|
+
}
|
|
372
|
+
this.schedulePendingDelivery(sessionId);
|
|
373
|
+
// Best effort: delivery also starts/retries the pane, but awaiting this path
|
|
374
|
+
// makes the setup Terminal attachable as soon as the enqueue RPC returns.
|
|
375
|
+
await this.startTerminal(sessionId).catch(() => undefined);
|
|
376
|
+
return { ok: true, queued: true };
|
|
377
|
+
}
|
|
378
|
+
/** Explicitly restore the target daemon's live runner without starting a turn. */
|
|
379
|
+
async startTerminal(sessionIdInput) {
|
|
380
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
381
|
+
const request = await this.liveSessionRequest(sessionId);
|
|
382
|
+
const start = request.execution.runner.startLiveSession?.bind(request.execution.runner) ??
|
|
383
|
+
request.execution.runner.ensureLiveSession.bind(request.execution.runner);
|
|
384
|
+
const started = await start(sessionId, request.options);
|
|
385
|
+
if (!started) {
|
|
386
|
+
throw new MachineSessionServiceFailure("failed_precondition", `Provider terminal unavailable (${request.liveRuntime})`, request.execution.runner.lastLiveSessionError?.(sessionId));
|
|
387
|
+
}
|
|
388
|
+
return { ok: true };
|
|
389
|
+
}
|
|
390
|
+
async ensureLiveSession(sessionId, meta = this.ports.registry.get(sessionId)) {
|
|
391
|
+
const request = await this.liveSessionRequest(sessionId, meta);
|
|
392
|
+
const live = await request.execution.runner.ensureLiveSession(sessionId, request.options);
|
|
393
|
+
if (!live) {
|
|
394
|
+
throw new MachineSessionServiceFailure("failed_precondition", `live session unavailable (${request.liveRuntime})`, request.execution.runner.lastLiveSessionError?.(sessionId));
|
|
395
|
+
}
|
|
396
|
+
return request.execution;
|
|
397
|
+
}
|
|
398
|
+
async liveSessionRequest(sessionId, meta = this.ports.registry.get(sessionId)) {
|
|
399
|
+
const execution = this.requireExecution();
|
|
400
|
+
const consoleMeta = meta?.source === "console" ? meta : undefined;
|
|
401
|
+
const bound = consoleMeta ? null : ((await execution.sessionStore?.get(sessionId)) ?? null);
|
|
402
|
+
if (!meta && !bound) {
|
|
403
|
+
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
404
|
+
}
|
|
405
|
+
const liveRuntime = await execution.runtime.resolveRuntime({
|
|
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
|
+
}
|
|
422
|
+
const options = {
|
|
423
|
+
...(bound?.cwd
|
|
424
|
+
? { cwd: bound.cwd }
|
|
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 } : {}),
|
|
434
|
+
};
|
|
435
|
+
return { execution, liveRuntime, options };
|
|
436
|
+
}
|
|
437
|
+
async resolveInteraction(sessionIdInput, interactionIdInput, resolution) {
|
|
438
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
439
|
+
const interactionId = validOpaqueToken(interactionIdInput, "interactionId", MAX_SESSION_ID_CHARS);
|
|
440
|
+
return this.requireExecution().runner.resolveInteraction(sessionId, interactionId, structuredClone(resolution));
|
|
441
|
+
}
|
|
442
|
+
async delete(sessionIdInput) {
|
|
443
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
444
|
+
try {
|
|
445
|
+
await this.ports.lifecycle?.beforeDelete?.(sessionId);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session-owned resources could not be fenced before deletion", error instanceof Error ? error.message : String(error));
|
|
449
|
+
}
|
|
450
|
+
this.cancelledPendingDeliveries.add(sessionId);
|
|
451
|
+
this.ports.execution?.runner.stopRunner(sessionId);
|
|
452
|
+
this.ports.events.close?.(sessionId);
|
|
453
|
+
await this.ports.pendingMessages?.delete(sessionId);
|
|
454
|
+
await this.ports.resources?.deleteSession(sessionId);
|
|
455
|
+
this.ports.registry.remove(sessionId);
|
|
456
|
+
await this.ports.log.deleteSession(sessionId);
|
|
457
|
+
this.ports.runtimeState.remove?.(sessionId);
|
|
458
|
+
try {
|
|
459
|
+
await this.ports.lifecycle?.afterDelete?.(sessionId);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
throw new MachineSessionServiceFailure("outcome_unknown", "Session was deleted but resource finalization did not complete", error instanceof Error ? error.message : String(error));
|
|
463
|
+
}
|
|
464
|
+
return { ok: true };
|
|
465
|
+
}
|
|
466
|
+
requireAgents() {
|
|
467
|
+
if (!this.ports.agents) {
|
|
468
|
+
throw new MachineSessionServiceFailure("failed_precondition", "session agent options are unavailable");
|
|
469
|
+
}
|
|
470
|
+
return this.ports.agents;
|
|
471
|
+
}
|
|
472
|
+
requireResources() {
|
|
473
|
+
if (!this.ports.resources) {
|
|
474
|
+
throw new MachineSessionServiceFailure("failed_precondition", "Session resources are unavailable");
|
|
475
|
+
}
|
|
476
|
+
return this.ports.resources;
|
|
477
|
+
}
|
|
478
|
+
requireResourceSession(sessionIdInput) {
|
|
479
|
+
const sessionId = validOpaqueToken(sessionIdInput, "sessionId", MAX_SESSION_ID_CHARS);
|
|
480
|
+
if (!this.ports.registry.get(sessionId)) {
|
|
481
|
+
throw new MachineSessionServiceFailure("not_found", "session not found");
|
|
482
|
+
}
|
|
483
|
+
return sessionId;
|
|
484
|
+
}
|
|
485
|
+
async resumePendingDeliveries() {
|
|
486
|
+
const pending = this.ports.pendingMessages;
|
|
487
|
+
if (!pending)
|
|
488
|
+
return;
|
|
489
|
+
try {
|
|
490
|
+
for (const message of await pending.list()) {
|
|
491
|
+
if (message.state === "queued")
|
|
492
|
+
this.schedulePendingDelivery(message.sessionId);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// A later enqueue or daemon restart retries recovery; startup remains usable.
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
schedulePendingDelivery(sessionId) {
|
|
500
|
+
if (this.pendingDeliveries.has(sessionId))
|
|
501
|
+
return;
|
|
502
|
+
this.cancelledPendingDeliveries.delete(sessionId);
|
|
503
|
+
const delivery = this.deliverPendingMessage(sessionId)
|
|
504
|
+
.catch(() => undefined)
|
|
505
|
+
.finally(() => {
|
|
506
|
+
if (this.pendingDeliveries.get(sessionId) === delivery) {
|
|
507
|
+
this.pendingDeliveries.delete(sessionId);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
this.pendingDeliveries.set(sessionId, delivery);
|
|
511
|
+
}
|
|
512
|
+
async deliverPendingMessage(sessionId) {
|
|
513
|
+
const pendingStore = this.ports.pendingMessages;
|
|
514
|
+
if (!pendingStore)
|
|
515
|
+
return;
|
|
516
|
+
for (;;) {
|
|
517
|
+
if (this.cancelledPendingDeliveries.has(sessionId))
|
|
518
|
+
return;
|
|
519
|
+
const pending = await pendingStore.get(sessionId);
|
|
520
|
+
if (!pending || pending.state !== "queued")
|
|
521
|
+
return;
|
|
522
|
+
let injectionAttempted = false;
|
|
523
|
+
try {
|
|
524
|
+
const execution = await this.ensureLiveSession(sessionId);
|
|
525
|
+
if (this.cancelledPendingDeliveries.has(sessionId))
|
|
526
|
+
return;
|
|
527
|
+
const current = await pendingStore.get(sessionId);
|
|
528
|
+
if (!current ||
|
|
529
|
+
current.state !== "queued" ||
|
|
530
|
+
current.createdAt !== pending.createdAt ||
|
|
531
|
+
current.message !== pending.message)
|
|
532
|
+
return;
|
|
533
|
+
injectionAttempted = true;
|
|
534
|
+
const outcome = await execution.runner.injectMessage(sessionId, pending.message);
|
|
535
|
+
if (outcome === "injected") {
|
|
536
|
+
await Promise.resolve(pendingStore.delete(sessionId)).catch(async () => {
|
|
537
|
+
await Promise.resolve(pendingStore.markOutcomeUnknown(sessionId)).catch(() => undefined);
|
|
538
|
+
});
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (outcome === "failed") {
|
|
542
|
+
await pendingStore.markOutcomeUnknown(sessionId);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (injectionAttempted) {
|
|
548
|
+
await Promise.resolve(pendingStore.markOutcomeUnknown(sessionId)).catch(() => undefined);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (error instanceof MachineSessionServiceFailure && error.code === "not_found") {
|
|
552
|
+
await pendingStore.delete(sessionId);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
await retryDelay(this.pendingMessageRetryMs);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
runtimeReadiness(runtime, cache) {
|
|
560
|
+
const cached = cache?.get(runtime);
|
|
561
|
+
if (cached)
|
|
562
|
+
return cached;
|
|
563
|
+
const pending = this.probeRuntimeReadiness(runtime);
|
|
564
|
+
cache?.set(runtime, pending);
|
|
565
|
+
return pending;
|
|
566
|
+
}
|
|
567
|
+
async probeRuntimeReadiness(runtime) {
|
|
568
|
+
const runner = this.ports.execution?.runner;
|
|
569
|
+
if (!runner?.getStatus) {
|
|
570
|
+
return { ready: false, reason: "Runtime readiness could not be checked." };
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const status = await runner.getStatus({ runtime });
|
|
574
|
+
const ready = status.codex_available &&
|
|
575
|
+
status.logged_in &&
|
|
576
|
+
status.session_store_writable &&
|
|
577
|
+
status.codex_sessions_accessible;
|
|
578
|
+
if (ready)
|
|
579
|
+
return { ready: true };
|
|
580
|
+
return {
|
|
581
|
+
ready: false,
|
|
582
|
+
reason: boundedLaunchReason(runtimeSetupReason(runtime, status)),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return { ready: false, reason: "Runtime readiness check failed." };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
requireExecution() {
|
|
590
|
+
if (!this.ports.execution) {
|
|
591
|
+
throw new MachineSessionServiceFailure("failed_precondition", "session execution is unavailable");
|
|
592
|
+
}
|
|
593
|
+
return this.ports.execution;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/** One-line title derived from the first user message; no model call. */
|
|
597
|
+
export function synthesizeSessionTitle(message, limit = 60) {
|
|
598
|
+
const collapsed = message.replace(/\s+/g, " ").trim();
|
|
599
|
+
if (collapsed.length <= limit)
|
|
600
|
+
return collapsed;
|
|
601
|
+
return `${collapsed.slice(0, limit - 1).trimEnd()}…`;
|
|
602
|
+
}
|
|
603
|
+
/** Keep target-owned setup diagnostics comfortably inside the wire DTO bound. */
|
|
604
|
+
function boundedLaunchReason(reason) {
|
|
605
|
+
const normalized = reason.replace(/\s+/g, " ").trim();
|
|
606
|
+
return [...normalized].slice(0, 512).join("") || "Runtime needs setup.";
|
|
607
|
+
}
|
|
608
|
+
function runtimeSetupReason(runtime, status) {
|
|
609
|
+
const name = getRuntimeProfile(runtime).displayName;
|
|
610
|
+
if (!status.codex_available)
|
|
611
|
+
return `${name} CLI is unavailable.`;
|
|
612
|
+
if (!status.logged_in)
|
|
613
|
+
return `${name} is not authenticated.`;
|
|
614
|
+
if (!status.session_store_writable)
|
|
615
|
+
return "The Session store is not writable.";
|
|
616
|
+
if (!status.codex_sessions_accessible)
|
|
617
|
+
return `${name} Session storage is not accessible.`;
|
|
618
|
+
return status.issues.find((issue) => issue.trim().length > 0) ??
|
|
619
|
+
`${name} needs setup before it can start Sessions.`;
|
|
620
|
+
}
|
|
621
|
+
function retryDelay(ms) {
|
|
622
|
+
return new Promise((resolve) => {
|
|
623
|
+
const timer = setTimeout(resolve, ms);
|
|
624
|
+
timer.unref?.();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function listPage(sessions, hasMore, directoryTruncated) {
|
|
628
|
+
return {
|
|
629
|
+
sessions,
|
|
630
|
+
hasMore,
|
|
631
|
+
...(hasMore && sessions.length > 0 ? { nextCursor: encodeListCursor(sessions.at(-1)) } : {}),
|
|
632
|
+
directoryTruncated,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function isAfterListCursor(summary, cursor) {
|
|
636
|
+
const updatedAt = summaryUpdatedAt(summary);
|
|
637
|
+
return updatedAt < cursor.updatedAt || (updatedAt === cursor.updatedAt && summary.id > cursor.id);
|
|
638
|
+
}
|
|
639
|
+
function encodeListCursor(summary) {
|
|
640
|
+
const cursor = {
|
|
641
|
+
v: 1,
|
|
642
|
+
updatedAt: summaryUpdatedAt(summary),
|
|
643
|
+
id: summary.id,
|
|
644
|
+
};
|
|
645
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
646
|
+
}
|
|
647
|
+
function decodeListCursor(value) {
|
|
648
|
+
if (value.length === 0 ||
|
|
649
|
+
value.length > REMOTE_RUNTIME_SESSION_MAX_CURSOR_CHARS ||
|
|
650
|
+
!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
651
|
+
throw new MachineSessionServiceInputError("cursor is invalid");
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const bytes = Buffer.from(value, "base64url");
|
|
655
|
+
if (bytes.length === 0 || bytes.toString("base64url") !== value)
|
|
656
|
+
throw new Error();
|
|
657
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
658
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
659
|
+
throw new Error();
|
|
660
|
+
const record = parsed;
|
|
661
|
+
if (Object.keys(record).length !== 3 ||
|
|
662
|
+
!("v" in record) ||
|
|
663
|
+
!("updatedAt" in record) ||
|
|
664
|
+
!("id" in record)) {
|
|
665
|
+
throw new Error();
|
|
666
|
+
}
|
|
667
|
+
if (record.v !== 1) {
|
|
668
|
+
throw new MachineSessionServiceInputError("cursor version is not supported");
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
v: 1,
|
|
672
|
+
updatedAt: validTimestamp(record.updatedAt, "cursor.updatedAt"),
|
|
673
|
+
id: validOpaqueToken(record.id, "cursor.id", MAX_SESSION_ID_CHARS),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (error instanceof MachineSessionServiceInputError)
|
|
678
|
+
throw error;
|
|
679
|
+
throw new MachineSessionServiceInputError("cursor is invalid");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function snapshotPage(sessionId, items, runtime, hasMore) {
|
|
683
|
+
return {
|
|
684
|
+
sessionId,
|
|
685
|
+
items,
|
|
686
|
+
runtime,
|
|
687
|
+
hasMore,
|
|
688
|
+
...(hasMore && items.length > 0 ? { nextAfterId: items.at(-1).id } : {}),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function sessionSummary(id, meta, log, runtimeState, pendingState) {
|
|
692
|
+
const createdAt = meta?.createdAt ?? timestampFromEpoch(log?.createdAt, "createdAt");
|
|
693
|
+
const updatedAt = log
|
|
694
|
+
? timestampFromEpoch(log.updatedAt, "updatedAt")
|
|
695
|
+
: (meta?.updatedAt ?? createdAt);
|
|
696
|
+
return {
|
|
697
|
+
id,
|
|
698
|
+
...(meta?.provider === undefined ? {} : { provider: meta.provider }),
|
|
699
|
+
...(meta?.agent === undefined ? {} : { agent: meta.agent }),
|
|
700
|
+
...(meta?.title === undefined ? {} : { title: normalizeSessionTitle(meta.title) }),
|
|
701
|
+
status: runtimeState.snapshot(id).status,
|
|
702
|
+
...(pendingState === undefined
|
|
703
|
+
? {}
|
|
704
|
+
: {
|
|
705
|
+
setupStatus: pendingState === "queued"
|
|
706
|
+
? "waiting_for_provider"
|
|
707
|
+
: "delivery_unknown",
|
|
708
|
+
}),
|
|
709
|
+
createdAt: validTimestamp(createdAt, "createdAt"),
|
|
710
|
+
source: meta?.source ?? "unknown",
|
|
711
|
+
updatedAt: validTimestamp(updatedAt, "updatedAt"),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function compareMetaRecency(left, right) {
|
|
715
|
+
const leftTime = validTimestamp(left.updatedAt ?? left.createdAt, "updatedAt");
|
|
716
|
+
const rightTime = validTimestamp(right.updatedAt ?? right.createdAt, "updatedAt");
|
|
717
|
+
return rightTime.localeCompare(leftTime) || left.id.localeCompare(right.id);
|
|
718
|
+
}
|
|
719
|
+
function compareSummaryRecency(left, right) {
|
|
720
|
+
return summaryUpdatedAt(right).localeCompare(summaryUpdatedAt(left)) || left.id.localeCompare(right.id);
|
|
721
|
+
}
|
|
722
|
+
function summaryUpdatedAt(summary) {
|
|
723
|
+
return summary.updatedAt ?? summary.createdAt;
|
|
724
|
+
}
|
|
725
|
+
function cancellableWatch(sessionId, iterator, signal) {
|
|
726
|
+
let cancelled = false;
|
|
727
|
+
let cancellationResolve;
|
|
728
|
+
const cancellation = new Promise((resolve) => {
|
|
729
|
+
cancellationResolve = resolve;
|
|
730
|
+
});
|
|
731
|
+
let cancelPromise;
|
|
732
|
+
const onAbort = () => {
|
|
733
|
+
void cancel().catch(() => undefined);
|
|
734
|
+
};
|
|
735
|
+
const cancel = () => {
|
|
736
|
+
if (cancelPromise)
|
|
737
|
+
return cancelPromise;
|
|
738
|
+
cancelled = true;
|
|
739
|
+
cancellationResolve();
|
|
740
|
+
signal?.removeEventListener("abort", onAbort);
|
|
741
|
+
cancelPromise = Promise.resolve(iterator.return?.()).then(() => undefined);
|
|
742
|
+
return cancelPromise;
|
|
743
|
+
};
|
|
744
|
+
if (signal?.aborted)
|
|
745
|
+
void cancel().catch(() => undefined);
|
|
746
|
+
else
|
|
747
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
748
|
+
const consumer = {
|
|
749
|
+
async next() {
|
|
750
|
+
if (cancelled)
|
|
751
|
+
return { value: undefined, done: true };
|
|
752
|
+
try {
|
|
753
|
+
const result = await Promise.race([
|
|
754
|
+
iterator.next(),
|
|
755
|
+
cancellation.then(() => ({ value: undefined, done: true })),
|
|
756
|
+
]);
|
|
757
|
+
if (result.done)
|
|
758
|
+
await cancel();
|
|
759
|
+
return result;
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
await cancel().catch(() => undefined);
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
async return() {
|
|
767
|
+
await cancel();
|
|
768
|
+
return { value: undefined, done: true };
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
const watch = {
|
|
772
|
+
sessionId,
|
|
773
|
+
cancel,
|
|
774
|
+
[Symbol.asyncIterator]() {
|
|
775
|
+
return consumer;
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
return watch;
|
|
779
|
+
}
|
|
780
|
+
function cloneRuntimeSnapshot(snapshot) {
|
|
781
|
+
return {
|
|
782
|
+
status: snapshot.status,
|
|
783
|
+
activeResponseIds: [...snapshot.activeResponseIds],
|
|
784
|
+
pendingInteractions: snapshot.pendingInteractions.map((pending) => ({
|
|
785
|
+
responseId: pending.responseId,
|
|
786
|
+
interaction: structuredClone(pending.interaction),
|
|
787
|
+
})),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function requestLimit(value, fallback, maximum, field) {
|
|
791
|
+
return value === undefined ? Math.min(fallback, maximum) : boundedInteger(value, 1, maximum, field);
|
|
792
|
+
}
|
|
793
|
+
function boundedInteger(value, minimum, maximum, field) {
|
|
794
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
795
|
+
throw new MachineSessionServiceInputError(`${field} must be an integer between ${minimum} and ${maximum}`);
|
|
796
|
+
}
|
|
797
|
+
return value;
|
|
798
|
+
}
|
|
799
|
+
function validOpaqueToken(value, field, maximum) {
|
|
800
|
+
if (typeof value !== "string" ||
|
|
801
|
+
value.length === 0 ||
|
|
802
|
+
value.length > maximum ||
|
|
803
|
+
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
804
|
+
throw new MachineSessionServiceInputError(`${field} must be a non-empty opaque string of at most ${maximum} characters`);
|
|
805
|
+
}
|
|
806
|
+
return value;
|
|
807
|
+
}
|
|
808
|
+
function validTimestamp(value, field) {
|
|
809
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
810
|
+
throw new MachineSessionServiceInputError(`${field} must be a valid timestamp`);
|
|
811
|
+
}
|
|
812
|
+
return new Date(value).toISOString();
|
|
813
|
+
}
|
|
814
|
+
function timestampFromEpoch(value, field) {
|
|
815
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
816
|
+
throw new MachineSessionServiceInputError(`${field} is unavailable`);
|
|
817
|
+
}
|
|
818
|
+
return new Date(value).toISOString();
|
|
819
|
+
}
|
|
820
|
+
function jsonByteLength(value) {
|
|
821
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
822
|
+
}
|