@averyyy/pi-server 0.84.2-piclient.1 → 0.84.2-piclient.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/error-codes.d.ts +27 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +36 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/request-chunks.d.ts.map +1 -1
- package/dist/request-chunks.js +0 -1
- package/dist/request-chunks.js.map +1 -1
- package/dist/server.d.ts +0 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +179 -80
- package/dist/server.js.map +1 -1
- package/dist/session-persistence.d.ts.map +1 -1
- package/dist/session-persistence.js +42 -15
- package/dist/session-persistence.js.map +1 -1
- package/dist/session-store.d.ts +0 -3
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +11 -31
- package/dist/session-store.js.map +1 -1
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -4,11 +4,12 @@ import { compactLegacy, DEFAULT_COMPACTION_SETTINGS, prepareLegacyCompaction, }
|
|
|
4
4
|
import { createModels, createProvider, } from "@earendil-works/pi-ai";
|
|
5
5
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
|
+
import { PiServerError, PiServerErrorCode } from "./error-codes.js";
|
|
7
8
|
import { encodeErrorEvent, encodeProxyEvent } from "./event-encoding.js";
|
|
8
9
|
import { ReceiveUploadError, receiveUpload } from "./receive-upload.js";
|
|
9
10
|
import { CHUNK_ENDPOINT, receiveRequestChunk } from "./request-chunks.js";
|
|
10
11
|
import { deletePersistedSession, loadPersistedSessions, savePersistedSession } from "./session-persistence.js";
|
|
11
|
-
import { appendCompactionEntry, appendMessages, appendSessionEntries, deleteSession as deleteSessionFromStore,
|
|
12
|
+
import { appendCompactionEntry, appendMessages, appendSessionEntries, deleteSession as deleteSessionFromStore, getOrCreateSession, getSession, getSessionBranch, listSessions, replaceMessages, replaceSessionTree, setStaticContext, switchSessionLeaf, } from "./session-store.js";
|
|
12
13
|
export { loadConfig } from "./config.js";
|
|
13
14
|
const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
14
15
|
const PI_SERVER_VERSION = packageMetadata.version;
|
|
@@ -34,6 +35,7 @@ function createRequestModels(model, options) {
|
|
|
34
35
|
const STREAM_HEARTBEAT = ": keep-alive\n\n";
|
|
35
36
|
const JSON_HEARTBEAT = " \n";
|
|
36
37
|
const STREAM_HEARTBEAT_INTERVAL_MS = 25_000;
|
|
38
|
+
const STREAM_RUN_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
37
39
|
const streamRuns = new Map();
|
|
38
40
|
function readBody(req) {
|
|
39
41
|
return new Promise((resolve, reject) => {
|
|
@@ -48,6 +50,14 @@ function sendJson(res, status, body) {
|
|
|
48
50
|
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) });
|
|
49
51
|
res.end(data);
|
|
50
52
|
}
|
|
53
|
+
function sendError(res, status, message, code, details) {
|
|
54
|
+
const body = { error: message };
|
|
55
|
+
if (code)
|
|
56
|
+
body.code = code;
|
|
57
|
+
if (details)
|
|
58
|
+
body.details = details;
|
|
59
|
+
sendJson(res, status, body);
|
|
60
|
+
}
|
|
51
61
|
function logRequestError(req, error) {
|
|
52
62
|
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
53
63
|
console.error(`${req.method ?? "UNKNOWN"} ${req.url ?? "/"} failed: ${message}`);
|
|
@@ -83,27 +93,58 @@ function getStreamRun(sessionId, runId) {
|
|
|
83
93
|
}
|
|
84
94
|
function startStreamRun(sessionId, runId) {
|
|
85
95
|
const existing = getStreamRun(sessionId, runId);
|
|
86
|
-
if (existing?.status === "completed"
|
|
96
|
+
if (existing?.status === "completed")
|
|
87
97
|
return existing;
|
|
88
98
|
const now = Date.now();
|
|
89
99
|
const run = existing ?? {
|
|
90
100
|
sessionId,
|
|
91
101
|
runId,
|
|
92
102
|
status: "running",
|
|
93
|
-
events: [],
|
|
94
103
|
createdAt: now,
|
|
95
104
|
updatedAt: now,
|
|
96
105
|
};
|
|
106
|
+
if (existing) {
|
|
107
|
+
run.message = undefined;
|
|
108
|
+
run.errorMessage = undefined;
|
|
109
|
+
}
|
|
97
110
|
run.status = "running";
|
|
98
111
|
run.updatedAt = now;
|
|
99
112
|
streamRuns.set(runKey(sessionId, runId), run);
|
|
100
113
|
return run;
|
|
101
114
|
}
|
|
102
|
-
function
|
|
103
|
-
if (!run)
|
|
104
|
-
return;
|
|
105
|
-
run.
|
|
106
|
-
|
|
115
|
+
function replayStreamRun(run) {
|
|
116
|
+
if (!run.message)
|
|
117
|
+
return [];
|
|
118
|
+
if (run.message.stopReason !== "stop" &&
|
|
119
|
+
run.message.stopReason !== "length" &&
|
|
120
|
+
run.message.stopReason !== "toolUse" &&
|
|
121
|
+
run.message.stopReason !== "deferred") {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const events = [{ type: "start" }];
|
|
125
|
+
for (const [contentIndex, content] of run.message.content.entries()) {
|
|
126
|
+
switch (content.type) {
|
|
127
|
+
case "text":
|
|
128
|
+
events.push({ type: "text_start", contentIndex }, { type: "text_delta", contentIndex, delta: content.text }, { type: "text_end", contentIndex, contentSignature: content.textSignature });
|
|
129
|
+
break;
|
|
130
|
+
case "thinking":
|
|
131
|
+
events.push({ type: "thinking_start", contentIndex }, { type: "thinking_delta", contentIndex, delta: content.thinking }, { type: "thinking_end", contentIndex, contentSignature: content.thinkingSignature });
|
|
132
|
+
break;
|
|
133
|
+
case "toolCall":
|
|
134
|
+
events.push({ type: "toolcall_start", contentIndex, id: content.id, toolName: content.name }, { type: "toolcall_delta", contentIndex, delta: JSON.stringify(content.arguments) }, { type: "toolcall_end", contentIndex, toolCall: content });
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
events.push({
|
|
141
|
+
type: "done",
|
|
142
|
+
reason: run.message.stopReason,
|
|
143
|
+
usage: run.message.usage,
|
|
144
|
+
deferred: run.message.deferred,
|
|
145
|
+
message: run.message,
|
|
146
|
+
});
|
|
147
|
+
return events;
|
|
107
148
|
}
|
|
108
149
|
function completeStreamRun(run, message) {
|
|
109
150
|
if (!run)
|
|
@@ -120,6 +161,40 @@ function failStreamRun(run, errorMessage) {
|
|
|
120
161
|
run.errorMessage = errorMessage;
|
|
121
162
|
run.updatedAt = Date.now();
|
|
122
163
|
}
|
|
164
|
+
function touchStreamRun(run) {
|
|
165
|
+
if (run)
|
|
166
|
+
run.updatedAt = Date.now();
|
|
167
|
+
}
|
|
168
|
+
function writeStreamEvent(res, event) {
|
|
169
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
170
|
+
res.write(encodeProxyEvent(event));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function writeStreamError(res, message) {
|
|
174
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
175
|
+
res.write(encodeErrorEvent(message));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function endStreamResponse(res) {
|
|
179
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
180
|
+
res.end();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function cleanupExpiredStreamRuns(nowMs) {
|
|
184
|
+
for (const [key, run] of streamRuns) {
|
|
185
|
+
if (nowMs - run.updatedAt > STREAM_RUN_TTL_MS) {
|
|
186
|
+
streamRuns.delete(key);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function deleteStreamRunsForSession(sessionId) {
|
|
191
|
+
for (const [key, run] of streamRuns) {
|
|
192
|
+
if (run.sessionId === sessionId)
|
|
193
|
+
streamRuns.delete(key);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const streamRunCleanupTimer = setInterval(() => cleanupExpiredStreamRuns(Date.now()), STREAM_RUN_TTL_MS / 4);
|
|
197
|
+
streamRunCleanupTimer.unref();
|
|
123
198
|
function sessionHistoryFullResponseBody(session, baseMessageCount) {
|
|
124
199
|
return {
|
|
125
200
|
sessionId: session.sessionId,
|
|
@@ -156,12 +231,9 @@ function sessionTreePatchResponseBody(session, baseMessageCount, entriesFrom, ba
|
|
|
156
231
|
},
|
|
157
232
|
};
|
|
158
233
|
}
|
|
159
|
-
export function resolveStreamOptions(_config, model, body) {
|
|
160
|
-
return { model, options: { ...(body.options ?? {}) } };
|
|
161
|
-
}
|
|
162
234
|
function handleSessionInit(config, body, res) {
|
|
163
235
|
if (!body.sessionId) {
|
|
164
|
-
|
|
236
|
+
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
165
237
|
return;
|
|
166
238
|
}
|
|
167
239
|
if (body.staticContext) {
|
|
@@ -176,11 +248,11 @@ function handleSessionInit(config, body, res) {
|
|
|
176
248
|
}
|
|
177
249
|
function handleSessionUpdate(config, body, res) {
|
|
178
250
|
if (!body.sessionId) {
|
|
179
|
-
|
|
251
|
+
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
180
252
|
return;
|
|
181
253
|
}
|
|
182
254
|
if (!body.staticContext) {
|
|
183
|
-
|
|
255
|
+
sendError(res, 400, "staticContext is required for update", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
184
256
|
return;
|
|
185
257
|
}
|
|
186
258
|
setStaticContext(body.sessionId, body.staticContext);
|
|
@@ -190,11 +262,11 @@ function handleSessionUpdate(config, body, res) {
|
|
|
190
262
|
}
|
|
191
263
|
function handleSessionSync(config, body, res) {
|
|
192
264
|
if (!body.sessionId) {
|
|
193
|
-
|
|
265
|
+
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
194
266
|
return;
|
|
195
267
|
}
|
|
196
268
|
if (!Array.isArray(body.messages)) {
|
|
197
|
-
|
|
269
|
+
sendError(res, 400, "messages is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
198
270
|
return;
|
|
199
271
|
}
|
|
200
272
|
if (body.staticContext) {
|
|
@@ -206,11 +278,11 @@ function handleSessionSync(config, body, res) {
|
|
|
206
278
|
}
|
|
207
279
|
function handleSessionAppend(config, body, res) {
|
|
208
280
|
if (!body.sessionId) {
|
|
209
|
-
|
|
281
|
+
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
210
282
|
return;
|
|
211
283
|
}
|
|
212
284
|
if (!Array.isArray(body.messages)) {
|
|
213
|
-
|
|
285
|
+
sendError(res, 400, "messages is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
214
286
|
return;
|
|
215
287
|
}
|
|
216
288
|
if (body.staticContext) {
|
|
@@ -222,11 +294,11 @@ function handleSessionAppend(config, body, res) {
|
|
|
222
294
|
}
|
|
223
295
|
function handleSessionTreeSync(config, body, res) {
|
|
224
296
|
if (!body.sessionId) {
|
|
225
|
-
|
|
297
|
+
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
226
298
|
return;
|
|
227
299
|
}
|
|
228
300
|
if (!Array.isArray(body.entries)) {
|
|
229
|
-
|
|
301
|
+
sendError(res, 400, "entries is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
230
302
|
return;
|
|
231
303
|
}
|
|
232
304
|
if (body.staticContext) {
|
|
@@ -238,28 +310,50 @@ function handleSessionTreeSync(config, body, res) {
|
|
|
238
310
|
}
|
|
239
311
|
function handleSessionTreeAppend(config, body, res) {
|
|
240
312
|
if (!body.sessionId) {
|
|
241
|
-
|
|
313
|
+
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
242
314
|
return;
|
|
243
315
|
}
|
|
244
316
|
if (!Array.isArray(body.entries)) {
|
|
245
|
-
|
|
317
|
+
sendError(res, 400, "entries is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
246
318
|
return;
|
|
247
319
|
}
|
|
248
|
-
|
|
249
|
-
|
|
320
|
+
try {
|
|
321
|
+
if (body.staticContext) {
|
|
322
|
+
setStaticContext(body.sessionId, body.staticContext);
|
|
323
|
+
}
|
|
324
|
+
const session = appendSessionEntries(body.sessionId, body.entries, body.leafId ?? null);
|
|
325
|
+
persistSession(config, session);
|
|
326
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error instanceof PiServerError) {
|
|
330
|
+
sendError(res, 400, error.message, error.code, error.details);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
334
|
+
sendError(res, 500, message, PiServerErrorCode.INTERNAL_ERROR);
|
|
335
|
+
}
|
|
250
336
|
}
|
|
251
|
-
const session = appendSessionEntries(body.sessionId, body.entries, body.leafId ?? null);
|
|
252
|
-
persistSession(config, session);
|
|
253
|
-
sendJson(res, 200, sessionResponseBody(session));
|
|
254
337
|
}
|
|
255
338
|
function handleSessionTreeSwitch(config, body, res) {
|
|
256
339
|
if (!body.sessionId) {
|
|
257
|
-
|
|
340
|
+
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
258
341
|
return;
|
|
259
342
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
343
|
+
try {
|
|
344
|
+
const session = switchSessionLeaf(body.sessionId, body.leafId ?? null);
|
|
345
|
+
persistSession(config, session);
|
|
346
|
+
sendJson(res, 200, sessionResponseBody(session));
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (error instanceof PiServerError) {
|
|
350
|
+
sendError(res, 400, error.message, error.code, error.details);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
354
|
+
sendError(res, 500, message, PiServerErrorCode.INTERNAL_ERROR);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
263
357
|
}
|
|
264
358
|
function prepareSessionCompact(body) {
|
|
265
359
|
if (!body.sessionId) {
|
|
@@ -270,7 +364,7 @@ function prepareSessionCompact(body) {
|
|
|
270
364
|
}
|
|
271
365
|
const session = getSession(body.sessionId);
|
|
272
366
|
if (!session) {
|
|
273
|
-
return { status: 404, body: { error: "session not found" } };
|
|
367
|
+
return { status: 404, body: { error: "session not found", code: PiServerErrorCode.SESSION_NOT_FOUND } };
|
|
274
368
|
}
|
|
275
369
|
const entries = getSessionBranch(session);
|
|
276
370
|
const preparationResult = prepareLegacyCompaction(entries, body.settings ?? DEFAULT_COMPACTION_SETTINGS, body.preparation);
|
|
@@ -290,7 +384,7 @@ function prepareSessionCompact(body) {
|
|
|
290
384
|
async function completeSessionCompact(config, body, prepared) {
|
|
291
385
|
const result = await compactLegacy(prepared.preparation, createRequestModels(body.model, prepared.options), body.model, body.customInstructions, undefined, prepared.options.reasoning);
|
|
292
386
|
if (!result.ok) {
|
|
293
|
-
return { status: 500, body: { error: result.error.message } };
|
|
387
|
+
return { status: 500, body: { error: result.error.message, code: PiServerErrorCode.INTERNAL_ERROR } };
|
|
294
388
|
}
|
|
295
389
|
const baseTreeHash = prepared.session.treeHash;
|
|
296
390
|
const baseEntryCount = prepared.session.entries.length;
|
|
@@ -399,23 +493,10 @@ async function handleSessionCompact(config, body, res) {
|
|
|
399
493
|
}
|
|
400
494
|
await handleSessionCompactJsonStream(config, body, prepared, res);
|
|
401
495
|
}
|
|
402
|
-
function handleDropLastAssistantError(config, body, res) {
|
|
403
|
-
if (!body.sessionId) {
|
|
404
|
-
sendJson(res, 400, { error: "sessionId is required" });
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const dropped = dropLastAssistantError(body.sessionId);
|
|
408
|
-
const session = getSession(body.sessionId);
|
|
409
|
-
if (session) {
|
|
410
|
-
persistSession(config, session);
|
|
411
|
-
}
|
|
412
|
-
const messageCount = session?.messages.length ?? 0;
|
|
413
|
-
sendJson(res, 200, { success: true, dropped, messageCount });
|
|
414
|
-
}
|
|
415
496
|
function handleSessionHistory(sessionId, from, entriesFrom, revision, baseTreeHash, res) {
|
|
416
497
|
const session = getSession(sessionId);
|
|
417
498
|
if (!session) {
|
|
418
|
-
|
|
499
|
+
sendError(res, 404, "session not found", PiServerErrorCode.SESSION_NOT_FOUND);
|
|
419
500
|
return;
|
|
420
501
|
}
|
|
421
502
|
const baseMessageCount = from ?? 0;
|
|
@@ -431,7 +512,7 @@ function handleSessionHistory(sessionId, from, entriesFrom, revision, baseTreeHa
|
|
|
431
512
|
function handleSessionRun(sessionId, runId, res) {
|
|
432
513
|
const run = getStreamRun(sessionId, runId);
|
|
433
514
|
if (!run) {
|
|
434
|
-
|
|
515
|
+
sendError(res, 404, "run not found", PiServerErrorCode.RUN_NOT_FOUND);
|
|
435
516
|
return;
|
|
436
517
|
}
|
|
437
518
|
sendJson(res, 200, run);
|
|
@@ -446,33 +527,53 @@ export function buildStreamContext(session, body) {
|
|
|
446
527
|
}
|
|
447
528
|
function handleStream(config, body, res) {
|
|
448
529
|
if (!body.sessionId) {
|
|
449
|
-
|
|
530
|
+
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
450
531
|
return;
|
|
451
532
|
}
|
|
452
533
|
if (!body.model) {
|
|
453
|
-
|
|
534
|
+
sendError(res, 400, "model is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
454
535
|
return;
|
|
455
536
|
}
|
|
537
|
+
cleanupExpiredStreamRuns(Date.now());
|
|
456
538
|
const session = getOrCreateSession(body.sessionId);
|
|
457
539
|
if (body.staticContext) {
|
|
458
540
|
setStaticContext(body.sessionId, body.staticContext);
|
|
459
541
|
persistSession(config, session);
|
|
460
542
|
}
|
|
461
543
|
if (!session.staticContext && !body.staticContext) {
|
|
462
|
-
|
|
544
|
+
sendError(res, 400, "Session has no static context. Initialize with /api/session/init first.", PiServerErrorCode.SESSION_NO_STATIC_CONTEXT);
|
|
463
545
|
return;
|
|
464
546
|
}
|
|
465
547
|
if (body.ephemeralMessages !== undefined && !Array.isArray(body.ephemeralMessages)) {
|
|
466
|
-
|
|
548
|
+
sendError(res, 400, "ephemeralMessages must be an array", PiServerErrorCode.INVALID_REQUEST);
|
|
467
549
|
return;
|
|
468
550
|
}
|
|
469
551
|
if (body.contextOverlay !== undefined && !Array.isArray(body.contextOverlay)) {
|
|
470
|
-
|
|
552
|
+
sendError(res, 400, "contextOverlay must be an array", PiServerErrorCode.INVALID_REQUEST);
|
|
471
553
|
return;
|
|
472
554
|
}
|
|
473
555
|
const context = buildStreamContext(session, body);
|
|
474
556
|
const existingRun = body.runId ? getStreamRun(body.sessionId, body.runId) : undefined;
|
|
475
|
-
const
|
|
557
|
+
const resolvedModel = body.model;
|
|
558
|
+
const streamOptions = { ...(body.options ?? {}) };
|
|
559
|
+
if (existingRun?.status === "completed") {
|
|
560
|
+
res.writeHead(200, {
|
|
561
|
+
"Content-Type": "text/event-stream",
|
|
562
|
+
"Cache-Control": "no-cache",
|
|
563
|
+
Connection: "keep-alive",
|
|
564
|
+
});
|
|
565
|
+
res.flushHeaders();
|
|
566
|
+
res.write(STREAM_HEARTBEAT);
|
|
567
|
+
for (const event of replayStreamRun(existingRun)) {
|
|
568
|
+
writeStreamEvent(res, event);
|
|
569
|
+
}
|
|
570
|
+
endStreamResponse(res);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (existingRun?.status === "running") {
|
|
574
|
+
sendError(res, 409, "A stream with this runId is already in progress", PiServerErrorCode.RUN_IN_PROGRESS);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
476
577
|
res.writeHead(200, {
|
|
477
578
|
"Content-Type": "text/event-stream",
|
|
478
579
|
"Cache-Control": "no-cache",
|
|
@@ -480,16 +581,10 @@ function handleStream(config, body, res) {
|
|
|
480
581
|
});
|
|
481
582
|
res.flushHeaders();
|
|
482
583
|
res.write(STREAM_HEARTBEAT);
|
|
483
|
-
if (existingRun?.status === "completed") {
|
|
484
|
-
for (const event of existingRun.events) {
|
|
485
|
-
res.write(encodeProxyEvent(event));
|
|
486
|
-
}
|
|
487
|
-
res.end();
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
584
|
const run = body.runId ? startStreamRun(body.sessionId, body.runId) : undefined;
|
|
491
585
|
const heartbeat = setInterval(() => {
|
|
492
|
-
|
|
586
|
+
touchStreamRun(run);
|
|
587
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
493
588
|
res.write(STREAM_HEARTBEAT);
|
|
494
589
|
}
|
|
495
590
|
}, STREAM_HEARTBEAT_INTERVAL_MS);
|
|
@@ -502,8 +597,8 @@ function handleStream(config, body, res) {
|
|
|
502
597
|
clearInterval(heartbeat);
|
|
503
598
|
const message = err instanceof Error ? err.message : String(err);
|
|
504
599
|
failStreamRun(run, message);
|
|
505
|
-
res
|
|
506
|
-
res
|
|
600
|
+
writeStreamError(res, message);
|
|
601
|
+
endStreamResponse(res);
|
|
507
602
|
return;
|
|
508
603
|
}
|
|
509
604
|
(async () => {
|
|
@@ -511,8 +606,8 @@ function handleStream(config, body, res) {
|
|
|
511
606
|
for await (const event of stream) {
|
|
512
607
|
const proxyEvent = toProxyEvent(event);
|
|
513
608
|
if (proxyEvent) {
|
|
514
|
-
|
|
515
|
-
res
|
|
609
|
+
touchStreamRun(run);
|
|
610
|
+
writeStreamEvent(res, proxyEvent);
|
|
516
611
|
}
|
|
517
612
|
if (event.type === "done") {
|
|
518
613
|
completeStreamRun(run, event.message);
|
|
@@ -525,13 +620,13 @@ function handleStream(config, body, res) {
|
|
|
525
620
|
finally {
|
|
526
621
|
clearInterval(heartbeat);
|
|
527
622
|
}
|
|
528
|
-
res
|
|
623
|
+
endStreamResponse(res);
|
|
529
624
|
})().catch((err) => {
|
|
530
625
|
clearInterval(heartbeat);
|
|
531
626
|
const message = err instanceof Error ? err.message : String(err);
|
|
532
627
|
failStreamRun(run, message);
|
|
533
|
-
res
|
|
534
|
-
res
|
|
628
|
+
writeStreamError(res, message);
|
|
629
|
+
endStreamResponse(res);
|
|
535
630
|
});
|
|
536
631
|
}
|
|
537
632
|
async function handlePostRequest(config, pathname, body, res) {
|
|
@@ -574,10 +669,6 @@ async function handlePostRequest(config, pathname, body, res) {
|
|
|
574
669
|
handleSessionTreeSwitch(config, body, res);
|
|
575
670
|
return true;
|
|
576
671
|
}
|
|
577
|
-
if (pathname === "/api/session/drop-last-assistant-error") {
|
|
578
|
-
handleDropLastAssistantError(config, body, res);
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
672
|
if (pathname === "/api/session/compact") {
|
|
582
673
|
await handleSessionCompact(config, body, res);
|
|
583
674
|
return true;
|
|
@@ -618,19 +709,19 @@ export function createPiServer(configOverride) {
|
|
|
618
709
|
const fromParam = url.searchParams.get("from");
|
|
619
710
|
const from = fromParam === null ? undefined : Number(fromParam);
|
|
620
711
|
if (from !== undefined && (!Number.isInteger(from) || from < 0)) {
|
|
621
|
-
|
|
712
|
+
sendError(res, 400, "from must be a non-negative integer", PiServerErrorCode.INVALID_REQUEST);
|
|
622
713
|
return;
|
|
623
714
|
}
|
|
624
715
|
const entriesFromParam = url.searchParams.get("entriesFrom");
|
|
625
716
|
const entriesFrom = entriesFromParam === null ? undefined : Number(entriesFromParam);
|
|
626
717
|
if (entriesFrom !== undefined && (!Number.isInteger(entriesFrom) || entriesFrom < 0)) {
|
|
627
|
-
|
|
718
|
+
sendError(res, 400, "entriesFrom must be a non-negative integer", PiServerErrorCode.INVALID_REQUEST);
|
|
628
719
|
return;
|
|
629
720
|
}
|
|
630
721
|
const revisionParam = url.searchParams.get("revision");
|
|
631
722
|
const revision = revisionParam === null ? undefined : Number(revisionParam);
|
|
632
723
|
if (revision !== undefined && (!Number.isInteger(revision) || revision < 0)) {
|
|
633
|
-
|
|
724
|
+
sendError(res, 400, "revision must be a non-negative integer", PiServerErrorCode.INVALID_REQUEST);
|
|
634
725
|
return;
|
|
635
726
|
}
|
|
636
727
|
handleSessionHistory(decodeURIComponent(encodedSessionId), from, entriesFrom, revision, url.searchParams.get("baseTreeHash") ?? undefined, res);
|
|
@@ -644,12 +735,15 @@ export function createPiServer(configOverride) {
|
|
|
644
735
|
sendJson(res, 200, chunkResult.ack);
|
|
645
736
|
return;
|
|
646
737
|
}
|
|
647
|
-
await handlePostRequest(config, chunkResult.target, JSON.parse(chunkResult.bodyJson), res);
|
|
738
|
+
const handled = await handlePostRequest(config, chunkResult.target, JSON.parse(chunkResult.bodyJson), res);
|
|
739
|
+
if (!handled && !res.headersSent) {
|
|
740
|
+
sendError(res, 404, "Not found", PiServerErrorCode.INVALID_REQUEST);
|
|
741
|
+
}
|
|
648
742
|
}
|
|
649
743
|
catch (err) {
|
|
650
744
|
logRequestError(req, err);
|
|
651
745
|
if (!res.headersSent) {
|
|
652
|
-
|
|
746
|
+
sendError(res, 400, err instanceof Error ? err.message : String(err), PiServerErrorCode.INVALID_REQUEST);
|
|
653
747
|
}
|
|
654
748
|
else {
|
|
655
749
|
res.write(encodeErrorEvent(err instanceof Error ? err.message : String(err)));
|
|
@@ -663,11 +757,15 @@ export function createPiServer(configOverride) {
|
|
|
663
757
|
const body = JSON.parse(await readBody(req));
|
|
664
758
|
if (await handlePostRequest(config, url.pathname, body, res))
|
|
665
759
|
return;
|
|
760
|
+
if (!res.headersSent) {
|
|
761
|
+
sendError(res, 404, "Not found", PiServerErrorCode.INVALID_REQUEST);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
666
764
|
}
|
|
667
765
|
catch (err) {
|
|
668
766
|
logRequestError(req, err);
|
|
669
767
|
if (!res.headersSent) {
|
|
670
|
-
|
|
768
|
+
sendError(res, 500, err instanceof Error ? err.message : String(err), PiServerErrorCode.INTERNAL_ERROR);
|
|
671
769
|
}
|
|
672
770
|
else {
|
|
673
771
|
res.write(encodeErrorEvent(err instanceof Error ? err.message : String(err)));
|
|
@@ -679,11 +777,12 @@ export function createPiServer(configOverride) {
|
|
|
679
777
|
if (req.method === "DELETE" && url.pathname.startsWith("/api/session/")) {
|
|
680
778
|
const sessionId = decodeURIComponent(url.pathname.slice("/api/session/".length));
|
|
681
779
|
deleteSessionFromStore(sessionId);
|
|
780
|
+
deleteStreamRunsForSession(sessionId);
|
|
682
781
|
deletePersistedSession(config.sessionStoreDir, sessionId);
|
|
683
782
|
sendJson(res, 200, { deleted: sessionId });
|
|
684
783
|
return;
|
|
685
784
|
}
|
|
686
|
-
|
|
785
|
+
sendError(res, 404, "Not found", PiServerErrorCode.INVALID_REQUEST);
|
|
687
786
|
});
|
|
688
787
|
return server;
|
|
689
788
|
}
|